From 9fd224cccf0ac17982f3ed1ac8b6a6653a883cc3 Mon Sep 17 00:00:00 2001 From: PhilReact Date: Fri, 20 Dec 2024 18:51:05 +0200 Subject: [PATCH] added import/export settings --- src/App.tsx | 3 + src/atoms/global.ts | 5 + src/components/Apps/AppsNavBar.tsx | 8 +- src/components/Save/Save.tsx | 439 +++++++++++++++++++--------- src/useQortalGetSaveSettings.tsx | 9 +- src/useRetrieveDataLocalStorage.tsx | 22 +- 6 files changed, 335 insertions(+), 151 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index f1cdb87..6e58e83 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -113,6 +113,7 @@ import { enabledDevModeAtom, fullScreenAtom, hasSettingsChangedAtom, + isUsingImportExportSettingsAtom, oldPinnedAppsAtom, settingsLocalLastUpdatedAtom, settingsQDNLastUpdatedAtom, @@ -453,6 +454,7 @@ function App() { const resetAtomSortablePinnedAppsAtom = useResetRecoilState( sortablePinnedAppsAtom ); + const resetAtomIsUsingImportExportSettingsAtom = useResetRecoilState(isUsingImportExportSettingsAtom) const resetAtomCanSaveSettingToQdnAtom = useResetRecoilState( canSaveSettingToQdnAtom ); @@ -470,6 +472,7 @@ function App() { resetAtomSettingsQDNLastUpdatedAtom(); resetAtomSettingsLocalLastUpdatedAtom(); resetAtomOldPinnedAppsAtom(); + resetAtomIsUsingImportExportSettingsAtom() }; useEffect(() => { if (!isMobile) return; diff --git a/src/atoms/global.ts b/src/atoms/global.ts index dfb9f82..025cca6 100644 --- a/src/atoms/global.ts +++ b/src/atoms/global.ts @@ -67,6 +67,11 @@ export const oldPinnedAppsAtom = atom({ key: 'oldPinnedAppsAtom', default: [], }); +export const isUsingImportExportSettingsAtom = atom({ + key: 'isUsingImportExportSettingsAtom', + default: null, +}); + export const fullScreenAtom = atom({ key: 'fullScreenAtom', diff --git a/src/components/Apps/AppsNavBar.tsx b/src/components/Apps/AppsNavBar.tsx index e387a08..67786db 100644 --- a/src/components/Apps/AppsNavBar.tsx +++ b/src/components/Apps/AppsNavBar.tsx @@ -31,8 +31,12 @@ import { sortablePinnedAppsAtom, } from "../../atoms/global"; -export function saveToLocalStorage(key, subKey, newValue) { +export function saveToLocalStorage(key, subKey, newValue, otherRootData = {}, deleteWholeKey) { try { + if(deleteWholeKey){ + localStorage.setItem(key, null); + return + } // Fetch existing data const existingData = localStorage.getItem(key); let combinedData = {}; @@ -43,12 +47,14 @@ export function saveToLocalStorage(key, subKey, newValue) { // Merge with the new data under the subKey combinedData = { ...parsedData, + ...otherRootData, timestamp: Date.now(), // Update the root timestamp [subKey]: newValue, // Assuming the data is an array }; } else { // If no existing data, just use the new data under the subKey combinedData = { + ...otherRootData, timestamp: Date.now(), // Set the initial root timestamp [subKey]: newValue, }; diff --git a/src/components/Save/Save.tsx b/src/components/Save/Save.tsx index ffb23dd..8fc6909 100644 --- a/src/components/Save/Save.tsx +++ b/src/components/Save/Save.tsx @@ -4,12 +4,13 @@ import isEqual from "lodash/isEqual"; // Import deep comparison utility import { canSaveSettingToQdnAtom, hasSettingsChangedAtom, + isUsingImportExportSettingsAtom, oldPinnedAppsAtom, settingsLocalLastUpdatedAtom, settingsQDNLastUpdatedAtom, sortablePinnedAppsAtom, } from "../../atoms/global"; -import { Box, ButtonBase, Popover, Typography } from "@mui/material"; +import { Box, Button, ButtonBase, Popover, Typography } from "@mui/material"; import { objectToBase64 } from "../../qdn/encryption/group-encryption"; import { MyContext } from "../../App"; import { getFee } from "../../background"; @@ -19,6 +20,42 @@ import { IconWrapper } from "../Desktop/DesktopFooter"; import { Spacer } from "../../common/Spacer"; import { LoadingButton } from "@mui/lab"; import { saveToLocalStorage } from "../Apps/AppsNavBar"; +import { decryptData, encryptData } from "../../qortalRequests/get"; +import { saveFileToDiskGeneric } from "../../utils/generateWallet/generateWallet"; +import { base64ToUint8Array, uint8ArrayToObject } from "../../backgroundFunctions/encryption"; + + +export const handleImportClick = async () => { + const fileInput = document.createElement('input'); + fileInput.type = 'file'; + fileInput.accept = '.base64,.txt'; + + // Create a promise to handle file selection and reading synchronously + return await new Promise((resolve, reject) => { + fileInput.onchange = () => { + const file = fileInput.files[0]; + if (!file) { + reject(new Error('No file selected')); + return; + } + + const reader = new FileReader(); + reader.onload = (e) => { + resolve(e.target.result); // Resolve with the file content + }; + reader.onerror = () => { + reject(new Error('Error reading file')); + }; + + reader.readAsText(file); // Read the file as text (Base64 string) + }; + + // Trigger the file input dialog + fileInput.click(); + }); + +} + export const Save = ({ isDesktop, disableWidth, myName }) => { const [pinnedApps, setPinnedApps] = useRecoilState(sortablePinnedAppsAtom); const [settingsQdnLastUpdated, setSettingsQdnLastUpdated] = useRecoilState( @@ -28,6 +65,7 @@ export const Save = ({ isDesktop, disableWidth, myName }) => { settingsLocalLastUpdatedAtom ); const setHasSettingsChangedAtom = useSetRecoilState(hasSettingsChangedAtom); + const [isUsingImportExportSettings, setIsUsingImportExportSettings] = useRecoilState(isUsingImportExportSettingsAtom); const [canSave] = useRecoilState(canSaveSettingToQdnAtom); const [openSnack, setOpenSnack] = useState(false); @@ -66,6 +104,8 @@ export const Save = ({ isDesktop, disableWidth, myName }) => { settingsLocalLastUpdated, ]); + + useEffect(() => { setHasSettingsChangedAtom(hasChanged); }, [hasChanged]); @@ -219,7 +259,8 @@ export const Save = ({ isDesktop, disableWidth, myName }) => { overflow: "auto", }} > - { width: '100%' }} > - {!myName ? ( - - - You need a registered Qortal name to save your pinned apps to QDN. - - - ) : ( - <> - {hasChanged && ( - { fontSize: "14px", }} > - You have unsaved changes to your pinned apps. Save them to QDN. + You are using the export/import way of saving settings. - - - Save to QDN - - - {!isNaN(settingsQdnLastUpdated) && settingsQdnLastUpdated > 0 && ( - <> - - Don't like your current local changes? Would you like to - reset to your saved QDN pinned apps? - - - - Revert to QDN - - - )} - {!isNaN(settingsQdnLastUpdated) && settingsQdnLastUpdated === 0 && ( - <> - - Don't like your current local changes? Would you like to - reset to the default pinned apps? - - - - Revert to default - - - )} - - )} - {!isNaN(settingsQdnLastUpdated) && settingsQdnLastUpdated === -100 && ( - - - The app was unable to download your existing QDN-saved pinned - apps. Would you like to overwrite those changes? - - - + + + + )} + {!isUsingImportExportSettings && ( + {!myName ? ( + { fontSize: "14px", }} > - You currently do not have any changes to your pinned apps + You need a registered Qortal name to save your pinned apps to QDN. - - - )} - - )} - - + + ) : ( + <> + {hasChanged && ( + + + You have unsaved changes to your pinned apps. Save them to QDN. + + + + Save to QDN + + + {!isNaN(settingsQdnLastUpdated) && settingsQdnLastUpdated > 0 && ( + <> + + Don't like your current local changes? Would you like to + reset to your saved QDN pinned apps? + + + + Revert to QDN + + + )} + {!isNaN(settingsQdnLastUpdated) && settingsQdnLastUpdated === 0 && ( + <> + + Don't like your current local changes? Would you like to + reset to the default pinned apps? + + + + Revert to default + + + )} + + )} + {!isNaN(settingsQdnLastUpdated) && settingsQdnLastUpdated === -100 && isUsingImportExportSettings !== true && ( + + + The app was unable to download your existing QDN-saved pinned + apps. Would you like to overwrite those changes? + + + + Overwrite to QDN + + + )} + {!hasChanged && ( + + + You currently do not have any changes to your pinned apps + + + + )} + + )} + + { + try { + const fileContent = await handleImportClick(); + const decryptedData = await decryptData({ + encryptedData: fileContent, + }); + const decryptToUnit8ArraySubject = + base64ToUint8Array(decryptedData); + const responseData = uint8ArrayToObject( + decryptToUnit8ArraySubject + ); + if(Array.isArray(responseData)){ + saveToLocalStorage("ext_saved_settings_import_export", "sortablePinnedApps", responseData, { + isUsingImportExport: true + }); + setPinnedApps(responseData) + setOldPinnedApps(responseData) + setIsUsingImportExportSettings(true) + } + + } catch (error) { + console.log("error", error); + } + }}> + + Import + + { + try { + const data64 = await objectToBase64(pinnedApps); + + const encryptedData = await encryptData({ + data64, + }); + const blob = new Blob([encryptedData], { + type: "text/plain", + }); + + const timestamp = new Date() + .toISOString() + .replace(/:/g, "-"); // Safe timestamp for filenames + const filename = `qortal-new-ui-backup-settings-${timestamp}.txt`; + await saveFileToDiskGeneric(blob, filename) + + } catch (error) { + console.log('error', error) + } + }}> + Export + + + + )} + { const setCanSave = useSetRecoilState(canSaveSettingToQdnAtom); const setSettingsQDNLastUpdated = useSetRecoilState(settingsQDNLastUpdatedAtom); const [settingsLocalLastUpdated] = useRecoilState(settingsLocalLastUpdatedAtom); + const [isUsingImportExportSettings] = useRecoilState(isUsingImportExportSettingsAtom); + const [oldPinnedApps, setOldPinnedApps] = useRecoilState(oldPinnedAppsAtom) const getSavedSettings = useCallback(async (myName, settingsLocalLastUpdated)=> { @@ -87,8 +89,9 @@ export const useQortalGetSaveSettings = (myName, isAuthenticated) => { } }, []) useEffect(()=> { - if(!myName || !settingsLocalLastUpdated || !isAuthenticated) return + if(!myName || !settingsLocalLastUpdated || !isAuthenticated || isUsingImportExportSettings === null) return + if(isUsingImportExportSettings) return getSavedSettings(myName, settingsLocalLastUpdated) - }, [getSavedSettings, myName, settingsLocalLastUpdated, isAuthenticated]) + }, [getSavedSettings, myName, settingsLocalLastUpdated, isAuthenticated, isUsingImportExportSettings]) } diff --git a/src/useRetrieveDataLocalStorage.tsx b/src/useRetrieveDataLocalStorage.tsx index 8f808ab..2a248bd 100644 --- a/src/useRetrieveDataLocalStorage.tsx +++ b/src/useRetrieveDataLocalStorage.tsx @@ -1,6 +1,6 @@ import React, { useCallback, useEffect } from 'react' import { useSetRecoilState } from 'recoil'; -import { settingsLocalLastUpdatedAtom, sortablePinnedAppsAtom } from './atoms/global'; +import { isUsingImportExportSettingsAtom, oldPinnedAppsAtom, settingsLocalLastUpdatedAtom, settingsQDNLastUpdatedAtom, sortablePinnedAppsAtom } from './atoms/global'; function fetchFromLocalStorage(key) { try { @@ -18,7 +18,10 @@ function fetchFromLocalStorage(key) { export const useRetrieveDataLocalStorage = () => { const setSortablePinnedApps = useSetRecoilState(sortablePinnedAppsAtom); const setSettingsLocalLastUpdated = useSetRecoilState(settingsLocalLastUpdatedAtom); - + const setIsUsingImportExportSettings = useSetRecoilState(isUsingImportExportSettingsAtom) + const setSettingsQDNLastUpdated = useSetRecoilState(settingsQDNLastUpdatedAtom); + const setOldPinnedApps = useSetRecoilState(oldPinnedAppsAtom) + const getSortablePinnedApps = useCallback(()=> { const pinnedAppsLocal = fetchFromLocalStorage('ext_saved_settings') if(pinnedAppsLocal?.sortablePinnedApps){ @@ -28,10 +31,25 @@ export const useRetrieveDataLocalStorage = () => { setSettingsLocalLastUpdated(-1) } + }, []) + const getSortablePinnedAppsImportExport = useCallback(()=> { + const pinnedAppsLocal = fetchFromLocalStorage('ext_saved_settings_import_export') + if(pinnedAppsLocal?.sortablePinnedApps){ + setOldPinnedApps(pinnedAppsLocal?.sortablePinnedApps) + + + setIsUsingImportExportSettings(true) + setSettingsQDNLastUpdated(pinnedAppsLocal?.timestamp || 0) + + } else { + setIsUsingImportExportSettings(false) + } + }, []) useEffect(()=> { getSortablePinnedApps() + getSortablePinnedAppsImportExport() }, [getSortablePinnedApps]) }