Skip to content

Commit 36bf18b

Browse files
authored
feat: refactor all app storage managment (#310)
1 parent da35cfb commit 36bf18b

15 files changed

+526
-305
lines changed

src/appConfig.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { disabledSettings, options, qsOptions } from './optionsStorage'
22
import { miscUiState } from './globalState'
33
import { setLoadingScreenStatus } from './appStatus'
4+
import { setStorageDataOnAppConfigLoad } from './react/appStorageProvider'
45

56
export type AppConfig = {
67
// defaultHost?: string
@@ -42,6 +43,8 @@ export const loadAppConfig = (appConfig: AppConfig) => {
4243
}
4344
}
4445
}
46+
47+
setStorageDataOnAppConfigLoad()
4548
}
4649

4750
export const isBundledConfigUsed = !!process.env.INLINED_APP_CONFIG

src/browserfs.ts

Lines changed: 5 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { getFixedFilesize } from './downloadAndOpenFile'
1515
import { packetsReplayState } from './react/state/packetsReplayState'
1616
import { createFullScreenProgressReporter } from './core/progressReporter'
1717
import { showNotification } from './react/NotificationProvider'
18+
import { resetAppStorage } from './react/appStorageProvider'
1819
const { GoogleDriveFileSystem } = require('google-drive-browserfs/src/backends/GoogleDrive')
1920

2021
browserfs.install(window)
@@ -620,24 +621,13 @@ export const openWorldZip = async (...args: Parameters<typeof openWorldZipInner>
620621
}
621622
}
622623

623-
export const resetLocalStorageWorld = () => {
624-
for (const key of Object.keys(localStorage)) {
625-
if (/^[\da-fA-F]{8}(?:\b-[\da-fA-F]{4}){3}\b-[\da-fA-F]{12}$/g.test(key) || key === '/') {
626-
localStorage.removeItem(key)
627-
}
628-
}
629-
}
630-
631-
export const resetLocalStorageWithoutWorld = () => {
632-
for (const key of Object.keys(localStorage)) {
633-
if (!/^[\da-fA-F]{8}(?:\b-[\da-fA-F]{4}){3}\b-[\da-fA-F]{12}$/g.test(key) && key !== '/') {
634-
localStorage.removeItem(key)
635-
}
636-
}
624+
export const resetLocalStorage = () => {
637625
resetOptions()
626+
resetAppStorage()
638627
}
639628

640-
window.resetLocalStorageWorld = resetLocalStorageWorld
629+
window.resetLocalStorage = resetLocalStorage
630+
641631
export const openFilePicker = (specificCase?: 'resourcepack') => {
642632
// create and show input picker
643633
let picker: HTMLInputElement = document.body.querySelector('input#file-zip-picker')!

src/controls.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,12 @@ import { showNotification } from './react/NotificationProvider'
2525
import { lastConnectOptions } from './react/AppStatusProvider'
2626
import { onCameraMove, onControInit } from './cameraRotationControls'
2727
import { createNotificationProgressReporter } from './core/progressReporter'
28+
import { appStorage } from './react/appStorageProvider'
2829

2930

30-
export const customKeymaps = proxy(JSON.parse(localStorage.keymap || '{}')) as UserOverridesConfig
31+
export const customKeymaps = proxy(appStorage.keybindings)
3132
subscribe(customKeymaps, () => {
32-
localStorage.keymap = JSON.stringify(customKeymaps)
33+
appStorage.keybindings = customKeymaps
3334
})
3435

3536
const controlOptions = {

src/optionsGuiScheme.tsx

Lines changed: 106 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,21 @@ import { useSnapshot } from 'valtio'
33
import { openURL } from 'renderer/viewer/lib/simpleUtils'
44
import { noCase } from 'change-case'
55
import { gameAdditionalState, miscUiState, openOptionsMenu, showModal } from './globalState'
6-
import { AppOptions, options } from './optionsStorage'
6+
import { AppOptions, getChangedSettings, options, resetOptions } from './optionsStorage'
77
import Button from './react/Button'
88
import { OptionMeta, OptionSlider } from './react/OptionsItems'
99
import Slider from './react/Slider'
1010
import { getScreenRefreshRate } from './utils'
1111
import { setLoadingScreenStatus } from './appStatus'
12-
import { openFilePicker, resetLocalStorageWithoutWorld } from './browserfs'
12+
import { openFilePicker, resetLocalStorage } from './browserfs'
1313
import { completeResourcepackPackInstall, getResourcePackNames, resourcePackState, uninstallResourcePack } from './resourcePack'
1414
import { downloadPacketsReplay, packetsRecordingState } from './packetsReplay/packetsReplayLegacy'
15-
import { showOptionsModal } from './react/SelectOption'
15+
import { showInputsModal, showOptionsModal } from './react/SelectOption'
1616
import supportedVersions from './supportedVersions.mjs'
1717
import { getVersionAutoSelect } from './connect'
1818
import { createNotificationProgressReporter } from './core/progressReporter'
19+
import { customKeymaps } from './controls'
20+
import { appStorage } from './react/appStorageProvider'
1921

2022
export const guiOptionsScheme: {
2123
[t in OptionsGroupType]: Array<{ [K in keyof AppOptions]?: Partial<OptionMeta<AppOptions[K]>> } & { custom? }>
@@ -450,16 +452,31 @@ export const guiOptionsScheme: {
450452
return <Button
451453
inScreen
452454
onClick={() => {
453-
if (confirm('Are you sure you want to reset all settings?')) resetLocalStorageWithoutWorld()
455+
if (confirm('Are you sure you want to reset all settings?')) resetOptions()
454456
}}
455-
>Reset all settings</Button>
457+
>Reset settings</Button>
458+
},
459+
},
460+
{
461+
custom () {
462+
return <Button
463+
inScreen
464+
onClick={() => {
465+
if (confirm('Are you sure you want to remove all data (settings, keybindings, servers, username, auth, proxies)?')) resetLocalStorage()
466+
}}
467+
>Remove all data</Button>
456468
},
457469
},
458470
{
459471
custom () {
460472
return <Category>Developer</Category>
461473
},
462474
},
475+
{
476+
custom () {
477+
return <Button label='Export/Import...' onClick={() => openOptionsMenu('export-import')} inScreen />
478+
}
479+
},
463480
{
464481
custom () {
465482
const { active } = useSnapshot(packetsRecordingState)
@@ -521,8 +538,91 @@ export const guiOptionsScheme: {
521538
},
522539
},
523540
],
541+
'export-import': [
542+
{
543+
custom () {
544+
return <Category>Export/Import Data</Category>
545+
}
546+
},
547+
{
548+
custom () {
549+
return <Button
550+
inScreen
551+
disabled={true}
552+
onClick={() => {}}
553+
>Import Data</Button>
554+
}
555+
},
556+
{
557+
custom () {
558+
return <Button
559+
inScreen
560+
onClick={async () => {
561+
const data = await showInputsModal('Export Profile', {
562+
profileName: {
563+
type: 'text',
564+
},
565+
exportSettings: {
566+
type: 'checkbox',
567+
defaultValue: true,
568+
},
569+
exportKeybindings: {
570+
type: 'checkbox',
571+
defaultValue: true,
572+
},
573+
exportServers: {
574+
type: 'checkbox',
575+
defaultValue: true,
576+
},
577+
saveUsernameAndProxy: {
578+
type: 'checkbox',
579+
defaultValue: true,
580+
},
581+
})
582+
const fileName = `${data.profileName ? `${data.profileName}-` : ''}web-client-profile.json`
583+
const json = {
584+
_about: 'Minecraft Web Client (mcraft.fun) Profile',
585+
...data.exportSettings ? {
586+
options: getChangedSettings(),
587+
} : {},
588+
...data.exportKeybindings ? {
589+
keybindings: customKeymaps,
590+
} : {},
591+
...data.saveUsernameAndProxy ? {
592+
username: appStorage.username,
593+
proxy: appStorage.proxiesData?.selected,
594+
} : {},
595+
}
596+
const blob = new Blob([JSON.stringify(json, null, 2)], { type: 'application/json' })
597+
const url = URL.createObjectURL(blob)
598+
const a = document.createElement('a')
599+
a.href = url
600+
a.download = fileName
601+
a.click()
602+
URL.revokeObjectURL(url)
603+
}}
604+
>Export Data</Button>
605+
}
606+
},
607+
{
608+
custom () {
609+
return <Button
610+
inScreen
611+
disabled
612+
>Export Worlds</Button>
613+
}
614+
},
615+
{
616+
custom () {
617+
return <Button
618+
inScreen
619+
disabled
620+
>Export Resource Pack</Button>
621+
}
622+
}
623+
],
524624
}
525-
export type OptionsGroupType = 'main' | 'render' | 'interface' | 'controls' | 'sound' | 'advanced' | 'VR'
625+
export type OptionsGroupType = 'main' | 'render' | 'interface' | 'controls' | 'sound' | 'advanced' | 'VR' | 'export-import'
526626

527627
const Category = ({ children }) => <div style={{
528628
fontSize: 9,

src/optionsStorage.ts

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
1-
// todo implement async options storage
2-
31
import { proxy, subscribe } from 'valtio/vanilla'
4-
// weird webpack configuration bug: it cant import valtio/utils in this file
52
import { subscribeKey } from 'valtio/utils'
63
import { omitObj } from '@zardoy/utils'
74
import { appQueryParamsArray } from './appParams'
85
import type { AppConfig } from './appConfig'
6+
import { appStorage } from './react/appStorageProvider'
97

108
const isDev = process.env.NODE_ENV === 'development'
119
const initialAppConfig = process.env?.INLINED_APP_CONFIG as AppConfig ?? {}
@@ -164,12 +162,31 @@ const migrateOptions = (options: Partial<AppOptions & Record<string, any>>) => {
164162

165163
export type AppOptions = typeof defaultOptions
166164

167-
// when opening html file locally in browser, localStorage is shared between all ever opened html files, so we try to avoid conflicts
168-
const localStorageKey = process.env?.SINGLE_FILE_BUILD ? 'minecraftWebClientOptions' : 'options'
165+
const isDeepEqual = (a: any, b: any): boolean => {
166+
if (a === b) return true
167+
if (typeof a !== typeof b) return false
168+
if (typeof a !== 'object') return false
169+
if (a === null || b === null) return a === b
170+
if (Array.isArray(a) && Array.isArray(b)) {
171+
if (a.length !== b.length) return false
172+
return a.every((item, index) => isDeepEqual(item, b[index]))
173+
}
174+
const keysA = Object.keys(a)
175+
const keysB = Object.keys(b)
176+
if (keysA.length !== keysB.length) return false
177+
return keysA.every(key => isDeepEqual(a[key], b[key]))
178+
}
179+
180+
export const getChangedSettings = () => {
181+
return Object.fromEntries(
182+
Object.entries(options).filter(([key, value]) => !isDeepEqual(defaultOptions[key], value))
183+
)
184+
}
185+
169186
export const options: AppOptions = proxy({
170187
...defaultOptions,
171188
...initialAppConfig.defaultSettings,
172-
...migrateOptions(JSON.parse(localStorage[localStorageKey] || '{}')),
189+
...migrateOptions(appStorage.options),
173190
...qsOptions
174191
})
175192

@@ -181,14 +198,14 @@ export const resetOptions = () => {
181198

182199
Object.defineProperty(window, 'debugChangedOptions', {
183200
get () {
184-
return Object.fromEntries(Object.entries(options).filter(([key, v]) => defaultOptions[key] !== v))
201+
return getChangedSettings()
185202
},
186203
})
187204

188205
subscribe(options, () => {
189206
// Don't save disabled settings to localStorage
190207
const saveOptions = omitObj(options, [...disabledSettings.value] as any)
191-
localStorage[localStorageKey] = JSON.stringify(saveOptions)
208+
appStorage.options = saveOptions
192209
})
193210

194211
type WatchValue = <T extends Record<string, any>>(proxy: T, callback: (p: T, isChanged: boolean) => void) => () => void

src/react/AddServerOrConnect.tsx

Lines changed: 2 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { appQueryParams } from '../appParams'
33
import { fetchServerStatus, isServerValid } from '../api/mcStatusApi'
44
import { parseServerAddress } from '../parseServerAddress'
55
import Screen from './Screen'
6-
import Input from './Input'
6+
import Input, { INPUT_LABEL_WIDTH, InputWithLabel } from './Input'
77
import Button from './Button'
88
import SelectGameVersion from './SelectGameVersion'
99
import { usePassesScaledDimensions } from './UIProvider'
@@ -32,8 +32,6 @@ interface Props {
3232
allowAutoConnect?: boolean
3333
}
3434

35-
const ELEMENTS_WIDTH = 190
36-
3735
export default ({ onBack, onConfirm, title = 'Add a Server', initialData, parseQs, onQsConnect, placeholders, accounts, versions, allowAutoConnect }: Props) => {
3836
const isSmallHeight = !usePassesScaledDimensions(null, 350)
3937
const qsParamName = parseQs ? appQueryParams.name : undefined
@@ -256,20 +254,8 @@ export default ({ onBack, onConfirm, title = 'Add a Server', initialData, parseQ
256254

257255
const ButtonWrapper = ({ ...props }: React.ComponentProps<typeof Button>) => {
258256
props.style ??= {}
259-
props.style.width = ELEMENTS_WIDTH
257+
props.style.width = INPUT_LABEL_WIDTH
260258
return <Button {...props} />
261259
}
262260

263-
const InputWithLabel = ({ label, span, ...props }: React.ComponentProps<typeof Input> & { label, span? }) => {
264-
return <div style={{
265-
display: 'flex',
266-
flexDirection: 'column',
267-
gridRow: span ? 'span 2 / span 2' : undefined,
268-
}}
269-
>
270-
<label style={{ fontSize: 12, marginBottom: 1, color: 'lightgray' }}>{label}</label>
271-
<Input rootStyles={{ width: ELEMENTS_WIDTH }} {...props} />
272-
</div>
273-
}
274-
275261
const fallbackIfNotFound = (index: number) => (index === -1 ? undefined : index)

src/react/Chat.css

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -90,11 +90,11 @@ div.chat-wrapper {
9090
::-webkit-scrollbar {
9191
width: 5px;
9292
height: 5px;
93-
background-color: rgb(24, 24, 24);
93+
background-color: #272727;
9494
}
9595

9696
::-webkit-scrollbar-thumb {
97-
background-color: rgb(50, 50, 50);
97+
background-color: #747474;
9898
}
9999

100100
.chat-completions-items>div {
@@ -160,7 +160,6 @@ input[type=text],
160160
padding-bottom: 1px;
161161
padding-left: 2px;
162162
padding-right: 2px;
163-
height: 15px;
164163
}
165164

166165
.chat-mobile-input-hidden {

src/react/Input.tsx

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ interface Props extends Omit<React.ComponentProps<'input'>, 'width'> {
1010
width?: number
1111
}
1212

13-
export default ({ autoFocus, rootStyles, inputRef, validateInput, defaultValue, width, ...inputProps }: Props) => {
13+
const Input = ({ autoFocus, rootStyles, inputRef, validateInput, defaultValue, width, ...inputProps }: Props) => {
1414
if (width) rootStyles = { ...rootStyles, width }
1515

1616
const ref = useRef<HTMLInputElement>(null!)
@@ -51,3 +51,19 @@ export default ({ autoFocus, rootStyles, inputRef, validateInput, defaultValue,
5151
/>
5252
</div>
5353
}
54+
55+
export default Input
56+
57+
export const INPUT_LABEL_WIDTH = 190
58+
59+
export const InputWithLabel = ({ label, span, ...props }: React.ComponentProps<typeof Input> & { label, span? }) => {
60+
return <div style={{
61+
display: 'flex',
62+
flexDirection: 'column',
63+
gridRow: span ? 'span 2 / span 2' : undefined,
64+
}}
65+
>
66+
<label style={{ fontSize: 12, marginBottom: 1, color: 'lightgray' }}>{label}</label>
67+
<Input rootStyles={{ width: INPUT_LABEL_WIDTH }} {...props} />
68+
</div>
69+
}

0 commit comments

Comments
 (0)