feat: refactor all app storage managment (#310)
This commit is contained in:
parent
da35cfb8a2
commit
36bf18b02f
15 changed files with 526 additions and 305 deletions
|
|
@ -1,6 +1,7 @@
|
|||
import { disabledSettings, options, qsOptions } from './optionsStorage'
|
||||
import { miscUiState } from './globalState'
|
||||
import { setLoadingScreenStatus } from './appStatus'
|
||||
import { setStorageDataOnAppConfigLoad } from './react/appStorageProvider'
|
||||
|
||||
export type AppConfig = {
|
||||
// defaultHost?: string
|
||||
|
|
@ -42,6 +43,8 @@ export const loadAppConfig = (appConfig: AppConfig) => {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
setStorageDataOnAppConfigLoad()
|
||||
}
|
||||
|
||||
export const isBundledConfigUsed = !!process.env.INLINED_APP_CONFIG
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import { getFixedFilesize } from './downloadAndOpenFile'
|
|||
import { packetsReplayState } from './react/state/packetsReplayState'
|
||||
import { createFullScreenProgressReporter } from './core/progressReporter'
|
||||
import { showNotification } from './react/NotificationProvider'
|
||||
import { resetAppStorage } from './react/appStorageProvider'
|
||||
const { GoogleDriveFileSystem } = require('google-drive-browserfs/src/backends/GoogleDrive')
|
||||
|
||||
browserfs.install(window)
|
||||
|
|
@ -620,24 +621,13 @@ export const openWorldZip = async (...args: Parameters<typeof openWorldZipInner>
|
|||
}
|
||||
}
|
||||
|
||||
export const resetLocalStorageWorld = () => {
|
||||
for (const key of Object.keys(localStorage)) {
|
||||
if (/^[\da-fA-F]{8}(?:\b-[\da-fA-F]{4}){3}\b-[\da-fA-F]{12}$/g.test(key) || key === '/') {
|
||||
localStorage.removeItem(key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const resetLocalStorageWithoutWorld = () => {
|
||||
for (const key of Object.keys(localStorage)) {
|
||||
if (!/^[\da-fA-F]{8}(?:\b-[\da-fA-F]{4}){3}\b-[\da-fA-F]{12}$/g.test(key) && key !== '/') {
|
||||
localStorage.removeItem(key)
|
||||
}
|
||||
}
|
||||
export const resetLocalStorage = () => {
|
||||
resetOptions()
|
||||
resetAppStorage()
|
||||
}
|
||||
|
||||
window.resetLocalStorageWorld = resetLocalStorageWorld
|
||||
window.resetLocalStorage = resetLocalStorage
|
||||
|
||||
export const openFilePicker = (specificCase?: 'resourcepack') => {
|
||||
// create and show input picker
|
||||
let picker: HTMLInputElement = document.body.querySelector('input#file-zip-picker')!
|
||||
|
|
|
|||
|
|
@ -25,11 +25,12 @@ import { showNotification } from './react/NotificationProvider'
|
|||
import { lastConnectOptions } from './react/AppStatusProvider'
|
||||
import { onCameraMove, onControInit } from './cameraRotationControls'
|
||||
import { createNotificationProgressReporter } from './core/progressReporter'
|
||||
import { appStorage } from './react/appStorageProvider'
|
||||
|
||||
|
||||
export const customKeymaps = proxy(JSON.parse(localStorage.keymap || '{}')) as UserOverridesConfig
|
||||
export const customKeymaps = proxy(appStorage.keybindings)
|
||||
subscribe(customKeymaps, () => {
|
||||
localStorage.keymap = JSON.stringify(customKeymaps)
|
||||
appStorage.keybindings = customKeymaps
|
||||
})
|
||||
|
||||
const controlOptions = {
|
||||
|
|
|
|||
|
|
@ -3,19 +3,21 @@ import { useSnapshot } from 'valtio'
|
|||
import { openURL } from 'renderer/viewer/lib/simpleUtils'
|
||||
import { noCase } from 'change-case'
|
||||
import { gameAdditionalState, miscUiState, openOptionsMenu, showModal } from './globalState'
|
||||
import { AppOptions, options } from './optionsStorage'
|
||||
import { AppOptions, getChangedSettings, options, resetOptions } from './optionsStorage'
|
||||
import Button from './react/Button'
|
||||
import { OptionMeta, OptionSlider } from './react/OptionsItems'
|
||||
import Slider from './react/Slider'
|
||||
import { getScreenRefreshRate } from './utils'
|
||||
import { setLoadingScreenStatus } from './appStatus'
|
||||
import { openFilePicker, resetLocalStorageWithoutWorld } from './browserfs'
|
||||
import { openFilePicker, resetLocalStorage } from './browserfs'
|
||||
import { completeResourcepackPackInstall, getResourcePackNames, resourcePackState, uninstallResourcePack } from './resourcePack'
|
||||
import { downloadPacketsReplay, packetsRecordingState } from './packetsReplay/packetsReplayLegacy'
|
||||
import { showOptionsModal } from './react/SelectOption'
|
||||
import { showInputsModal, showOptionsModal } from './react/SelectOption'
|
||||
import supportedVersions from './supportedVersions.mjs'
|
||||
import { getVersionAutoSelect } from './connect'
|
||||
import { createNotificationProgressReporter } from './core/progressReporter'
|
||||
import { customKeymaps } from './controls'
|
||||
import { appStorage } from './react/appStorageProvider'
|
||||
|
||||
export const guiOptionsScheme: {
|
||||
[t in OptionsGroupType]: Array<{ [K in keyof AppOptions]?: Partial<OptionMeta<AppOptions[K]>> } & { custom? }>
|
||||
|
|
@ -450,9 +452,19 @@ export const guiOptionsScheme: {
|
|||
return <Button
|
||||
inScreen
|
||||
onClick={() => {
|
||||
if (confirm('Are you sure you want to reset all settings?')) resetLocalStorageWithoutWorld()
|
||||
if (confirm('Are you sure you want to reset all settings?')) resetOptions()
|
||||
}}
|
||||
>Reset all settings</Button>
|
||||
>Reset settings</Button>
|
||||
},
|
||||
},
|
||||
{
|
||||
custom () {
|
||||
return <Button
|
||||
inScreen
|
||||
onClick={() => {
|
||||
if (confirm('Are you sure you want to remove all data (settings, keybindings, servers, username, auth, proxies)?')) resetLocalStorage()
|
||||
}}
|
||||
>Remove all data</Button>
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
@ -460,6 +472,11 @@ export const guiOptionsScheme: {
|
|||
return <Category>Developer</Category>
|
||||
},
|
||||
},
|
||||
{
|
||||
custom () {
|
||||
return <Button label='Export/Import...' onClick={() => openOptionsMenu('export-import')} inScreen />
|
||||
}
|
||||
},
|
||||
{
|
||||
custom () {
|
||||
const { active } = useSnapshot(packetsRecordingState)
|
||||
|
|
@ -521,8 +538,91 @@ export const guiOptionsScheme: {
|
|||
},
|
||||
},
|
||||
],
|
||||
'export-import': [
|
||||
{
|
||||
custom () {
|
||||
return <Category>Export/Import Data</Category>
|
||||
}
|
||||
},
|
||||
{
|
||||
custom () {
|
||||
return <Button
|
||||
inScreen
|
||||
disabled={true}
|
||||
onClick={() => {}}
|
||||
>Import Data</Button>
|
||||
}
|
||||
},
|
||||
{
|
||||
custom () {
|
||||
return <Button
|
||||
inScreen
|
||||
onClick={async () => {
|
||||
const data = await showInputsModal('Export Profile', {
|
||||
profileName: {
|
||||
type: 'text',
|
||||
},
|
||||
exportSettings: {
|
||||
type: 'checkbox',
|
||||
defaultValue: true,
|
||||
},
|
||||
exportKeybindings: {
|
||||
type: 'checkbox',
|
||||
defaultValue: true,
|
||||
},
|
||||
exportServers: {
|
||||
type: 'checkbox',
|
||||
defaultValue: true,
|
||||
},
|
||||
saveUsernameAndProxy: {
|
||||
type: 'checkbox',
|
||||
defaultValue: true,
|
||||
},
|
||||
})
|
||||
const fileName = `${data.profileName ? `${data.profileName}-` : ''}web-client-profile.json`
|
||||
const json = {
|
||||
_about: 'Minecraft Web Client (mcraft.fun) Profile',
|
||||
...data.exportSettings ? {
|
||||
options: getChangedSettings(),
|
||||
} : {},
|
||||
...data.exportKeybindings ? {
|
||||
keybindings: customKeymaps,
|
||||
} : {},
|
||||
...data.saveUsernameAndProxy ? {
|
||||
username: appStorage.username,
|
||||
proxy: appStorage.proxiesData?.selected,
|
||||
} : {},
|
||||
}
|
||||
const blob = new Blob([JSON.stringify(json, null, 2)], { type: 'application/json' })
|
||||
const url = URL.createObjectURL(blob)
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = fileName
|
||||
a.click()
|
||||
URL.revokeObjectURL(url)
|
||||
}}
|
||||
>Export Data</Button>
|
||||
}
|
||||
},
|
||||
{
|
||||
custom () {
|
||||
return <Button
|
||||
inScreen
|
||||
disabled
|
||||
>Export Worlds</Button>
|
||||
}
|
||||
},
|
||||
{
|
||||
custom () {
|
||||
return <Button
|
||||
inScreen
|
||||
disabled
|
||||
>Export Resource Pack</Button>
|
||||
}
|
||||
}
|
||||
],
|
||||
}
|
||||
export type OptionsGroupType = 'main' | 'render' | 'interface' | 'controls' | 'sound' | 'advanced' | 'VR'
|
||||
export type OptionsGroupType = 'main' | 'render' | 'interface' | 'controls' | 'sound' | 'advanced' | 'VR' | 'export-import'
|
||||
|
||||
const Category = ({ children }) => <div style={{
|
||||
fontSize: 9,
|
||||
|
|
|
|||
|
|
@ -1,11 +1,9 @@
|
|||
// todo implement async options storage
|
||||
|
||||
import { proxy, subscribe } from 'valtio/vanilla'
|
||||
// weird webpack configuration bug: it cant import valtio/utils in this file
|
||||
import { subscribeKey } from 'valtio/utils'
|
||||
import { omitObj } from '@zardoy/utils'
|
||||
import { appQueryParamsArray } from './appParams'
|
||||
import type { AppConfig } from './appConfig'
|
||||
import { appStorage } from './react/appStorageProvider'
|
||||
|
||||
const isDev = process.env.NODE_ENV === 'development'
|
||||
const initialAppConfig = process.env?.INLINED_APP_CONFIG as AppConfig ?? {}
|
||||
|
|
@ -164,12 +162,31 @@ const migrateOptions = (options: Partial<AppOptions & Record<string, any>>) => {
|
|||
|
||||
export type AppOptions = typeof defaultOptions
|
||||
|
||||
// when opening html file locally in browser, localStorage is shared between all ever opened html files, so we try to avoid conflicts
|
||||
const localStorageKey = process.env?.SINGLE_FILE_BUILD ? 'minecraftWebClientOptions' : 'options'
|
||||
const isDeepEqual = (a: any, b: any): boolean => {
|
||||
if (a === b) return true
|
||||
if (typeof a !== typeof b) return false
|
||||
if (typeof a !== 'object') return false
|
||||
if (a === null || b === null) return a === b
|
||||
if (Array.isArray(a) && Array.isArray(b)) {
|
||||
if (a.length !== b.length) return false
|
||||
return a.every((item, index) => isDeepEqual(item, b[index]))
|
||||
}
|
||||
const keysA = Object.keys(a)
|
||||
const keysB = Object.keys(b)
|
||||
if (keysA.length !== keysB.length) return false
|
||||
return keysA.every(key => isDeepEqual(a[key], b[key]))
|
||||
}
|
||||
|
||||
export const getChangedSettings = () => {
|
||||
return Object.fromEntries(
|
||||
Object.entries(options).filter(([key, value]) => !isDeepEqual(defaultOptions[key], value))
|
||||
)
|
||||
}
|
||||
|
||||
export const options: AppOptions = proxy({
|
||||
...defaultOptions,
|
||||
...initialAppConfig.defaultSettings,
|
||||
...migrateOptions(JSON.parse(localStorage[localStorageKey] || '{}')),
|
||||
...migrateOptions(appStorage.options),
|
||||
...qsOptions
|
||||
})
|
||||
|
||||
|
|
@ -181,14 +198,14 @@ export const resetOptions = () => {
|
|||
|
||||
Object.defineProperty(window, 'debugChangedOptions', {
|
||||
get () {
|
||||
return Object.fromEntries(Object.entries(options).filter(([key, v]) => defaultOptions[key] !== v))
|
||||
return getChangedSettings()
|
||||
},
|
||||
})
|
||||
|
||||
subscribe(options, () => {
|
||||
// Don't save disabled settings to localStorage
|
||||
const saveOptions = omitObj(options, [...disabledSettings.value] as any)
|
||||
localStorage[localStorageKey] = JSON.stringify(saveOptions)
|
||||
appStorage.options = saveOptions
|
||||
})
|
||||
|
||||
type WatchValue = <T extends Record<string, any>>(proxy: T, callback: (p: T, isChanged: boolean) => void) => () => void
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { appQueryParams } from '../appParams'
|
|||
import { fetchServerStatus, isServerValid } from '../api/mcStatusApi'
|
||||
import { parseServerAddress } from '../parseServerAddress'
|
||||
import Screen from './Screen'
|
||||
import Input from './Input'
|
||||
import Input, { INPUT_LABEL_WIDTH, InputWithLabel } from './Input'
|
||||
import Button from './Button'
|
||||
import SelectGameVersion from './SelectGameVersion'
|
||||
import { usePassesScaledDimensions } from './UIProvider'
|
||||
|
|
@ -32,8 +32,6 @@ interface Props {
|
|||
allowAutoConnect?: boolean
|
||||
}
|
||||
|
||||
const ELEMENTS_WIDTH = 190
|
||||
|
||||
export default ({ onBack, onConfirm, title = 'Add a Server', initialData, parseQs, onQsConnect, placeholders, accounts, versions, allowAutoConnect }: Props) => {
|
||||
const isSmallHeight = !usePassesScaledDimensions(null, 350)
|
||||
const qsParamName = parseQs ? appQueryParams.name : undefined
|
||||
|
|
@ -256,20 +254,8 @@ export default ({ onBack, onConfirm, title = 'Add a Server', initialData, parseQ
|
|||
|
||||
const ButtonWrapper = ({ ...props }: React.ComponentProps<typeof Button>) => {
|
||||
props.style ??= {}
|
||||
props.style.width = ELEMENTS_WIDTH
|
||||
props.style.width = INPUT_LABEL_WIDTH
|
||||
return <Button {...props} />
|
||||
}
|
||||
|
||||
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 rootStyles={{ width: ELEMENTS_WIDTH }} {...props} />
|
||||
</div>
|
||||
}
|
||||
|
||||
const fallbackIfNotFound = (index: number) => (index === -1 ? undefined : index)
|
||||
|
|
|
|||
|
|
@ -90,11 +90,11 @@ div.chat-wrapper {
|
|||
::-webkit-scrollbar {
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
background-color: rgb(24, 24, 24);
|
||||
background-color: #272727;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background-color: rgb(50, 50, 50);
|
||||
background-color: #747474;
|
||||
}
|
||||
|
||||
.chat-completions-items>div {
|
||||
|
|
@ -160,7 +160,6 @@ input[type=text],
|
|||
padding-bottom: 1px;
|
||||
padding-left: 2px;
|
||||
padding-right: 2px;
|
||||
height: 15px;
|
||||
}
|
||||
|
||||
.chat-mobile-input-hidden {
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ interface Props extends Omit<React.ComponentProps<'input'>, 'width'> {
|
|||
width?: number
|
||||
}
|
||||
|
||||
export default ({ autoFocus, rootStyles, inputRef, validateInput, defaultValue, width, ...inputProps }: Props) => {
|
||||
const Input = ({ autoFocus, rootStyles, inputRef, validateInput, defaultValue, width, ...inputProps }: Props) => {
|
||||
if (width) rootStyles = { ...rootStyles, width }
|
||||
|
||||
const ref = useRef<HTMLInputElement>(null!)
|
||||
|
|
@ -51,3 +51,19 @@ export default ({ autoFocus, rootStyles, inputRef, validateInput, defaultValue,
|
|||
/>
|
||||
</div>
|
||||
}
|
||||
|
||||
export default Input
|
||||
|
||||
export const INPUT_LABEL_WIDTH = 190
|
||||
|
||||
export 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 rootStyles={{ width: INPUT_LABEL_WIDTH }} {...props} />
|
||||
</div>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -122,24 +122,6 @@ export default () => {
|
|||
singleplayerAvailable={singleplayerAvailable}
|
||||
connectToServerAction={() => showModal({ reactType: 'serversList' })}
|
||||
singleplayerAction={async () => {
|
||||
const oldFormatSave = fs.existsSync('./world/level.dat')
|
||||
if (oldFormatSave) {
|
||||
setLoadingScreenStatus('Migrating old save, don\'t close the page')
|
||||
try {
|
||||
await mkdirRecursive('/data/worlds/local')
|
||||
await copyFilesAsync('/world/', '/data/worlds/local')
|
||||
try {
|
||||
await removeFileRecursiveAsync('/world/')
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn(err)
|
||||
alert('Failed to migrate world from localStorage')
|
||||
} finally {
|
||||
setLoadingScreenStatus(undefined)
|
||||
}
|
||||
}
|
||||
showModal({ reactType: 'singleplayer' })
|
||||
}}
|
||||
githubAction={() => openGithub()}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,14 @@
|
|||
import { proxy, useSnapshot } from 'valtio'
|
||||
import { useEffect, useRef } from 'react'
|
||||
import { noCase } from 'change-case'
|
||||
import { titleCase } from 'title-case'
|
||||
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'
|
||||
import Input, { InputWithLabel } from './Input'
|
||||
|
||||
const state = proxy({
|
||||
title: '',
|
||||
|
|
@ -12,6 +16,8 @@ const state = proxy({
|
|||
showCancel: true,
|
||||
minecraftJsonMessage: null as null | Record<string, any>,
|
||||
behavior: 'resolve-close' as 'resolve-close' | 'close-resolve',
|
||||
inputs: {} as Record<string, InputOption>,
|
||||
inputsConfirmButton: ''
|
||||
})
|
||||
|
||||
let resolve
|
||||
|
|
@ -35,17 +41,63 @@ export const showOptionsModal = async <T extends string> (
|
|||
title,
|
||||
options,
|
||||
showCancel: cancel,
|
||||
minecraftJsonMessage: minecraftJsonMessageParsed
|
||||
minecraftJsonMessage: minecraftJsonMessageParsed,
|
||||
inputs: {},
|
||||
inputsConfirmButton: ''
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
type InputOption = {
|
||||
type: 'text' | 'checkbox'
|
||||
defaultValue?: string | boolean
|
||||
label?: string
|
||||
}
|
||||
export const showInputsModal = async <T extends Record<string, InputOption>>(
|
||||
title: string,
|
||||
inputs: T,
|
||||
{ cancel = true, minecraftJsonMessage }: { cancel?: boolean, minecraftJsonMessage? } = {}
|
||||
): Promise<{
|
||||
[K in keyof T]: T[K] extends { type: 'text' }
|
||||
? string
|
||||
: T[K] extends { type: 'checkbox' }
|
||||
? boolean
|
||||
: never
|
||||
}> => {
|
||||
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,
|
||||
inputs,
|
||||
showCancel: cancel,
|
||||
minecraftJsonMessage: minecraftJsonMessageParsed,
|
||||
options: [],
|
||||
inputsConfirmButton: 'Confirm'
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export default () => {
|
||||
const { title, options, showCancel, minecraftJsonMessage } = useSnapshot(state)
|
||||
const { title, options, showCancel, minecraftJsonMessage, inputs, inputsConfirmButton } = useSnapshot(state)
|
||||
const isModalActive = useIsModalActive('general-select')
|
||||
const inputValues = useRef({})
|
||||
|
||||
useEffect(() => {
|
||||
inputValues.current = Object.fromEntries(Object.entries(inputs).map(([key, input]) => [key, input.defaultValue ?? (input.type === 'checkbox' ? false : '')]))
|
||||
}, [inputs])
|
||||
|
||||
if (!isModalActive) return
|
||||
|
||||
const resolveClose = (value: string | undefined) => {
|
||||
const resolveClose = (value: any) => {
|
||||
if (state.behavior === 'resolve-close') {
|
||||
resolve(value)
|
||||
hideCurrentModal()
|
||||
|
|
@ -59,17 +111,66 @@ export default () => {
|
|||
{minecraftJsonMessage && <div style={{ textAlign: 'center', }}>
|
||||
<MessageFormattedString message={minecraftJsonMessage} />
|
||||
</div>}
|
||||
{options.map(option => <Button
|
||||
key={option} onClick={() => {
|
||||
resolveClose(option)
|
||||
}}
|
||||
>{option}
|
||||
</Button>)}
|
||||
{showCancel && <Button
|
||||
style={{ marginTop: 30 }} onClick={() => {
|
||||
resolveClose(undefined)
|
||||
}}
|
||||
>Cancel
|
||||
</Button>}
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 9 }}>
|
||||
{options.length > 0 && <div style={{ display: 'flex', flexDirection: 'column', gap: 5 }}>
|
||||
{options.map(option => <Button
|
||||
key={option} onClick={() => {
|
||||
resolveClose(option)
|
||||
}}
|
||||
>{option}
|
||||
</Button>)}
|
||||
</div>}
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
|
||||
{Object.entries(inputs).map(([key, input]) => {
|
||||
const label = input.label ?? titleCase(noCase(key))
|
||||
return <div key={key}>
|
||||
{input.type === 'text' && (
|
||||
<InputWithLabel
|
||||
label={label}
|
||||
autoFocus
|
||||
type='text'
|
||||
defaultValue={input.defaultValue as string}
|
||||
onChange={(e) => {
|
||||
inputValues.current[key] = e.target.value
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{input.type === 'checkbox' && (
|
||||
<label style={{ display: 'flex', alignItems: 'center', gap: 4, fontSize: 12 }}>
|
||||
<input
|
||||
type='checkbox'
|
||||
style={{ marginBottom: -1, }}
|
||||
defaultChecked={input.defaultValue as boolean}
|
||||
onChange={(e) => {
|
||||
inputValues.current[key] = e.target.checked
|
||||
}}
|
||||
/>
|
||||
{label}
|
||||
</label>
|
||||
)}
|
||||
</div>
|
||||
})}
|
||||
</div>
|
||||
{inputs && inputsConfirmButton && (
|
||||
<Button
|
||||
// style={{ marginTop: 30 }}
|
||||
onClick={() => {
|
||||
resolveClose(inputValues.current)
|
||||
}}
|
||||
>
|
||||
{inputsConfirmButton}
|
||||
</Button>
|
||||
)}
|
||||
{showCancel && (
|
||||
<Button
|
||||
// style={{ marginTop: 30 }}
|
||||
onClick={() => {
|
||||
resolveClose(undefined)
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</Screen>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,64 +1,61 @@
|
|||
import React from 'react'
|
||||
import React, { useMemo } from 'react'
|
||||
import { useSnapshot } from 'valtio'
|
||||
import { miscUiState } from '../globalState'
|
||||
import { appQueryParams } from '../appParams'
|
||||
import Singleplayer from './Singleplayer'
|
||||
import Input from './Input'
|
||||
import Button from './Button'
|
||||
import PixelartIcon, { pixelartIcons } from './PixelartIcon'
|
||||
|
||||
import Select from './Select'
|
||||
import { BaseServerInfo } from './AddServerOrConnect'
|
||||
import { useIsSmallWidth } from './simpleHooks'
|
||||
import { appStorage, SavedProxiesData, ServerHistoryEntry } from './appStorageProvider'
|
||||
|
||||
const getInitialProxies = () => {
|
||||
const proxies = [] as string[]
|
||||
if (miscUiState.appConfig?.defaultProxy) {
|
||||
proxies.push(miscUiState.appConfig.defaultProxy)
|
||||
}
|
||||
return proxies
|
||||
}
|
||||
|
||||
export const getCurrentProxy = (): string | undefined => {
|
||||
return appQueryParams.proxy ?? appStorage.proxiesData?.selected ?? getInitialProxies()[0]
|
||||
}
|
||||
|
||||
export const getCurrentUsername = () => {
|
||||
return appQueryParams.username ?? appStorage.username
|
||||
}
|
||||
|
||||
interface Props extends React.ComponentProps<typeof Singleplayer> {
|
||||
joinServer: (info: BaseServerInfo | string, additional: {
|
||||
shouldSave?: boolean
|
||||
index?: number
|
||||
}) => void
|
||||
initialProxies: SavedProxiesLocalStorage
|
||||
updateProxies: (proxies: SavedProxiesLocalStorage) => void
|
||||
username: string
|
||||
setUsername: (username: string) => void
|
||||
onProfileClick?: () => void
|
||||
setQuickConnectIp?: (ip: string) => void
|
||||
serverHistory?: Array<{
|
||||
ip: string
|
||||
versionOverride?: string
|
||||
numConnects: number
|
||||
}>
|
||||
}
|
||||
|
||||
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,
|
||||
onProfileClick,
|
||||
setQuickConnectIp,
|
||||
serverHistory,
|
||||
...props
|
||||
}: Props) => {
|
||||
const [proxies, setProxies] = React.useState(initialProxies)
|
||||
|
||||
const updateProxies = (newData: SavedProxiesLocalStorage) => {
|
||||
setProxies(newData)
|
||||
updateProxiesProp(newData)
|
||||
}
|
||||
|
||||
const snap = useSnapshot(appStorage)
|
||||
const username = useMemo(() => getCurrentUsername(), [appQueryParams.username, appStorage.username])
|
||||
const [serverIp, setServerIp] = React.useState('')
|
||||
const [save, setSave] = React.useState(true)
|
||||
const [activeHighlight, setActiveHighlight] = React.useState(undefined as 'quick-connect' | 'server-list' | undefined)
|
||||
|
||||
const updateProxies = (newData: SavedProxiesData) => {
|
||||
appStorage.proxiesData = newData
|
||||
}
|
||||
|
||||
const setUsername = (username: string) => {
|
||||
appStorage.username = username
|
||||
}
|
||||
|
||||
const getActiveHighlightStyles = (type: typeof activeHighlight) => {
|
||||
const styles: React.CSSProperties = {
|
||||
transition: 'filter 0.2s',
|
||||
|
|
@ -71,6 +68,8 @@ export default ({
|
|||
|
||||
const isSmallWidth = useIsSmallWidth()
|
||||
|
||||
const initialProxies = getInitialProxies()
|
||||
const proxiesData = snap.proxiesData ?? { proxies: initialProxies, selected: initialProxies[0] }
|
||||
return <Singleplayer
|
||||
{...props}
|
||||
firstRowChildrenOverride={<form
|
||||
|
|
@ -85,7 +84,6 @@ export default ({
|
|||
onMouseEnter={() => setActiveHighlight('quick-connect')}
|
||||
onMouseLeave={() => setActiveHighlight(undefined)}
|
||||
>
|
||||
{/* todo history */}
|
||||
<Input
|
||||
required
|
||||
placeholder='Quick Connect IP (:version)'
|
||||
|
|
@ -102,8 +100,8 @@ export default ({
|
|||
spellCheck="false"
|
||||
/>
|
||||
<datalist id="server-history">
|
||||
{serverHistory?.map((server) => (
|
||||
<option key={server.ip} value={`${server.ip}${server.versionOverride ? `:${server.versionOverride}` : ''}`} />
|
||||
{[...(snap.serversHistory ?? [])].sort((a, b) => b.numConnects - a.numConnects).map((server) => (
|
||||
<option key={server.ip} value={`${server.ip}${server.version ? `:${server.version}` : ''}`} />
|
||||
))}
|
||||
</datalist>
|
||||
<label style={{ fontSize: 10, display: 'flex', alignItems: 'center', gap: 5, height: '100%', marginTop: '-1px' }}>
|
||||
|
|
@ -126,10 +124,10 @@ export default ({
|
|||
? <PixelartIcon iconName={pixelartIcons.server} styles={{ fontSize: 14, color: 'lightgray', marginLeft: 2 }} onClick={onProfileClick} />
|
||||
: <span style={{ color: 'lightgray', fontSize: 14 }}>Proxy:</span>}
|
||||
<Select
|
||||
initialOptions={proxies.proxies.map(p => { return { value: p, label: p } })}
|
||||
defaultValue={{ value: proxies.selected, label: proxies.selected }}
|
||||
initialOptions={proxiesData.proxies.map(p => { return { value: p, label: p } })}
|
||||
defaultValue={{ value: proxiesData.selected, label: proxiesData.selected }}
|
||||
updateOptions={(newSel) => {
|
||||
updateProxies({ proxies: [...proxies.proxies], selected: newSel })
|
||||
updateProxies({ proxies: [...proxiesData.proxies], selected: newSel })
|
||||
}}
|
||||
containerStyle={{
|
||||
width: isSmallWidth ? 140 : 180,
|
||||
|
|
@ -139,6 +137,7 @@ export default ({
|
|||
<Input
|
||||
rootStyles={{ width: 80 }}
|
||||
value={username}
|
||||
disabled={appQueryParams.username !== undefined}
|
||||
onChange={({ target: { value } }) => setUsername(value)}
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
|
|
|
|||
|
|
@ -8,13 +8,14 @@ import { appQueryParams } from '../appParams'
|
|||
import { fetchServerStatus, isServerValid } from '../api/mcStatusApi'
|
||||
import { getServerInfo } from '../mineflayer/mc-protocol'
|
||||
import { parseServerAddress } from '../parseServerAddress'
|
||||
import ServersList from './ServersList'
|
||||
import ServersList, { getCurrentProxy, getCurrentUsername } from './ServersList'
|
||||
import AddServerOrConnect, { BaseServerInfo } from './AddServerOrConnect'
|
||||
import { useDidUpdateEffect } from './utils'
|
||||
import { useIsModalActive } from './utilsApp'
|
||||
import { showOptionsModal } from './SelectOption'
|
||||
import { useCopyKeybinding } from './simpleHooks'
|
||||
import { AuthenticatedAccount, getInitialServersList, getServerConnectionHistory, setNewServersList, StoreServerItem } from './serversStorage'
|
||||
import { AuthenticatedAccount, getInitialServersList, getServerConnectionHistory, setNewServersList } from './serversStorage'
|
||||
import { appStorage, StoreServerItem } from './appStorageProvider'
|
||||
|
||||
type AdditionalDisplayData = {
|
||||
textNameRightGrayed: string
|
||||
|
|
@ -25,19 +26,6 @@ type AdditionalDisplayData = {
|
|||
}
|
||||
|
||||
const serversListQs = appQueryParams.serversList
|
||||
const proxyQs = appQueryParams.proxy
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// todo move to base
|
||||
const normalizeIp = (ip: string) => ip.replace(/https?:\/\//, '').replace(/\/(:|$)/, '')
|
||||
|
|
@ -46,45 +34,30 @@ const FETCH_DELAY = 100 // ms between each request
|
|||
const MAX_CONCURRENT_REQUESTS = 10
|
||||
|
||||
const Inner = ({ hidden, customServersList }: { hidden?: boolean, customServersList?: string[] }) => {
|
||||
const [proxies, setProxies] = useState<readonly string[]>(localStorage['proxies'] ? JSON.parse(localStorage['proxies']) : getInitialProxies())
|
||||
const [selectedProxy, setSelectedProxy] = useState(proxyQs ?? 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 { authenticatedAccounts } = useSnapshot(appStorage)
|
||||
const [quickConnectIp, setQuickConnectIp] = useState('')
|
||||
const [selectedIndex, setSelectedIndex] = useState(0)
|
||||
|
||||
// Save username to localStorage when component mounts if it doesn't exist
|
||||
useEffect(() => {
|
||||
if (!localStorage['username']) {
|
||||
localStorage.setItem('username', defaultUsername)
|
||||
}
|
||||
}, [])
|
||||
const { serversList: savedServersList } = useSnapshot(appStorage)
|
||||
|
||||
const setAuthenticatedAccounts = (newState: typeof authenticatedAccounts) => {
|
||||
_setAuthenticatedAccounts(newState)
|
||||
localStorage.setItem('authenticatedAccounts', JSON.stringify(newState))
|
||||
}
|
||||
const serversListDisplay = useMemo(() => {
|
||||
return (
|
||||
customServersList
|
||||
? customServersList.map((row): StoreServerItem => {
|
||||
const [ip, name] = row.split(' ')
|
||||
const [_ip, _port, version] = ip.split(':')
|
||||
return {
|
||||
ip,
|
||||
versionOverride: version,
|
||||
name,
|
||||
}
|
||||
})
|
||||
: [...getInitialServersList()]
|
||||
)
|
||||
}, [customServersList, savedServersList])
|
||||
|
||||
const setDefaultUsername = (newState: typeof defaultUsername) => {
|
||||
_setDefaultUsername(newState)
|
||||
localStorage.setItem('username', newState)
|
||||
}
|
||||
|
||||
const saveNewProxy = () => {
|
||||
if (!selectedProxy || proxyQs) return
|
||||
localStorage.setItem('selectedProxy', selectedProxy)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (proxies.length) {
|
||||
localStorage.setItem('proxies', JSON.stringify(proxies))
|
||||
}
|
||||
saveNewProxy()
|
||||
}, [proxies])
|
||||
|
||||
const [serversList, setServersList] = useState<StoreServerItem[]>(() => (customServersList ? [] : getInitialServersList()))
|
||||
const [additionalData, setAdditionalData] = useState<Record<string, AdditionalDisplayData>>({})
|
||||
const [additionalServerData, setAdditionalServerData] = useState<Record<string, AdditionalDisplayData>>({})
|
||||
|
||||
// Add keyboard handler for moving servers
|
||||
useEffect(() => {
|
||||
|
|
@ -92,49 +65,31 @@ const Inner = ({ hidden, customServersList }: { hidden?: boolean, customServersL
|
|||
if (['input', 'textarea', 'select'].includes((e.target as HTMLElement)?.tagName?.toLowerCase())) return
|
||||
if (!e.shiftKey || selectedIndex === undefined) return
|
||||
if (e.key !== 'ArrowUp' && e.key !== 'ArrowDown') return
|
||||
if (customServersList) return
|
||||
e.preventDefault()
|
||||
e.stopImmediatePropagation()
|
||||
|
||||
const newIndex = e.key === 'ArrowUp'
|
||||
? Math.max(0, selectedIndex - 1)
|
||||
: Math.min(serversList.length - 1, selectedIndex + 1)
|
||||
: Math.min(serversListDisplay.length - 1, selectedIndex + 1)
|
||||
|
||||
if (newIndex === selectedIndex) return
|
||||
|
||||
// Move server in the list
|
||||
const newList = [...serversList]
|
||||
const newList = [...serversListDisplay]
|
||||
const oldItem = newList[selectedIndex]
|
||||
newList[selectedIndex] = newList[newIndex]
|
||||
newList[newIndex] = oldItem
|
||||
|
||||
setServersList(newList)
|
||||
appStorage.serversList = newList
|
||||
setSelectedIndex(newIndex)
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', handleKeyDown)
|
||||
return () => document.removeEventListener('keydown', handleKeyDown)
|
||||
}, [selectedIndex, serversList])
|
||||
}, [selectedIndex, serversListDisplay])
|
||||
|
||||
useEffect(() => {
|
||||
if (customServersList) {
|
||||
setServersList(customServersList.map(row => {
|
||||
const [ip, name] = row.split(' ')
|
||||
const [_ip, _port, version] = ip.split(':')
|
||||
return {
|
||||
ip,
|
||||
versionOverride: version,
|
||||
name,
|
||||
}
|
||||
}))
|
||||
}
|
||||
}, [customServersList])
|
||||
|
||||
useDidUpdateEffect(() => {
|
||||
// save data only on user changes
|
||||
setNewServersList(serversList)
|
||||
}, [serversList])
|
||||
|
||||
const serversListSorted = useMemo(() => serversList.map((server, index) => ({ ...server, index })), [serversList])
|
||||
const serversListSorted = useMemo(() => serversListDisplay.map((server, index) => ({ ...server, index })), [serversListDisplay])
|
||||
// by lastJoined
|
||||
// const serversListSorted = useMemo(() => {
|
||||
// return serversList.map((server, index) => ({ ...server, index })).sort((a, b) => (b.lastJoined ?? 0) - (a.lastJoined ?? 0))
|
||||
|
|
@ -185,7 +140,7 @@ const Inner = ({ hidden, customServersList }: { hidden?: boolean, customServersL
|
|||
data = await fetchServerStatus(server.ip/* , signal */) // DONT ADD SIGNAL IT WILL CRUSH JS RUNTIME
|
||||
}
|
||||
if (data) {
|
||||
setAdditionalData(old => ({
|
||||
setAdditionalServerData(old => ({
|
||||
...old,
|
||||
[server.ip]: data
|
||||
}))
|
||||
|
|
@ -224,7 +179,7 @@ const Inner = ({ hidden, customServersList }: { hidden?: boolean, customServersL
|
|||
}, [isEditScreenModal])
|
||||
|
||||
useCopyKeybinding(() => {
|
||||
const item = serversList[selectedIndex]
|
||||
const item = serversListDisplay[selectedIndex]
|
||||
if (!item) return
|
||||
let str = `${item.ip}`
|
||||
if (item.versionOverride) {
|
||||
|
|
@ -236,8 +191,8 @@ const Inner = ({ hidden, customServersList }: { hidden?: boolean, customServersL
|
|||
const editModalJsx = isEditScreenModal ? <AddServerOrConnect
|
||||
allowAutoConnect={miscUiState.appConfig?.allowAutoConnect}
|
||||
placeholders={{
|
||||
proxyOverride: selectedProxy,
|
||||
usernameOverride: defaultUsername,
|
||||
proxyOverride: getCurrentProxy(),
|
||||
usernameOverride: getCurrentUsername(),
|
||||
}}
|
||||
parseQs={!serverEditScreen}
|
||||
onBack={() => {
|
||||
|
|
@ -247,12 +202,13 @@ const Inner = ({ hidden, customServersList }: { hidden?: boolean, customServersL
|
|||
if (!serverEditScreen) return
|
||||
if (serverEditScreen === true) {
|
||||
const server: StoreServerItem = { ...info, lastJoined: Date.now() } // so it appears first
|
||||
setServersList(old => [...old, server])
|
||||
appStorage.serversList = [...(appStorage.serversList ?? []), server]
|
||||
} else {
|
||||
const index = serversList.indexOf(serverEditScreen)
|
||||
const { lastJoined } = serversList[index]
|
||||
serversList[index] = { ...info, lastJoined }
|
||||
setServersList([...serversList])
|
||||
const index = appStorage.serversList?.indexOf(serverEditScreen)
|
||||
if (index !== undefined) {
|
||||
const { lastJoined } = appStorage.serversList![index]
|
||||
appStorage.serversList![index] = { ...info, lastJoined }
|
||||
}
|
||||
}
|
||||
setServerEditScreen(null)
|
||||
}}
|
||||
|
|
@ -262,9 +218,9 @@ const Inner = ({ hidden, customServersList }: { hidden?: boolean, customServersL
|
|||
} : serverEditScreen}
|
||||
onQsConnect={(info) => {
|
||||
const connectOptions: ConnectOptions = {
|
||||
username: info.usernameOverride || defaultUsername,
|
||||
username: info.usernameOverride || getCurrentUsername() || '',
|
||||
server: normalizeIp(info.ip),
|
||||
proxy: info.proxyOverride || selectedProxy,
|
||||
proxy: info.proxyOverride || getCurrentProxy(),
|
||||
botVersion: info.versionOverride,
|
||||
ignoreQs: true,
|
||||
}
|
||||
|
|
@ -304,11 +260,11 @@ const Inner = ({ hidden, customServersList }: { hidden?: boolean, customServersL
|
|||
}
|
||||
|
||||
const lastJoinedUsername = serversListSorted.find(s => s.usernameOverride)?.usernameOverride
|
||||
let username = overrides.usernameOverride || defaultUsername
|
||||
let username = overrides.usernameOverride || getCurrentUsername() || ''
|
||||
if (!username) {
|
||||
username = prompt('Username', lastJoinedUsername || '')
|
||||
if (!username) return
|
||||
setDefaultUsername(username)
|
||||
const promptUsername = prompt('Enter username', lastJoinedUsername || '')
|
||||
if (!promptUsername) return
|
||||
username = promptUsername
|
||||
}
|
||||
let authenticatedAccount: AuthenticatedAccount | true | undefined
|
||||
if (overrides.authenticatedAccountOverride) {
|
||||
|
|
@ -321,15 +277,15 @@ const Inner = ({ hidden, customServersList }: { hidden?: boolean, customServersL
|
|||
const options = {
|
||||
username,
|
||||
server: normalizeIp(ip),
|
||||
proxy: overrides.proxyOverride || selectedProxy,
|
||||
proxy: overrides.proxyOverride || getCurrentProxy(),
|
||||
botVersion: overrides.versionOverride ?? /* legacy */ overrides['version'],
|
||||
ignoreQs: true,
|
||||
autoLoginPassword: server?.autoLogin?.[username],
|
||||
authenticatedAccount,
|
||||
saveServerToHistory: shouldSave,
|
||||
onSuccessfulPlay () {
|
||||
if (shouldSave && !serversList.some(s => s.ip === ip)) {
|
||||
const newServersList: StoreServerItem[] = [...serversList, {
|
||||
if (shouldSave && !serversListDisplay.some(s => s.ip === ip)) {
|
||||
const newServersList: StoreServerItem[] = [...serversListDisplay, {
|
||||
ip,
|
||||
lastJoined: Date.now(),
|
||||
versionOverride: overrides.versionOverride,
|
||||
|
|
@ -341,10 +297,10 @@ const Inner = ({ hidden, customServersList }: { hidden?: boolean, customServersL
|
|||
|
||||
if (shouldSave === undefined) { // loading saved
|
||||
// find and update
|
||||
const server = serversList.find(s => s.ip === ip)
|
||||
const server = serversListDisplay.find(s => s.ip === ip)
|
||||
if (server) {
|
||||
// move to top
|
||||
const newList = [...serversList]
|
||||
const newList = [...serversListDisplay]
|
||||
const index = newList.indexOf(server)
|
||||
const thisItem = newList[index]
|
||||
newList.splice(index, 1)
|
||||
|
|
@ -352,40 +308,31 @@ const Inner = ({ hidden, customServersList }: { hidden?: boolean, customServersL
|
|||
|
||||
server.lastJoined = Date.now()
|
||||
server.numConnects = (server.numConnects || 0) + 1
|
||||
setNewServersList(serversList)
|
||||
setNewServersList(newList)
|
||||
}
|
||||
}
|
||||
|
||||
// save new selected proxy (if new)
|
||||
if (!proxies.includes(selectedProxy)) {
|
||||
// setProxies([...proxies, selectedProxy])
|
||||
localStorage.setItem('proxies', JSON.stringify([...proxies, selectedProxy]))
|
||||
}
|
||||
saveNewProxy()
|
||||
},
|
||||
serverIndex: shouldSave ? serversList.length.toString() : indexOrIp // assume last
|
||||
serverIndex: shouldSave ? serversListDisplay.length.toString() : indexOrIp // assume last
|
||||
} satisfies ConnectOptions
|
||||
dispatchEvent(new CustomEvent('connect', { detail: options }))
|
||||
// qsOptions
|
||||
}}
|
||||
lockedEditing={!!customServersList}
|
||||
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(authenticatedAccounts.filter(a => a.username !== username))
|
||||
appStorage.authenticatedAccounts = authenticatedAccounts.filter(a => a.username !== username)
|
||||
}}
|
||||
onWorldAction={(action, index) => {
|
||||
const server = serversList[index]
|
||||
const server = serversListDisplay[index]
|
||||
if (!server) return
|
||||
|
||||
if (action === 'edit') {
|
||||
setServerEditScreen(server)
|
||||
}
|
||||
if (action === 'delete') {
|
||||
setServersList(old => old.filter(s => s !== server))
|
||||
appStorage.serversList = appStorage.serversList!.filter(s => s !== server)
|
||||
}
|
||||
}}
|
||||
onGeneralAction={(action) => {
|
||||
|
|
@ -397,7 +344,7 @@ const Inner = ({ hidden, customServersList }: { hidden?: boolean, customServersL
|
|||
}
|
||||
}}
|
||||
worldData={serversListSorted.map(server => {
|
||||
const additional = additionalData[server.ip]
|
||||
const additional = additionalServerData[server.ip]
|
||||
return {
|
||||
name: server.index.toString(),
|
||||
title: server.name || server.ip,
|
||||
|
|
@ -410,27 +357,11 @@ const Inner = ({ hidden, customServersList }: { hidden?: boolean, customServersL
|
|||
group: 'Your Servers'
|
||||
}
|
||||
})}
|
||||
initialProxies={{
|
||||
proxies,
|
||||
selected: selectedProxy,
|
||||
}}
|
||||
updateProxies={({ proxies, selected }) => {
|
||||
// new proxy is saved in joinServer
|
||||
setProxies(proxies)
|
||||
setSelectedProxy(selected)
|
||||
}}
|
||||
hidden={hidden}
|
||||
onRowSelect={(_, i) => {
|
||||
setSelectedIndex(i)
|
||||
}}
|
||||
selectedRow={selectedIndex}
|
||||
serverHistory={getServerConnectionHistory()
|
||||
.sort((a, b) => b.numConnects - a.numConnects)
|
||||
.map(server => ({
|
||||
ip: server.ip,
|
||||
versionOverride: server.version,
|
||||
numConnects: server.numConnects
|
||||
}))}
|
||||
/>
|
||||
return <>
|
||||
{serversListJsx}
|
||||
|
|
|
|||
135
src/react/appStorageProvider.ts
Normal file
135
src/react/appStorageProvider.ts
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
import { proxy, ref, subscribe } from 'valtio'
|
||||
import { UserOverridesConfig } from 'contro-max/build/types/store'
|
||||
import { subscribeKey } from 'valtio/utils'
|
||||
import { CustomCommand } from './KeybindingsCustom'
|
||||
import { AuthenticatedAccount } from './serversStorage'
|
||||
import type { BaseServerInfo } from './AddServerOrConnect'
|
||||
|
||||
// when opening html file locally in browser, localStorage is shared between all ever opened html files, so we try to avoid conflicts
|
||||
const localStoragePrefix = process.env?.SINGLE_FILE_BUILD ? 'minecraft-web-client:' : ''
|
||||
|
||||
export interface SavedProxiesData {
|
||||
proxies: string[]
|
||||
selected: string
|
||||
}
|
||||
|
||||
export interface ServerHistoryEntry {
|
||||
ip: string
|
||||
version?: string
|
||||
numConnects: number
|
||||
lastConnected: number
|
||||
}
|
||||
|
||||
export interface StoreServerItem extends BaseServerInfo {
|
||||
lastJoined?: number
|
||||
description?: string
|
||||
optionsOverride?: Record<string, any>
|
||||
autoLogin?: Record<string, string>
|
||||
numConnects?: number // Track number of connections
|
||||
}
|
||||
|
||||
type StorageData = {
|
||||
customCommands: Record<string, CustomCommand> | undefined
|
||||
username: string | undefined
|
||||
keybindings: UserOverridesConfig | undefined
|
||||
options: any
|
||||
proxiesData: SavedProxiesData | undefined
|
||||
serversHistory: ServerHistoryEntry[]
|
||||
authenticatedAccounts: AuthenticatedAccount[]
|
||||
serversList: StoreServerItem[] | undefined
|
||||
}
|
||||
|
||||
const oldKeysAliases: Partial<Record<keyof StorageData, string>> = {
|
||||
serversHistory: 'serverConnectionHistory',
|
||||
}
|
||||
|
||||
const migrateLegacyData = () => {
|
||||
const proxies = localStorage.getItem('proxies')
|
||||
const selectedProxy = localStorage.getItem('selectedProxy')
|
||||
if (proxies && selectedProxy) {
|
||||
appStorage.proxiesData = {
|
||||
proxies: JSON.parse(proxies),
|
||||
selected: selectedProxy,
|
||||
}
|
||||
}
|
||||
|
||||
const username = localStorage.getItem('username')
|
||||
if (username && !username.startsWith('"')) {
|
||||
appStorage.username = username
|
||||
}
|
||||
|
||||
const serversHistoryLegacy = localStorage.getItem('serverConnectionHistory')
|
||||
if (serversHistoryLegacy) {
|
||||
appStorage.serversHistory = JSON.parse(serversHistoryLegacy)
|
||||
}
|
||||
localStorage.removeItem('proxies')
|
||||
localStorage.removeItem('selectedProxy')
|
||||
localStorage.removeItem('serverConnectionHistory')
|
||||
}
|
||||
|
||||
const defaultStorageData: StorageData = {
|
||||
customCommands: undefined,
|
||||
username: undefined,
|
||||
keybindings: undefined,
|
||||
options: {},
|
||||
proxiesData: undefined,
|
||||
serversHistory: [],
|
||||
authenticatedAccounts: [],
|
||||
serversList: undefined,
|
||||
}
|
||||
|
||||
export const setStorageDataOnAppConfigLoad = () => {
|
||||
appStorage.username ??= `mcrafter${Math.floor(Math.random() * 1000)}`
|
||||
}
|
||||
|
||||
export const appStorage = proxy({ ...defaultStorageData })
|
||||
window.appStorage = appStorage
|
||||
|
||||
// Restore data from localStorage
|
||||
for (const key of Object.keys(defaultStorageData)) {
|
||||
const prefixedKey = `${localStoragePrefix}${key}`
|
||||
const aliasedKey = oldKeysAliases[key]
|
||||
const storedValue = localStorage.getItem(prefixedKey) ?? (aliasedKey ? localStorage.getItem(aliasedKey) : undefined)
|
||||
if (storedValue) {
|
||||
try {
|
||||
const parsed = JSON.parse(storedValue)
|
||||
// appStorage[key] = parsed && typeof parsed === 'object' ? ref(parsed) : parsed
|
||||
appStorage[key] = parsed
|
||||
} catch (e) {
|
||||
console.error(`Failed to parse stored value for ${key}:`, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const saveKey = (key: keyof StorageData) => {
|
||||
const prefixedKey = `${localStoragePrefix}${key}`
|
||||
const value = appStorage[key]
|
||||
if (value === undefined) {
|
||||
localStorage.removeItem(prefixedKey)
|
||||
} else {
|
||||
localStorage.setItem(prefixedKey, JSON.stringify(value))
|
||||
}
|
||||
}
|
||||
|
||||
subscribe(appStorage, (ops) => {
|
||||
for (const op of ops) {
|
||||
const [type, path, value] = op
|
||||
const key = path[0]
|
||||
saveKey(key as keyof StorageData)
|
||||
}
|
||||
})
|
||||
// Subscribe to changes and save to localStorage
|
||||
|
||||
export const resetAppStorage = () => {
|
||||
for (const key of Object.keys(appStorage)) {
|
||||
appStorage[key as keyof StorageData] = defaultStorageData[key as keyof StorageData]
|
||||
}
|
||||
|
||||
for (const key of Object.keys(localStorage)) {
|
||||
if (key.startsWith(localStoragePrefix)) {
|
||||
localStorage.removeItem(key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
migrateLegacyData()
|
||||
|
|
@ -1,16 +1,10 @@
|
|||
import { appQueryParams } from '../appParams'
|
||||
import { miscUiState } from '../globalState'
|
||||
import { BaseServerInfo } from './AddServerOrConnect'
|
||||
import { appStorage, StoreServerItem } from './appStorageProvider'
|
||||
|
||||
const serversListQs = appQueryParams.serversList
|
||||
|
||||
export interface StoreServerItem extends BaseServerInfo {
|
||||
lastJoined?: number
|
||||
description?: string
|
||||
optionsOverride?: Record<string, any>
|
||||
autoLogin?: Record<string, string>
|
||||
numConnects?: number // Track number of connections
|
||||
}
|
||||
export interface AuthenticatedAccount {
|
||||
// type: 'microsoft'
|
||||
username: string
|
||||
|
|
@ -19,6 +13,7 @@ export interface AuthenticatedAccount {
|
|||
expiresOn: number
|
||||
}
|
||||
}
|
||||
|
||||
export interface ServerConnectionHistory {
|
||||
ip: string
|
||||
numConnects: number
|
||||
|
|
@ -28,7 +23,7 @@ export interface ServerConnectionHistory {
|
|||
|
||||
export function updateServerConnectionHistory (ip: string, version?: string) {
|
||||
try {
|
||||
const history: ServerConnectionHistory[] = JSON.parse(localStorage.getItem('serverConnectionHistory') || '[]')
|
||||
const history = [...(appStorage.serversHistory ?? [])]
|
||||
const existingServer = history.find(s => s.ip === ip)
|
||||
if (existingServer) {
|
||||
existingServer.numConnects++
|
||||
|
|
@ -42,53 +37,35 @@ export function updateServerConnectionHistory (ip: string, version?: string) {
|
|||
version
|
||||
})
|
||||
}
|
||||
localStorage.setItem('serverConnectionHistory', JSON.stringify(history))
|
||||
appStorage.serversHistory = history
|
||||
} catch (err) {
|
||||
console.error('Failed to update server connection history:', err)
|
||||
}
|
||||
}
|
||||
|
||||
export const updateLoadedServerData = (callback: (data: StoreServerItem) => StoreServerItem, index = miscUiState.loadedServerIndex) => {
|
||||
if (!index) index = miscUiState.loadedServerIndex
|
||||
if (!index) return
|
||||
// function assumes component is not mounted to avoid sync issues after save
|
||||
const servers = getInitialServersList()
|
||||
|
||||
const servers = [...(appStorage.serversList ?? [])]
|
||||
const server = servers[index]
|
||||
servers[index] = callback(server)
|
||||
setNewServersList(servers)
|
||||
}
|
||||
|
||||
export const setNewServersList = (serversList: StoreServerItem[], force = false) => {
|
||||
if (serversListQs && !force) return
|
||||
localStorage['serversList'] = JSON.stringify(serversList)
|
||||
|
||||
// cleanup legacy
|
||||
localStorage.removeItem('serverHistory')
|
||||
localStorage.removeItem('server')
|
||||
localStorage.removeItem('password')
|
||||
localStorage.removeItem('version')
|
||||
appStorage.serversList = serversList
|
||||
}
|
||||
|
||||
export const getInitialServersList = () => {
|
||||
if (localStorage['serversList']) return JSON.parse(localStorage['serversList']) as StoreServerItem[]
|
||||
// If we already have servers in appStorage, use those
|
||||
if (appStorage.serversList) return appStorage.serversList
|
||||
|
||||
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'],
|
||||
versionOverride: localStorage['version'],
|
||||
lastJoined: Date.now()
|
||||
}
|
||||
servers.push(legacyLastJoinedServer)
|
||||
}
|
||||
|
||||
if (servers.length === 0) { // server list is empty, let's suggest some
|
||||
if (servers.length === 0) {
|
||||
// server list is empty, let's suggest some
|
||||
for (const server of miscUiState.appConfig?.promoteServers ?? []) {
|
||||
servers.push({
|
||||
ip: server.ip,
|
||||
|
|
@ -100,16 +77,13 @@ export const getInitialServersList = () => {
|
|||
|
||||
return servers
|
||||
}
|
||||
|
||||
export const updateAuthenticatedAccountData = (callback: (data: AuthenticatedAccount[]) => AuthenticatedAccount[]) => {
|
||||
const accounts = JSON.parse(localStorage['authenticatedAccounts'] || '[]') as AuthenticatedAccount[]
|
||||
const accounts = appStorage.authenticatedAccounts
|
||||
const newAccounts = callback(accounts)
|
||||
localStorage['authenticatedAccounts'] = JSON.stringify(newAccounts)
|
||||
appStorage.authenticatedAccounts = newAccounts
|
||||
}
|
||||
|
||||
export function getServerConnectionHistory (): ServerConnectionHistory[] {
|
||||
try {
|
||||
return JSON.parse(localStorage.getItem('serverConnectionHistory') || '[]')
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
return appStorage.serversHistory ?? []
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +0,0 @@
|
|||
import { CustomCommand } from './KeybindingsCustom'
|
||||
|
||||
type StorageData = {
|
||||
customCommands: Record<string, CustomCommand>
|
||||
// ...
|
||||
}
|
||||
|
||||
export const getStoredValue = <T extends keyof StorageData> (name: T): StorageData[T] | undefined => {
|
||||
return localStorage[name] ? JSON.parse(localStorage[name]) : undefined
|
||||
}
|
||||
export const setStoredValue = <T extends keyof StorageData> (name: T, value: StorageData[T]) => {
|
||||
localStorage[name] = JSON.stringify(value)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue