diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 07ca67ae7..91a178d05 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -247,7 +247,7 @@ importers: version: 1.0.0 minecraft-inventory-gui: specifier: github:zardoy/minecraft-inventory-gui#next - version: github.com/zardoy/minecraft-inventory-gui/bf2e7ba3afdc606604d85682d081778281d533cb(@types/react@18.2.20)(react@18.2.0) + version: github.com/zardoy/minecraft-inventory-gui/6685fa4a10590a5decef90a454ba0b9e045e7737(@types/react@18.2.20)(react@18.2.0) mineflayer: specifier: github:PrismarineJS/mineflayer version: github.com/PrismarineJS/mineflayer/d77a0511d02fe552170249ec19c2ff3030d17137 @@ -15419,9 +15419,9 @@ packages: vec3: 0.1.8 dev: false - github.com/zardoy/minecraft-inventory-gui/bf2e7ba3afdc606604d85682d081778281d533cb(@types/react@18.2.20)(react@18.2.0): - resolution: {tarball: https://codeload.github.com/zardoy/minecraft-inventory-gui/tar.gz/bf2e7ba3afdc606604d85682d081778281d533cb} - id: github.com/zardoy/minecraft-inventory-gui/bf2e7ba3afdc606604d85682d081778281d533cb + github.com/zardoy/minecraft-inventory-gui/6685fa4a10590a5decef90a454ba0b9e045e7737(@types/react@18.2.20)(react@18.2.0): + resolution: {tarball: https://codeload.github.com/zardoy/minecraft-inventory-gui/tar.gz/6685fa4a10590a5decef90a454ba0b9e045e7737} + id: github.com/zardoy/minecraft-inventory-gui/6685fa4a10590a5decef90a454ba0b9e045e7737 name: minecraft-inventory-gui version: 1.0.1 dependencies: diff --git a/src/react/ChatContainer.css b/src/react/ChatContainer.css index 69da94b02..419177835 100644 --- a/src/react/ChatContainer.css +++ b/src/react/ChatContainer.css @@ -47,7 +47,8 @@ div.chat-wrapper { .input-mobile .chat-completions { transform: none; top: 100%; - padding-left: 0; + padding-left: calc(env(safe-area-inset-left) / 2); + margin-top: 20px; /* input height */ } diff --git a/src/react/ChatContainer.tsx b/src/react/ChatContainer.tsx index a468313ec..e965a3265 100644 --- a/src/react/ChatContainer.tsx +++ b/src/react/ChatContainer.tsx @@ -6,6 +6,7 @@ import { miscUiState } from '../globalState' import { MessagePart } from './MessageFormatted' import './ChatContainer.css' import { isIos } from './utils' +import { reactKeyForMessage } from './Scoreboard' export type Message = { parts: MessageFormatPart[], @@ -145,7 +146,7 @@ export default ({ messages, opacity = 1, fetchCompletionItems, opened, sendMessa const auxInputFocus = (fireKey: string) => { chatInput.current.focus() - document.dispatchEvent(new KeyboardEvent('keydown', { code: fireKey })) + chatInput.current.dispatchEvent(new KeyboardEvent('keydown', { code: fireKey, bubbles: true })) } const getDefaultCompleteValue = () => { @@ -206,7 +207,7 @@ export default ({ messages, opacity = 1, fetchCompletionItems, opened, sendMessa }}> {opacity &&
{messages.map((m) => ( - + ))}
|| undefined} diff --git a/src/react/MessageFormattedString.tsx b/src/react/MessageFormattedString.tsx index 11b29e6be..d1f8a10f0 100644 --- a/src/react/MessageFormattedString.tsx +++ b/src/react/MessageFormattedString.tsx @@ -4,10 +4,16 @@ import { formatMessage } from '../botUtils' import MessageFormatted from './MessageFormatted' /** like MessageFormatted, but receives raw string or json instead, uses window.loadedData */ -export default ({ message }: { message: string | Record }) => { +export default ({ message }: { message: string | Record | null }) => { const messageJson = useMemo(() => { - return formatMessage(typeof message === 'string' ? fromFormattedString(message) : message) + if (!message) return null + try { + return formatMessage(typeof message === 'string' ? fromFormattedString(message) : message) + } catch (err) { + console.error(err) // todo ensure its being logged + return null + } }, [message]) - return + return messageJson ? : null } diff --git a/src/react/Scoreboard.css b/src/react/Scoreboard.css new file mode 100644 index 000000000..b2bb85213 --- /dev/null +++ b/src/react/Scoreboard.css @@ -0,0 +1,37 @@ +.scoreboard-container { + z-index: -2; + pointer-events: none; + white-space: nowrap; + position: fixed; + right: 0px; + top: 50%; + transform: translateY(-50%); + background-color: rgba(0, 0, 0, 0.4); + min-width: 100px; + max-width: 400px; + display: flex; + flex-direction: column; + padding-inline: 5px; + font-size: 0.5rem; + max-height: 70%; + overflow: hidden; +} + +@media (max-width: 800px) { + .scoreboard-container { + font-size: 0.4rem; + } +} + +.scoreboard-title { + text-align: center; +} + +.item-container { + display: flex; +} + +.item-value { + margin-left: auto; + color: lightcoral; +} diff --git a/src/react/Scoreboard.stories.tsx b/src/react/Scoreboard.stories.tsx new file mode 100644 index 000000000..76d37c171 --- /dev/null +++ b/src/react/Scoreboard.stories.tsx @@ -0,0 +1,27 @@ +import type { Meta, StoryObj } from '@storybook/react' + +import Scoreboard from './Scoreboard' + +const meta: Meta = { + component: Scoreboard +} + +export default meta +type Story = StoryObj; + +export const Primary: Story = { + args: { + title: 'Scoreboard', + items: [ + { + name: 'item 1', + value: 9 + }, + { + name: 'item 2', + value: 8 + } + ], + open: true + } +} diff --git a/src/react/Scoreboard.tsx b/src/react/Scoreboard.tsx new file mode 100644 index 000000000..546a0f586 --- /dev/null +++ b/src/react/Scoreboard.tsx @@ -0,0 +1,40 @@ +import './Scoreboard.css' +import MessageFormattedString from './MessageFormattedString' + + +export type ScoreboardItems = Array<{name: string, value: number, displayName?: any}> + +type ScoreboardProps = { + title: string, + items: ScoreboardItems, + open: boolean +} + +export const reactKeyForMessage = (message) => { + return typeof message === 'string' ? message : JSON.stringify(message) +} + +export default function Scoreboard ({ title, items, open }: ScoreboardProps) { + + if (!open) return null + return ( +
+
+ +
+ { + items.map((item) => { + const message = item.displayName ?? item.name + return
+
+ +
+
+ {item.value} +
+
+ }) + } +
+ ) +} diff --git a/src/react/ScoreboardProvider.tsx b/src/react/ScoreboardProvider.tsx new file mode 100644 index 000000000..c354513b4 --- /dev/null +++ b/src/react/ScoreboardProvider.tsx @@ -0,0 +1,41 @@ +import { useMemo, useState } from 'react' +import Scoreboard from './Scoreboard' +import type { ScoreboardItems } from './Scoreboard' + + +export default function ScoreboardProvider () { + const [title, setTitle] = useState('Scoreboard') + const [items, setItems] = useState([]) + const [open, setOpen] = useState(false) + + useMemo(() => { // useMemo instead of useEffect to register them asap and not after the initial dom render + const updateSidebarScoreboard = () => { + if (bot.scoreboard.sidebar) { + setTitle(bot.scoreboard.sidebar.title) + setItems([...bot.scoreboard.sidebar.items]) + setOpen(true) + } else { + setOpen(false) + } + } + + bot.on('scoreboardCreated', updateSidebarScoreboard) // not used atm but still good to have + bot.on('scoreboardTitleChanged', updateSidebarScoreboard) + bot.on('scoreUpdated', updateSidebarScoreboard) + bot.on('scoreRemoved', updateSidebarScoreboard) + bot.on('scoreboardDeleted', updateSidebarScoreboard) + bot.on('scoreboardPosition', () => { + void Promise.resolve().then(() => { + updateSidebarScoreboard() + }) // mineflayer bug: wait for the next tick to get the correct scoreboard position + }) + }, []) + + return( + + ) +} diff --git a/src/reactUi.tsx b/src/reactUi.tsx index af637853d..45415578a 100644 --- a/src/reactUi.tsx +++ b/src/reactUi.tsx @@ -15,6 +15,7 @@ import SelectOption from './react/SelectOption' import EnterFullscreenButton from './react/EnterFullscreenButton' import ChatProvider from './react/ChatProvider' import TitleProvider from './react/TitleProvider' +import ScoreboardProvider from './react/ScoreboardProvider' import SoundMuffler from './react/SoundMuffler' import TouchControls from './react/TouchControls' import widgets from './react/widgets' @@ -62,6 +63,7 @@ const InGameUi = () => { +