release (#228)
This commit is contained in:
commit
ce601caf2a
32 changed files with 354 additions and 163 deletions
|
|
@ -197,7 +197,8 @@
|
|||
"no-async-promise-executor": "off",
|
||||
"no-bitwise": "off",
|
||||
"unicorn/filename-case": "off",
|
||||
"max-depth": "off"
|
||||
"max-depth": "off",
|
||||
"unicorn/no-typeof-undefined": "off"
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ RUN pnpm i
|
|||
# ENTRYPOINT ["pnpm", "run", "run-all"]
|
||||
|
||||
# only for prod
|
||||
RUN pnpm run build
|
||||
RUN GITHUB_REPOSITORY=zardoy/minecraft-web-client pnpm run build
|
||||
|
||||
# ---- Run Stage ----
|
||||
FROM node:18-alpine
|
||||
|
|
|
|||
|
|
@ -38,11 +38,13 @@ Whatever offline mode you used (zip, folder, just single player), you can always
|
|||
|
||||

|
||||
|
||||
### Servers
|
||||
### Servers & Proxy
|
||||
|
||||
You can play almost on any Java server, vanilla servers are fully supported.
|
||||
See the [Mineflayer](https://github.com/PrismarineJS/mineflayer) repo for the list of supported versions (should support majority of versions).
|
||||
There is a builtin proxy, but you can also host your one! Just clone the repo, run `pnpm i` (following CONTRIBUTING.MD) and run `pnpm prod-start`, then you can specify `http://localhost:8080` in the proxy field.
|
||||
There is a builtin proxy, but you can also host your one! Just clone the repo, run `pnpm i` (following CONTRIBUTING.MD) and run `pnpm prod-start`, then you can specify `http://localhost:8080` in the proxy field. Or you can deploy it to the cloud service:
|
||||
|
||||
[](https://app.koyeb.com/deploy?name=minecraft-web-client&type=git&repository=zardoy%2Fminecraft-web-client&branch=next&builder=dockerfile&env%5B%5D=&ports=8080%3Bhttp%3B%2F)
|
||||
|
||||
Proxy servers are used to connect to Minecraft servers which use TCP protocol. When you connect connect to a server with a proxy, websocket connection is created between you (browser client) and the proxy server located in Europe, then the proxy connects to the Minecraft server and sends the data to the client (you) without any packet deserialization to avoid any additional delays. That said all the Minecraft protocol packets are processed by the client, right in your browser.
|
||||
|
||||
|
|
@ -139,7 +141,7 @@ Server specific:
|
|||
Single player specific:
|
||||
|
||||
- `?loadSave=<save_name>` - Load the save on load with the specified folder name (not title)
|
||||
- `?singleplayer=1` - Create empty world on load. Nothing will be saved
|
||||
- `?singleplayer=1` or `?sp=1` - Create empty world on load. Nothing will be saved
|
||||
- `?version=<version>` - Set the version for the singleplayer world (when used with `?singleplayer=1`)
|
||||
- `?noSave=true` - Disable auto save on unload / disconnect / export whenever a world is loaded. Only manual save with `/save` command will work.
|
||||
- `?serverSetting=<key>:<value>` - Set local server [options](https://github.com/zardoy/space-squid/tree/everything/src/modules.ts#L51). For example `?serverSetting=noInitialChunksSend:true` will disable initial chunks loading on the loading screen.
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ Minecraft UI components for React extracted from [mcraft.fun](https://mcraft.fun
|
|||
pnpm i minecraft-react
|
||||
```
|
||||
|
||||

|
||||

|
||||
|
||||
## Usage
|
||||
|
||||
|
|
|
|||
BIN
docs-assets/npm-banner.jpeg
Normal file
BIN
docs-assets/npm-banner.jpeg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 96 KiB |
|
|
@ -29,17 +29,27 @@ function getUsernameTexture (username: string, { fontFamily = 'sans-serif' }: an
|
|||
const padding = 5
|
||||
ctx.font = `${fontSize}px ${fontFamily}`
|
||||
|
||||
const textWidth = ctx.measureText(username).width + padding * 2
|
||||
const lines = String(username).split('\n')
|
||||
|
||||
let textWidth = 0
|
||||
for (const line of lines) {
|
||||
const width = ctx.measureText(line).width + padding * 2
|
||||
if (width > textWidth) textWidth = width
|
||||
}
|
||||
|
||||
canvas.width = textWidth
|
||||
canvas.height = fontSize + padding * 2
|
||||
canvas.height = (fontSize + padding * 2) * lines.length
|
||||
|
||||
ctx.fillStyle = 'rgba(0, 0, 0, 0.3)'
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height)
|
||||
|
||||
ctx.font = `${fontSize}px ${fontFamily}`
|
||||
ctx.fillStyle = 'white'
|
||||
ctx.fillText(username, padding, fontSize)
|
||||
let i = 0
|
||||
for (const line of lines) {
|
||||
i++
|
||||
ctx.fillText(line, padding + (textWidth - ctx.measureText(line).width) / 2, fontSize * i)
|
||||
}
|
||||
|
||||
return canvas
|
||||
}
|
||||
|
|
@ -454,6 +464,7 @@ export class Entities extends EventEmitter {
|
|||
// ---
|
||||
// not player
|
||||
const displayText = entity.metadata?.[3] && this.parseEntityLabel(entity.metadata[2])
|
||||
|| entity.metadata?.[23] && this.parseEntityLabel(entity.metadata[23]) // text displays
|
||||
if (entity.name !== 'player' && displayText) {
|
||||
addNametag({ ...entity, username: displayText }, this.entitiesOptions, this.entities[entity.id].children.find(c => c.name === 'mesh'))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -224,8 +224,8 @@ export const knownNotHandled = [
|
|||
'item_display', 'item_frame',
|
||||
'lightning_bolt', 'marker',
|
||||
'painting', 'spawner_minecart',
|
||||
'spectral_arrow', 'text_display',
|
||||
'tnt', 'trader_llama', 'zombie_horse'
|
||||
'spectral_arrow', 'tnt',
|
||||
'trader_llama', 'zombie_horse'
|
||||
]
|
||||
|
||||
export const temporaryMap = {
|
||||
|
|
|
|||
|
|
@ -16390,6 +16390,10 @@
|
|||
},
|
||||
"render_controllers": ["controller.render.strider"]
|
||||
},
|
||||
"text_display": {
|
||||
"identifier": "minecraft:text_display",
|
||||
"geometry": {}
|
||||
},
|
||||
"trident": {
|
||||
"identifier": "minecraft:thrown_trident",
|
||||
"textures": {
|
||||
|
|
|
|||
|
|
@ -484,7 +484,7 @@ export function getSectionGeometry (sx, sy, sz, world: World) {
|
|||
}
|
||||
}
|
||||
if (invisibleBlocks.has(block.name)) continue
|
||||
if (block.name.includes('_sign') || block.name === 'sign') {
|
||||
if ((block.name.includes('_sign') || block.name === 'sign') && !world.config.disableSignsMapsSupport) {
|
||||
const key = `${cursor.x},${cursor.y},${cursor.z}`
|
||||
const props: any = block.getProperties()
|
||||
const facingRotationMap = {
|
||||
|
|
|
|||
|
|
@ -7,7 +7,9 @@ export const defaultMesherConfig = {
|
|||
smoothLighting: true,
|
||||
outputFormat: 'threeJs' as 'threeJs' | 'webgpu',
|
||||
textureSize: 1024, // for testing
|
||||
debugModelVariant: undefined as undefined | number[]
|
||||
debugModelVariant: undefined as undefined | number[],
|
||||
clipWorldBelowY: undefined as undefined | number,
|
||||
disableSignsMapsSupport: false
|
||||
}
|
||||
|
||||
export type MesherConfig = typeof defaultMesherConfig
|
||||
|
|
|
|||
|
|
@ -99,7 +99,7 @@ export class Viewer {
|
|||
|
||||
setBlockStateId (pos: Vec3, stateId: number) {
|
||||
if (!this.world.loadedChunks[`${Math.floor(pos.x / 16) * 16},${Math.floor(pos.z / 16) * 16}`]) {
|
||||
console.warn('[should be unreachable] setBlockStateId called for unloaded chunk', pos)
|
||||
console.debug('[should be unreachable] setBlockStateId called for unloaded chunk', pos)
|
||||
}
|
||||
this.world.setBlockStateId(pos, stateId)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -321,6 +321,10 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
|
|||
console.log('texture loaded')
|
||||
}
|
||||
|
||||
get worldMinYRender () {
|
||||
return Math.floor(Math.max(this.worldConfig.minY, this.mesherConfig.clipWorldBelowY ?? -Infinity) / 16) * 16
|
||||
}
|
||||
|
||||
addColumn (x: number, z: number, chunk: any, isLightUpdate: boolean) {
|
||||
if (!this.active) return
|
||||
if (this.workers.length === 0) throw new Error('workers not initialized yet')
|
||||
|
|
@ -330,7 +334,7 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
|
|||
// todo optimize
|
||||
worker.postMessage({ type: 'chunk', x, z, chunk })
|
||||
}
|
||||
for (let y = this.worldConfig.minY; y < this.worldConfig.worldHeight; y += 16) {
|
||||
for (let y = this.worldMinYRender; y < this.worldConfig.worldHeight; y += 16) {
|
||||
const loc = new Vec3(x, y, z)
|
||||
this.setSectionDirty(loc)
|
||||
if (this.neighborChunkUpdates && (!isLightUpdate || this.mesherConfig.smoothLighting)) {
|
||||
|
|
|
|||
|
|
@ -52,6 +52,7 @@ exports.getSwAdditionalEntries = () => {
|
|||
'*.png',
|
||||
'*.woff',
|
||||
'mesher.js',
|
||||
'manifest.json',
|
||||
'worldSaveWorker.js',
|
||||
`textures/entity/squid/squid.png`,
|
||||
// everything but not .map
|
||||
|
|
|
|||
|
|
@ -2,6 +2,11 @@
|
|||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import { fileURLToPath } from 'url'
|
||||
import { execSync } from 'child_process'
|
||||
|
||||
// write release tag
|
||||
const commitShort = execSync('git rev-parse --short HEAD').toString().trim()
|
||||
fs.writeFileSync('./assets/release.json', JSON.stringify({ latestTag: `${commitShort} (docker)` }), 'utf8')
|
||||
|
||||
const packageJson = JSON.parse(fs.readFileSync('./package.json', 'utf8'))
|
||||
delete packageJson.optionalDependencies
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { versionToNumber } from 'prismarine-viewer/viewer/prepare/utils'
|
||||
import * as nbt from 'prismarine-nbt'
|
||||
|
||||
export const displayClientChat = (text: string) => {
|
||||
const message = {
|
||||
|
|
@ -18,3 +19,22 @@ export const displayClientChat = (text: string) => {
|
|||
sender: 'minecraft:chat'
|
||||
})
|
||||
}
|
||||
|
||||
export const parseFormattedMessagePacket = (arg) => {
|
||||
if (typeof arg === 'object') {
|
||||
try {
|
||||
return {
|
||||
formatted: nbt.simplify(arg),
|
||||
plain: ''
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('Failed to parse formatted message', arg, err)
|
||||
return {
|
||||
plain: JSON.stringify(arg)
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
plain: String(arg)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -52,6 +52,7 @@ export const contro = new ControMax({
|
|||
selectItem: ['KeyH'] // default will be removed
|
||||
},
|
||||
ui: {
|
||||
toggleFullscreen: ['F11'],
|
||||
back: [null/* 'Escape' */, 'B'],
|
||||
leftClick: [null, 'A'],
|
||||
rightClick: [null, 'Y'],
|
||||
|
|
@ -419,6 +420,10 @@ contro.on('trigger', ({ command }) => {
|
|||
if (command === 'ui.pauseMenu') {
|
||||
showModal({ reactType: 'pause-screen' })
|
||||
}
|
||||
|
||||
if (command === 'ui.toggleFullscreen') {
|
||||
void goFullscreen(true)
|
||||
}
|
||||
})
|
||||
|
||||
contro.on('release', ({ command }) => {
|
||||
|
|
@ -742,10 +747,6 @@ window.addEventListener('keydown', (e) => {
|
|||
|
||||
// #region experimental debug things
|
||||
window.addEventListener('keydown', (e) => {
|
||||
if (e.code === 'F11') {
|
||||
e.preventDefault()
|
||||
void goFullscreen(true)
|
||||
}
|
||||
if (e.code === 'KeyL' && e.altKey) {
|
||||
console.clear()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,7 @@
|
|||
import { saveServer } from './flyingSquidUtils'
|
||||
import { watchUnloadForCleanup } from './gameUnload'
|
||||
import { showModal } from './globalState'
|
||||
import { options } from './optionsStorage'
|
||||
import { chatInputValueGlobal } from './react/Chat'
|
||||
import { showNotification } from './react/NotificationProvider'
|
||||
|
||||
|
|
@ -10,4 +13,15 @@ export default () => {
|
|||
showModal({ reactType: 'chat' })
|
||||
})
|
||||
})
|
||||
|
||||
if (options.singleplayerAutoSave) {
|
||||
const autoSaveInterval = setInterval(() => {
|
||||
if (options.singleplayerAutoSave) {
|
||||
void saveServer(true)
|
||||
}
|
||||
}, 2000)
|
||||
watchUnloadForCleanup(() => {
|
||||
clearInterval(autoSaveInterval)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
18
src/index.ts
18
src/index.ts
|
|
@ -102,6 +102,7 @@ import packetsPatcher from './packetsPatcher'
|
|||
import { mainMenuState } from './react/MainMenuRenderApp'
|
||||
import { ItemsRenderer } from 'mc-assets/dist/itemsRenderer'
|
||||
import './mobileShim'
|
||||
import { parseFormattedMessagePacket } from './botUtils'
|
||||
|
||||
window.debug = debug
|
||||
window.THREE = THREE
|
||||
|
|
@ -402,7 +403,9 @@ async function connect (connectOptions: ConnectOptions) {
|
|||
setLoadingScreenStatus(`Loading data for ${version}`)
|
||||
if (!document.fonts.check('1em mojangles')) {
|
||||
// todo instead re-render signs on load
|
||||
await document.fonts.load('1em mojangles').catch(() => { })
|
||||
await document.fonts.load('1em mojangles').catch(() => {
|
||||
console.error('Failed to load font, signs wont be rendered correctly')
|
||||
})
|
||||
}
|
||||
await window._MC_DATA_RESOLVER.promise // ensure data is loaded
|
||||
await downloadSoundsIfNeeded()
|
||||
|
|
@ -457,7 +460,7 @@ async function connect (connectOptions: ConnectOptions) {
|
|||
flyingSquidEvents()
|
||||
}
|
||||
|
||||
if (connectOptions.authenticatedAccount) username = 'not-used'
|
||||
if (connectOptions.authenticatedAccount) username = 'you'
|
||||
let initialLoadingText: string
|
||||
if (singleplayer) {
|
||||
initialLoadingText = 'Local server is still starting'
|
||||
|
|
@ -637,14 +640,7 @@ async function connect (connectOptions: ConnectOptions) {
|
|||
|
||||
bot.on('kicked', (kickReason) => {
|
||||
console.log('You were kicked!', kickReason)
|
||||
let kickReasonString = typeof kickReason === 'string' ? kickReason : JSON.stringify(kickReason)
|
||||
let kickReasonFormatted = undefined as undefined | Record<string, any>
|
||||
if (typeof kickReason === 'object') {
|
||||
try {
|
||||
kickReasonFormatted = nbt.simplify(kickReason)
|
||||
kickReasonString = ''
|
||||
} catch {}
|
||||
}
|
||||
const { formatted: kickReasonFormatted, plain: kickReasonString } = parseFormattedMessagePacket(kickReason)
|
||||
setLoadingScreenStatus(`The Minecraft server kicked you. Kick reason: ${kickReasonString}`, true, undefined, undefined, kickReasonFormatted)
|
||||
destroyAll()
|
||||
})
|
||||
|
|
@ -932,7 +928,7 @@ watchValue(miscUiState, async s => {
|
|||
const qs = new URLSearchParams(window.location.search)
|
||||
const moreServerOptions = {} as Record<string, any>
|
||||
if (qs.has('version')) moreServerOptions.version = qs.get('version')
|
||||
if (qs.get('singleplayer') === '1') {
|
||||
if (qs.get('singleplayer') === '1' || qs.get('sp') === '1') {
|
||||
loadSingleplayer({}, {
|
||||
worldFolder: undefined,
|
||||
...moreServerOptions
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ export default async ({ tokenCaches, proxyBaseUrl, setProgressText = (text) => {
|
|||
const authFlow = {
|
||||
async getMinecraftJavaToken () {
|
||||
setProgressText('Authenticating with Microsoft account')
|
||||
if (!window.crypto && !isPageSecure()) throw new Error('Crypto API is available only in secure contexts. Be sure to use https!')
|
||||
let result = null
|
||||
await fetch(authEndpoint, {
|
||||
method: 'POST',
|
||||
|
|
@ -43,22 +44,29 @@ export default async ({ tokenCaches, proxyBaseUrl, setProgressText = (text) => {
|
|||
connectingServer,
|
||||
connectingServerVersion: connectingVersion
|
||||
}),
|
||||
}).then(async response => {
|
||||
if (!response.ok) {
|
||||
throw new Error(`Auth server error (${response.status}): ${await response.text()}`)
|
||||
}
|
||||
|
||||
const reader = response.body!.getReader()
|
||||
const decoder = new TextDecoder('utf8')
|
||||
|
||||
const processText = ({ done, value = undefined as Uint8Array | undefined }) => {
|
||||
if (done) {
|
||||
return
|
||||
})
|
||||
.catch(e => {
|
||||
throw new Error(`Failed to connect to auth server (network error): ${e.message}`)
|
||||
})
|
||||
.then(async response => {
|
||||
if (!response.ok) {
|
||||
throw new Error(`Auth server error (${response.status}): ${await response.text()}`)
|
||||
}
|
||||
|
||||
const processChunk = (chunkStr) => {
|
||||
try {
|
||||
const json = JSON.parse(chunkStr)
|
||||
const reader = response.body!.getReader()
|
||||
const decoder = new TextDecoder('utf8')
|
||||
|
||||
const processText = ({ done, value = undefined as Uint8Array | undefined }) => {
|
||||
if (done) {
|
||||
return
|
||||
}
|
||||
|
||||
const processChunk = (chunkStr) => {
|
||||
let json: any
|
||||
try {
|
||||
json = JSON.parse(chunkStr)
|
||||
} catch (err) {}
|
||||
if (!json) return
|
||||
if (json.user_code) {
|
||||
onMsaCodeCallback(json)
|
||||
// this.codeCallback(json)
|
||||
|
|
@ -66,26 +74,22 @@ export default async ({ tokenCaches, proxyBaseUrl, setProgressText = (text) => {
|
|||
if (json.error) throw new Error(json.error)
|
||||
if (json.token) result = json
|
||||
if (json.newCache) setCacheResult(json.newCache)
|
||||
} catch (err) {
|
||||
}
|
||||
|
||||
const strings = decoder.decode(value)
|
||||
|
||||
for (const chunk of strings.split('\n\n')) {
|
||||
processChunk(chunk)
|
||||
}
|
||||
|
||||
return reader.read().then(processText)
|
||||
}
|
||||
|
||||
const strings = decoder.decode(value)
|
||||
|
||||
for (const chunk of strings.split('\n\n')) {
|
||||
processChunk(chunk)
|
||||
}
|
||||
|
||||
return reader.read().then(processText)
|
||||
}
|
||||
return reader.read().then(processText)
|
||||
})
|
||||
if (!window.crypto && !isPageSecure()) throw new Error('Crypto API is available only in secure contexts. Be sure to use https!')
|
||||
})
|
||||
const restoredData = await restoreData(result)
|
||||
if (!restoredData?.certificates?.profileKeys?.privatePEM) {
|
||||
throw new Error(`Authentication server issue: it didn't return auth data. Most probably because the auth request was rejected by the end authority and retrying won't help until the issue is resolved.`)
|
||||
if (restoredData?.certificates?.profileKeys?.privatePEM) {
|
||||
restoredData.certificates.profileKeys.private = restoredData.certificates.profileKeys.privatePEM
|
||||
}
|
||||
restoredData.certificates.profileKeys.private = restoredData.certificates.profileKeys.privatePEM
|
||||
return restoredData
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -74,7 +74,7 @@ export const guiOptionsScheme: {
|
|||
},
|
||||
smoothLighting: {},
|
||||
newVersionsLighting: {
|
||||
text: 'Lighting in newer versions',
|
||||
text: 'Lighting in Newer Versions',
|
||||
},
|
||||
lowMemoryMode: {
|
||||
text: 'Low Memory Mode',
|
||||
|
|
@ -98,6 +98,19 @@ export const guiOptionsScheme: {
|
|||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
custom () {
|
||||
return <Category>Resource Packs</Category>
|
||||
},
|
||||
serverResourcePacks: {
|
||||
text: 'Download From Server',
|
||||
values: [
|
||||
'prompt',
|
||||
'always',
|
||||
'never'
|
||||
],
|
||||
}
|
||||
}
|
||||
],
|
||||
main: [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -52,6 +52,9 @@ const defaultOptions = {
|
|||
|
||||
// antiAliasing: false,
|
||||
|
||||
clipWorldBelowY: undefined as undefined | number, // will be removed
|
||||
disableSignsMapsSupport: false,
|
||||
singleplayerAutoSave: false,
|
||||
showChunkBorders: false, // todo rename option
|
||||
frameLimit: false as number | false,
|
||||
alwaysBackupWorldBeforeLoading: undefined as boolean | undefined | null,
|
||||
|
|
@ -157,7 +160,7 @@ subscribe(options, () => {
|
|||
localStorage.options = JSON.stringify(saveOptions)
|
||||
})
|
||||
|
||||
type WatchValue = <T extends Record<string, any>>(proxy: T, callback: (p: T) => void) => void
|
||||
type WatchValue = <T extends Record<string, any>>(proxy: T, callback: (p: T, isChanged: boolean) => void) => void
|
||||
|
||||
export const watchValue: WatchValue = (proxy, callback) => {
|
||||
const watchedProps = new Set<string>()
|
||||
|
|
@ -166,10 +169,10 @@ export const watchValue: WatchValue = (proxy, callback) => {
|
|||
watchedProps.add(p.toString())
|
||||
return Reflect.get(target, p, receiver)
|
||||
},
|
||||
}))
|
||||
}), false)
|
||||
for (const prop of watchedProps) {
|
||||
subscribeKey(proxy, prop, () => {
|
||||
callback(proxy)
|
||||
callback(proxy, true)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ interface Props {
|
|||
initialData?: BaseServerInfo
|
||||
parseQs?: boolean
|
||||
onQsConnect?: (server: BaseServerInfo) => void
|
||||
defaults?: Pick<BaseServerInfo, 'proxyOverride' | 'usernameOverride'>
|
||||
placeholders?: Pick<BaseServerInfo, 'proxyOverride' | 'usernameOverride'>
|
||||
accounts?: string[]
|
||||
authenticatedAccounts?: number
|
||||
versions?: string[]
|
||||
|
|
@ -30,7 +30,7 @@ interface Props {
|
|||
|
||||
const ELEMENTS_WIDTH = 190
|
||||
|
||||
export default ({ onBack, onConfirm, title = 'Add a Server', initialData, parseQs, onQsConnect, defaults, accounts, versions, authenticatedAccounts }: Props) => {
|
||||
export default ({ onBack, onConfirm, title = 'Add a Server', initialData, parseQs, onQsConnect, placeholders, accounts, versions, authenticatedAccounts }: Props) => {
|
||||
const qsParams = parseQs ? new URLSearchParams(window.location.search) : undefined
|
||||
const qsParamName = qsParams?.get('name')
|
||||
const qsParamIp = qsParams?.get('ip')
|
||||
|
|
@ -111,8 +111,8 @@ export default ({ onBack, onConfirm, title = 'Add a Server', initialData, parseQ
|
|||
/>
|
||||
</div>
|
||||
|
||||
<InputWithLabel label="Proxy Override" value={proxyOverride} disabled={lockConnect && qsParamProxy !== null} onChange={({ target: { value } }) => setProxyOverride(value)} placeholder={defaults?.proxyOverride} />
|
||||
<InputWithLabel label="Username Override" value={usernameOverride} disabled={!noAccountSelected || lockConnect && qsParamUsername !== null} onChange={({ target: { value } }) => setUsernameOverride(value)} placeholder={defaults?.usernameOverride} />
|
||||
<InputWithLabel label="Proxy Override" value={proxyOverride} disabled={lockConnect && qsParamProxy !== null} onChange={({ target: { value } }) => setProxyOverride(value)} placeholder={placeholders?.proxyOverride} />
|
||||
<InputWithLabel label="Username Override" value={usernameOverride} disabled={!noAccountSelected || lockConnect && qsParamUsername !== null} onChange={({ target: { value } }) => setUsernameOverride(value)} placeholder={placeholders?.usernameOverride} />
|
||||
<label style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import triangle from './ps_icons/playstation_triangle_console_controller_gamepad
|
|||
import square from './ps_icons/playstation_square_console_controller_gamepad_icon.svg'
|
||||
import circle from './ps_icons/circle_playstation_console_controller_gamepad_icon.svg'
|
||||
import cross from './ps_icons/cross_playstation_console_controller_gamepad_icon.svg'
|
||||
import { parseBindingName } from './parseKeybindingName'
|
||||
|
||||
|
||||
type Props = {
|
||||
|
|
@ -30,24 +31,6 @@ export default ({ type, val, isPS }: Props) => {
|
|||
</>
|
||||
}
|
||||
|
||||
async function parseBindingName (binding: string) {
|
||||
if (!binding) return ''
|
||||
|
||||
const { keyboard } = navigator
|
||||
const layoutMap = await keyboard?.getLayoutMap?.() ?? new Map()
|
||||
|
||||
const mapKey = key => layoutMap.get(key) || key
|
||||
|
||||
const cut = binding.replaceAll(/(Numpad|Digit|Key)/g, '')
|
||||
const parts = cut.includes('+') ? cut.split('+') : [cut]
|
||||
|
||||
for (let i = 0; i < parts.length; i++) {
|
||||
parts[i] = mapKey(parts[i]).split(/(?=[A-Z\d])/).reverse().join(' ')
|
||||
}
|
||||
|
||||
return parts.join(' + ')
|
||||
}
|
||||
|
||||
const buttonsMap = {
|
||||
'A': cross,
|
||||
'B': circle,
|
||||
|
|
|
|||
|
|
@ -1,49 +1,73 @@
|
|||
import { proxy, useSnapshot } from 'valtio'
|
||||
import { hideCurrentModal, showModal } from '../globalState'
|
||||
import { parseFormattedMessagePacket } from '../botUtils'
|
||||
import Screen from './Screen'
|
||||
import { useIsModalActive } from './utilsApp'
|
||||
import Button from './Button'
|
||||
import MessageFormattedString from './MessageFormattedString'
|
||||
|
||||
const state = proxy({
|
||||
title: '',
|
||||
options: [] as string[],
|
||||
showCancel: true,
|
||||
minecraftJsonMessage: null as null | Record<string, any>,
|
||||
behavior: 'resolve-close' as 'resolve-close' | 'close-resolve',
|
||||
})
|
||||
|
||||
let resolve
|
||||
export const showOptionsModal = async <T extends string> (
|
||||
title: string,
|
||||
options: T[],
|
||||
{ cancel = true }: { cancel?: boolean } = {}
|
||||
{ cancel = true, minecraftJsonMessage }: { cancel?: boolean, minecraftJsonMessage? } = {}
|
||||
): Promise<T | undefined> => {
|
||||
showModal({ reactType: 'general-select' })
|
||||
let minecraftJsonMessageParsed
|
||||
if (minecraftJsonMessage) {
|
||||
const parseResult = parseFormattedMessagePacket(minecraftJsonMessage)
|
||||
minecraftJsonMessageParsed = parseResult.formatted
|
||||
if (parseResult.plain) {
|
||||
title += ` (${parseResult.plain})`
|
||||
}
|
||||
}
|
||||
return new Promise((_resolve) => {
|
||||
resolve = _resolve
|
||||
Object.assign(state, {
|
||||
title,
|
||||
options,
|
||||
showCancel: cancel,
|
||||
minecraftJsonMessage: minecraftJsonMessageParsed
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export default () => {
|
||||
const { title, options, showCancel } = useSnapshot(state)
|
||||
const { title, options, showCancel, minecraftJsonMessage } = useSnapshot(state)
|
||||
const isModalActive = useIsModalActive('general-select')
|
||||
if (!isModalActive) return
|
||||
|
||||
const resolveClose = (value: string | undefined) => {
|
||||
if (state.behavior === 'resolve-close') {
|
||||
resolve(value)
|
||||
hideCurrentModal()
|
||||
} else {
|
||||
hideCurrentModal()
|
||||
resolve(value)
|
||||
}
|
||||
}
|
||||
|
||||
return <Screen title={title} backdrop>
|
||||
{minecraftJsonMessage && <div style={{ textAlign: 'center', }}>
|
||||
<MessageFormattedString message={minecraftJsonMessage} />
|
||||
</div>}
|
||||
{options.map(option => <Button
|
||||
key={option} onClick={() => {
|
||||
hideCurrentModal()
|
||||
resolve(option)
|
||||
resolveClose(option)
|
||||
}}
|
||||
>{option}
|
||||
</Button>)}
|
||||
{showCancel && <Button
|
||||
style={{ marginTop: 30 }} onClick={() => {
|
||||
hideCurrentModal()
|
||||
resolve(undefined)
|
||||
resolveClose(undefined)
|
||||
}}
|
||||
>Cancel
|
||||
</Button>}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ interface Props extends React.ComponentProps<typeof Singleplayer> {
|
|||
username: string
|
||||
setUsername: (username: string) => void
|
||||
onProfileClick?: () => void
|
||||
setQuickConnectIp?: (ip: string) => void
|
||||
}
|
||||
|
||||
export interface SavedProxiesLocalStorage {
|
||||
|
|
@ -29,7 +30,7 @@ type ProxyStatusResult = {
|
|||
status: 'success' | 'error' | 'unknown'
|
||||
}
|
||||
|
||||
export default ({ initialProxies, updateProxies: updateProxiesProp, joinServer, username, setUsername, onProfileClick, ...props }: Props) => {
|
||||
export default ({ initialProxies, updateProxies: updateProxiesProp, joinServer, username, setUsername, onProfileClick, setQuickConnectIp, ...props }: Props) => {
|
||||
const [proxies, setProxies] = React.useState(initialProxies)
|
||||
|
||||
const updateProxies = (newData: SavedProxiesLocalStorage) => {
|
||||
|
|
@ -39,6 +40,17 @@ export default ({ initialProxies, updateProxies: updateProxiesProp, joinServer,
|
|||
|
||||
const [serverIp, setServerIp] = React.useState('')
|
||||
const [save, setSave] = React.useState(true)
|
||||
const [activeHighlight, setActiveHighlight] = React.useState(undefined as 'quick-connect' | 'server-list' | undefined)
|
||||
|
||||
const getActiveHighlightStyles = (type: typeof activeHighlight) => {
|
||||
const styles: React.CSSProperties = {
|
||||
transition: 'filter 0.2s',
|
||||
}
|
||||
if (activeHighlight && activeHighlight !== type) {
|
||||
styles.filter = 'brightness(0.7)'
|
||||
}
|
||||
return styles
|
||||
}
|
||||
|
||||
return <Singleplayer
|
||||
{...props}
|
||||
|
|
@ -66,9 +78,22 @@ export default ({ initialProxies, updateProxies: updateProxiesProp, joinServer,
|
|||
})
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', gap: 5, alignItems: 'center' }}>
|
||||
<div
|
||||
style={{ display: 'flex', gap: 5, alignItems: 'center', ...getActiveHighlightStyles('quick-connect') }}
|
||||
className='quick-connect-row'
|
||||
onMouseEnter={() => setActiveHighlight('quick-connect')}
|
||||
onMouseLeave={() => setActiveHighlight(undefined)}
|
||||
>
|
||||
{/* todo history */}
|
||||
<Input required placeholder='Quick Connect IP (:version)' value={serverIp} onChange={({ target: { value } }) => setServerIp(value)} />
|
||||
<Input
|
||||
required
|
||||
placeholder='Quick Connect IP (:version)'
|
||||
value={serverIp}
|
||||
onChange={({ target: { value } }) => {
|
||||
setQuickConnectIp?.(value)
|
||||
setServerIp(value)
|
||||
}}
|
||||
/>
|
||||
<label style={{ fontSize: 10, display: 'flex', alignItems: 'center', gap: 5, height: '100%', marginTop: '-1px' }}>
|
||||
<input
|
||||
type='checkbox' checked={save}
|
||||
|
|
@ -108,6 +133,10 @@ export default ({ initialProxies, updateProxies: updateProxiesProp, joinServer,
|
|||
}
|
||||
props.onWorldAction?.(action, serverName)
|
||||
}}
|
||||
setListHovered={(hovered) => {
|
||||
setActiveHighlight(hovered ? 'server-list' : undefined)
|
||||
}}
|
||||
listStyle={getActiveHighlightStyles('server-list')}
|
||||
secondRowStyles={getActiveHighlightStyles('server-list')}
|
||||
/>
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { useUtilsEffect } from '@zardoy/react-util'
|
||||
import { useSnapshot } from 'valtio'
|
||||
import { ConnectOptions } from '../connect'
|
||||
import { hideCurrentModal, miscUiState, showModal } from '../globalState'
|
||||
import { activeModalStack, hideCurrentModal, miscUiState, showModal } from '../globalState'
|
||||
import supportedVersions from '../supportedVersions.mjs'
|
||||
import ServersList from './ServersList'
|
||||
import AddServerOrConnect, { BaseServerInfo } from './AddServerOrConnect'
|
||||
|
|
@ -132,29 +133,31 @@ export const updateAuthenticatedAccountData = (callback: (data: AuthenticatedAcc
|
|||
// todo move to base
|
||||
const normalizeIp = (ip: string) => ip.replace(/https?:\/\//, '').replace(/\/(:|$)/, '')
|
||||
|
||||
const Inner = () => {
|
||||
const Inner = ({ hidden }: { hidden?: boolean }) => {
|
||||
const [proxies, setProxies] = useState<readonly string[]>(localStorage['proxies'] ? JSON.parse(localStorage['proxies']) : getInitialProxies())
|
||||
const [selectedProxy, setSelectedProxy] = useState(localStorage.getItem('selectedProxy') ?? proxies?.[0] ?? '')
|
||||
const [serverEditScreen, setServerEditScreen] = useState<StoreServerItem | true | null>(null) // true for add
|
||||
const [defaultUsername, setDefaultUsername] = useState(localStorage['username'] ?? (`mcrafter${Math.floor(Math.random() * 1000)}`))
|
||||
const [authenticatedAccounts, setAuthenticatedAccounts] = useState<AuthenticatedAccount[]>(JSON.parse(localStorage['authenticatedAccounts'] || '[]'))
|
||||
const [defaultUsername, _setDefaultUsername] = useState(localStorage['username'] ?? (`mcrafter${Math.floor(Math.random() * 1000)}`))
|
||||
const [authenticatedAccounts, _setAuthenticatedAccounts] = useState<AuthenticatedAccount[]>(JSON.parse(localStorage['authenticatedAccounts'] || '[]'))
|
||||
const [quickConnectIp, setQuickConnectIp] = useState('')
|
||||
|
||||
const setAuthenticatedAccounts = (newState: typeof authenticatedAccounts) => {
|
||||
_setAuthenticatedAccounts(newState)
|
||||
localStorage.setItem('authenticatedAccounts', JSON.stringify(newState))
|
||||
}
|
||||
|
||||
const setDefaultUsername = (newState: typeof defaultUsername) => {
|
||||
_setDefaultUsername(newState)
|
||||
localStorage.setItem('username', newState)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
localStorage.setItem('authenticatedAccounts', JSON.stringify(authenticatedAccounts))
|
||||
}, [authenticatedAccounts])
|
||||
|
||||
useEffect(() => {
|
||||
localStorage.setItem('username', defaultUsername)
|
||||
}, [defaultUsername])
|
||||
|
||||
useEffect(() => {
|
||||
// TODO! do not unmount on connecting screen
|
||||
// if (proxies.length) {
|
||||
// localStorage.setItem('proxies', JSON.stringify(proxies))
|
||||
// }
|
||||
// if (selectedProxy) {
|
||||
// localStorage.setItem('selectedProxy', selectedProxy)
|
||||
// }
|
||||
if (proxies.length) {
|
||||
localStorage.setItem('proxies', JSON.stringify(proxies))
|
||||
}
|
||||
if (selectedProxy) {
|
||||
localStorage.setItem('selectedProxy', selectedProxy)
|
||||
}
|
||||
}, [proxies])
|
||||
|
||||
const [serversList, setServersList] = useState<StoreServerItem[]>(() => getInitialServersList())
|
||||
|
|
@ -195,7 +198,7 @@ const Inner = () => {
|
|||
})
|
||||
}
|
||||
}
|
||||
void update()
|
||||
void update().catch((err) => {})
|
||||
}, [serversListSorted])
|
||||
|
||||
const isEditScreenModal = useIsModalActive('editServer')
|
||||
|
|
@ -215,46 +218,46 @@ const Inner = () => {
|
|||
}
|
||||
}, [isEditScreenModal])
|
||||
|
||||
if (isEditScreenModal) {
|
||||
return <AddServerOrConnect
|
||||
defaults={{
|
||||
proxyOverride: selectedProxy,
|
||||
usernameOverride: defaultUsername,
|
||||
}}
|
||||
parseQs={!serverEditScreen}
|
||||
onBack={() => {
|
||||
hideCurrentModal()
|
||||
}}
|
||||
onConfirm={(info) => {
|
||||
if (!serverEditScreen) return
|
||||
if (serverEditScreen === true) {
|
||||
const server: StoreServerItem = { ...info, lastJoined: Date.now() } // so it appears first
|
||||
setServersList(old => [...old, server])
|
||||
} else {
|
||||
const index = serversList.indexOf(serverEditScreen)
|
||||
const { lastJoined } = serversList[index]
|
||||
serversList[index] = { ...info, lastJoined }
|
||||
setServersList([...serversList])
|
||||
}
|
||||
setServerEditScreen(null)
|
||||
}}
|
||||
accounts={authenticatedAccounts.map(a => a.username)}
|
||||
initialData={!serverEditScreen || serverEditScreen === true ? undefined : serverEditScreen}
|
||||
onQsConnect={(info) => {
|
||||
const connectOptions: ConnectOptions = {
|
||||
username: info.usernameOverride || defaultUsername,
|
||||
server: normalizeIp(info.ip),
|
||||
proxy: info.proxyOverride || selectedProxy,
|
||||
botVersion: info.versionOverride,
|
||||
ignoreQs: true,
|
||||
}
|
||||
dispatchEvent(new CustomEvent('connect', { detail: connectOptions }))
|
||||
}}
|
||||
versions={supportedVersions}
|
||||
/>
|
||||
}
|
||||
const editModalJsx = isEditScreenModal ? <AddServerOrConnect
|
||||
placeholders={{
|
||||
proxyOverride: selectedProxy,
|
||||
usernameOverride: defaultUsername,
|
||||
}}
|
||||
parseQs={!serverEditScreen}
|
||||
onBack={() => {
|
||||
hideCurrentModal()
|
||||
}}
|
||||
onConfirm={(info) => {
|
||||
if (!serverEditScreen) return
|
||||
if (serverEditScreen === true) {
|
||||
const server: StoreServerItem = { ...info, lastJoined: Date.now() } // so it appears first
|
||||
setServersList(old => [...old, server])
|
||||
} else {
|
||||
const index = serversList.indexOf(serverEditScreen)
|
||||
const { lastJoined } = serversList[index]
|
||||
serversList[index] = { ...info, lastJoined }
|
||||
setServersList([...serversList])
|
||||
}
|
||||
setServerEditScreen(null)
|
||||
}}
|
||||
accounts={authenticatedAccounts.map(a => a.username)}
|
||||
initialData={!serverEditScreen || serverEditScreen === true ? {
|
||||
ip: quickConnectIp
|
||||
} : serverEditScreen}
|
||||
onQsConnect={(info) => {
|
||||
const connectOptions: ConnectOptions = {
|
||||
username: info.usernameOverride || defaultUsername,
|
||||
server: normalizeIp(info.ip),
|
||||
proxy: info.proxyOverride || selectedProxy,
|
||||
botVersion: info.versionOverride,
|
||||
ignoreQs: true,
|
||||
}
|
||||
dispatchEvent(new CustomEvent('connect', { detail: connectOptions }))
|
||||
}}
|
||||
versions={supportedVersions}
|
||||
/> : null
|
||||
|
||||
return <ServersList
|
||||
const serversListJsx = <ServersList
|
||||
joinServer={(overrides, { shouldSave }) => {
|
||||
const indexOrIp = overrides.ip
|
||||
let ip = indexOrIp
|
||||
|
|
@ -327,10 +330,11 @@ const Inner = () => {
|
|||
}}
|
||||
username={defaultUsername}
|
||||
setUsername={setDefaultUsername}
|
||||
setQuickConnectIp={setQuickConnectIp}
|
||||
onProfileClick={async () => {
|
||||
const username = await showOptionsModal('Select authenticated account to remove', authenticatedAccounts.map(a => a.username))
|
||||
if (!username) return
|
||||
setAuthenticatedAccounts(old => old.filter(a => a.username !== username))
|
||||
setAuthenticatedAccounts(authenticatedAccounts.filter(a => a.username !== username))
|
||||
}}
|
||||
onWorldAction={(action, index) => {
|
||||
const server = serversList[index]
|
||||
|
|
@ -372,12 +376,21 @@ const Inner = () => {
|
|||
setProxies(proxies)
|
||||
setSelectedProxy(selected)
|
||||
}}
|
||||
hidden={hidden}
|
||||
/>
|
||||
return <>
|
||||
{serversListJsx}
|
||||
{editModalJsx}
|
||||
</>
|
||||
}
|
||||
|
||||
export default () => {
|
||||
const modalStack = useSnapshot(activeModalStack)
|
||||
const hasServersListModal = modalStack.some(x => x.reactType === 'serversList')
|
||||
const editServerModalActive = useIsModalActive('editServer')
|
||||
const isServersListModalActive = useIsModalActive('serversList')
|
||||
|
||||
const eitherModal = isServersListModalActive || editServerModalActive
|
||||
return eitherModal ? <Inner /> : null
|
||||
const render = eitherModal || hasServersListModal
|
||||
return render ? <Inner hidden={!isServersListModalActive} /> : null
|
||||
}
|
||||
|
|
|
|||
|
|
@ -85,9 +85,15 @@ interface Props {
|
|||
warning?: string
|
||||
warningAction?: () => void
|
||||
warningActionLabel?: string
|
||||
hidden?: boolean
|
||||
|
||||
onWorldAction (action: 'load' | 'export' | 'delete' | 'edit', worldName: string): void
|
||||
onGeneralAction (action: 'cancel' | 'create'): void
|
||||
onRowSelect? (name: string, index: number): void
|
||||
defaultSelectedRow?: number
|
||||
listStyle?: React.CSSProperties
|
||||
setListHovered?: (hovered: boolean) => void
|
||||
secondRowStyles?: React.CSSProperties
|
||||
}
|
||||
|
||||
export default ({
|
||||
|
|
@ -104,7 +110,13 @@ export default ({
|
|||
disabledProviders,
|
||||
error,
|
||||
isReadonly,
|
||||
warning, warningAction, warningActionLabel
|
||||
warning, warningAction, warningActionLabel,
|
||||
hidden,
|
||||
onRowSelect,
|
||||
defaultSelectedRow,
|
||||
listStyle,
|
||||
setListHovered,
|
||||
secondRowStyles
|
||||
}: Props) => {
|
||||
const containerRef = useRef<any>()
|
||||
const firstButton = useRef<HTMLButtonElement>(null)
|
||||
|
|
@ -122,13 +134,17 @@ export default ({
|
|||
})
|
||||
|
||||
const [search, setSearch] = useState('')
|
||||
const [focusedWorld, setFocusedWorld] = useState('')
|
||||
const [focusedWorld, setFocusedWorld] = useState(defaultSelectedRow ? worldData?.[defaultSelectedRow]?.name ?? '' : '')
|
||||
|
||||
useEffect(() => {
|
||||
setFocusedWorld('')
|
||||
}, [activeProvider])
|
||||
|
||||
return <div ref={containerRef}>
|
||||
const onRowSelectHandler = (name: string, index: number) => {
|
||||
onRowSelect?.(name, index)
|
||||
setFocusedWorld(name)
|
||||
}
|
||||
return <div ref={containerRef} hidden={hidden}>
|
||||
<div className="dirt-bg" />
|
||||
<div className={classNames('fullscreen', styles.root)}>
|
||||
<span className={classNames('screen-title', styles.title)}>{serversLayout ? 'Join Java Servers' : 'Select Saved World'}</span>
|
||||
|
|
@ -141,9 +157,13 @@ export default ({
|
|||
setActiveProvider?.(tab as any)
|
||||
}} fullSize
|
||||
/>
|
||||
<div style={{
|
||||
marginTop: 3,
|
||||
}}
|
||||
<div
|
||||
style={{
|
||||
marginTop: 3,
|
||||
...listStyle
|
||||
}}
|
||||
onMouseEnter={() => setListHovered?.(true)}
|
||||
onMouseLeave={() => setListHovered?.(false)}
|
||||
>
|
||||
{
|
||||
providerActions && <div style={{
|
||||
|
|
@ -159,9 +179,9 @@ export default ({
|
|||
}
|
||||
{
|
||||
worldData
|
||||
? worldData.filter(data => data.title.toLowerCase().includes(search.toLowerCase())).map(({ name, size, detail, ...rest }) => (
|
||||
? worldData.filter(data => data.title.toLowerCase().includes(search.toLowerCase())).map(({ name, size, detail, ...rest }, index) => (
|
||||
<World
|
||||
{...rest} size={size} name={name} onFocus={setFocusedWorld} isFocused={focusedWorld === name} key={name} onInteraction={(interaction) => {
|
||||
{...rest} size={size} name={name} onFocus={row => onRowSelectHandler(row, index)} isFocused={focusedWorld === name} key={name} onInteraction={(interaction) => {
|
||||
if (interaction === 'enter') onWorldAction('load', name)
|
||||
else if (interaction === 'space') firstButton.current?.focus()
|
||||
}}
|
||||
|
|
@ -192,7 +212,7 @@ export default ({
|
|||
<Button rootRef={firstButton} disabled={!focusedWorld} onClick={() => onWorldAction('load', focusedWorld)}>Load World</Button>
|
||||
<Button onClick={() => onGeneralAction('create')} disabled={isReadonly}>Create New World</Button>
|
||||
</div>}
|
||||
<div>
|
||||
<div style={{ ...secondRowStyles }}>
|
||||
{serversLayout ? <Button style={{ width: 100 }} disabled={!focusedWorld} onClick={() => onWorldAction('edit', focusedWorld)}>Edit</Button> : <Button style={{ width: 100 }} disabled={!focusedWorld} onClick={() => onWorldAction('export', focusedWorld)}>Export</Button>}
|
||||
<Button style={{ width: 100 }} disabled={!focusedWorld} onClick={() => onWorldAction('delete', focusedWorld)}>Delete</Button>
|
||||
{serversLayout ?
|
||||
|
|
|
|||
14
src/react/parseKeybindingName.test.ts
Normal file
14
src/react/parseKeybindingName.test.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
import { test, expect } from 'vitest'
|
||||
import { parseBindingName } from './parseKeybindingName'
|
||||
|
||||
test('display keybinding correctly', async () => {
|
||||
expect(await parseBindingName('unknown')).toMatchInlineSnapshot('"unknown"')
|
||||
expect(await parseBindingName('KeyT')).toMatchInlineSnapshot('"T"')
|
||||
expect(await parseBindingName('Digit1')).toMatchInlineSnapshot('"1"')
|
||||
expect(await parseBindingName('Numpad1')).toMatchInlineSnapshot('"Numpad 1"')
|
||||
expect(await parseBindingName('MetaLeft')).toMatchInlineSnapshot('"Left Meta"')
|
||||
expect(await parseBindingName('Space')).toMatchInlineSnapshot('"Space"')
|
||||
expect(await parseBindingName('Escape')).toMatchInlineSnapshot('"Escape"')
|
||||
expect(await parseBindingName('F11')).toMatchInlineSnapshot('"F11"')
|
||||
expect(await parseBindingName('Mouse 55')).toMatchInlineSnapshot('"Mouse 55"')
|
||||
})
|
||||
17
src/react/parseKeybindingName.ts
Normal file
17
src/react/parseKeybindingName.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
export async function parseBindingName (binding: string) {
|
||||
if (!binding) return ''
|
||||
|
||||
const { keyboard } = (typeof navigator === 'undefined' ? undefined : navigator) ?? {}
|
||||
const layoutMap = await keyboard?.getLayoutMap?.() ?? new Map<string, string>()
|
||||
|
||||
const mapKey = (key: string) => layoutMap.get(key) || key
|
||||
|
||||
const cut = binding.replaceAll(/(Digit|Key)/g, '')
|
||||
const parts = cut.includes('+') ? cut.split('+') : [cut]
|
||||
|
||||
for (let i = 0; i < parts.length; i++) {
|
||||
parts[i] = mapKey(parts[i]).split(/(?<=[a-z])(?=\d)/).join(' ').split(/(?=[A-Z])/).reverse().join(' ')
|
||||
}
|
||||
|
||||
return parts.join(' + ')
|
||||
}
|
||||
|
|
@ -295,14 +295,16 @@ export const onAppLoad = () => {
|
|||
// todo also handle resourcePack
|
||||
const handleResourcePackRequest = async (packet) => {
|
||||
if (options.serverResourcePacks === 'never') return
|
||||
const promptMessage = ('promptMessage' in packet && packet.promptMessage) ? JSON.stringify(packet.promptMessage) : 'Do you want to use server resource pack?'
|
||||
const promptMessagePacket = ('promptMessage' in packet && packet.promptMessage) ? packet.promptMessage : undefined
|
||||
const promptMessageText = promptMessagePacket ? '' : 'Do you want to use server resource pack?'
|
||||
// TODO!
|
||||
const hash = 'hash' in packet ? packet.hash : '-'
|
||||
const forced = 'forced' in packet ? packet.forced : false
|
||||
const choice = options.serverResourcePacks === 'always'
|
||||
? true
|
||||
: await showOptionsModal(promptMessage, ['Download & Install (recommended)', 'Pretend Installed (not recommended)'], {
|
||||
cancel: !forced
|
||||
: await showOptionsModal(promptMessageText, ['Download & Install (recommended)', 'Pretend Installed (not recommended)'], {
|
||||
cancel: !forced,
|
||||
minecraftJsonMessage: promptMessagePacket,
|
||||
})
|
||||
if (!choice) return
|
||||
bot.acceptResourcePack()
|
||||
|
|
|
|||
|
|
@ -58,6 +58,13 @@ export const watchOptionsAfterViewerInit = () => {
|
|||
watchValue(options, o => {
|
||||
viewer.world.displayStats = o.renderDebug === 'advanced'
|
||||
})
|
||||
watchValue(options, (o, isChanged) => {
|
||||
viewer.world.mesherConfig.clipWorldBelowY = o.clipWorldBelowY
|
||||
viewer.world.mesherConfig.disableSignsMapsSupport = o.disableSignsMapsSupport
|
||||
if (isChanged) {
|
||||
(viewer.world as WorldRendererThree).rerenderAllChunks()
|
||||
}
|
||||
})
|
||||
|
||||
viewer.world.mesherConfig.smoothLighting = options.smoothLighting
|
||||
subscribeKey(options, 'smoothLighting', () => {
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ export default defineConfig({
|
|||
include: [
|
||||
'../../src/botUtils.test.ts',
|
||||
'../../src/markdownToFormattedText.test.ts',
|
||||
'../../src/react/parseKeybindingName.test.ts',
|
||||
'lib/mesher/test/tests.test.ts',
|
||||
'sign-renderer/tests.test.ts'
|
||||
],
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue