diff --git a/gui/package.json b/gui/package.json index b3055e1dc2..8a5e54a704 100644 --- a/gui/package.json +++ b/gui/package.json @@ -16,10 +16,10 @@ "@tauri-apps/plugin-shell": "github:tauri-apps/tauri-plugin-shell#v2", "@tauri-apps/plugin-window": "github:tauri-apps/tauri-plugin-window#v2", "@vitejs/plugin-react": "^3.0.0", + "browser-fs-access": "^0.34.1", "browserslist": "^4.18.1", "classnames": "^2.3.1", "eslint-config-react-app": "^7.0.0", - "file-saver": "^2.0.5", "flatbuffers": "^22.10.26", "identity-obj-proxy": "^3.0.0", "intl-pluralrules": "^1.3.1", diff --git a/gui/public/i18n/en/translation.ftl b/gui/public/i18n/en/translation.ftl index a0096f37ef..e5db291aec 100644 --- a/gui/public/i18n/en/translation.ftl +++ b/gui/public/i18n/en/translation.ftl @@ -747,6 +747,9 @@ onboarding-choose_proportions-manual_proportions = Manual proportions onboarding-choose_proportions-manual_proportions-subtitle = For small touches onboarding-choose_proportions-manual_proportions-description = This will let you adjust your proportions manually by modifying them directly onboarding-choose_proportions-export = Export proportions +onboarding-choose_proportions-import = Import proportions +onboarding-choose_proportions-import-success = Imported +onboarding-choose_proportions-import-failed = Failed onboarding-choose_proportions-file_type = Body proportions file ## Tracker manual proportions setup diff --git a/gui/src/components/onboarding/pages/body-proportions/ProportionsChoose.tsx b/gui/src/components/onboarding/pages/body-proportions/ProportionsChoose.tsx index 63226335c9..58081f91f5 100644 --- a/gui/src/components/onboarding/pages/body-proportions/ProportionsChoose.tsx +++ b/gui/src/components/onboarding/pages/body-proportions/ProportionsChoose.tsx @@ -5,21 +5,30 @@ import classNames from 'classnames'; import { Typography } from '../../../commons/Typography'; import { Button } from '../../../commons/Button'; import { - SkeletonConfigResponseT, RpcMessage, SkeletonConfigRequestT, + SkeletonBone, + ChangeSkeletonConfigRequestT, } from 'solarxr-protocol'; import { useWebsocketAPI } from '../../../../hooks/websocket-api'; -import saveAs from 'file-saver'; import { save } from '@tauri-apps/plugin-dialog'; import { writeTextFile } from '@tauri-apps/plugin-fs'; import { useIsTauri } from '../../../../hooks/breakpoint'; import { useAppContext } from '../../../../hooks/app'; import { error } from '../../../../utils/logging'; +import { fileOpen, fileSave } from 'browser-fs-access'; +import { useDebouncedEffect } from '../../../../hooks/timeout'; export const MIN_HEIGHT = 0.4; export const MAX_HEIGHT = 4; export const DEFAULT_HEIGHT = 1.5; +export const CURRENT_EXPORT_VERSION = 1; + +enum ImportStatus { + FAILED, + SUCCESS, + OK, +} export function ProportionsChoose() { const isTauri = useIsTauri(); @@ -27,8 +36,15 @@ export function ProportionsChoose() { const { applyProgress, state } = useOnboarding(); const { useRPCPacket, sendRPCPacket } = useWebsocketAPI(); const [animated, setAnimated] = useState(false); + const [importState, setImportState] = useState(ImportStatus.OK); const { computedTrackers } = useAppContext(); + useDebouncedEffect( + () => setImportState(ImportStatus.OK), + [importState], + 2000 + ); + const hmdTracker = useMemo( () => computedTrackers.find( @@ -48,9 +64,27 @@ export function ProportionsChoose() { [hmdTracker?.tracker.position?.y] ); + const importStatusKey = useMemo(() => { + switch (importState) { + case ImportStatus.FAILED: + return 'onboarding-choose_proportions-import-failed'; + case ImportStatus.SUCCESS: + return 'onboarding-choose_proportions-import-success'; + case ImportStatus.OK: + return 'onboarding-choose_proportions-import'; + } + }, [importState]); + useRPCPacket( RpcMessage.SkeletonConfigResponse, - (data: SkeletonConfigResponseT) => { + (data: SkeletonConfigExport) => { + // Convert the skeleton part enums into a string + data.skeletonParts.forEach((x) => { + if (typeof x.bone === 'number') + x.bone = SkeletonBone[x.bone] as SkeletonBoneKey; + }); + data.version = CURRENT_EXPORT_VERSION; + const blob = new Blob([JSON.stringify(data)], { type: 'application/json', }); @@ -71,13 +105,52 @@ export function ProportionsChoose() { error(err); }); } else { - saveAs(blob, 'body-proportions.json'); + fileSave(blob, { + fileName: 'body-proportions.json', + extensions: ['.json'], + }); } } ); applyProgress(0.85); + const onImport = async () => { + const file = await fileOpen({ + mimeTypes: ['application/json'], + }); + + const text = await file.text(); + const config = JSON.parse(text) as SkeletonConfigExport; + if ( + !config?.skeletonParts?.length || + !Array.isArray(config.skeletonParts) + ) { + error( + 'failed to import body proportions because skeletonParts is not an array/empty' + ); + return setImportState(ImportStatus.FAILED); + } + + for (const bone of [...config.skeletonParts]) { + if ( + (typeof bone.bone === 'string' && !(bone.bone in SkeletonBone)) || + (typeof bone.bone === 'number' && + typeof SkeletonBone[bone.bone] !== 'string') + ) { + error( + `failed to import body proportions because ${bone.bone} is not a valid bone` + ); + return setImportState(ImportStatus.FAILED); + } + } + + parseConfigImport(config).forEach((req) => + sendRPCPacket(RpcMessage.ChangeSkeletonConfigRequest, req) + ); + setImportState(ImportStatus.SUCCESS); + }; + return ( <>
@@ -196,7 +269,7 @@ export function ProportionsChoose() {
-
+
{!state.alonePage && ( +
); } + +function parseConfigImport( + config: SkeletonConfigExport +): ChangeSkeletonConfigRequestT[] { + if (!config.version) config.version = 1; + if (config.version < 1) { + // Add config migration stuff here, this one is just an example. + } + + return config.skeletonParts.map((part) => { + const bone = + typeof part.bone === 'string' ? SkeletonBone[part.bone] : part.bone; + + return new ChangeSkeletonConfigRequestT(bone, part.value); + }); +} + +type SkeletonBoneKey = keyof typeof SkeletonBone; + +interface SkeletonConfigExport { + version?: number; + skeletonParts: { + bone: SkeletonBoneKey | SkeletonBone; + value: number; + }[]; +} diff --git a/package-lock.json b/package-lock.json index 3c57d3d0a6..f1efb34b26 100644 --- a/package-lock.json +++ b/package-lock.json @@ -33,10 +33,10 @@ "@tauri-apps/plugin-shell": "github:tauri-apps/tauri-plugin-shell#v2", "@tauri-apps/plugin-window": "github:tauri-apps/tauri-plugin-window#v2", "@vitejs/plugin-react": "^3.0.0", + "browser-fs-access": "^0.34.1", "browserslist": "^4.18.1", "classnames": "^2.3.1", "eslint-config-react-app": "^7.0.0", - "file-saver": "^2.0.5", "flatbuffers": "^22.10.26", "identity-obj-proxy": "^3.0.0", "intl-pluralrules": "^1.3.1", @@ -4246,6 +4246,11 @@ "node": ">=8" } }, + "node_modules/browser-fs-access": { + "version": "0.34.1", + "resolved": "https://registry.npmjs.org/browser-fs-access/-/browser-fs-access-0.34.1.tgz", + "integrity": "sha512-HPaRf2yimp8kWSuWJXc8Mi78dPbDzfduA+Gyq14H4jlMvd6XNfIRm36Y2yRLaa4x0gwcGuepj4zf14oiTlxrxQ==" + }, "node_modules/browserslist": { "version": "4.21.10", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.10.tgz", @@ -5794,11 +5799,6 @@ "node": "^10.12.0 || >=12.0.0" } }, - "node_modules/file-saver": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.5.tgz", - "integrity": "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==" - }, "node_modules/filesize": { "version": "8.0.7", "resolved": "https://registry.npmjs.org/filesize/-/filesize-8.0.7.tgz",