feat: brand new Servers List UI /w auto login feature! (#110)
This commit is contained in:
parent
cfb9b17fd4
commit
826c66b9ec
34 changed files with 976 additions and 719 deletions
18
config.json
18
config.json
|
|
@ -2,6 +2,20 @@
|
|||
"version": 1,
|
||||
"defaultHost": "<from-proxy>",
|
||||
"defaultProxy": "proxy.mcraft.fun",
|
||||
"defaultVersion": "1.18.2",
|
||||
"mapsProvider": "https://maps.mcraft.fun/"
|
||||
"mapsProvider": "https://maps.mcraft.fun/",
|
||||
"promoteServers": [
|
||||
{
|
||||
"ip": "kaboom.pw",
|
||||
"description": "Chaos and destruction server. Free for everyone."
|
||||
},
|
||||
{
|
||||
"ip": "go.mineberry.org",
|
||||
"version": "1.18.2",
|
||||
"description": "One of the best servers here. Join now!"
|
||||
},
|
||||
{
|
||||
"ip": "play.minemalia.com",
|
||||
"description": "Only login with existing accounts."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -53,20 +53,19 @@ it('Loads & renders singleplayer', () => {
|
|||
})
|
||||
|
||||
it.only('Joins to server', () => {
|
||||
// visit('/?version=1.16.1')
|
||||
visit('/?ip=localhost&version=1.16.1')
|
||||
window.localStorage.version = ''
|
||||
visit()
|
||||
// todo replace with data-test
|
||||
cy.get('[data-test-id="connect-screen-button"]', { includeShadowDom: true }).click()
|
||||
cy.get('input#serverip', { includeShadowDom: true }).clear().focus().type('localhost')
|
||||
cy.get('input#botversion', { includeShadowDom: true }).clear().focus().type('1.16.1') // todo needs to fix autoversion
|
||||
cy.get('[data-test-id="connect-to-server"]', { includeShadowDom: true }).click()
|
||||
// cy.get('[data-test-id="servers-screen-button"]').click()
|
||||
// cy.get('[data-test-id="server-ip"]').clear().focus().type('localhost')
|
||||
// cy.get('[data-test-id="version"]').clear().focus().type('1.16.1') // todo needs to fix autoversion
|
||||
cy.get('[data-test-id="connect-qs"]').click()
|
||||
testWorldLoad()
|
||||
})
|
||||
|
||||
it('Loads & renders zip world', () => {
|
||||
cleanVisit()
|
||||
cy.get('[data-test-id="select-file-folder"]', { includeShadowDom: true }).click({ shiftKey: true })
|
||||
cy.get('[data-test-id="select-file-folder"]').click({ shiftKey: true })
|
||||
cy.get('input[type="file"]').selectFile('cypress/superflat.zip', { force: true })
|
||||
testWorldLoad()
|
||||
})
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@
|
|||
"dependencies": {
|
||||
"@dimaka/interface": "0.0.3-alpha.0",
|
||||
"@floating-ui/react": "^0.26.1",
|
||||
"@mui/base": "5.0.0-beta.40",
|
||||
"@nxg-org/mineflayer-auto-jump": "^0.7.7",
|
||||
"@nxg-org/mineflayer-tracker": "^1.2.1",
|
||||
"@react-oauth/google": "^0.12.1",
|
||||
|
|
@ -60,7 +61,6 @@
|
|||
"google-drive-browserfs": "github:zardoy/browserfs#google-drive",
|
||||
"iconify-icon": "^1.0.8",
|
||||
"jszip": "^3.10.1",
|
||||
"lit": "^2.8.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"minecraft-assets": "^1.12.2",
|
||||
"minecraft-data": "3.62.0",
|
||||
|
|
|
|||
94
pnpm-lock.yaml
generated
94
pnpm-lock.yaml
generated
|
|
@ -26,6 +26,9 @@ importers:
|
|||
'@floating-ui/react':
|
||||
specifier: ^0.26.1
|
||||
version: 0.26.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
'@mui/base':
|
||||
specifier: 5.0.0-beta.40
|
||||
version: 5.0.0-beta.40(@types/react@18.2.20)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
'@nxg-org/mineflayer-auto-jump':
|
||||
specifier: ^0.7.7
|
||||
version: 0.7.7
|
||||
|
|
@ -104,9 +107,6 @@ importers:
|
|||
jszip:
|
||||
specifier: ^3.10.1
|
||||
version: 3.10.1
|
||||
lit:
|
||||
specifier: ^2.8.0
|
||||
version: 2.8.0
|
||||
lodash-es:
|
||||
specifier: ^4.17.21
|
||||
version: 4.17.21
|
||||
|
|
@ -1110,6 +1110,10 @@ packages:
|
|||
resolution: {integrity: sha512-ee7jVNlWN09+KftVOu9n7S8gQzD/Z6hN/I8VBRXW4P1+Xe7kJGXMwu8vds4aGIMHZnNbdpSWCfZZtinytpcAvA==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@babel/runtime@7.24.5':
|
||||
resolution: {integrity: sha512-Nms86NXrsaeU9vbBJKni6gXiEXZ4CVpYVzEjDH9Sb8vmZ3UljyA1GSOJl/6LGPO8EHLuSF9H+IxNXHPX8QHJ4g==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@babel/template@7.22.5':
|
||||
resolution: {integrity: sha512-X7yV7eiwAxdj9k94NEylvbVHLiVG1nvzCV2EAowhxLTwODV1jl9UzZ48leOC0sH7OnuHrIkllaBgneUykIcZaw==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
|
@ -1661,6 +1665,12 @@ packages:
|
|||
react: ^18.2.0
|
||||
react-dom: '>=16.8.0'
|
||||
|
||||
'@floating-ui/react-dom@2.0.9':
|
||||
resolution: {integrity: sha512-q0umO0+LQK4+p6aGyvzASqKbKOJcAHJ7ycE9CuUvfx3s9zTHWmGJTPOIlM/hmSBfUfg/XfY5YhLBLR/LHwShQQ==}
|
||||
peerDependencies:
|
||||
react: ^18.2.0
|
||||
react-dom: '>=16.8.0'
|
||||
|
||||
'@floating-ui/react@0.26.1':
|
||||
resolution: {integrity: sha512-5gyJIJ2tZOPMgmZ/vEcVhdmQiy75b7LPO71sYIiDsxGcZ4hxLuygQWCuT0YXHqppt//Eese+L6t5KnX/gZ3tVA==}
|
||||
peerDependencies:
|
||||
|
|
@ -1931,6 +1941,35 @@ packages:
|
|||
resolution: {integrity: sha512-h9u4u/jiIRKbq25PM+zymTyW6bhTzELvOoUd+AvYriWOAKpLGnIamaET3pnHYoI5iYphAHBI4ayx0MehR+VVPQ==}
|
||||
engines: {node: '>= 10'}
|
||||
|
||||
'@mui/base@5.0.0-beta.40':
|
||||
resolution: {integrity: sha512-I/lGHztkCzvwlXpjD2+SNmvNQvB4227xBXhISPjEaJUXGImOQ9f3D2Yj/T3KasSI/h0MLWy74X0J6clhPmsRbQ==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
peerDependencies:
|
||||
'@types/react': ^17.0.0 || ^18.0.0
|
||||
react: ^18.2.0
|
||||
react-dom: ^17.0.0 || ^18.0.0
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
|
||||
'@mui/types@7.2.14':
|
||||
resolution: {integrity: sha512-MZsBZ4q4HfzBsywtXgM1Ksj6HDThtiwmOKUXH1pKYISI9gAVXCNHNpo7TlGoGrBaYWZTdNoirIN7JsQcQUjmQQ==}
|
||||
peerDependencies:
|
||||
'@types/react': ^17.0.0 || ^18.0.0
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
|
||||
'@mui/utils@5.15.14':
|
||||
resolution: {integrity: sha512-0lF/7Hh/ezDv5X7Pry6enMsbYyGKjADzvHyo3Qrc/SSlTsQ1VkbDMbH0m2t3OR5iIVLwMoxwM7yGd+6FCMtTFA==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
peerDependencies:
|
||||
'@types/react': ^17.0.0 || ^18.0.0
|
||||
react: ^18.2.0
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
|
||||
'@ndelangen/get-tarball@3.0.9':
|
||||
resolution: {integrity: sha512-9JKTEik4vq+yGosHYhZ1tiH/3WpUS0Nh0kej4Agndhox8pAdWhEx5knFVRcb/ya9knCRCs1rPxNrSXTDdfVqpA==}
|
||||
|
||||
|
|
@ -2774,6 +2813,9 @@ packages:
|
|||
'@types/pretty-hrtime@1.0.1':
|
||||
resolution: {integrity: sha512-VjID5MJb1eGKthz2qUerWT8+R4b9N+CHvGCzg9fn4kWZgaF9AhdYikQio3R7wV8YY1NsQKPaCwKz1Yff+aHNUQ==}
|
||||
|
||||
'@types/prop-types@15.7.12':
|
||||
resolution: {integrity: sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==}
|
||||
|
||||
'@types/prop-types@15.7.5':
|
||||
resolution: {integrity: sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==}
|
||||
|
||||
|
|
@ -3664,6 +3706,10 @@ packages:
|
|||
resolution: {integrity: sha512-6/bPho624p3S2pMyvP5kKBPXnI3ufHLObBFCfgx+LkeR5lg2XYy2hqZqUf45ypD8COn2bhgGJSUE+l5dhNBieA==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
clsx@2.1.1:
|
||||
resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
color-convert@0.5.3:
|
||||
resolution: {integrity: sha512-RwBeO/B/vZR3dfKL1ye/vx8MHZ40ugzpyfeVG5GsiuGnrlMWe2o8wxBbLCpw9CsxV+wHuzYlCiWnybrIA0ling==}
|
||||
|
||||
|
|
@ -9458,6 +9504,10 @@ snapshots:
|
|||
dependencies:
|
||||
regenerator-runtime: 0.14.0
|
||||
|
||||
'@babel/runtime@7.24.5':
|
||||
dependencies:
|
||||
regenerator-runtime: 0.14.0
|
||||
|
||||
'@babel/template@7.22.5':
|
||||
dependencies:
|
||||
'@babel/code-frame': 7.22.13
|
||||
|
|
@ -9864,6 +9914,12 @@ snapshots:
|
|||
react: 18.2.0
|
||||
react-dom: 18.2.0(react@18.2.0)
|
||||
|
||||
'@floating-ui/react-dom@2.0.9(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
|
||||
dependencies:
|
||||
'@floating-ui/dom': 1.5.3
|
||||
react: 18.2.0
|
||||
react-dom: 18.2.0(react@18.2.0)
|
||||
|
||||
'@floating-ui/react@0.26.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
|
||||
dependencies:
|
||||
'@floating-ui/react-dom': 2.0.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
|
|
@ -10279,6 +10335,34 @@ snapshots:
|
|||
|
||||
'@msgpack/msgpack@2.8.0': {}
|
||||
|
||||
'@mui/base@5.0.0-beta.40(@types/react@18.2.20)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
|
||||
dependencies:
|
||||
'@babel/runtime': 7.24.5
|
||||
'@floating-ui/react-dom': 2.0.9(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
'@mui/types': 7.2.14(@types/react@18.2.20)
|
||||
'@mui/utils': 5.15.14(@types/react@18.2.20)(react@18.2.0)
|
||||
'@popperjs/core': 2.11.8
|
||||
clsx: 2.1.1
|
||||
prop-types: 15.8.1
|
||||
react: 18.2.0
|
||||
react-dom: 18.2.0(react@18.2.0)
|
||||
optionalDependencies:
|
||||
'@types/react': 18.2.20
|
||||
|
||||
'@mui/types@7.2.14(@types/react@18.2.20)':
|
||||
optionalDependencies:
|
||||
'@types/react': 18.2.20
|
||||
|
||||
'@mui/utils@5.15.14(@types/react@18.2.20)(react@18.2.0)':
|
||||
dependencies:
|
||||
'@babel/runtime': 7.24.5
|
||||
'@types/prop-types': 15.7.12
|
||||
prop-types: 15.8.1
|
||||
react: 18.2.0
|
||||
react-is: 18.2.0
|
||||
optionalDependencies:
|
||||
'@types/react': 18.2.20
|
||||
|
||||
'@ndelangen/get-tarball@3.0.9':
|
||||
dependencies:
|
||||
gunzip-maybe: 1.4.2
|
||||
|
|
@ -11577,6 +11661,8 @@ snapshots:
|
|||
|
||||
'@types/pretty-hrtime@1.0.1': {}
|
||||
|
||||
'@types/prop-types@15.7.12': {}
|
||||
|
||||
'@types/prop-types@15.7.5': {}
|
||||
|
||||
'@types/qs@6.9.8': {}
|
||||
|
|
@ -12705,6 +12791,8 @@ snapshots:
|
|||
|
||||
clsx@1.1.1: {}
|
||||
|
||||
clsx@2.1.1: {}
|
||||
|
||||
color-convert@0.5.3: {}
|
||||
|
||||
color-convert@1.9.3:
|
||||
|
|
|
|||
|
|
@ -19,6 +19,10 @@ export function openURL (url, newTab = true) {
|
|||
}
|
||||
}
|
||||
|
||||
export const isMobile = () => {
|
||||
return window.matchMedia('(pointer: coarse)').matches || navigator.userAgent.includes('Mobile')
|
||||
}
|
||||
|
||||
export function chunkPos (pos: { x: number, z: number }) {
|
||||
const x = Math.floor(pos.x / 16)
|
||||
const z = Math.floor(pos.z / 16)
|
||||
|
|
|
|||
15
src/connect.ts
Normal file
15
src/connect.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
export type ConnectOptions = {
|
||||
server?: string;
|
||||
singleplayer?: any;
|
||||
username: string;
|
||||
password?: any;
|
||||
proxy?: any;
|
||||
botVersion?: any;
|
||||
serverOverrides?;
|
||||
serverOverridesFlat?;
|
||||
peerId?: string;
|
||||
ignoreQs?: boolean;
|
||||
onSuccessfulPlay?: () => void
|
||||
autoLoginPassword?: string
|
||||
serverIndex?: string
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import { isMobile } from 'prismarine-viewer/viewer/lib/simpleUtils'
|
||||
import { WorldRendererThree } from 'prismarine-viewer/viewer/lib/worldrendererThree'
|
||||
import { isMobile } from './menus/components/common'
|
||||
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
if (sessionStorage.lastReload) {
|
||||
|
|
|
|||
|
|
@ -20,9 +20,6 @@ const inner = async () => {
|
|||
if (resourcePackState.resourcePackInstalled) {
|
||||
if (!confirm(`You are going to install a new resource pack, which will REPLACE the current one: ${await getResourcePackName()} Continue?`)) return
|
||||
}
|
||||
} else {
|
||||
const menu = document.getElementById('play-screen')
|
||||
menu.style = 'display: none;'
|
||||
}
|
||||
const name = mapUrl.slice(mapUrl.lastIndexOf('/') + 1).slice(-25)
|
||||
const downloadThing = texturepack ? 'texturepack' : 'world'
|
||||
|
|
@ -78,7 +75,7 @@ export default async () => {
|
|||
try {
|
||||
return await inner()
|
||||
} catch (err) {
|
||||
setLoadingScreenStatus(`Failed to download. Either refresh page or remove mapUrl param from URL. Reason: ${err.message}`)
|
||||
setLoadingScreenStatus(`Failed to download. Either refresh page or remove map param from URL. Reason: ${err.message}`)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -56,10 +56,10 @@ const showModalInner = (modal: Modal) => {
|
|||
return true
|
||||
}
|
||||
|
||||
export const showModal = (elem: (HTMLElement & Record<string, any>) | { reactType: string }) => {
|
||||
export const showModal = (elem: /* (HTMLElement & Record<string, any>) | */{ reactType: string }) => {
|
||||
const resolved = elem instanceof HTMLElement ? { elem: ref(elem) } : elem
|
||||
const curModal = activeModalStack.at(-1)
|
||||
if (elem === curModal?.elem || (elem.reactType && elem.reactType === curModal?.reactType) || !showModalInner(resolved)) return
|
||||
if (/* elem === curModal?.elem || */(elem.reactType && elem.reactType === curModal?.reactType) || !showModalInner(resolved)) return
|
||||
if (curModal) defaultModalActions.hide(curModal)
|
||||
activeModalStack.push(resolved)
|
||||
}
|
||||
|
|
@ -118,11 +118,12 @@ export const showContextmenu = (items: ContextMenuItem[], { clientX, clientY })
|
|||
// ---
|
||||
|
||||
export type AppConfig = {
|
||||
defaultHost?: string
|
||||
defaultHostSave?: string
|
||||
// defaultHost?: string
|
||||
// defaultHostSave?: string
|
||||
defaultProxy?: string
|
||||
defaultProxySave?: string
|
||||
defaultVersion?: string
|
||||
// defaultProxySave?: string
|
||||
// defaultVersion?: string
|
||||
promoteServers?: Array<{ip, description, version?}>
|
||||
mapsProvider?: string
|
||||
}
|
||||
|
||||
|
|
@ -130,12 +131,14 @@ export const miscUiState = proxy({
|
|||
currentDisplayQr: null as string | null,
|
||||
currentTouch: null as boolean | null,
|
||||
serverIp: null as string | null,
|
||||
username: '',
|
||||
hasErrors: false,
|
||||
singleplayer: false,
|
||||
flyingSquid: false,
|
||||
wanOpened: false,
|
||||
/** wether game hud is shown (in playing state) */
|
||||
gameLoaded: false,
|
||||
loadedServerIndex: '',
|
||||
/** currently trying to load or loaded mc version, after all data is loaded */
|
||||
loadedDataVersion: null as string | null,
|
||||
appLoaded: false,
|
||||
|
|
|
|||
10
src/globals.d.ts
vendored
10
src/globals.d.ts
vendored
|
|
@ -27,7 +27,6 @@ declare const customEvents: import('typed-emitter').default<{
|
|||
declare const beforeRenderFrame: Array<() => void>
|
||||
|
||||
declare interface Document {
|
||||
getElementById (id): any
|
||||
exitPointerLock?(): void
|
||||
}
|
||||
|
||||
|
|
@ -37,14 +36,7 @@ declare namespace JSX {
|
|||
}
|
||||
}
|
||||
|
||||
declare interface DocumentFragment {
|
||||
getElementById (id): HTMLElement & Record<string, any>
|
||||
querySelector (id): HTMLElement & Record<string, any>
|
||||
}
|
||||
|
||||
declare interface Window extends Record<string, any> {
|
||||
|
||||
}
|
||||
declare interface Window extends Record<string, any> {}
|
||||
|
||||
type StringKeys<T extends object> = Extract<keyof T, string>
|
||||
|
||||
|
|
|
|||
41
src/index.ts
41
src/index.ts
|
|
@ -10,9 +10,6 @@ import initCollisionShapes from './getCollisionShapes'
|
|||
import { itemsAtlases, onGameLoad } from './inventoryWindows'
|
||||
import { supportedVersions } from 'minecraft-protocol'
|
||||
|
||||
import './menus/components/button'
|
||||
import './menus/components/edit_box'
|
||||
import './menus/play_screen'
|
||||
import 'core-js/features/array/at'
|
||||
import 'core-js/features/promise/with-resolvers'
|
||||
|
||||
|
|
@ -90,6 +87,7 @@ import { saveToBrowserMemory } from './react/PauseScreen'
|
|||
import { ViewerWrapper } from 'prismarine-viewer/viewer/lib/viewerWrapper'
|
||||
import './devReload'
|
||||
import './water'
|
||||
import { ConnectOptions } from './connect'
|
||||
|
||||
window.debug = debug
|
||||
window.THREE = THREE
|
||||
|
|
@ -240,13 +238,10 @@ const cleanConnectIp = (host: string | undefined, defaultPort: string | undefine
|
|||
}
|
||||
}
|
||||
|
||||
async function connect (connectOptions: {
|
||||
server?: string; singleplayer?: any; username: string; password?: any; proxy?: any; botVersion?: any; serverOverrides?; serverOverridesFlat?; peerId?: string; ignoreQs?: boolean
|
||||
}) {
|
||||
async function connect (connectOptions: ConnectOptions) {
|
||||
if (miscUiState.gameLoaded) return
|
||||
miscUiState.hasErrors = false
|
||||
lastConnectOptions.value = connectOptions
|
||||
document.getElementById('play-screen').style = 'display: none;'
|
||||
removePanorama()
|
||||
|
||||
const { singleplayer } = connectOptions
|
||||
|
|
@ -331,7 +326,7 @@ async function connect (connectOptions: {
|
|||
})
|
||||
|
||||
if (proxy) {
|
||||
console.log(`using proxy ${proxy.host}${proxy.port && `:${proxy.port}`}`)
|
||||
console.log(`using proxy ${proxy.host}:${proxy.port || location.port}`)
|
||||
|
||||
net['setProxy']({ hostname: proxy.host, port: proxy.port })
|
||||
}
|
||||
|
|
@ -528,12 +523,6 @@ async function connect (connectOptions: {
|
|||
bot.once('login', () => {
|
||||
worldInteractions.initBot()
|
||||
|
||||
// server is ok, add it to the history
|
||||
if (!connectOptions.server) return
|
||||
const serverHistory: string[] = JSON.parse(localStorage.getItem('serverHistory') || '[]')
|
||||
serverHistory.unshift(connectOptions.server)
|
||||
localStorage.setItem('serverHistory', JSON.stringify([...new Set(serverHistory)]))
|
||||
|
||||
setLoadingScreenStatus('Loading world')
|
||||
})
|
||||
|
||||
|
|
@ -548,10 +537,16 @@ async function connect (connectOptions: {
|
|||
window.pathfinder = pathfinder
|
||||
|
||||
miscUiState.gameLoaded = true
|
||||
miscUiState.loadedServerIndex = connectOptions.serverIndex ?? ''
|
||||
customEvents.emit('gameLoaded')
|
||||
if (p2pConnectTimeout) clearTimeout(p2pConnectTimeout)
|
||||
|
||||
setLoadingScreenStatus('Placing blocks (starting viewer)')
|
||||
localStorage.lastConnectOptions = JSON.stringify(connectOptions)
|
||||
connectOptions.onSuccessfulPlay?.()
|
||||
if (connectOptions.autoLoginPassword) {
|
||||
bot.chat(`/login ${connectOptions.autoLoginPassword}`)
|
||||
}
|
||||
|
||||
console.log('bot spawned - starting viewer')
|
||||
|
||||
|
|
@ -726,6 +721,7 @@ async function connect (connectOptions: {
|
|||
|
||||
console.log('Done!')
|
||||
|
||||
// todo
|
||||
onGameLoad(async () => {
|
||||
if (!viewer.world.downloadedBlockStatesData && !viewer.world.customBlockStatesData) {
|
||||
await new Promise<void>(resolve => {
|
||||
|
|
@ -733,6 +729,7 @@ async function connect (connectOptions: {
|
|||
})
|
||||
}
|
||||
miscUiState.serverIp = server.host as string | null
|
||||
miscUiState.username = username
|
||||
})
|
||||
|
||||
if (appStatusState.isError) return
|
||||
|
|
@ -839,12 +836,26 @@ void window.fetch('config.json').then(async res => res.json()).then(c => c, (err
|
|||
miscUiState.appConfig = config
|
||||
})
|
||||
|
||||
// qs open actions
|
||||
downloadAndOpenFile().then((downloadAction) => {
|
||||
if (downloadAction) return
|
||||
const qs = new URLSearchParams(window.location.search)
|
||||
if (qs.get('reconnect') && process.env.NODE_ENV === 'development') {
|
||||
const ip = qs.get('ip')
|
||||
const lastConnect = JSON.parse(localStorage.lastConnectOptions ?? {})
|
||||
void connect({
|
||||
...lastConnect, // todo mixing is not good idea
|
||||
ip: ip || undefined
|
||||
})
|
||||
return
|
||||
}
|
||||
if (qs.get('ip') || qs.get('proxy')) {
|
||||
// show server editor for connect or save
|
||||
showModal({ reactType: 'editServer' })
|
||||
}
|
||||
|
||||
void Promise.resolve().then(() => {
|
||||
// try to connect to peer
|
||||
const qs = new URLSearchParams(window.location.search)
|
||||
const peerId = qs.get('connectPeer')
|
||||
const version = qs.get('peerVersion')
|
||||
if (peerId) {
|
||||
|
|
|
|||
|
|
@ -1,136 +0,0 @@
|
|||
//@ts-check
|
||||
import { LitElement, html, css, unsafeCSS } from 'lit'
|
||||
import widgetsGui from 'minecraft-assets/minecraft-assets/data/1.17.1/gui/widgets.png'
|
||||
import { playSound, loadSound } from '../../basicSounds'
|
||||
|
||||
class Button extends LitElement {
|
||||
static get styles () {
|
||||
return css`
|
||||
.button {
|
||||
--txrV: 66px;
|
||||
position: relative;
|
||||
width: 200px;
|
||||
height: 20px;
|
||||
font-family: minecraft, mojangles, monospace;
|
||||
font-size: 10px;
|
||||
color: white;
|
||||
text-shadow: 1px 1px #222;
|
||||
border: none;
|
||||
z-index: 1;
|
||||
outline: none;
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.button:hover,
|
||||
.button:focus-visible {
|
||||
--txrV: 86px;
|
||||
}
|
||||
|
||||
.button:disabled {
|
||||
--txrV: 46px;
|
||||
color: #A0A0A0;
|
||||
text-shadow: 1px 1px #111;
|
||||
}
|
||||
|
||||
.button::after {
|
||||
content: '';
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: calc(50% + 1px);
|
||||
height: 20px;
|
||||
background: url('${unsafeCSS(widgetsGui)}');
|
||||
background-size: 256px;
|
||||
background-position-y: calc(var(--txrV) * -1);
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.button::before {
|
||||
content: '';
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 50%;
|
||||
width: 50%;
|
||||
height: 20px;
|
||||
background: url('${unsafeCSS(widgetsGui)}');
|
||||
background-size: 256px;
|
||||
background-position-x: calc(-200px + 100%);
|
||||
background-position-y: calc(var(--txrV) * -1);
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.icon {
|
||||
position: absolute;
|
||||
top: 3px;
|
||||
left: 3px;
|
||||
font-size: 14px;
|
||||
}
|
||||
`
|
||||
}
|
||||
|
||||
static get properties () {
|
||||
return {
|
||||
label: {
|
||||
type: String,
|
||||
attribute: 'pmui-label'
|
||||
},
|
||||
width: {
|
||||
type: String,
|
||||
attribute: 'pmui-width'
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
},
|
||||
onPress: {
|
||||
type: Function,
|
||||
attribute: 'pmui-click'
|
||||
},
|
||||
icon: {
|
||||
type: Function,
|
||||
attribute: 'pmui-icon'
|
||||
},
|
||||
testId: {
|
||||
type: String,
|
||||
attribute: 'pmui-test-id'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
constructor () {
|
||||
super()
|
||||
this.label = ''
|
||||
this.icon = undefined
|
||||
this.testId = undefined
|
||||
this.disabled = false
|
||||
this.width = '200px'
|
||||
this.onPress = () => { }
|
||||
}
|
||||
|
||||
render () {
|
||||
return html`
|
||||
<button
|
||||
class="button"
|
||||
?disabled=${this.disabled}
|
||||
@click=${this.onBtnClick}
|
||||
style="width: ${this.width};"
|
||||
data-test-id=${this.testId}
|
||||
>
|
||||
<!-- todo self host icons -->
|
||||
${this.icon ? html`<iconify-icon class="icon" icon="${this.icon}"></iconify-icon>` : ''}
|
||||
${this.label}
|
||||
</button>
|
||||
`
|
||||
}
|
||||
|
||||
onBtnClick (e) {
|
||||
playSound('button_click.mp3')
|
||||
this.dispatchEvent(new window.CustomEvent('pmui-click', { detail: e }))
|
||||
}
|
||||
}
|
||||
|
||||
loadSound('button_click.mp3')
|
||||
window.customElements.define('pmui-button', Button)
|
||||
|
|
@ -1,60 +0,0 @@
|
|||
import { css } from 'lit'
|
||||
|
||||
const commonCss = css`
|
||||
.bg {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
background: rgba(0, 0, 0, 0.75);
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.title {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 50%;
|
||||
transform: translate(-50%);
|
||||
font-size: 10px;
|
||||
color: white;
|
||||
text-align: center;
|
||||
text-shadow: 1px 1px #222;
|
||||
}
|
||||
|
||||
.text {
|
||||
color: white;
|
||||
font-size: 10px;
|
||||
text-shadow: 1px 1px #222;
|
||||
}
|
||||
`
|
||||
|
||||
/** @returns {boolean} */
|
||||
function isMobile () {
|
||||
return window.matchMedia('(pointer: coarse)').matches || navigator.userAgent.includes('Mobile')
|
||||
}
|
||||
|
||||
// todo there are better workarounds and proper way to detect notch
|
||||
/** @returns {boolean} */
|
||||
function isProbablyIphone () {
|
||||
if (!isMobile()) return false
|
||||
const smallest = window.innerWidth < window.innerHeight ? window.innerWidth : window.innerHeight
|
||||
return smallest < 600
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} url
|
||||
*/
|
||||
function openURL (url, newTab = true) {
|
||||
if (newTab) {
|
||||
window.open(url, '_blank', 'noopener,noreferrer')
|
||||
} else {
|
||||
window.open(url, '_self')
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
isProbablyIphone,
|
||||
commonCss,
|
||||
isMobile,
|
||||
openURL,
|
||||
}
|
||||
|
|
@ -1,161 +0,0 @@
|
|||
const { LitElement, html, css } = require('lit')
|
||||
const { ifDefined } = require('lit/directives/if-defined.js')
|
||||
|
||||
class EditBox extends LitElement {
|
||||
static get styles () {
|
||||
return css`
|
||||
.edit-container {
|
||||
position: relative;
|
||||
width: 200px;
|
||||
height: 20px;
|
||||
background: black;
|
||||
border: 1px solid grey;
|
||||
}
|
||||
.edit-container.invalid {
|
||||
border: 1px solid #c70000;
|
||||
}
|
||||
|
||||
.edit-container.warning {
|
||||
border: 1px solid rgb(159, 151, 0);
|
||||
}
|
||||
|
||||
.edit-container.invalid:hover,
|
||||
.edit-container.invalid:focus-within {
|
||||
border-color: red;
|
||||
}
|
||||
.edit-container.warning:hover,
|
||||
.edit-container.warning:focus-within {
|
||||
border-color: yellow;
|
||||
}
|
||||
|
||||
.edit-container:hover,
|
||||
.edit-container:focus-within {
|
||||
border-color: white;
|
||||
}
|
||||
|
||||
.edit-container label {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
pointer-events: none;
|
||||
bottom: 21px;
|
||||
left: 0;
|
||||
font-size: 10px;
|
||||
color: rgb(206, 206, 206);
|
||||
text-shadow: 1px 1px black;
|
||||
}
|
||||
|
||||
.edit-box {
|
||||
position: relative;
|
||||
outline: none;
|
||||
border: none;
|
||||
background: none;
|
||||
left: 1px;
|
||||
width: calc(100% - 2px);
|
||||
height: 100%;
|
||||
font-family: minecraft, mojangles, monospace;
|
||||
font-size: 10px;
|
||||
color: white;
|
||||
text-shadow: 1px 1px #222;
|
||||
}
|
||||
|
||||
input::-webkit-outer-spin-button,
|
||||
input::-webkit-inner-spin-button {
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Firefox */
|
||||
input[type=number] {
|
||||
appearance: textfield;
|
||||
-moz-appearance: textfield;
|
||||
}
|
||||
`
|
||||
}
|
||||
|
||||
constructor () {
|
||||
super()
|
||||
this.width = '200px'
|
||||
this.id = ''
|
||||
this.value = ''
|
||||
this.label = ''
|
||||
this.required = false
|
||||
}
|
||||
|
||||
static get properties () {
|
||||
return {
|
||||
width: {
|
||||
type: String,
|
||||
attribute: 'pmui-width'
|
||||
},
|
||||
id: {
|
||||
type: String,
|
||||
attribute: 'pmui-id'
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
attribute: 'pmui-label'
|
||||
},
|
||||
value: {
|
||||
type: String,
|
||||
attribute: 'pmui-value'
|
||||
},
|
||||
autocompleteValues: {
|
||||
type: Array,
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
attribute: 'pmui-type'
|
||||
},
|
||||
inputMode: {
|
||||
type: String,
|
||||
attribute: 'pmui-inputmode'
|
||||
},
|
||||
required: {
|
||||
type: Boolean,
|
||||
attribute: 'pmui-required'
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
attribute: 'pmui-placeholder'
|
||||
},
|
||||
state: {
|
||||
type: String,
|
||||
attribute: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
return html`
|
||||
<div
|
||||
class="edit-container ${this.state ?? ''}"
|
||||
style="width: ${this.width};"
|
||||
>
|
||||
<label for="${this.id}">${this.label}</label>
|
||||
${this.autocompleteValues ? html`
|
||||
<datalist id="${this.id}-list">
|
||||
${this.autocompleteValues.map(value => html`
|
||||
<option value="${value}"></option>
|
||||
`)}
|
||||
</datalist>
|
||||
` : ''}
|
||||
<input
|
||||
id="${this.id}"
|
||||
type="${this.type ?? 'text'}"
|
||||
name=""
|
||||
spellcheck="false"
|
||||
?required=${this.required}
|
||||
autocomplete="off"
|
||||
autocapitalize="off"
|
||||
value="${this.value}"
|
||||
placeholder=${ifDefined(this.placeholder || undefined)}
|
||||
list=${ifDefined(this.autocompleteValues ? `${this.id}-list` : undefined)}
|
||||
inputmode=${ifDefined(this.inputMode || undefined)}
|
||||
@input=${({ target: { value } }) => { this.value = this.inputMode === 'decimal' ? value.replaceAll(',', '.') : value }}
|
||||
class="edit-box">
|
||||
</div>
|
||||
`
|
||||
}
|
||||
}
|
||||
|
||||
window.customElements.define('pmui-editbox', EditBox)
|
||||
|
|
@ -1,250 +0,0 @@
|
|||
//@ts-check
|
||||
const { LitElement, html, css } = require('lit')
|
||||
const viewerSupportedVersions = require('prismarine-viewer/viewer/supportedVersions.json')
|
||||
const { supportedVersions } = require('minecraft-protocol')
|
||||
const { hideCurrentModal, miscUiState } = require('../globalState')
|
||||
const { commonCss } = require('./components/common')
|
||||
|
||||
const fullySupporedVersions = viewerSupportedVersions
|
||||
|
||||
class PlayScreen extends LitElement {
|
||||
static get styles () {
|
||||
return css`
|
||||
${commonCss}
|
||||
.title {
|
||||
top: 12px;
|
||||
}
|
||||
|
||||
.edit-boxes {
|
||||
position: fixed;
|
||||
top: 59px;
|
||||
left: 50%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px 0;
|
||||
transform: translate(-50%);
|
||||
width: 310px;
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 0 4px;
|
||||
}
|
||||
|
||||
.button-wrapper {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 0 4px;
|
||||
position: absolute;
|
||||
bottom: 9px;
|
||||
left: 50%;
|
||||
transform: translate(-50%);
|
||||
width: 310px;
|
||||
}
|
||||
|
||||
.extra-info-version {
|
||||
font-size: 10px;
|
||||
color: rgb(206, 206, 206);
|
||||
text-shadow: 1px 1px black;
|
||||
position: absolute;
|
||||
left: calc(50% + 2px);
|
||||
bottom: -34px;
|
||||
}
|
||||
|
||||
.extra-info-proxy {
|
||||
font-size: 8px;
|
||||
color: rgb(206, 206, 206);
|
||||
text-shadow: 1px 1px black;
|
||||
margin:0;
|
||||
margin-top:-12px;
|
||||
}
|
||||
|
||||
a {
|
||||
color: white;
|
||||
}
|
||||
`
|
||||
}
|
||||
|
||||
static get properties () {
|
||||
return {
|
||||
server: { type: String },
|
||||
serverImplicit: { type: String },
|
||||
serverport: { type: Number },
|
||||
proxy: { type: String },
|
||||
proxyImplicit: { type: String },
|
||||
proxyport: { type: Number },
|
||||
username: { type: String },
|
||||
password: { type: String },
|
||||
version: { type: String }
|
||||
}
|
||||
}
|
||||
|
||||
constructor () {
|
||||
super()
|
||||
this.version = ''
|
||||
this.serverport = ''
|
||||
this.proxyport = ''
|
||||
this.server = ''
|
||||
this.proxy = ''
|
||||
this.username = ''
|
||||
this.password = ''
|
||||
this.serverImplicit = ''
|
||||
this.proxyImplicit = ''
|
||||
// todo set them sooner add indicator
|
||||
void window.fetch('config.json').then(async res => res.json()).then(c => c, (error) => {
|
||||
console.warn('Failed to load optional config.json', error)
|
||||
return {}
|
||||
}).then(async (/** @type {import('../globalState').AppConfig} */config) => {
|
||||
miscUiState.appConfig = config
|
||||
const params = new URLSearchParams(window.location.search)
|
||||
|
||||
const getParam = (localStorageKey, qs = localStorageKey) => {
|
||||
const qsValue = qs ? params.get(qs) : undefined
|
||||
if (qsValue) {
|
||||
this.style.display = 'block'
|
||||
}
|
||||
return qsValue || window.localStorage.getItem(localStorageKey)
|
||||
}
|
||||
|
||||
if (config.defaultHost === '<from-proxy>' || config.defaultHostSave === '<from-proxy>') {
|
||||
let proxy = config.defaultProxy || config.defaultProxySave || params.get('proxy')
|
||||
const cleanUrl = url => url.replaceAll(/(https?:\/\/|\/$)/g, '')
|
||||
if (proxy && cleanUrl(proxy) !== cleanUrl(location.origin + location.pathname)) {
|
||||
if (!proxy.startsWith('http')) proxy = 'https://' + proxy
|
||||
const proxyConfig = await fetch(proxy + '/config.json').then(async res => res.json()).then(c => c, (error) => {
|
||||
console.warn(`Failed to load config.json from proxy ${proxy}`, error)
|
||||
return {}
|
||||
})
|
||||
if (config.defaultHost === '<from-proxy>' && proxyConfig.defaultHost) {
|
||||
config.defaultHost = proxyConfig.defaultHost
|
||||
} else {
|
||||
config.defaultHost = ''
|
||||
}
|
||||
if (config.defaultHostSave === '<from-proxy>' && proxyConfig.defaultHostSave) {
|
||||
config.defaultHostSave = proxyConfig.defaultHostSave
|
||||
} else {
|
||||
config.defaultHostSave = ''
|
||||
}
|
||||
}
|
||||
this.server = this.serverImplicit
|
||||
}
|
||||
|
||||
this.serverImplicit = config.defaultHost ?? ''
|
||||
this.proxyImplicit = config.defaultProxy ?? ''
|
||||
this.server = getParam('server', 'ip') ?? config.defaultHostSave ?? ''
|
||||
this.proxy = getParam('proxy') ?? config.defaultProxySave ?? ''
|
||||
this.version = getParam('version') || (window.localStorage.getItem('version') ?? config.defaultVersion ?? '')
|
||||
this.username = getParam('username') || 'pviewer' + (Math.floor(Math.random() * 1000))
|
||||
this.password = getParam('password') || ''
|
||||
if (process.env.NODE_ENV === 'development' && params.get('reconnect') && this.server && this.username) {
|
||||
this.onConnectPress()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
render () {
|
||||
return html`
|
||||
<div class="backdrop"></div>
|
||||
|
||||
<p class="title">Join a Server</p>
|
||||
|
||||
<main class="edit-boxes">
|
||||
<div class="wrapper">
|
||||
<pmui-editbox
|
||||
pmui-width="150px"
|
||||
pmui-label="Server IP"
|
||||
pmui-id="serverip"
|
||||
pmui-value="${this.server}"
|
||||
pmui-type="url"
|
||||
pmui-required="${this.serverImplicit === ''}}"
|
||||
pmui-placeholder="${this.serverImplicit}"
|
||||
.autocompleteValues=${JSON.parse(localStorage.getItem('serverHistory') || '[]')}
|
||||
@input=${e => { this.server = e.target.value }}
|
||||
></pmui-editbox>
|
||||
<pmui-editbox
|
||||
pmui-width="150px"
|
||||
pmui-label="Server Port"
|
||||
pmui-id="port"
|
||||
pmui-value="${this.serverport}"
|
||||
pmui-type="number"
|
||||
pmui-placeholder="25565"
|
||||
@input=${e => { this.serverport = e.target.value }}
|
||||
></pmui-editbox>
|
||||
</div>
|
||||
<div class="wrapper">
|
||||
<pmui-editbox
|
||||
pmui-width="150px"
|
||||
pmui-label="Proxy IP"
|
||||
pmui-id="proxy"
|
||||
pmui-value="${this.proxy}"
|
||||
pmui-required="${this.proxyImplicit === ''}}"
|
||||
pmui-placeholder="${this.proxyImplicit}"
|
||||
pmui-type="url"
|
||||
@input=${e => { this.proxy = e.target.value }}
|
||||
></pmui-editbox>
|
||||
<pmui-editbox
|
||||
pmui-width="150px"
|
||||
pmui-label="Proxy Port"
|
||||
pmui-id="port"
|
||||
pmui-value="${this.proxyport}"
|
||||
pmui-type="number"
|
||||
@input=${e => { this.proxyport = e.target.value }}
|
||||
></pmui-editbox>
|
||||
</div>
|
||||
<div class="wrapper">
|
||||
<p class="extra-info-proxy">Enter proxy url you want to use. <a href="https://github.com/zardoy/prismarine-web-client/issues/3">Learn more</a>.</p>
|
||||
</div>
|
||||
<div class="wrapper">
|
||||
<pmui-editbox
|
||||
pmui-width="150px"
|
||||
pmui-label="Username"
|
||||
pmui-id="username"
|
||||
pmui-value="${this.username}"
|
||||
@input=${e => { this.username = e.target.value }}
|
||||
></pmui-editbox>
|
||||
<pmui-editbox
|
||||
pmui-width="150px"
|
||||
pmui-label="Bot Version"
|
||||
pmui-id="botversion"
|
||||
pmui-value="${this.version}"
|
||||
pmui-inputmode="decimal"
|
||||
state="${this.version && (fullySupporedVersions.includes(/** @type {any} */(this.version)) ? '' : supportedVersions.includes(this.version) ? 'warning' : 'invalid')}"
|
||||
.autocompleteValues=${supportedVersions}
|
||||
@input=${e => { this.version = e.target.value = e.target.value.replaceAll(',', '.') }}
|
||||
></pmui-editbox>
|
||||
</div>
|
||||
<p class="extra-info-version">Leave blank and it will be chosen automatically</p>
|
||||
</main>
|
||||
|
||||
<div class="button-wrapper">
|
||||
<pmui-button pmui-test-id="connect-to-server" pmui-width="150px" pmui-label="Connect" @pmui-click=${this.onConnectPress}></pmui-button>
|
||||
<pmui-button pmui-width="150px" pmui-label="Cancel" @pmui-click=${() => hideCurrentModal()}></pmui-button>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
|
||||
onConnectPress () {
|
||||
const server = this.server ? `${this.server}${this.serverport && `:${this.serverport}`}` : this.serverImplicit
|
||||
const proxy = this.proxy ? `${this.proxy}${this.proxyport && `:${this.proxyport}`}` : this.proxyImplicit
|
||||
|
||||
window.localStorage.setItem('username', this.username)
|
||||
window.localStorage.setItem('password', this.password)
|
||||
window.localStorage.setItem('server', server)
|
||||
window.localStorage.setItem('proxy', proxy)
|
||||
window.localStorage.setItem('version', this.version)
|
||||
|
||||
window.dispatchEvent(new window.CustomEvent('connect', {
|
||||
detail: {
|
||||
server,
|
||||
proxy,
|
||||
username: this.username,
|
||||
password: this.password,
|
||||
botVersion: this.version
|
||||
}
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
window.customElements.define('pmui-playscreen', PlayScreen)
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import { useState } from 'react'
|
||||
import { useSnapshot } from 'valtio'
|
||||
import { openURL } from 'prismarine-viewer/viewer/lib/simpleUtils'
|
||||
import { miscUiState, openOptionsMenu, showModal } from './globalState'
|
||||
import { openURL } from './menus/components/common'
|
||||
import { AppOptions, options } from './optionsStorage'
|
||||
import Button from './react/Button'
|
||||
import { OptionMeta, OptionSlider } from './react/OptionsItems'
|
||||
|
|
|
|||
110
src/react/AddServer.tsx
Normal file
110
src/react/AddServer.tsx
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
import React from 'react'
|
||||
import Screen from './Screen'
|
||||
import Input from './Input'
|
||||
import Button from './Button'
|
||||
import { useIsSmallWidth } from './simpleHooks'
|
||||
|
||||
export interface NewServerInfo {
|
||||
ip: string
|
||||
name?: string
|
||||
versionOverride?: string
|
||||
proxyOverride?: string
|
||||
usernameOverride?: string
|
||||
passwordOverride?: string
|
||||
}
|
||||
|
||||
interface Props {
|
||||
onBack: () => void
|
||||
onConfirm: (info: NewServerInfo) => void
|
||||
title?: string
|
||||
initialData?: NewServerInfo
|
||||
parseQs?: boolean
|
||||
onQsConnect?: (server: NewServerInfo) => void
|
||||
defaults?: Pick<NewServerInfo, 'proxyOverride' | 'usernameOverride'>
|
||||
}
|
||||
|
||||
export default ({ onBack, onConfirm, title = 'Add a Server', initialData, parseQs, onQsConnect, defaults }: Props) => {
|
||||
const qsParams = parseQs ? new URLSearchParams(window.location.search) : undefined
|
||||
|
||||
const [serverName, setServerName] = React.useState(initialData?.name ?? qsParams?.get('name') ?? '')
|
||||
|
||||
const ipWithoutPort = initialData?.ip.split(':')[0]
|
||||
const port = initialData?.ip.split(':')[1]
|
||||
|
||||
const [serverIp, setServerIp] = React.useState(ipWithoutPort ?? qsParams?.get('ip') ?? '')
|
||||
const [serverPort, setServerPort] = React.useState(port ?? '')
|
||||
const [versionOverride, setVersionOverride] = React.useState(initialData?.versionOverride ?? qsParams?.get('version') ?? '')
|
||||
const [proxyOverride, setProxyOverride] = React.useState(initialData?.proxyOverride ?? qsParams?.get('proxy') ?? '')
|
||||
const [usernameOverride, setUsernameOverride] = React.useState(initialData?.usernameOverride ?? qsParams?.get('username') ?? '')
|
||||
const [passwordOverride, setPasswordOverride] = React.useState(initialData?.passwordOverride ?? qsParams?.get('password') ?? '')
|
||||
const smallWidth = useIsSmallWidth()
|
||||
|
||||
return <Screen title={qsParams?.get('ip') ? 'Connect to Server' : title} backdrop>
|
||||
<form style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
height: '100%'
|
||||
}}
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault()
|
||||
let ip = serverIp.includes(':') ? serverIp : `${serverIp}:${serverPort}`
|
||||
ip = ip.replace(/:$/, '')
|
||||
onConfirm({
|
||||
name: serverName,
|
||||
ip,
|
||||
versionOverride,
|
||||
proxyOverride,
|
||||
usernameOverride,
|
||||
passwordOverride
|
||||
})
|
||||
}}
|
||||
>
|
||||
<div style={{
|
||||
display: 'grid',
|
||||
gap: 3,
|
||||
gridTemplateColumns: smallWidth ? '1fr' : '1fr 1fr'
|
||||
}}>
|
||||
<div style={{ gridColumn: smallWidth ? '' : 'span 2', display: 'flex', justifyContent: 'center' }}>
|
||||
<InputWithLabel label="Server Name" value={serverName} onChange={({ target: { value } }) => setServerName(value)} placeholder='Defaults to IP' />
|
||||
</div>
|
||||
<InputWithLabel required label="Server IP" value={serverIp} onChange={({ target: { value } }) => setServerIp(value)} />
|
||||
<InputWithLabel label="Server Port" value={serverPort} onChange={({ target: { value } }) => setServerPort(value)} placeholder='25565' />
|
||||
<div style={{ gridColumn: smallWidth ? '' : 'span 2' }}>Overrides:</div>
|
||||
<InputWithLabel label="Version Override" value={versionOverride} onChange={({ target: { value } }) => setVersionOverride(value)} placeholder='Optional, but recommended to specify' />
|
||||
<InputWithLabel label="Proxy Override" value={proxyOverride} onChange={({ target: { value } }) => setProxyOverride(value)} placeholder={defaults?.proxyOverride} />
|
||||
<InputWithLabel label="Username Override" value={usernameOverride} onChange={({ target: { value } }) => setUsernameOverride(value)} placeholder={defaults?.usernameOverride} />
|
||||
<InputWithLabel label="Password Override" value={passwordOverride} onChange={({ target: { value } }) => setPasswordOverride(value)} /* placeholder='For advanced usage only' */ />
|
||||
<Button onClick={() => {
|
||||
onBack()
|
||||
}}>Cancel</Button>
|
||||
<Button type='submit'>Save</Button>
|
||||
{qsParams?.get('ip') && <div style={{ gridColumn: smallWidth ? '' : 'span 2', display: 'flex', justifyContent: 'center' }}>
|
||||
<Button
|
||||
data-test-id='connect-qs'
|
||||
onClick={() => {
|
||||
onQsConnect?.({
|
||||
name: serverName,
|
||||
ip: serverIp,
|
||||
versionOverride,
|
||||
proxyOverride,
|
||||
usernameOverride,
|
||||
passwordOverride
|
||||
})
|
||||
}}
|
||||
>Connect</Button>
|
||||
</div>}
|
||||
</div>
|
||||
</form>
|
||||
</Screen>
|
||||
}
|
||||
|
||||
const InputWithLabel = ({ label, span, ...props }: React.ComponentProps<typeof Input> & { label, span?}) => {
|
||||
return <div style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gridRow: span ? 'span 2 / span 2' : undefined,
|
||||
}}>
|
||||
<label style={{ fontSize: 12, marginBottom: 1, color: 'lightgray' }}>{label}</label>
|
||||
<Input {...props} />
|
||||
</div>
|
||||
}
|
||||
|
|
@ -15,7 +15,7 @@ interface Props extends React.ComponentProps<'button'> {
|
|||
|
||||
void loadSound('button_click.mp3')
|
||||
|
||||
export default (({ label, icon, children, inScreen, rootRef, ...args }) => {
|
||||
export default (({ label, icon, children, inScreen, rootRef, type = 'button', ...args }) => {
|
||||
const onClick = (e) => {
|
||||
void playSound('button_click.mp3')
|
||||
args.onClick?.(e)
|
||||
|
|
@ -29,7 +29,7 @@ export default (({ label, icon, children, inScreen, rootRef, ...args }) => {
|
|||
args.style.width = 20
|
||||
}
|
||||
|
||||
return <button ref={rootRef} {...args} className={classNames(buttonCss.button, args.className)} onClick={onClick}>
|
||||
return <button ref={rootRef} {...args} className={classNames(buttonCss.button, args.className)} onClick={onClick} type={type}>
|
||||
{icon && <iconify-icon class={buttonCss.icon} icon={icon}></iconify-icon>}
|
||||
{label}
|
||||
{children}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ import { hideCurrentModal, miscUiState } from '../globalState'
|
|||
import { options } from '../optionsStorage'
|
||||
import ChatContainer, { Message, fadeMessage } from './ChatContainer'
|
||||
import { useIsModalActive } from './utils'
|
||||
import { hideNotification, showNotification } from './NotificationProvider'
|
||||
import { updateLoadedServerData } from './ServersListProvider'
|
||||
|
||||
export default () => {
|
||||
const [messages, setMessages] = useState([] as Message[])
|
||||
|
|
@ -43,6 +45,17 @@ export default () => {
|
|||
opened={isChatActive}
|
||||
sendMessage={(message) => {
|
||||
const builtinHandled = tryHandleBuiltinCommand(message)
|
||||
if (miscUiState.loadedServerIndex && (message.startsWith('/login') || message.startsWith('/register'))) {
|
||||
showNotification('Click here to save your password in browser for auto-login', undefined, false, undefined, () => {
|
||||
updateLoadedServerData((server) => {
|
||||
server.autoLogin ??= {}
|
||||
const password = message.split(' ')[1]
|
||||
server.autoLogin[miscUiState.username] = password
|
||||
return server
|
||||
})
|
||||
hideNotification()
|
||||
})
|
||||
}
|
||||
if (!builtinHandled) {
|
||||
bot.chat(message)
|
||||
}
|
||||
|
|
@ -52,7 +65,10 @@ export default () => {
|
|||
}}
|
||||
fetchCompletionItems={async (triggerKind, completeValue) => {
|
||||
if ((triggerKind === 'explicit' || options.autoRequestCompletions)) {
|
||||
let items = await bot.tabComplete(completeValue, true, true)
|
||||
let items = [] as string[]
|
||||
try {
|
||||
items = await bot.tabComplete(completeValue, true, true)
|
||||
} catch (err) {}
|
||||
if (typeof items[0] === 'object') {
|
||||
// @ts-expect-error
|
||||
if (items[0].match) items = items.map(i => i.match)
|
||||
|
|
|
|||
|
|
@ -5,13 +5,15 @@ import { useUsingTouch } from './utils'
|
|||
interface Props extends React.ComponentProps<'input'> {
|
||||
rootStyles?: React.CSSProperties
|
||||
autoFocus?: boolean
|
||||
inputRef?: React.RefObject<HTMLInputElement>
|
||||
}
|
||||
|
||||
export default ({ autoFocus, rootStyles, ...inputProps }: Props) => {
|
||||
export default ({ autoFocus, rootStyles, inputRef, ...inputProps }: Props) => {
|
||||
const ref = useRef<HTMLInputElement>(null!)
|
||||
const isTouch = useUsingTouch()
|
||||
|
||||
useEffect(() => {
|
||||
if (inputRef) (inputRef as any).current = ref.current
|
||||
if (!autoFocus || isTouch) return // Don't make screen keyboard popup on mobile
|
||||
ref.current.focus()
|
||||
}, [])
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import React, { useEffect, useState } from 'react'
|
||||
import { openURL } from '../menus/components/common'
|
||||
import { openURL } from 'prismarine-viewer/viewer/lib/simpleUtils'
|
||||
import { haveDirectoryPicker } from '../utils'
|
||||
import styles from './mainMenu.module.css'
|
||||
import Button from './Button'
|
||||
|
|
@ -67,7 +67,7 @@ export default ({ connectToServerAction, mapsProvider, singleplayerAction, optio
|
|||
placement: 'top',
|
||||
}}
|
||||
onClick={connectToServerAction}
|
||||
data-test-id='connect-screen-button'
|
||||
data-test-id='servers-screen-button'
|
||||
>
|
||||
Connect to server
|
||||
</ButtonWithTooltip>
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@ import fs from 'fs'
|
|||
import { Transition } from 'react-transition-group'
|
||||
import { useSnapshot } from 'valtio'
|
||||
import { useEffect } from 'react'
|
||||
import { openURL } from 'prismarine-viewer/viewer/lib/simpleUtils'
|
||||
import { activeModalStack, miscUiState, openOptionsMenu, showModal } from '../globalState'
|
||||
import { openURL } from '../menus/components/common'
|
||||
import { openGithub, setLoadingScreenStatus } from '../utils'
|
||||
import { openFilePicker, copyFilesAsync, mkdirRecursive, openWorldDirectory, removeFileRecursiveAsync } from '../browserfs'
|
||||
|
||||
|
|
@ -25,7 +25,7 @@ export default () => {
|
|||
return <Transition in={!noDisplay} timeout={disableAnimation ? 0 : 100} mountOnEnter unmountOnExit>
|
||||
{(state) => <div style={{ transition: state === 'exiting' || disableAnimation ? '' : '100ms opacity ease-in', ...state === 'entered' ? { opacity: 1 } : { opacity: 0 } }}>
|
||||
<MainMenu
|
||||
connectToServerAction={() => showModal(document.getElementById('play-screen'))}
|
||||
connectToServerAction={() => showModal({ reactType: 'serversList' })}
|
||||
singleplayerAction={async () => {
|
||||
const oldFormatSave = fs.existsSync('./world/level.dat')
|
||||
if (oldFormatSave) {
|
||||
|
|
|
|||
|
|
@ -137,5 +137,5 @@ export const messageFormatStylesMap = {
|
|||
strikethrough: 'text-decoration:line-through',
|
||||
underlined: 'text-decoration:underline',
|
||||
italic: 'font-style:italic',
|
||||
obfuscated: 'color: #222326;background-color: #222326;'
|
||||
obfuscated: 'filter:blur(2px)',
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { join } from 'path'
|
|||
import { useEffect } from 'react'
|
||||
import { useSnapshot } from 'valtio'
|
||||
import { usedServerPathsV1 } from 'flying-squid/dist/lib/modules/world'
|
||||
import { openURL } from 'prismarine-viewer/viewer/lib/simpleUtils'
|
||||
import {
|
||||
activeModalStack,
|
||||
showModal,
|
||||
|
|
@ -13,7 +14,6 @@ import { fsState } from '../loadSave'
|
|||
import { disconnect } from '../flyingSquidUtils'
|
||||
import { pointerLock, setLoadingScreenStatus } from '../utils'
|
||||
import { closeWan, openToWanAndCopyJoinLink, getJoinLink } from '../localServerMultiplayer'
|
||||
import { openURL } from '../menus/components/common'
|
||||
import { copyFilesAsyncWithProgress, mkdirRecursive, uniqueFileNameFromWorldName } from '../browserfs'
|
||||
import { useIsModalActive } from './utils'
|
||||
import { showOptionsModal } from './SelectOption'
|
||||
|
|
|
|||
55
src/react/ServersList.stories.tsx
Normal file
55
src/react/ServersList.stories.tsx
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
import type { Meta, StoryObj } from '@storybook/react'
|
||||
import 'iconify-icon'
|
||||
|
||||
import { useState } from 'react'
|
||||
import AddServer from './AddServer'
|
||||
import ServersList from './ServersList'
|
||||
|
||||
const meta: Meta<typeof ServersList> = {
|
||||
component: ServersList,
|
||||
render (args) {
|
||||
const [addOpen, setAddOpen] = useState(false)
|
||||
const [username, setUsername] = useState('')
|
||||
|
||||
return addOpen ?
|
||||
<AddServer onBack={() => {
|
||||
setAddOpen(false)
|
||||
}}
|
||||
onConfirm={(info) => {
|
||||
console.log('add server', info)
|
||||
}} /> :
|
||||
<ServersList
|
||||
worldData={[{
|
||||
name: 'test',
|
||||
title: 'Server',
|
||||
formattedTextOverride: 'play yes',
|
||||
}]}
|
||||
joinServer={(ip) => {
|
||||
console.log('joinServer', ip)
|
||||
}}
|
||||
initialProxies={{
|
||||
proxies: ['localhost', 'mc.hypixel.net'],
|
||||
selected: 'localhost',
|
||||
}}
|
||||
updateProxies={newData => {
|
||||
console.log('setProxies', newData)
|
||||
}}
|
||||
onWorldAction={() => {}}
|
||||
onGeneralAction={(action) => {
|
||||
if (action === 'create') {
|
||||
setAddOpen(true)
|
||||
}
|
||||
}}
|
||||
username={username}
|
||||
setUsername={setUsername}
|
||||
/>
|
||||
},
|
||||
}
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof ServersList>;
|
||||
|
||||
export const Primary: Story = {
|
||||
args: {
|
||||
},
|
||||
}
|
||||
165
src/react/ServersList.tsx
Normal file
165
src/react/ServersList.tsx
Normal file
|
|
@ -0,0 +1,165 @@
|
|||
import React from 'react'
|
||||
import { useAutocomplete } from '@mui/base'
|
||||
import { omitObj } from '@zardoy/utils'
|
||||
import Singleplayer from './Singleplayer'
|
||||
import Input from './Input'
|
||||
import Button from './Button'
|
||||
import PixelartIcon from './PixelartIcon'
|
||||
|
||||
interface Props extends React.ComponentProps<typeof Singleplayer> {
|
||||
joinServer: (ip: string, overrides: {
|
||||
username?: string
|
||||
password?: string
|
||||
proxy?: string
|
||||
version?: string
|
||||
shouldSave?: boolean
|
||||
}) => void
|
||||
initialProxies: SavedProxiesLocalStorage
|
||||
updateProxies: (proxies: SavedProxiesLocalStorage) => void
|
||||
username: string
|
||||
setUsername: (username: string) => void
|
||||
}
|
||||
|
||||
export interface SavedProxiesLocalStorage {
|
||||
proxies: readonly string[]
|
||||
selected: string
|
||||
}
|
||||
|
||||
type ProxyStatusResult = {
|
||||
time: number
|
||||
ping: number
|
||||
status: 'success' | 'error' | 'unknown'
|
||||
}
|
||||
|
||||
export default ({ initialProxies, updateProxies: updateProxiesProp, joinServer, username, setUsername, ...props }: Props) => {
|
||||
const [proxies, setProxies] = React.useState(initialProxies)
|
||||
|
||||
const updateProxies = (newData: SavedProxiesLocalStorage) => {
|
||||
setProxies(newData)
|
||||
updateProxiesProp(newData)
|
||||
}
|
||||
|
||||
const autocomplete = useAutocomplete({
|
||||
value: proxies.selected,
|
||||
options: proxies.proxies.filter(proxy => proxy !== proxies.selected),
|
||||
onInputChange (event, value, reason) {
|
||||
// console.log('onChange', { event, value, reason, details })
|
||||
if (value) {
|
||||
updateProxies({
|
||||
...proxies,
|
||||
selected: value
|
||||
})
|
||||
}
|
||||
},
|
||||
freeSolo: true
|
||||
})
|
||||
|
||||
const [serverIp, setServerIp] = React.useState('')
|
||||
const [save, setSave] = React.useState(true)
|
||||
|
||||
return <Singleplayer {...props}
|
||||
firstRowChildrenOverride={<form style={{ width: '100%', display: 'flex', justifyContent: 'center' }} onSubmit={(e) => {
|
||||
e.preventDefault()
|
||||
joinServer(serverIp, {
|
||||
shouldSave: save,
|
||||
})
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', gap: 5, alignItems: 'center' }}>
|
||||
{/* todo history */}
|
||||
<Input required placeholder='Quick Connect IP' value={serverIp} onChange={({ target: { value } }) => setServerIp(value)} />
|
||||
<label style={{ fontSize: 10, display: 'flex', alignItems: 'center', gap: 5, height: '100%', marginTop: '-1px' }}>
|
||||
<input type='checkbox' checked={save}
|
||||
style={{ borderRadius: 0 }}
|
||||
onChange={({ target: { checked } }) => setSave(checked)}
|
||||
/> Save</label>
|
||||
<Button style={{ width: 90 }} type='submit'>Join Server</Button>
|
||||
</div>
|
||||
</form>}
|
||||
searchRowChildrenOverride={
|
||||
<div style={{
|
||||
// marginTop: 12,
|
||||
}}>
|
||||
<div style={{ display: 'flex', gap: 3, alignItems: 'center' }}>
|
||||
<span style={{ color: 'lightgray', fontSize: 14 }}>Proxy:</span>
|
||||
<div {...autocomplete.getRootProps()} style={{ position: 'relative', width: 130 }}>
|
||||
<ProxyRender
|
||||
{...omitObj(autocomplete.getInputProps(), 'ref')}
|
||||
inputRef={autocomplete.getInputProps().ref as any}
|
||||
status='unknown'
|
||||
ip=''
|
||||
/>
|
||||
{autocomplete.groupedOptions && <ul {...autocomplete.getListboxProps()} style={{
|
||||
position: 'absolute',
|
||||
zIndex: 1,
|
||||
// marginTop: 10,
|
||||
}}>
|
||||
{autocomplete.groupedOptions.map((proxy, index) => {
|
||||
const { itemRef, ...optionProps } = autocomplete.getOptionProps({ option: proxy, index })
|
||||
return <ProxyRender {...optionProps as any} ip={proxy} disabled />
|
||||
})}
|
||||
</ul>}
|
||||
</div>
|
||||
<PixelartIcon iconName='user' styles={{ fontSize: 14, color: 'lightgray', marginLeft: 2 }} />
|
||||
<Input rootStyles={{ width: 80 }} value={username} onChange={({ target: { value } }) => setUsername(value)} />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
serversLayout
|
||||
onWorldAction={(action, serverName) => {
|
||||
if (action === 'load') {
|
||||
joinServer(serverName, {})
|
||||
}
|
||||
props.onWorldAction?.(action, serverName)
|
||||
}}
|
||||
/>
|
||||
}
|
||||
|
||||
type Status = 'unknown' | 'error' | 'success'
|
||||
|
||||
const ProxyRender = ({ status, ip, inputRef, value, setValue, ...props }: {
|
||||
status: Status
|
||||
ip: string
|
||||
} & Record<string, any>) => {
|
||||
const iconPerStatus = {
|
||||
unknown: 'cellular-signal-0',
|
||||
error: 'cellular-signal-off',
|
||||
success: 'cellular-signal-3',
|
||||
}
|
||||
|
||||
return <div style={{
|
||||
position: 'relative',
|
||||
}} {...props}>
|
||||
<Input
|
||||
inputRef={inputRef}
|
||||
style={{
|
||||
paddingLeft: 16,
|
||||
}}
|
||||
rootStyles={{
|
||||
width: '100%',
|
||||
}}
|
||||
value={value}
|
||||
// onChange={({ target: { value } }) => setValue?.(value)}
|
||||
onChange={props.onChange}
|
||||
/>
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
inset: 0,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 2
|
||||
}}>
|
||||
<PixelartIcon iconName={iconPerStatus.unknown} />
|
||||
<div style={{
|
||||
fontSize: 10,
|
||||
// color: 'lightgray',
|
||||
// ellipsis
|
||||
width: '100%',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
}}>
|
||||
{ip}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
339
src/react/ServersListProvider.tsx
Normal file
339
src/react/ServersListProvider.tsx
Normal file
|
|
@ -0,0 +1,339 @@
|
|||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { proxy } from 'valtio'
|
||||
import { qsOptions } from '../optionsStorage'
|
||||
import { ConnectOptions } from '../connect'
|
||||
import { hideCurrentModal, miscUiState, showModal } from '../globalState'
|
||||
import ServersList from './ServersList'
|
||||
import AddServer from './AddServer'
|
||||
import { useDidUpdateEffect, useIsModalActive } from './utils'
|
||||
|
||||
interface StoreServerItem {
|
||||
ip: string,
|
||||
name?: string
|
||||
version?: string
|
||||
lastJoined?: number
|
||||
description?: string
|
||||
proxyOverride?: string
|
||||
usernameOverride?: string
|
||||
passwordOverride?: string
|
||||
optionsOverride?: Record<string, any>
|
||||
autoLogin?: Record<string, string>
|
||||
}
|
||||
|
||||
type ServerResponse = {
|
||||
version: {
|
||||
name_raw: string
|
||||
}
|
||||
// display tooltip
|
||||
players?: {
|
||||
online: number
|
||||
max: number
|
||||
list: Array<{
|
||||
name_raw: string
|
||||
name_clean: string
|
||||
}>
|
||||
}
|
||||
icon: string
|
||||
motd: {
|
||||
raw: string
|
||||
}
|
||||
// todo circle error icon
|
||||
mods?: Array<{ name, version }>
|
||||
// todo display via hammer icon
|
||||
software?: string
|
||||
plugins?: Array<{ name, version }>
|
||||
}
|
||||
|
||||
type AdditionalDisplayData = {
|
||||
formattedText: string
|
||||
textNameRight: string
|
||||
icon?: string
|
||||
}
|
||||
|
||||
const getInitialServersList = () => {
|
||||
if (localStorage['serversList']) return JSON.parse(localStorage['serversList']) as StoreServerItem[]
|
||||
|
||||
const servers = [] as StoreServerItem[]
|
||||
|
||||
const legacyServersList = localStorage['serverHistory'] ? JSON.parse(localStorage['serverHistory']) as string[] : null
|
||||
if (legacyServersList) {
|
||||
for (const server of legacyServersList) {
|
||||
if (!server || localStorage['server'] === server) continue
|
||||
servers.push({ ip: server, lastJoined: Date.now() })
|
||||
}
|
||||
}
|
||||
|
||||
if (localStorage['server']) {
|
||||
const legacyLastJoinedServer: StoreServerItem = {
|
||||
ip: localStorage['server'],
|
||||
passwordOverride: localStorage['password'],
|
||||
version: localStorage['version'],
|
||||
lastJoined: Date.now()
|
||||
}
|
||||
servers.push(legacyLastJoinedServer)
|
||||
}
|
||||
|
||||
if (servers.length === 0) { // server list is empty, let's suggest some
|
||||
for (const server of miscUiState.appConfig?.promoteServers ?? []) {
|
||||
servers.push({
|
||||
ip: server.ip,
|
||||
description: server.description,
|
||||
version: server.version,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return servers
|
||||
}
|
||||
|
||||
const setNewServersList = (serversList: StoreServerItem[]) => {
|
||||
localStorage['serversList'] = JSON.stringify(serversList)
|
||||
|
||||
// cleanup legacy
|
||||
localStorage.removeItem('serverHistory')
|
||||
localStorage.removeItem('server')
|
||||
localStorage.removeItem('password')
|
||||
localStorage.removeItem('version')
|
||||
}
|
||||
|
||||
const getInitialProxies = () => {
|
||||
const proxies = [] as string[]
|
||||
if (miscUiState.appConfig?.defaultProxy) {
|
||||
proxies.push(miscUiState.appConfig.defaultProxy)
|
||||
}
|
||||
if (localStorage['proxy']) {
|
||||
proxies.push(localStorage['proxy'])
|
||||
localStorage.removeItem('proxy')
|
||||
}
|
||||
return proxies
|
||||
}
|
||||
|
||||
export const updateLoadedServerData = (callback: (data: StoreServerItem) => StoreServerItem) => {
|
||||
// function assumes component is not mounted to avoid sync issues after save
|
||||
const { loadedServerIndex } = miscUiState
|
||||
if (!loadedServerIndex) return
|
||||
const servers = getInitialServersList()
|
||||
const server = servers[loadedServerIndex]
|
||||
servers[loadedServerIndex] = callback(server)
|
||||
setNewServersList(servers)
|
||||
}
|
||||
|
||||
const Inner = () => {
|
||||
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)}`))
|
||||
|
||||
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)
|
||||
// }
|
||||
}, [proxies])
|
||||
|
||||
const [serversList, setServersList] = useState<StoreServerItem[]>(() => getInitialServersList())
|
||||
const [additionalData, setAdditionalData] = useState<Record<string, AdditionalDisplayData>>({})
|
||||
|
||||
useDidUpdateEffect(() => {
|
||||
// save data only on user changes
|
||||
setNewServersList(serversList)
|
||||
}, [serversList])
|
||||
|
||||
// by lastJoined
|
||||
const serversListSorted = useMemo(() => {
|
||||
return serversList.map((server, index) => ({ ...server, index })).sort((a, b) => (b.lastJoined ?? 0) - (a.lastJoined ?? 0))
|
||||
}, [serversList])
|
||||
|
||||
useEffect(() => {
|
||||
const update = async () => {
|
||||
for (const server of serversListSorted) {
|
||||
const isInLocalNetwork = server.ip.startsWith('192.168.') || server.ip.startsWith('10.') || server.ip.startsWith('172.') || server.ip.startsWith('127.') || server.ip.startsWith('localhost')
|
||||
if (isInLocalNetwork) continue
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await fetch(`https://api.mcstatus.io/v2/status/java/${server.ip}`).then(async r => r.json()).then((data: ServerResponse) => {
|
||||
const versionClean = data.version.name_raw.replace(/^[^\d.]+/, '')
|
||||
setAdditionalData(old => {
|
||||
return ({
|
||||
...old,
|
||||
[server.ip]: {
|
||||
formattedText: data.motd.raw,
|
||||
textNameRight: `${versionClean} ${data.players?.online ?? '??'}/${data.players?.max ?? '??'}`,
|
||||
icon: data.icon,
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
void update()
|
||||
}, [serversListSorted])
|
||||
|
||||
const isEditScreenModal = useIsModalActive('editServer')
|
||||
|
||||
useEffect(() => {
|
||||
if (!isEditScreenModal) {
|
||||
setServerEditScreen(null)
|
||||
}
|
||||
}, [isEditScreenModal])
|
||||
|
||||
useEffect(() => {
|
||||
if (serverEditScreen && !isEditScreenModal) {
|
||||
showModal({ reactType: 'editServer' })
|
||||
}
|
||||
}, [serverEditScreen])
|
||||
|
||||
if (isEditScreenModal) {
|
||||
return <AddServer
|
||||
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)
|
||||
serversList[index] = info
|
||||
setServersList([...serversList])
|
||||
}
|
||||
setServerEditScreen(null)
|
||||
}}
|
||||
initialData={!serverEditScreen || serverEditScreen === true ? undefined : serverEditScreen}
|
||||
onQsConnect={(info) => {
|
||||
const connectOptions: ConnectOptions = {
|
||||
username: info.usernameOverride || defaultUsername,
|
||||
server: info.ip,
|
||||
proxy: info.proxyOverride || selectedProxy,
|
||||
botVersion: info.versionOverride,
|
||||
password: info.passwordOverride,
|
||||
ignoreQs: true,
|
||||
}
|
||||
dispatchEvent(new CustomEvent('connect', { detail: connectOptions }))
|
||||
}}
|
||||
/>
|
||||
}
|
||||
|
||||
return <ServersList
|
||||
joinServer={(indexOrIp, overrides) => {
|
||||
let ip = indexOrIp
|
||||
let server: StoreServerItem | undefined
|
||||
if (overrides.shouldSave === undefined) {
|
||||
// hack: inner component doesn't know of overrides for existing servers
|
||||
server = serversListSorted.find(s => s.index.toString() === indexOrIp)!
|
||||
ip = server.ip
|
||||
overrides = server
|
||||
}
|
||||
|
||||
const lastJoinedUsername = serversListSorted.find(s => s.usernameOverride)?.usernameOverride
|
||||
let username = overrides.username || defaultUsername
|
||||
if (!username) {
|
||||
username = prompt('Username', lastJoinedUsername || '')
|
||||
if (!username) return
|
||||
setDefaultUsername(username)
|
||||
}
|
||||
const options = {
|
||||
username,
|
||||
server: ip,
|
||||
proxy: overrides.proxy || selectedProxy,
|
||||
botVersion: overrides.version,
|
||||
password: overrides.password,
|
||||
ignoreQs: true,
|
||||
autoLoginPassword: server?.autoLogin?.[username],
|
||||
onSuccessfulPlay () {
|
||||
if (overrides.shouldSave && !serversList.some(s => s.ip === ip)) {
|
||||
const newServersList = [...serversList, {
|
||||
ip,
|
||||
lastJoined: Date.now(),
|
||||
}]
|
||||
// setServersList(newServersList)
|
||||
setNewServersList(newServersList) // component is not mounted
|
||||
}
|
||||
|
||||
if (overrides.shouldSave === undefined) { // loading saved
|
||||
// find and update
|
||||
const server = serversList.find(s => s.ip === ip)
|
||||
if (server) {
|
||||
server.lastJoined = Date.now()
|
||||
// setServersList([...serversList])
|
||||
setNewServersList(serversList) // component is not mounted
|
||||
}
|
||||
}
|
||||
|
||||
// save new selected proxy (if new)
|
||||
if (!proxies.includes(selectedProxy)) {
|
||||
// setProxies([...proxies, selectedProxy])
|
||||
localStorage.setItem('proxies', JSON.stringify([...proxies, selectedProxy]))
|
||||
}
|
||||
if (selectedProxy) {
|
||||
localStorage.setItem('selectedProxy', selectedProxy)
|
||||
}
|
||||
},
|
||||
serverIndex: overrides.shouldSave ? serversList.length.toString() : indexOrIp // assume last
|
||||
} satisfies ConnectOptions
|
||||
dispatchEvent(new CustomEvent('connect', { detail: options }))
|
||||
// qsOptions
|
||||
}}
|
||||
username={defaultUsername}
|
||||
setUsername={setDefaultUsername}
|
||||
onWorldAction={(action, index) => {
|
||||
const server = serversList[index]
|
||||
if (!server) return
|
||||
|
||||
if (action === 'edit') {
|
||||
setServerEditScreen(server)
|
||||
}
|
||||
if (action === 'delete') {
|
||||
setServersList(old => old.filter(s => s !== server))
|
||||
}
|
||||
}}
|
||||
onGeneralAction={(action) => {
|
||||
if (action === 'create') {
|
||||
setServerEditScreen(true)
|
||||
}
|
||||
if (action === 'cancel') {
|
||||
hideCurrentModal()
|
||||
}
|
||||
}}
|
||||
worldData={serversListSorted.map(server => {
|
||||
const additional = additionalData[server.ip]
|
||||
return {
|
||||
name: server.index.toString(),
|
||||
title: server.name || server.ip,
|
||||
detail: (server.version ?? '') + ' ' + (server.usernameOverride ?? ''),
|
||||
// lastPlayed: server.lastJoined,
|
||||
formattedTextOverride: additional?.formattedText,
|
||||
worldNameRight: additional?.textNameRight ?? '',
|
||||
iconSrc: additional?.icon,
|
||||
}
|
||||
})}
|
||||
initialProxies={{
|
||||
proxies,
|
||||
selected: selectedProxy,
|
||||
}}
|
||||
updateProxies={({ proxies, selected }) => {
|
||||
// new proxy is saved in joinServer
|
||||
setProxies(proxies)
|
||||
setSelectedProxy(selected)
|
||||
}}
|
||||
/>
|
||||
}
|
||||
|
||||
export default () => {
|
||||
const editServerModalActive = useIsModalActive('editServer')
|
||||
const isServersListModalActive = useIsModalActive('serversList')
|
||||
const eitherModal = isServersListModalActive || editServerModalActive
|
||||
return eitherModal ? <Inner /> : null
|
||||
}
|
||||
|
|
@ -10,22 +10,25 @@ import styles from './singleplayer.module.css'
|
|||
import Input from './Input'
|
||||
import Button from './Button'
|
||||
import Tabs from './Tabs'
|
||||
import MessageFormattedString from './MessageFormattedString'
|
||||
|
||||
export interface WorldProps {
|
||||
name: string
|
||||
title: string
|
||||
iconBase64?: string
|
||||
size?: number
|
||||
lastPlayed?: number
|
||||
isFocused?: boolean
|
||||
onFocus?: (name: string) => void
|
||||
iconSrc?: string
|
||||
detail?: string
|
||||
formattedTextOverride?: string
|
||||
worldNameRight?: string
|
||||
onFocus?: (name: string) => void
|
||||
onInteraction?(interaction: 'enter' | 'space')
|
||||
}
|
||||
|
||||
const World = ({ name, isFocused, title, lastPlayed, size, detail = '', onFocus, onInteraction, iconBase64 }: WorldProps) => {
|
||||
const World = ({ name, isFocused, title, lastPlayed, size, detail = '', onFocus, onInteraction, iconSrc, formattedTextOverride, worldNameRight }: WorldProps) => {
|
||||
const timeRelativeFormatted = useMemo(() => {
|
||||
if (!lastPlayed) return
|
||||
if (!lastPlayed) return ''
|
||||
const formatter = new Intl.RelativeTimeFormat('en', { numeric: 'auto' })
|
||||
const diff = Date.now() - lastPlayed
|
||||
const minutes = Math.floor(diff / 1000 / 60)
|
||||
|
|
@ -38,7 +41,7 @@ const World = ({ name, isFocused, title, lastPlayed, size, detail = '', onFocus,
|
|||
return formatter.format(-minutes, 'minute')
|
||||
}, [lastPlayed])
|
||||
const sizeFormatted = useMemo(() => {
|
||||
if (!size) return
|
||||
if (!size) return ''
|
||||
return filesize(size)
|
||||
}, [size])
|
||||
|
||||
|
|
@ -48,18 +51,29 @@ const World = ({ name, isFocused, title, lastPlayed, size, detail = '', onFocus,
|
|||
onInteraction?.(e.code === 'Enter' ? 'enter' : 'space')
|
||||
}
|
||||
}} onDoubleClick={() => onInteraction?.('enter')}>
|
||||
<img className={`${styles.world_image} ${iconBase64 ? '' : styles.image_missing}`} src={iconBase64 ? `data:image/png;base64,${iconBase64}` : missingWorldPreview} alt='world preview' />
|
||||
<img className={`${styles.world_image} ${iconSrc ? '' : styles.image_missing}`} src={iconSrc ?? missingWorldPreview} alt='world preview' />
|
||||
<div className={styles.world_info}>
|
||||
<div className={styles.world_title} title='level.dat world name'>{title}</div>
|
||||
<div className={styles.world_info_description_line}>{timeRelativeFormatted} {detail.slice(-30)}</div>
|
||||
<div className={styles.world_info_description_line}>{sizeFormatted}</div>
|
||||
<div className={styles.world_title}>
|
||||
<div>{title}</div>
|
||||
<div className={styles.world_title_right}>{worldNameRight}</div>
|
||||
</div>
|
||||
{formattedTextOverride ? <div className={styles.world_info_formatted}>
|
||||
<MessageFormattedString message={formattedTextOverride} />
|
||||
</div> :
|
||||
<>
|
||||
<div className={styles.world_info_description_line}>{timeRelativeFormatted} {detail.slice(-30)}</div>
|
||||
<div className={styles.world_info_description_line}>{sizeFormatted}</div>
|
||||
</>}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
interface Props {
|
||||
worldData: WorldProps[] | null // null means loading
|
||||
providers: Record<string, string>
|
||||
serversLayout?: boolean
|
||||
firstRowChildrenOverride?: React.ReactNode
|
||||
searchRowChildrenOverride?: React.ReactNode
|
||||
providers?: Record<string, string>
|
||||
activeProvider?: string
|
||||
setActiveProvider?: (provider: string) => void
|
||||
providerActions?: Record<string, (() => void) | undefined | JSX.Element>
|
||||
|
|
@ -74,9 +88,24 @@ interface Props {
|
|||
onGeneralAction (action: 'cancel' | 'create'): void
|
||||
}
|
||||
|
||||
export default ({ worldData, onGeneralAction, onWorldAction, activeProvider, setActiveProvider, providerActions, providers, disabledProviders, error, isReadonly, warning, warningAction, warningActionLabel }: Props) => {
|
||||
export default ({
|
||||
worldData,
|
||||
onGeneralAction,
|
||||
onWorldAction,
|
||||
firstRowChildrenOverride,
|
||||
serversLayout,
|
||||
searchRowChildrenOverride,
|
||||
activeProvider,
|
||||
setActiveProvider,
|
||||
providerActions,
|
||||
providers = {},
|
||||
disabledProviders,
|
||||
error,
|
||||
isReadonly,
|
||||
warning, warningAction, warningActionLabel
|
||||
}: Props) => {
|
||||
const containerRef = useRef<any>()
|
||||
const firstButton = useRef<HTMLButtonElement>(null!)
|
||||
const firstButton = useRef<HTMLButtonElement>(null)
|
||||
|
||||
useTypedEventListener(window, 'keydown', (e) => {
|
||||
if (e.code === 'ArrowDown' || e.code === 'ArrowUp') {
|
||||
|
|
@ -100,10 +129,10 @@ export default ({ worldData, onGeneralAction, onWorldAction, activeProvider, set
|
|||
return <div ref={containerRef}>
|
||||
<div className="dirt-bg" />
|
||||
<div className={classNames('fullscreen', styles.root)}>
|
||||
<div style={{ display: 'flex', flexDirection: 'column' }}>
|
||||
<span className={classNames('screen-title', styles.title)}>Select Saved World</span>
|
||||
<span className={classNames('screen-title', styles.title)}>{serversLayout ? 'Join Java Servers' : 'Select Saved World'}</span>
|
||||
{searchRowChildrenOverride || <div style={{ display: 'flex', flexDirection: 'column' }}>
|
||||
<Input autoFocus value={search} onChange={({ target: { value } }) => setSearch(value)} />
|
||||
</div>
|
||||
</div>}
|
||||
<div className={classNames(styles.content, !worldData && styles.content_loading)}>
|
||||
<Tabs tabs={Object.keys(providers)} disabledTabs={disabledProviders} activeTab={activeProvider ?? ''} labels={providers} onTabChange={(tab) => {
|
||||
setActiveProvider?.(tab as any)
|
||||
|
|
@ -147,15 +176,17 @@ export default ({ worldData, onGeneralAction, onWorldAction, activeProvider, set
|
|||
}
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', minWidth: 400 }}>
|
||||
<div>
|
||||
<Button rootRef={firstButton} disabled={!focusedWorld} onClick={() => onWorldAction('load', focusedWorld)}>LOAD WORLD</Button>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', minWidth: 400, paddingBottom: 3 }}>
|
||||
{firstRowChildrenOverride || <div>
|
||||
<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>
|
||||
<Button style={{ width: 100 }} disabled={!focusedWorld} onClick={() => onWorldAction('export', focusedWorld)}>Export</Button>
|
||||
{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>
|
||||
<Button style={{ width: 100 }} /* disabled={!focusedWorld} */ onClick={() => onWorldAction('edit', focusedWorld)} disabled>Edit</Button>
|
||||
{serversLayout ?
|
||||
<Button style={{ width: 100 }} onClick={() => onGeneralAction('create')}>Add</Button> :
|
||||
<Button style={{ width: 100 }} /* disabled={!focusedWorld} */ onClick={() => onWorldAction('edit', focusedWorld)} disabled>Edit</Button>}
|
||||
<Button style={{ width: 100 }} onClick={() => onGeneralAction('cancel')}>Cancel</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@ export const readWorlds = (abortController: AbortController) => {
|
|||
title: levelName ?? folder,
|
||||
lastPlayed: levelDat.LastPlayed && longArrayToNumber(levelDat.LastPlayed),
|
||||
detail: `${levelDat.Version?.Name ?? 'unknown version'}, ${folder}`,
|
||||
iconBase64,
|
||||
iconSrc: iconBase64 ? `data:image/png;base64,${iconBase64}` : undefined,
|
||||
size,
|
||||
} satisfies WorldProps
|
||||
}))).filter((x, i) => {
|
||||
|
|
|
|||
|
|
@ -1,39 +1,40 @@
|
|||
.container {
|
||||
position: relative;
|
||||
width: 200px;
|
||||
height: 20px;
|
||||
background: black;
|
||||
border: 1px solid grey;
|
||||
box-sizing: content-box;
|
||||
position: relative;
|
||||
width: 200px;
|
||||
height: 20px;
|
||||
background: black;
|
||||
border: 1px solid grey;
|
||||
box-sizing: content-box;
|
||||
}
|
||||
|
||||
.input {
|
||||
position: relative;
|
||||
outline: none;
|
||||
border: none;
|
||||
background: none;
|
||||
left: 1px;
|
||||
width: calc(100% - 2px);
|
||||
height: 100%;
|
||||
font-family: minecraft, mojangles, monospace;
|
||||
font-size: 10px;
|
||||
color: white;
|
||||
text-shadow: 1px 1px #222;
|
||||
position: relative;
|
||||
outline: none;
|
||||
border: none;
|
||||
background: none;
|
||||
left: 1px;
|
||||
width: calc(100% - 2px);
|
||||
height: 100%;
|
||||
font-family: minecraft, mojangles, monospace;
|
||||
font-size: 10px;
|
||||
color: white;
|
||||
text-shadow: 1px 1px #222;
|
||||
padding-left: 2px;
|
||||
}
|
||||
|
||||
.container:hover,
|
||||
.container:focus-within {
|
||||
border-color: white;
|
||||
}
|
||||
.container:focus-within {
|
||||
border-color: white;
|
||||
}
|
||||
|
||||
input::-webkit-outer-spin-button,
|
||||
input::-webkit-inner-spin-button {
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
input::-webkit-outer-spin-button,
|
||||
input::-webkit-inner-spin-button {
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Firefox */
|
||||
input[type=number] {
|
||||
appearance: textfield;
|
||||
-moz-appearance: textfield;
|
||||
}
|
||||
/* Firefox */
|
||||
input[type=number] {
|
||||
appearance: textfield;
|
||||
-moz-appearance: textfield;
|
||||
}
|
||||
|
|
|
|||
6
src/react/simpleHooks.ts
Normal file
6
src/react/simpleHooks.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
import { useMedia } from 'react-use'
|
||||
|
||||
const SMALL_SCREEN_MEDIA = '@media (max-width: 440px)'
|
||||
export const useIsSmallWidth = () => {
|
||||
return useMedia(SMALL_SCREEN_MEDIA.replace('@media ', ''))
|
||||
}
|
||||
|
|
@ -28,11 +28,25 @@
|
|||
display: flex;
|
||||
outline: none;
|
||||
}
|
||||
.world_title {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
.world_title_right {
|
||||
color: #999;
|
||||
font-size: 9px;
|
||||
}
|
||||
.world_info {
|
||||
margin-left: 3px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-size: 11px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.world_info_formatted {
|
||||
font-size: 10px;
|
||||
white-space: pre;
|
||||
}
|
||||
.world_info_description_line {
|
||||
color: #999;
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ import TouchAreasControlsProvider from './react/TouchAreasControlsProvider'
|
|||
import NotificationProvider, { showNotification } from './react/NotificationProvider'
|
||||
import HotbarRenderApp from './react/HotbarRenderApp'
|
||||
import Crosshair from './react/Crosshair'
|
||||
import ServersListProvider from './react/ServersListProvider'
|
||||
|
||||
const RobustPortal = ({ children, to }) => {
|
||||
return createPortal(<PerComponentErrorBoundary>{children}</PerComponentErrorBoundary>, to)
|
||||
|
|
@ -139,6 +140,7 @@ const App = () => {
|
|||
<CreateWorldProvider />
|
||||
<AppStatusProvider />
|
||||
<SelectOption />
|
||||
<ServersListProvider />
|
||||
<OptionsRenderApp />
|
||||
<MainMenuRenderApp />
|
||||
<NotificationProvider />
|
||||
|
|
|
|||
|
|
@ -2,10 +2,10 @@
|
|||
|
||||
import { subscribeKey } from 'valtio/utils'
|
||||
import { WorldRendererThree } from 'prismarine-viewer/viewer/lib/worldrendererThree'
|
||||
import { isMobile } from 'prismarine-viewer/viewer/lib/simpleUtils'
|
||||
import { options, watchValue } from './optionsStorage'
|
||||
import { reloadChunks } from './utils'
|
||||
import { miscUiState } from './globalState'
|
||||
import { isMobile } from './menus/components/common'
|
||||
|
||||
subscribeKey(options, 'renderDistance', reloadChunks)
|
||||
subscribeKey(options, 'multiplayerRenderDistance', reloadChunks)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue