Compare commits
7 commits
next
...
storage-re
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
04aeb757a4 | ||
|
|
09516fa055 | ||
|
|
7f26150b10 | ||
|
|
5ede1224f8 | ||
|
|
046182e5c7 |
||
|
|
ed7c6d36ae | ||
|
|
bb9328acbf |
15 changed files with 526 additions and 305 deletions
|
|
@ -1,6 +1,7 @@
|
||||||
import { disabledSettings, options, qsOptions } from './optionsStorage'
|
import { disabledSettings, options, qsOptions } from './optionsStorage'
|
||||||
import { miscUiState } from './globalState'
|
import { miscUiState } from './globalState'
|
||||||
import { setLoadingScreenStatus } from './appStatus'
|
import { setLoadingScreenStatus } from './appStatus'
|
||||||
|
import { setStorageDataOnAppConfigLoad } from './react/appStorageProvider'
|
||||||
|
|
||||||
export type AppConfig = {
|
export type AppConfig = {
|
||||||
// defaultHost?: string
|
// defaultHost?: string
|
||||||
|
|
@ -42,6 +43,8 @@ export const loadAppConfig = (appConfig: AppConfig) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setStorageDataOnAppConfigLoad()
|
||||||
}
|
}
|
||||||
|
|
||||||
export const isBundledConfigUsed = !!process.env.INLINED_APP_CONFIG
|
export const isBundledConfigUsed = !!process.env.INLINED_APP_CONFIG
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ import { getFixedFilesize } from './downloadAndOpenFile'
|
||||||
import { packetsReplayState } from './react/state/packetsReplayState'
|
import { packetsReplayState } from './react/state/packetsReplayState'
|
||||||
import { createFullScreenProgressReporter } from './core/progressReporter'
|
import { createFullScreenProgressReporter } from './core/progressReporter'
|
||||||
import { showNotification } from './react/NotificationProvider'
|
import { showNotification } from './react/NotificationProvider'
|
||||||
|
import { resetAppStorage } from './react/appStorageProvider'
|
||||||
const { GoogleDriveFileSystem } = require('google-drive-browserfs/src/backends/GoogleDrive')
|
const { GoogleDriveFileSystem } = require('google-drive-browserfs/src/backends/GoogleDrive')
|
||||||
|
|
||||||
browserfs.install(window)
|
browserfs.install(window)
|
||||||
|
|
@ -620,24 +621,13 @@ export const openWorldZip = async (...args: Parameters<typeof openWorldZipInner>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const resetLocalStorageWorld = () => {
|
export const resetLocalStorage = () => {
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
resetOptions()
|
resetOptions()
|
||||||
|
resetAppStorage()
|
||||||
}
|
}
|
||||||
|
|
||||||
window.resetLocalStorageWorld = resetLocalStorageWorld
|
window.resetLocalStorage = resetLocalStorage
|
||||||
|
|
||||||
export const openFilePicker = (specificCase?: 'resourcepack') => {
|
export const openFilePicker = (specificCase?: 'resourcepack') => {
|
||||||
// create and show input picker
|
// create and show input picker
|
||||||
let picker: HTMLInputElement = document.body.querySelector('input#file-zip-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 { lastConnectOptions } from './react/AppStatusProvider'
|
||||||
import { onCameraMove, onControInit } from './cameraRotationControls'
|
import { onCameraMove, onControInit } from './cameraRotationControls'
|
||||||
import { createNotificationProgressReporter } from './core/progressReporter'
|
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, () => {
|
subscribe(customKeymaps, () => {
|
||||||
localStorage.keymap = JSON.stringify(customKeymaps)
|
appStorage.keybindings = customKeymaps
|
||||||
})
|
})
|
||||||
|
|
||||||
const controlOptions = {
|
const controlOptions = {
|
||||||
|
|
|
||||||
|
|
@ -3,19 +3,21 @@ import { useSnapshot } from 'valtio'
|
||||||
import { openURL } from 'renderer/viewer/lib/simpleUtils'
|
import { openURL } from 'renderer/viewer/lib/simpleUtils'
|
||||||
import { noCase } from 'change-case'
|
import { noCase } from 'change-case'
|
||||||
import { gameAdditionalState, miscUiState, openOptionsMenu, showModal } from './globalState'
|
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 Button from './react/Button'
|
||||||
import { OptionMeta, OptionSlider } from './react/OptionsItems'
|
import { OptionMeta, OptionSlider } from './react/OptionsItems'
|
||||||
import Slider from './react/Slider'
|
import Slider from './react/Slider'
|
||||||
import { getScreenRefreshRate } from './utils'
|
import { getScreenRefreshRate } from './utils'
|
||||||
import { setLoadingScreenStatus } from './appStatus'
|
import { setLoadingScreenStatus } from './appStatus'
|
||||||
import { openFilePicker, resetLocalStorageWithoutWorld } from './browserfs'
|
import { openFilePicker, resetLocalStorage } from './browserfs'
|
||||||
import { completeResourcepackPackInstall, getResourcePackNames, resourcePackState, uninstallResourcePack } from './resourcePack'
|
import { completeResourcepackPackInstall, getResourcePackNames, resourcePackState, uninstallResourcePack } from './resourcePack'
|
||||||
import { downloadPacketsReplay, packetsRecordingState } from './packetsReplay/packetsReplayLegacy'
|
import { downloadPacketsReplay, packetsRecordingState } from './packetsReplay/packetsReplayLegacy'
|
||||||
import { showOptionsModal } from './react/SelectOption'
|
import { showInputsModal, showOptionsModal } from './react/SelectOption'
|
||||||
import supportedVersions from './supportedVersions.mjs'
|
import supportedVersions from './supportedVersions.mjs'
|
||||||
import { getVersionAutoSelect } from './connect'
|
import { getVersionAutoSelect } from './connect'
|
||||||
import { createNotificationProgressReporter } from './core/progressReporter'
|
import { createNotificationProgressReporter } from './core/progressReporter'
|
||||||
|
import { customKeymaps } from './controls'
|
||||||
|
import { appStorage } from './react/appStorageProvider'
|
||||||
|
|
||||||
export const guiOptionsScheme: {
|
export const guiOptionsScheme: {
|
||||||
[t in OptionsGroupType]: Array<{ [K in keyof AppOptions]?: Partial<OptionMeta<AppOptions[K]>> } & { custom? }>
|
[t in OptionsGroupType]: Array<{ [K in keyof AppOptions]?: Partial<OptionMeta<AppOptions[K]>> } & { custom? }>
|
||||||
|
|
@ -450,9 +452,19 @@ export const guiOptionsScheme: {
|
||||||
return <Button
|
return <Button
|
||||||
inScreen
|
inScreen
|
||||||
onClick={() => {
|
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>
|
return <Category>Developer</Category>
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
custom () {
|
||||||
|
return <Button label='Export/Import...' onClick={() => openOptionsMenu('export-import')} inScreen />
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
custom () {
|
custom () {
|
||||||
const { active } = useSnapshot(packetsRecordingState)
|
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={{
|
const Category = ({ children }) => <div style={{
|
||||||
fontSize: 9,
|
fontSize: 9,
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,9 @@
|
||||||
// todo implement async options storage
|
|
||||||
|
|
||||||
import { proxy, subscribe } from 'valtio/vanilla'
|
import { proxy, subscribe } from 'valtio/vanilla'
|
||||||
// weird webpack configuration bug: it cant import valtio/utils in this file
|
|
||||||
import { subscribeKey } from 'valtio/utils'
|
import { subscribeKey } from 'valtio/utils'
|
||||||
import { omitObj } from '@zardoy/utils'
|
import { omitObj } from '@zardoy/utils'
|
||||||
import { appQueryParamsArray } from './appParams'
|
import { appQueryParamsArray } from './appParams'
|
||||||
import type { AppConfig } from './appConfig'
|
import type { AppConfig } from './appConfig'
|
||||||
|
import { appStorage } from './react/appStorageProvider'
|
||||||
|
|
||||||
const isDev = process.env.NODE_ENV === 'development'
|
const isDev = process.env.NODE_ENV === 'development'
|
||||||
const initialAppConfig = process.env?.INLINED_APP_CONFIG as AppConfig ?? {}
|
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
|
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 isDeepEqual = (a: any, b: any): boolean => {
|
||||||
const localStorageKey = process.env?.SINGLE_FILE_BUILD ? 'minecraftWebClientOptions' : 'options'
|
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({
|
export const options: AppOptions = proxy({
|
||||||
...defaultOptions,
|
...defaultOptions,
|
||||||
...initialAppConfig.defaultSettings,
|
...initialAppConfig.defaultSettings,
|
||||||
...migrateOptions(JSON.parse(localStorage[localStorageKey] || '{}')),
|
...migrateOptions(appStorage.options),
|
||||||
...qsOptions
|
...qsOptions
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -181,14 +198,14 @@ export const resetOptions = () => {
|
||||||
|
|
||||||
Object.defineProperty(window, 'debugChangedOptions', {
|
Object.defineProperty(window, 'debugChangedOptions', {
|
||||||
get () {
|
get () {
|
||||||
return Object.fromEntries(Object.entries(options).filter(([key, v]) => defaultOptions[key] !== v))
|
return getChangedSettings()
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
subscribe(options, () => {
|
subscribe(options, () => {
|
||||||
// Don't save disabled settings to localStorage
|
// Don't save disabled settings to localStorage
|
||||||
const saveOptions = omitObj(options, [...disabledSettings.value] as any)
|
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
|
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 { fetchServerStatus, isServerValid } from '../api/mcStatusApi'
|
||||||
import { parseServerAddress } from '../parseServerAddress'
|
import { parseServerAddress } from '../parseServerAddress'
|
||||||
import Screen from './Screen'
|
import Screen from './Screen'
|
||||||
import Input from './Input'
|
import Input, { INPUT_LABEL_WIDTH, InputWithLabel } from './Input'
|
||||||
import Button from './Button'
|
import Button from './Button'
|
||||||
import SelectGameVersion from './SelectGameVersion'
|
import SelectGameVersion from './SelectGameVersion'
|
||||||
import { usePassesScaledDimensions } from './UIProvider'
|
import { usePassesScaledDimensions } from './UIProvider'
|
||||||
|
|
@ -32,8 +32,6 @@ interface Props {
|
||||||
allowAutoConnect?: boolean
|
allowAutoConnect?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const ELEMENTS_WIDTH = 190
|
|
||||||
|
|
||||||
export default ({ onBack, onConfirm, title = 'Add a Server', initialData, parseQs, onQsConnect, placeholders, accounts, versions, allowAutoConnect }: Props) => {
|
export default ({ onBack, onConfirm, title = 'Add a Server', initialData, parseQs, onQsConnect, placeholders, accounts, versions, allowAutoConnect }: Props) => {
|
||||||
const isSmallHeight = !usePassesScaledDimensions(null, 350)
|
const isSmallHeight = !usePassesScaledDimensions(null, 350)
|
||||||
const qsParamName = parseQs ? appQueryParams.name : undefined
|
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>) => {
|
const ButtonWrapper = ({ ...props }: React.ComponentProps<typeof Button>) => {
|
||||||
props.style ??= {}
|
props.style ??= {}
|
||||||
props.style.width = ELEMENTS_WIDTH
|
props.style.width = INPUT_LABEL_WIDTH
|
||||||
return <Button {...props} />
|
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)
|
const fallbackIfNotFound = (index: number) => (index === -1 ? undefined : index)
|
||||||
|
|
|
||||||
|
|
@ -90,11 +90,11 @@ div.chat-wrapper {
|
||||||
::-webkit-scrollbar {
|
::-webkit-scrollbar {
|
||||||
width: 5px;
|
width: 5px;
|
||||||
height: 5px;
|
height: 5px;
|
||||||
background-color: rgb(24, 24, 24);
|
background-color: #272727;
|
||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb {
|
::-webkit-scrollbar-thumb {
|
||||||
background-color: rgb(50, 50, 50);
|
background-color: #747474;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-completions-items>div {
|
.chat-completions-items>div {
|
||||||
|
|
@ -160,7 +160,6 @@ input[type=text],
|
||||||
padding-bottom: 1px;
|
padding-bottom: 1px;
|
||||||
padding-left: 2px;
|
padding-left: 2px;
|
||||||
padding-right: 2px;
|
padding-right: 2px;
|
||||||
height: 15px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-mobile-input-hidden {
|
.chat-mobile-input-hidden {
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ interface Props extends Omit<React.ComponentProps<'input'>, 'width'> {
|
||||||
width?: number
|
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 }
|
if (width) rootStyles = { ...rootStyles, width }
|
||||||
|
|
||||||
const ref = useRef<HTMLInputElement>(null!)
|
const ref = useRef<HTMLInputElement>(null!)
|
||||||
|
|
@ -51,3 +51,19 @@ export default ({ autoFocus, rootStyles, inputRef, validateInput, defaultValue,
|
||||||
/>
|
/>
|
||||||
</div>
|
</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>
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -123,24 +123,6 @@ export default () => {
|
||||||
singleplayerAvailable={singleplayerAvailable}
|
singleplayerAvailable={singleplayerAvailable}
|
||||||
connectToServerAction={() => showModal({ reactType: 'serversList' })}
|
connectToServerAction={() => showModal({ reactType: 'serversList' })}
|
||||||
singleplayerAction={async () => {
|
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' })
|
showModal({ reactType: 'singleplayer' })
|
||||||
}}
|
}}
|
||||||
githubAction={() => openGithub()}
|
githubAction={() => openGithub()}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,14 @@
|
||||||
import { proxy, useSnapshot } from 'valtio'
|
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 { hideCurrentModal, showModal } from '../globalState'
|
||||||
import { parseFormattedMessagePacket } from '../botUtils'
|
import { parseFormattedMessagePacket } from '../botUtils'
|
||||||
import Screen from './Screen'
|
import Screen from './Screen'
|
||||||
import { useIsModalActive } from './utilsApp'
|
import { useIsModalActive } from './utilsApp'
|
||||||
import Button from './Button'
|
import Button from './Button'
|
||||||
import MessageFormattedString from './MessageFormattedString'
|
import MessageFormattedString from './MessageFormattedString'
|
||||||
|
import Input, { InputWithLabel } from './Input'
|
||||||
|
|
||||||
const state = proxy({
|
const state = proxy({
|
||||||
title: '',
|
title: '',
|
||||||
|
|
@ -12,6 +16,8 @@ const state = proxy({
|
||||||
showCancel: true,
|
showCancel: true,
|
||||||
minecraftJsonMessage: null as null | Record<string, any>,
|
minecraftJsonMessage: null as null | Record<string, any>,
|
||||||
behavior: 'resolve-close' as 'resolve-close' | 'close-resolve',
|
behavior: 'resolve-close' as 'resolve-close' | 'close-resolve',
|
||||||
|
inputs: {} as Record<string, InputOption>,
|
||||||
|
inputsConfirmButton: ''
|
||||||
})
|
})
|
||||||
|
|
||||||
let resolve
|
let resolve
|
||||||
|
|
@ -35,17 +41,63 @@ export const showOptionsModal = async <T extends string> (
|
||||||
title,
|
title,
|
||||||
options,
|
options,
|
||||||
showCancel: cancel,
|
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 () => {
|
export default () => {
|
||||||
const { title, options, showCancel, minecraftJsonMessage } = useSnapshot(state)
|
const { title, options, showCancel, minecraftJsonMessage, inputs, inputsConfirmButton } = useSnapshot(state)
|
||||||
const isModalActive = useIsModalActive('general-select')
|
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
|
if (!isModalActive) return
|
||||||
|
|
||||||
const resolveClose = (value: string | undefined) => {
|
const resolveClose = (value: any) => {
|
||||||
if (state.behavior === 'resolve-close') {
|
if (state.behavior === 'resolve-close') {
|
||||||
resolve(value)
|
resolve(value)
|
||||||
hideCurrentModal()
|
hideCurrentModal()
|
||||||
|
|
@ -59,17 +111,66 @@ export default () => {
|
||||||
{minecraftJsonMessage && <div style={{ textAlign: 'center', }}>
|
{minecraftJsonMessage && <div style={{ textAlign: 'center', }}>
|
||||||
<MessageFormattedString message={minecraftJsonMessage} />
|
<MessageFormattedString message={minecraftJsonMessage} />
|
||||||
</div>}
|
</div>}
|
||||||
{options.map(option => <Button
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 9 }}>
|
||||||
key={option} onClick={() => {
|
{options.length > 0 && <div style={{ display: 'flex', flexDirection: 'column', gap: 5 }}>
|
||||||
resolveClose(option)
|
{options.map(option => <Button
|
||||||
}}
|
key={option} onClick={() => {
|
||||||
>{option}
|
resolveClose(option)
|
||||||
</Button>)}
|
}}
|
||||||
{showCancel && <Button
|
>{option}
|
||||||
style={{ marginTop: 30 }} onClick={() => {
|
</Button>)}
|
||||||
resolveClose(undefined)
|
</div>}
|
||||||
}}
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
|
||||||
>Cancel
|
{Object.entries(inputs).map(([key, input]) => {
|
||||||
</Button>}
|
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>
|
</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 Singleplayer from './Singleplayer'
|
||||||
import Input from './Input'
|
import Input from './Input'
|
||||||
import Button from './Button'
|
import Button from './Button'
|
||||||
import PixelartIcon, { pixelartIcons } from './PixelartIcon'
|
import PixelartIcon, { pixelartIcons } from './PixelartIcon'
|
||||||
|
|
||||||
import Select from './Select'
|
import Select from './Select'
|
||||||
import { BaseServerInfo } from './AddServerOrConnect'
|
import { BaseServerInfo } from './AddServerOrConnect'
|
||||||
import { useIsSmallWidth } from './simpleHooks'
|
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> {
|
interface Props extends React.ComponentProps<typeof Singleplayer> {
|
||||||
joinServer: (info: BaseServerInfo | string, additional: {
|
joinServer: (info: BaseServerInfo | string, additional: {
|
||||||
shouldSave?: boolean
|
shouldSave?: boolean
|
||||||
index?: number
|
index?: number
|
||||||
}) => void
|
}) => void
|
||||||
initialProxies: SavedProxiesLocalStorage
|
|
||||||
updateProxies: (proxies: SavedProxiesLocalStorage) => void
|
|
||||||
username: string
|
|
||||||
setUsername: (username: string) => void
|
|
||||||
onProfileClick?: () => void
|
onProfileClick?: () => void
|
||||||
setQuickConnectIp?: (ip: string) => 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 ({
|
export default ({
|
||||||
initialProxies,
|
|
||||||
updateProxies: updateProxiesProp,
|
|
||||||
joinServer,
|
joinServer,
|
||||||
username,
|
|
||||||
setUsername,
|
|
||||||
onProfileClick,
|
onProfileClick,
|
||||||
setQuickConnectIp,
|
setQuickConnectIp,
|
||||||
serverHistory,
|
|
||||||
...props
|
...props
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const [proxies, setProxies] = React.useState(initialProxies)
|
const snap = useSnapshot(appStorage)
|
||||||
|
const username = useMemo(() => getCurrentUsername(), [appQueryParams.username, appStorage.username])
|
||||||
const updateProxies = (newData: SavedProxiesLocalStorage) => {
|
|
||||||
setProxies(newData)
|
|
||||||
updateProxiesProp(newData)
|
|
||||||
}
|
|
||||||
|
|
||||||
const [serverIp, setServerIp] = React.useState('')
|
const [serverIp, setServerIp] = React.useState('')
|
||||||
const [save, setSave] = React.useState(true)
|
const [save, setSave] = React.useState(true)
|
||||||
const [activeHighlight, setActiveHighlight] = React.useState(undefined as 'quick-connect' | 'server-list' | undefined)
|
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 getActiveHighlightStyles = (type: typeof activeHighlight) => {
|
||||||
const styles: React.CSSProperties = {
|
const styles: React.CSSProperties = {
|
||||||
transition: 'filter 0.2s',
|
transition: 'filter 0.2s',
|
||||||
|
|
@ -71,6 +68,8 @@ export default ({
|
||||||
|
|
||||||
const isSmallWidth = useIsSmallWidth()
|
const isSmallWidth = useIsSmallWidth()
|
||||||
|
|
||||||
|
const initialProxies = getInitialProxies()
|
||||||
|
const proxiesData = snap.proxiesData ?? { proxies: initialProxies, selected: initialProxies[0] }
|
||||||
return <Singleplayer
|
return <Singleplayer
|
||||||
{...props}
|
{...props}
|
||||||
firstRowChildrenOverride={<form
|
firstRowChildrenOverride={<form
|
||||||
|
|
@ -85,7 +84,6 @@ export default ({
|
||||||
onMouseEnter={() => setActiveHighlight('quick-connect')}
|
onMouseEnter={() => setActiveHighlight('quick-connect')}
|
||||||
onMouseLeave={() => setActiveHighlight(undefined)}
|
onMouseLeave={() => setActiveHighlight(undefined)}
|
||||||
>
|
>
|
||||||
{/* todo history */}
|
|
||||||
<Input
|
<Input
|
||||||
required
|
required
|
||||||
placeholder='Quick Connect IP (:version)'
|
placeholder='Quick Connect IP (:version)'
|
||||||
|
|
@ -102,8 +100,8 @@ export default ({
|
||||||
spellCheck="false"
|
spellCheck="false"
|
||||||
/>
|
/>
|
||||||
<datalist id="server-history">
|
<datalist id="server-history">
|
||||||
{serverHistory?.map((server) => (
|
{[...(snap.serversHistory ?? [])].sort((a, b) => b.numConnects - a.numConnects).map((server) => (
|
||||||
<option key={server.ip} value={`${server.ip}${server.versionOverride ? `:${server.versionOverride}` : ''}`} />
|
<option key={server.ip} value={`${server.ip}${server.version ? `:${server.version}` : ''}`} />
|
||||||
))}
|
))}
|
||||||
</datalist>
|
</datalist>
|
||||||
<label style={{ fontSize: 10, display: 'flex', alignItems: 'center', gap: 5, height: '100%', marginTop: '-1px' }}>
|
<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} />
|
? <PixelartIcon iconName={pixelartIcons.server} styles={{ fontSize: 14, color: 'lightgray', marginLeft: 2 }} onClick={onProfileClick} />
|
||||||
: <span style={{ color: 'lightgray', fontSize: 14 }}>Proxy:</span>}
|
: <span style={{ color: 'lightgray', fontSize: 14 }}>Proxy:</span>}
|
||||||
<Select
|
<Select
|
||||||
initialOptions={proxies.proxies.map(p => { return { value: p, label: p } })}
|
initialOptions={proxiesData.proxies.map(p => { return { value: p, label: p } })}
|
||||||
defaultValue={{ value: proxies.selected, label: proxies.selected }}
|
defaultValue={{ value: proxiesData.selected, label: proxiesData.selected }}
|
||||||
updateOptions={(newSel) => {
|
updateOptions={(newSel) => {
|
||||||
updateProxies({ proxies: [...proxies.proxies], selected: newSel })
|
updateProxies({ proxies: [...proxiesData.proxies], selected: newSel })
|
||||||
}}
|
}}
|
||||||
containerStyle={{
|
containerStyle={{
|
||||||
width: isSmallWidth ? 140 : 180,
|
width: isSmallWidth ? 140 : 180,
|
||||||
|
|
@ -139,6 +137,7 @@ export default ({
|
||||||
<Input
|
<Input
|
||||||
rootStyles={{ width: 80 }}
|
rootStyles={{ width: 80 }}
|
||||||
value={username}
|
value={username}
|
||||||
|
disabled={appQueryParams.username !== undefined}
|
||||||
onChange={({ target: { value } }) => setUsername(value)}
|
onChange={({ target: { value } }) => setUsername(value)}
|
||||||
autoCorrect="off"
|
autoCorrect="off"
|
||||||
autoCapitalize="off"
|
autoCapitalize="off"
|
||||||
|
|
|
||||||
|
|
@ -8,13 +8,14 @@ import { appQueryParams } from '../appParams'
|
||||||
import { fetchServerStatus, isServerValid } from '../api/mcStatusApi'
|
import { fetchServerStatus, isServerValid } from '../api/mcStatusApi'
|
||||||
import { getServerInfo } from '../mineflayer/mc-protocol'
|
import { getServerInfo } from '../mineflayer/mc-protocol'
|
||||||
import { parseServerAddress } from '../parseServerAddress'
|
import { parseServerAddress } from '../parseServerAddress'
|
||||||
import ServersList from './ServersList'
|
import ServersList, { getCurrentProxy, getCurrentUsername } from './ServersList'
|
||||||
import AddServerOrConnect, { BaseServerInfo } from './AddServerOrConnect'
|
import AddServerOrConnect, { BaseServerInfo } from './AddServerOrConnect'
|
||||||
import { useDidUpdateEffect } from './utils'
|
import { useDidUpdateEffect } from './utils'
|
||||||
import { useIsModalActive } from './utilsApp'
|
import { useIsModalActive } from './utilsApp'
|
||||||
import { showOptionsModal } from './SelectOption'
|
import { showOptionsModal } from './SelectOption'
|
||||||
import { useCopyKeybinding } from './simpleHooks'
|
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 = {
|
type AdditionalDisplayData = {
|
||||||
textNameRightGrayed: string
|
textNameRightGrayed: string
|
||||||
|
|
@ -25,19 +26,6 @@ type AdditionalDisplayData = {
|
||||||
}
|
}
|
||||||
|
|
||||||
const serversListQs = appQueryParams.serversList
|
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
|
// todo move to base
|
||||||
const normalizeIp = (ip: string) => ip.replace(/https?:\/\//, '').replace(/\/(:|$)/, '')
|
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 MAX_CONCURRENT_REQUESTS = 10
|
||||||
|
|
||||||
const Inner = ({ hidden, customServersList }: { hidden?: boolean, customServersList?: string[] }) => {
|
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 [serverEditScreen, setServerEditScreen] = useState<StoreServerItem | true | null>(null) // true for add
|
||||||
const [defaultUsername, _setDefaultUsername] = useState(localStorage['username'] ?? (`mcrafter${Math.floor(Math.random() * 1000)}`))
|
const { authenticatedAccounts } = useSnapshot(appStorage)
|
||||||
const [authenticatedAccounts, _setAuthenticatedAccounts] = useState<AuthenticatedAccount[]>(JSON.parse(localStorage['authenticatedAccounts'] || '[]'))
|
|
||||||
const [quickConnectIp, setQuickConnectIp] = useState('')
|
const [quickConnectIp, setQuickConnectIp] = useState('')
|
||||||
const [selectedIndex, setSelectedIndex] = useState(0)
|
const [selectedIndex, setSelectedIndex] = useState(0)
|
||||||
|
|
||||||
// Save username to localStorage when component mounts if it doesn't exist
|
const { serversList: savedServersList } = useSnapshot(appStorage)
|
||||||
useEffect(() => {
|
|
||||||
if (!localStorage['username']) {
|
|
||||||
localStorage.setItem('username', defaultUsername)
|
|
||||||
}
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const setAuthenticatedAccounts = (newState: typeof authenticatedAccounts) => {
|
const serversListDisplay = useMemo(() => {
|
||||||
_setAuthenticatedAccounts(newState)
|
return (
|
||||||
localStorage.setItem('authenticatedAccounts', JSON.stringify(newState))
|
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) => {
|
const [additionalServerData, setAdditionalServerData] = useState<Record<string, AdditionalDisplayData>>({})
|
||||||
_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>>({})
|
|
||||||
|
|
||||||
// Add keyboard handler for moving servers
|
// Add keyboard handler for moving servers
|
||||||
useEffect(() => {
|
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 (['input', 'textarea', 'select'].includes((e.target as HTMLElement)?.tagName?.toLowerCase())) return
|
||||||
if (!e.shiftKey || selectedIndex === undefined) return
|
if (!e.shiftKey || selectedIndex === undefined) return
|
||||||
if (e.key !== 'ArrowUp' && e.key !== 'ArrowDown') return
|
if (e.key !== 'ArrowUp' && e.key !== 'ArrowDown') return
|
||||||
|
if (customServersList) return
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
e.stopImmediatePropagation()
|
e.stopImmediatePropagation()
|
||||||
|
|
||||||
const newIndex = e.key === 'ArrowUp'
|
const newIndex = e.key === 'ArrowUp'
|
||||||
? Math.max(0, selectedIndex - 1)
|
? Math.max(0, selectedIndex - 1)
|
||||||
: Math.min(serversList.length - 1, selectedIndex + 1)
|
: Math.min(serversListDisplay.length - 1, selectedIndex + 1)
|
||||||
|
|
||||||
if (newIndex === selectedIndex) return
|
if (newIndex === selectedIndex) return
|
||||||
|
|
||||||
// Move server in the list
|
// Move server in the list
|
||||||
const newList = [...serversList]
|
const newList = [...serversListDisplay]
|
||||||
const oldItem = newList[selectedIndex]
|
const oldItem = newList[selectedIndex]
|
||||||
newList[selectedIndex] = newList[newIndex]
|
newList[selectedIndex] = newList[newIndex]
|
||||||
newList[newIndex] = oldItem
|
newList[newIndex] = oldItem
|
||||||
|
|
||||||
setServersList(newList)
|
appStorage.serversList = newList
|
||||||
setSelectedIndex(newIndex)
|
setSelectedIndex(newIndex)
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener('keydown', handleKeyDown)
|
document.addEventListener('keydown', handleKeyDown)
|
||||||
return () => document.removeEventListener('keydown', handleKeyDown)
|
return () => document.removeEventListener('keydown', handleKeyDown)
|
||||||
}, [selectedIndex, serversList])
|
}, [selectedIndex, serversListDisplay])
|
||||||
|
|
||||||
useEffect(() => {
|
const serversListSorted = useMemo(() => serversListDisplay.map((server, index) => ({ ...server, index })), [serversListDisplay])
|
||||||
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])
|
|
||||||
// by lastJoined
|
// by lastJoined
|
||||||
// const serversListSorted = useMemo(() => {
|
// const serversListSorted = useMemo(() => {
|
||||||
// return serversList.map((server, index) => ({ ...server, index })).sort((a, b) => (b.lastJoined ?? 0) - (a.lastJoined ?? 0))
|
// 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
|
data = await fetchServerStatus(server.ip/* , signal */) // DONT ADD SIGNAL IT WILL CRUSH JS RUNTIME
|
||||||
}
|
}
|
||||||
if (data) {
|
if (data) {
|
||||||
setAdditionalData(old => ({
|
setAdditionalServerData(old => ({
|
||||||
...old,
|
...old,
|
||||||
[server.ip]: data
|
[server.ip]: data
|
||||||
}))
|
}))
|
||||||
|
|
@ -224,7 +179,7 @@ const Inner = ({ hidden, customServersList }: { hidden?: boolean, customServersL
|
||||||
}, [isEditScreenModal])
|
}, [isEditScreenModal])
|
||||||
|
|
||||||
useCopyKeybinding(() => {
|
useCopyKeybinding(() => {
|
||||||
const item = serversList[selectedIndex]
|
const item = serversListDisplay[selectedIndex]
|
||||||
if (!item) return
|
if (!item) return
|
||||||
let str = `${item.ip}`
|
let str = `${item.ip}`
|
||||||
if (item.versionOverride) {
|
if (item.versionOverride) {
|
||||||
|
|
@ -236,8 +191,8 @@ const Inner = ({ hidden, customServersList }: { hidden?: boolean, customServersL
|
||||||
const editModalJsx = isEditScreenModal ? <AddServerOrConnect
|
const editModalJsx = isEditScreenModal ? <AddServerOrConnect
|
||||||
allowAutoConnect={miscUiState.appConfig?.allowAutoConnect}
|
allowAutoConnect={miscUiState.appConfig?.allowAutoConnect}
|
||||||
placeholders={{
|
placeholders={{
|
||||||
proxyOverride: selectedProxy,
|
proxyOverride: getCurrentProxy(),
|
||||||
usernameOverride: defaultUsername,
|
usernameOverride: getCurrentUsername(),
|
||||||
}}
|
}}
|
||||||
parseQs={!serverEditScreen}
|
parseQs={!serverEditScreen}
|
||||||
onBack={() => {
|
onBack={() => {
|
||||||
|
|
@ -247,12 +202,13 @@ const Inner = ({ hidden, customServersList }: { hidden?: boolean, customServersL
|
||||||
if (!serverEditScreen) return
|
if (!serverEditScreen) return
|
||||||
if (serverEditScreen === true) {
|
if (serverEditScreen === true) {
|
||||||
const server: StoreServerItem = { ...info, lastJoined: Date.now() } // so it appears first
|
const server: StoreServerItem = { ...info, lastJoined: Date.now() } // so it appears first
|
||||||
setServersList(old => [...old, server])
|
appStorage.serversList = [...(appStorage.serversList ?? []), server]
|
||||||
} else {
|
} else {
|
||||||
const index = serversList.indexOf(serverEditScreen)
|
const index = appStorage.serversList?.indexOf(serverEditScreen)
|
||||||
const { lastJoined } = serversList[index]
|
if (index !== undefined) {
|
||||||
serversList[index] = { ...info, lastJoined }
|
const { lastJoined } = appStorage.serversList![index]
|
||||||
setServersList([...serversList])
|
appStorage.serversList![index] = { ...info, lastJoined }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
setServerEditScreen(null)
|
setServerEditScreen(null)
|
||||||
}}
|
}}
|
||||||
|
|
@ -262,9 +218,9 @@ const Inner = ({ hidden, customServersList }: { hidden?: boolean, customServersL
|
||||||
} : serverEditScreen}
|
} : serverEditScreen}
|
||||||
onQsConnect={(info) => {
|
onQsConnect={(info) => {
|
||||||
const connectOptions: ConnectOptions = {
|
const connectOptions: ConnectOptions = {
|
||||||
username: info.usernameOverride || defaultUsername,
|
username: info.usernameOverride || getCurrentUsername() || '',
|
||||||
server: normalizeIp(info.ip),
|
server: normalizeIp(info.ip),
|
||||||
proxy: info.proxyOverride || selectedProxy,
|
proxy: info.proxyOverride || getCurrentProxy(),
|
||||||
botVersion: info.versionOverride,
|
botVersion: info.versionOverride,
|
||||||
ignoreQs: true,
|
ignoreQs: true,
|
||||||
}
|
}
|
||||||
|
|
@ -304,11 +260,11 @@ const Inner = ({ hidden, customServersList }: { hidden?: boolean, customServersL
|
||||||
}
|
}
|
||||||
|
|
||||||
const lastJoinedUsername = serversListSorted.find(s => s.usernameOverride)?.usernameOverride
|
const lastJoinedUsername = serversListSorted.find(s => s.usernameOverride)?.usernameOverride
|
||||||
let username = overrides.usernameOverride || defaultUsername
|
let username = overrides.usernameOverride || getCurrentUsername() || ''
|
||||||
if (!username) {
|
if (!username) {
|
||||||
username = prompt('Username', lastJoinedUsername || '')
|
const promptUsername = prompt('Enter username', lastJoinedUsername || '')
|
||||||
if (!username) return
|
if (!promptUsername) return
|
||||||
setDefaultUsername(username)
|
username = promptUsername
|
||||||
}
|
}
|
||||||
let authenticatedAccount: AuthenticatedAccount | true | undefined
|
let authenticatedAccount: AuthenticatedAccount | true | undefined
|
||||||
if (overrides.authenticatedAccountOverride) {
|
if (overrides.authenticatedAccountOverride) {
|
||||||
|
|
@ -321,15 +277,15 @@ const Inner = ({ hidden, customServersList }: { hidden?: boolean, customServersL
|
||||||
const options = {
|
const options = {
|
||||||
username,
|
username,
|
||||||
server: normalizeIp(ip),
|
server: normalizeIp(ip),
|
||||||
proxy: overrides.proxyOverride || selectedProxy,
|
proxy: overrides.proxyOverride || getCurrentProxy(),
|
||||||
botVersion: overrides.versionOverride ?? /* legacy */ overrides['version'],
|
botVersion: overrides.versionOverride ?? /* legacy */ overrides['version'],
|
||||||
ignoreQs: true,
|
ignoreQs: true,
|
||||||
autoLoginPassword: server?.autoLogin?.[username],
|
autoLoginPassword: server?.autoLogin?.[username],
|
||||||
authenticatedAccount,
|
authenticatedAccount,
|
||||||
saveServerToHistory: shouldSave,
|
saveServerToHistory: shouldSave,
|
||||||
onSuccessfulPlay () {
|
onSuccessfulPlay () {
|
||||||
if (shouldSave && !serversList.some(s => s.ip === ip)) {
|
if (shouldSave && !serversListDisplay.some(s => s.ip === ip)) {
|
||||||
const newServersList: StoreServerItem[] = [...serversList, {
|
const newServersList: StoreServerItem[] = [...serversListDisplay, {
|
||||||
ip,
|
ip,
|
||||||
lastJoined: Date.now(),
|
lastJoined: Date.now(),
|
||||||
versionOverride: overrides.versionOverride,
|
versionOverride: overrides.versionOverride,
|
||||||
|
|
@ -341,10 +297,10 @@ const Inner = ({ hidden, customServersList }: { hidden?: boolean, customServersL
|
||||||
|
|
||||||
if (shouldSave === undefined) { // loading saved
|
if (shouldSave === undefined) { // loading saved
|
||||||
// find and update
|
// find and update
|
||||||
const server = serversList.find(s => s.ip === ip)
|
const server = serversListDisplay.find(s => s.ip === ip)
|
||||||
if (server) {
|
if (server) {
|
||||||
// move to top
|
// move to top
|
||||||
const newList = [...serversList]
|
const newList = [...serversListDisplay]
|
||||||
const index = newList.indexOf(server)
|
const index = newList.indexOf(server)
|
||||||
const thisItem = newList[index]
|
const thisItem = newList[index]
|
||||||
newList.splice(index, 1)
|
newList.splice(index, 1)
|
||||||
|
|
@ -352,40 +308,31 @@ const Inner = ({ hidden, customServersList }: { hidden?: boolean, customServersL
|
||||||
|
|
||||||
server.lastJoined = Date.now()
|
server.lastJoined = Date.now()
|
||||||
server.numConnects = (server.numConnects || 0) + 1
|
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
|
} satisfies ConnectOptions
|
||||||
dispatchEvent(new CustomEvent('connect', { detail: options }))
|
dispatchEvent(new CustomEvent('connect', { detail: options }))
|
||||||
// qsOptions
|
// qsOptions
|
||||||
}}
|
}}
|
||||||
lockedEditing={!!customServersList}
|
lockedEditing={!!customServersList}
|
||||||
username={defaultUsername}
|
|
||||||
setUsername={setDefaultUsername}
|
|
||||||
setQuickConnectIp={setQuickConnectIp}
|
setQuickConnectIp={setQuickConnectIp}
|
||||||
onProfileClick={async () => {
|
onProfileClick={async () => {
|
||||||
const username = await showOptionsModal('Select authenticated account to remove', authenticatedAccounts.map(a => a.username))
|
const username = await showOptionsModal('Select authenticated account to remove', authenticatedAccounts.map(a => a.username))
|
||||||
if (!username) return
|
if (!username) return
|
||||||
setAuthenticatedAccounts(authenticatedAccounts.filter(a => a.username !== username))
|
appStorage.authenticatedAccounts = authenticatedAccounts.filter(a => a.username !== username)
|
||||||
}}
|
}}
|
||||||
onWorldAction={(action, index) => {
|
onWorldAction={(action, index) => {
|
||||||
const server = serversList[index]
|
const server = serversListDisplay[index]
|
||||||
if (!server) return
|
if (!server) return
|
||||||
|
|
||||||
if (action === 'edit') {
|
if (action === 'edit') {
|
||||||
setServerEditScreen(server)
|
setServerEditScreen(server)
|
||||||
}
|
}
|
||||||
if (action === 'delete') {
|
if (action === 'delete') {
|
||||||
setServersList(old => old.filter(s => s !== server))
|
appStorage.serversList = appStorage.serversList!.filter(s => s !== server)
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onGeneralAction={(action) => {
|
onGeneralAction={(action) => {
|
||||||
|
|
@ -397,7 +344,7 @@ const Inner = ({ hidden, customServersList }: { hidden?: boolean, customServersL
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
worldData={serversListSorted.map(server => {
|
worldData={serversListSorted.map(server => {
|
||||||
const additional = additionalData[server.ip]
|
const additional = additionalServerData[server.ip]
|
||||||
return {
|
return {
|
||||||
name: server.index.toString(),
|
name: server.index.toString(),
|
||||||
title: server.name || server.ip,
|
title: server.name || server.ip,
|
||||||
|
|
@ -410,27 +357,11 @@ const Inner = ({ hidden, customServersList }: { hidden?: boolean, customServersL
|
||||||
group: 'Your Servers'
|
group: 'Your Servers'
|
||||||
}
|
}
|
||||||
})}
|
})}
|
||||||
initialProxies={{
|
|
||||||
proxies,
|
|
||||||
selected: selectedProxy,
|
|
||||||
}}
|
|
||||||
updateProxies={({ proxies, selected }) => {
|
|
||||||
// new proxy is saved in joinServer
|
|
||||||
setProxies(proxies)
|
|
||||||
setSelectedProxy(selected)
|
|
||||||
}}
|
|
||||||
hidden={hidden}
|
hidden={hidden}
|
||||||
onRowSelect={(_, i) => {
|
onRowSelect={(_, i) => {
|
||||||
setSelectedIndex(i)
|
setSelectedIndex(i)
|
||||||
}}
|
}}
|
||||||
selectedRow={selectedIndex}
|
selectedRow={selectedIndex}
|
||||||
serverHistory={getServerConnectionHistory()
|
|
||||||
.sort((a, b) => b.numConnects - a.numConnects)
|
|
||||||
.map(server => ({
|
|
||||||
ip: server.ip,
|
|
||||||
versionOverride: server.version,
|
|
||||||
numConnects: server.numConnects
|
|
||||||
}))}
|
|
||||||
/>
|
/>
|
||||||
return <>
|
return <>
|
||||||
{serversListJsx}
|
{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 { appQueryParams } from '../appParams'
|
||||||
import { miscUiState } from '../globalState'
|
import { miscUiState } from '../globalState'
|
||||||
import { BaseServerInfo } from './AddServerOrConnect'
|
import { BaseServerInfo } from './AddServerOrConnect'
|
||||||
|
import { appStorage, StoreServerItem } from './appStorageProvider'
|
||||||
|
|
||||||
const serversListQs = appQueryParams.serversList
|
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 {
|
export interface AuthenticatedAccount {
|
||||||
// type: 'microsoft'
|
// type: 'microsoft'
|
||||||
username: string
|
username: string
|
||||||
|
|
@ -19,6 +13,7 @@ export interface AuthenticatedAccount {
|
||||||
expiresOn: number
|
expiresOn: number
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ServerConnectionHistory {
|
export interface ServerConnectionHistory {
|
||||||
ip: string
|
ip: string
|
||||||
numConnects: number
|
numConnects: number
|
||||||
|
|
@ -28,7 +23,7 @@ export interface ServerConnectionHistory {
|
||||||
|
|
||||||
export function updateServerConnectionHistory (ip: string, version?: string) {
|
export function updateServerConnectionHistory (ip: string, version?: string) {
|
||||||
try {
|
try {
|
||||||
const history: ServerConnectionHistory[] = JSON.parse(localStorage.getItem('serverConnectionHistory') || '[]')
|
const history = [...(appStorage.serversHistory ?? [])]
|
||||||
const existingServer = history.find(s => s.ip === ip)
|
const existingServer = history.find(s => s.ip === ip)
|
||||||
if (existingServer) {
|
if (existingServer) {
|
||||||
existingServer.numConnects++
|
existingServer.numConnects++
|
||||||
|
|
@ -42,53 +37,35 @@ export function updateServerConnectionHistory (ip: string, version?: string) {
|
||||||
version
|
version
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
localStorage.setItem('serverConnectionHistory', JSON.stringify(history))
|
appStorage.serversHistory = history
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to update server connection history:', err)
|
console.error('Failed to update server connection history:', err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const updateLoadedServerData = (callback: (data: StoreServerItem) => StoreServerItem, index = miscUiState.loadedServerIndex) => {
|
export const updateLoadedServerData = (callback: (data: StoreServerItem) => StoreServerItem, index = miscUiState.loadedServerIndex) => {
|
||||||
if (!index) index = miscUiState.loadedServerIndex
|
if (!index) index = miscUiState.loadedServerIndex
|
||||||
if (!index) return
|
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]
|
const server = servers[index]
|
||||||
servers[index] = callback(server)
|
servers[index] = callback(server)
|
||||||
setNewServersList(servers)
|
setNewServersList(servers)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const setNewServersList = (serversList: StoreServerItem[], force = false) => {
|
export const setNewServersList = (serversList: StoreServerItem[], force = false) => {
|
||||||
if (serversListQs && !force) return
|
if (serversListQs && !force) return
|
||||||
localStorage['serversList'] = JSON.stringify(serversList)
|
appStorage.serversList = serversList
|
||||||
|
|
||||||
// cleanup legacy
|
|
||||||
localStorage.removeItem('serverHistory')
|
|
||||||
localStorage.removeItem('server')
|
|
||||||
localStorage.removeItem('password')
|
|
||||||
localStorage.removeItem('version')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getInitialServersList = () => {
|
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 servers = [] as StoreServerItem[]
|
||||||
|
|
||||||
const legacyServersList = localStorage['serverHistory'] ? JSON.parse(localStorage['serverHistory']) as string[] : null
|
if (servers.length === 0) {
|
||||||
if (legacyServersList) {
|
// server list is empty, let's suggest some
|
||||||
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
|
|
||||||
for (const server of miscUiState.appConfig?.promoteServers ?? []) {
|
for (const server of miscUiState.appConfig?.promoteServers ?? []) {
|
||||||
servers.push({
|
servers.push({
|
||||||
ip: server.ip,
|
ip: server.ip,
|
||||||
|
|
@ -100,16 +77,13 @@ export const getInitialServersList = () => {
|
||||||
|
|
||||||
return servers
|
return servers
|
||||||
}
|
}
|
||||||
|
|
||||||
export const updateAuthenticatedAccountData = (callback: (data: AuthenticatedAccount[]) => AuthenticatedAccount[]) => {
|
export const updateAuthenticatedAccountData = (callback: (data: AuthenticatedAccount[]) => AuthenticatedAccount[]) => {
|
||||||
const accounts = JSON.parse(localStorage['authenticatedAccounts'] || '[]') as AuthenticatedAccount[]
|
const accounts = appStorage.authenticatedAccounts
|
||||||
const newAccounts = callback(accounts)
|
const newAccounts = callback(accounts)
|
||||||
localStorage['authenticatedAccounts'] = JSON.stringify(newAccounts)
|
appStorage.authenticatedAccounts = newAccounts
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getServerConnectionHistory (): ServerConnectionHistory[] {
|
export function getServerConnectionHistory (): ServerConnectionHistory[] {
|
||||||
try {
|
return appStorage.serversHistory ?? []
|
||||||
return JSON.parse(localStorage.getItem('serverConnectionHistory') || '[]')
|
|
||||||
} catch {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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