added import/export settings

This commit is contained in:
PhilReact 2024-12-20 18:51:05 +02:00
parent 9b0a45858d
commit 9fd224cccf
6 changed files with 335 additions and 151 deletions

View File

@ -113,6 +113,7 @@ import {
enabledDevModeAtom, enabledDevModeAtom,
fullScreenAtom, fullScreenAtom,
hasSettingsChangedAtom, hasSettingsChangedAtom,
isUsingImportExportSettingsAtom,
oldPinnedAppsAtom, oldPinnedAppsAtom,
settingsLocalLastUpdatedAtom, settingsLocalLastUpdatedAtom,
settingsQDNLastUpdatedAtom, settingsQDNLastUpdatedAtom,
@ -453,6 +454,7 @@ function App() {
const resetAtomSortablePinnedAppsAtom = useResetRecoilState( const resetAtomSortablePinnedAppsAtom = useResetRecoilState(
sortablePinnedAppsAtom sortablePinnedAppsAtom
); );
const resetAtomIsUsingImportExportSettingsAtom = useResetRecoilState(isUsingImportExportSettingsAtom)
const resetAtomCanSaveSettingToQdnAtom = useResetRecoilState( const resetAtomCanSaveSettingToQdnAtom = useResetRecoilState(
canSaveSettingToQdnAtom canSaveSettingToQdnAtom
); );
@ -470,6 +472,7 @@ function App() {
resetAtomSettingsQDNLastUpdatedAtom(); resetAtomSettingsQDNLastUpdatedAtom();
resetAtomSettingsLocalLastUpdatedAtom(); resetAtomSettingsLocalLastUpdatedAtom();
resetAtomOldPinnedAppsAtom(); resetAtomOldPinnedAppsAtom();
resetAtomIsUsingImportExportSettingsAtom()
}; };
useEffect(() => { useEffect(() => {
if (!isMobile) return; if (!isMobile) return;

View File

@ -67,6 +67,11 @@ export const oldPinnedAppsAtom = atom({
key: 'oldPinnedAppsAtom', key: 'oldPinnedAppsAtom',
default: [], default: [],
}); });
export const isUsingImportExportSettingsAtom = atom({
key: 'isUsingImportExportSettingsAtom',
default: null,
});
export const fullScreenAtom = atom({ export const fullScreenAtom = atom({
key: 'fullScreenAtom', key: 'fullScreenAtom',

View File

@ -31,8 +31,12 @@ import {
sortablePinnedAppsAtom, sortablePinnedAppsAtom,
} from "../../atoms/global"; } from "../../atoms/global";
export function saveToLocalStorage(key, subKey, newValue) { export function saveToLocalStorage(key, subKey, newValue, otherRootData = {}, deleteWholeKey) {
try { try {
if(deleteWholeKey){
localStorage.setItem(key, null);
return
}
// Fetch existing data // Fetch existing data
const existingData = localStorage.getItem(key); const existingData = localStorage.getItem(key);
let combinedData = {}; let combinedData = {};
@ -43,12 +47,14 @@ export function saveToLocalStorage(key, subKey, newValue) {
// Merge with the new data under the subKey // Merge with the new data under the subKey
combinedData = { combinedData = {
...parsedData, ...parsedData,
...otherRootData,
timestamp: Date.now(), // Update the root timestamp timestamp: Date.now(), // Update the root timestamp
[subKey]: newValue, // Assuming the data is an array [subKey]: newValue, // Assuming the data is an array
}; };
} else { } else {
// If no existing data, just use the new data under the subKey // If no existing data, just use the new data under the subKey
combinedData = { combinedData = {
...otherRootData,
timestamp: Date.now(), // Set the initial root timestamp timestamp: Date.now(), // Set the initial root timestamp
[subKey]: newValue, [subKey]: newValue,
}; };

View File

@ -4,12 +4,13 @@ import isEqual from "lodash/isEqual"; // Import deep comparison utility
import { import {
canSaveSettingToQdnAtom, canSaveSettingToQdnAtom,
hasSettingsChangedAtom, hasSettingsChangedAtom,
isUsingImportExportSettingsAtom,
oldPinnedAppsAtom, oldPinnedAppsAtom,
settingsLocalLastUpdatedAtom, settingsLocalLastUpdatedAtom,
settingsQDNLastUpdatedAtom, settingsQDNLastUpdatedAtom,
sortablePinnedAppsAtom, sortablePinnedAppsAtom,
} from "../../atoms/global"; } 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 { objectToBase64 } from "../../qdn/encryption/group-encryption";
import { MyContext } from "../../App"; import { MyContext } from "../../App";
import { getFee } from "../../background"; import { getFee } from "../../background";
@ -19,6 +20,42 @@ import { IconWrapper } from "../Desktop/DesktopFooter";
import { Spacer } from "../../common/Spacer"; import { Spacer } from "../../common/Spacer";
import { LoadingButton } from "@mui/lab"; import { LoadingButton } from "@mui/lab";
import { saveToLocalStorage } from "../Apps/AppsNavBar"; 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 }) => { export const Save = ({ isDesktop, disableWidth, myName }) => {
const [pinnedApps, setPinnedApps] = useRecoilState(sortablePinnedAppsAtom); const [pinnedApps, setPinnedApps] = useRecoilState(sortablePinnedAppsAtom);
const [settingsQdnLastUpdated, setSettingsQdnLastUpdated] = useRecoilState( const [settingsQdnLastUpdated, setSettingsQdnLastUpdated] = useRecoilState(
@ -28,6 +65,7 @@ export const Save = ({ isDesktop, disableWidth, myName }) => {
settingsLocalLastUpdatedAtom settingsLocalLastUpdatedAtom
); );
const setHasSettingsChangedAtom = useSetRecoilState(hasSettingsChangedAtom); const setHasSettingsChangedAtom = useSetRecoilState(hasSettingsChangedAtom);
const [isUsingImportExportSettings, setIsUsingImportExportSettings] = useRecoilState(isUsingImportExportSettingsAtom);
const [canSave] = useRecoilState(canSaveSettingToQdnAtom); const [canSave] = useRecoilState(canSaveSettingToQdnAtom);
const [openSnack, setOpenSnack] = useState(false); const [openSnack, setOpenSnack] = useState(false);
@ -66,6 +104,8 @@ export const Save = ({ isDesktop, disableWidth, myName }) => {
settingsLocalLastUpdated, settingsLocalLastUpdated,
]); ]);
useEffect(() => { useEffect(() => {
setHasSettingsChangedAtom(hasChanged); setHasSettingsChangedAtom(hasChanged);
}, [hasChanged]); }, [hasChanged]);
@ -219,7 +259,8 @@ export const Save = ({ isDesktop, disableWidth, myName }) => {
overflow: "auto", overflow: "auto",
}} }}
> >
<Box {isUsingImportExportSettings && (
<Box
sx={{ sx={{
padding: "15px", padding: "15px",
display: "flex", display: "flex",
@ -228,27 +269,7 @@ export const Save = ({ isDesktop, disableWidth, myName }) => {
width: '100%' width: '100%'
}} }}
> >
{!myName ? ( <Box
<Box
sx={{
width: "100%",
display: "flex",
flexDirection: "column",
alignItems: "center",
}}
>
<Typography
sx={{
fontSize: "14px",
}}
>
You need a registered Qortal name to save your pinned apps to QDN.
</Typography>
</Box>
) : (
<>
{hasChanged && (
<Box
sx={{ sx={{
width: "100%", width: "100%",
display: "flex", display: "flex",
@ -261,124 +282,45 @@ export const Save = ({ isDesktop, disableWidth, myName }) => {
fontSize: "14px", fontSize: "14px",
}} }}
> >
You have unsaved changes to your pinned apps. Save them to QDN. You are using the export/import way of saving settings.
</Typography> </Typography>
<Spacer height="10px" /> <Spacer height="40px" />
<LoadingButton <Button
sx={{ size="small"
backgroundColor: "var(--green)", onClick={()=> {
color: "black", saveToLocalStorage("ext_saved_settings_import_export", "sortablePinnedApps", null, true);
opacity: 0.7, setIsUsingImportExportSettings(false)
fontWeight: 'bold', }}
"&:hover": { variant="contained"
backgroundColor: "var(--green)", sx={{
color: "black",
opacity: 1,
},
}}
size="small"
loading={isLoading}
onClick={saveToQdn}
variant="contained"
>
Save to QDN
</LoadingButton>
<Spacer height="20px" />
{!isNaN(settingsQdnLastUpdated) && settingsQdnLastUpdated > 0 && (
<>
<Typography
sx={{
fontSize: "14px",
}}
>
Don't like your current local changes? Would you like to
reset to your saved QDN pinned apps?
</Typography>
<Spacer height="10px" />
<LoadingButton
size="small"
loading={isLoading}
onClick={revertChanges}
variant="contained"
sx={{
backgroundColor: "var(--danger)",
color: "black",
fontWeight: 'bold',
opacity: 0.7,
"&:hover": {
backgroundColor: "var(--danger)",
color: "black",
opacity: 1,
},
}}
>
Revert to QDN
</LoadingButton>
</>
)}
{!isNaN(settingsQdnLastUpdated) && settingsQdnLastUpdated === 0 && (
<>
<Typography
sx={{
fontSize: "14px",
}}
>
Don't like your current local changes? Would you like to
reset to the default pinned apps?
</Typography>
<Spacer height="10px" />
<LoadingButton
loading={isLoading}
onClick={revertChanges}
variant="contained"
>
Revert to default
</LoadingButton>
</>
)}
</Box>
)}
{!isNaN(settingsQdnLastUpdated) && settingsQdnLastUpdated === -100 && (
<Box
sx={{
width: "100%",
display: "flex",
flexDirection: "column",
alignItems: "center",
}}
>
<Typography
sx={{
fontSize: "14px",
}}
>
The app was unable to download your existing QDN-saved pinned
apps. Would you like to overwrite those changes?
</Typography>
<Spacer height="10px" />
<LoadingButton
size="small"
loading={isLoading}
onClick={saveToQdn}
variant="contained"
sx={{
backgroundColor: "var(--danger)",
color: "black",
fontWeight: 'bold',
opacity: 0.7,
"&:hover": {
backgroundColor: "var(--danger)", backgroundColor: "var(--danger)",
color: "black", color: "black",
opacity: 1, fontWeight: 'bold',
}, opacity: 0.7,
}} "&:hover": {
> backgroundColor: "var(--danger)",
Overwrite to QDN color: "black",
</LoadingButton> opacity: 1,
</Box> },
)} }}
{!hasChanged && ( >
Use QDN saving
</Button>
</Box>
</Box>
)}
{!isUsingImportExportSettings && (
<Box <Box
sx={{
padding: "15px",
display: "flex",
flexDirection: "column",
gap: 1,
width: '100%'
}}
>
{!myName ? (
<Box
sx={{ sx={{
width: "100%", width: "100%",
display: "flex", display: "flex",
@ -391,15 +333,222 @@ export const Save = ({ isDesktop, disableWidth, myName }) => {
fontSize: "14px", 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.
</Typography> </Typography>
</Box>
</Box> ) : (
)} <>
</> {hasChanged && (
)} <Box
sx={{
</Box> width: "100%",
display: "flex",
flexDirection: "column",
alignItems: "center",
}}
>
<Typography
sx={{
fontSize: "14px",
}}
>
You have unsaved changes to your pinned apps. Save them to QDN.
</Typography>
<Spacer height="10px" />
<LoadingButton
sx={{
backgroundColor: "var(--green)",
color: "black",
opacity: 0.7,
fontWeight: 'bold',
"&:hover": {
backgroundColor: "var(--green)",
color: "black",
opacity: 1,
},
}}
size="small"
loading={isLoading}
onClick={saveToQdn}
variant="contained"
>
Save to QDN
</LoadingButton>
<Spacer height="20px" />
{!isNaN(settingsQdnLastUpdated) && settingsQdnLastUpdated > 0 && (
<>
<Typography
sx={{
fontSize: "14px",
}}
>
Don't like your current local changes? Would you like to
reset to your saved QDN pinned apps?
</Typography>
<Spacer height="10px" />
<LoadingButton
size="small"
loading={isLoading}
onClick={revertChanges}
variant="contained"
sx={{
backgroundColor: "var(--danger)",
color: "black",
fontWeight: 'bold',
opacity: 0.7,
"&:hover": {
backgroundColor: "var(--danger)",
color: "black",
opacity: 1,
},
}}
>
Revert to QDN
</LoadingButton>
</>
)}
{!isNaN(settingsQdnLastUpdated) && settingsQdnLastUpdated === 0 && (
<>
<Typography
sx={{
fontSize: "14px",
}}
>
Don't like your current local changes? Would you like to
reset to the default pinned apps?
</Typography>
<Spacer height="10px" />
<LoadingButton
loading={isLoading}
onClick={revertChanges}
variant="contained"
>
Revert to default
</LoadingButton>
</>
)}
</Box>
)}
{!isNaN(settingsQdnLastUpdated) && settingsQdnLastUpdated === -100 && isUsingImportExportSettings !== true && (
<Box
sx={{
width: "100%",
display: "flex",
flexDirection: "column",
alignItems: "center",
}}
>
<Typography
sx={{
fontSize: "14px",
}}
>
The app was unable to download your existing QDN-saved pinned
apps. Would you like to overwrite those changes?
</Typography>
<Spacer height="10px" />
<LoadingButton
size="small"
loading={isLoading}
onClick={saveToQdn}
variant="contained"
sx={{
backgroundColor: "var(--danger)",
color: "black",
fontWeight: 'bold',
opacity: 0.7,
"&:hover": {
backgroundColor: "var(--danger)",
color: "black",
opacity: 1,
},
}}
>
Overwrite to QDN
</LoadingButton>
</Box>
)}
{!hasChanged && (
<Box
sx={{
width: "100%",
display: "flex",
flexDirection: "column",
alignItems: "center",
}}
>
<Typography
sx={{
fontSize: "14px",
}}
>
You currently do not have any changes to your pinned apps
</Typography>
</Box>
)}
</>
)}
<Box sx={{
display: 'flex',
gap: '10px',
justifyContent: 'flex-end',
width: '100%'
}}>
<ButtonBase onClick={async () => {
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
</ButtonBase>
<ButtonBase onClick={async () => {
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
</ButtonBase>
</Box>
</Box>
)}
</Popover> </Popover>
<CustomizedSnackbars <CustomizedSnackbars
duration={3500} duration={3500}

View File

@ -1,6 +1,6 @@
import React, { useCallback, useEffect } from 'react' import React, { useCallback, useEffect } from 'react'
import { useRecoilState, useSetRecoilState } from 'recoil'; import { useRecoilState, useSetRecoilState } from 'recoil';
import { canSaveSettingToQdnAtom, oldPinnedAppsAtom, settingsLocalLastUpdatedAtom, settingsQDNLastUpdatedAtom, sortablePinnedAppsAtom } from './atoms/global'; import { canSaveSettingToQdnAtom, isUsingImportExportSettingsAtom, oldPinnedAppsAtom, settingsLocalLastUpdatedAtom, settingsQDNLastUpdatedAtom, sortablePinnedAppsAtom } from './atoms/global';
import { getArbitraryEndpointReact, getBaseApiReact } from './App'; import { getArbitraryEndpointReact, getBaseApiReact } from './App';
import { decryptResource } from './components/Group/Group'; import { decryptResource } from './components/Group/Group';
import { base64ToUint8Array, uint8ArrayToObject } from './backgroundFunctions/encryption'; import { base64ToUint8Array, uint8ArrayToObject } from './backgroundFunctions/encryption';
@ -58,6 +58,8 @@ export const useQortalGetSaveSettings = (myName, isAuthenticated) => {
const setCanSave = useSetRecoilState(canSaveSettingToQdnAtom); const setCanSave = useSetRecoilState(canSaveSettingToQdnAtom);
const setSettingsQDNLastUpdated = useSetRecoilState(settingsQDNLastUpdatedAtom); const setSettingsQDNLastUpdated = useSetRecoilState(settingsQDNLastUpdatedAtom);
const [settingsLocalLastUpdated] = useRecoilState(settingsLocalLastUpdatedAtom); const [settingsLocalLastUpdated] = useRecoilState(settingsLocalLastUpdatedAtom);
const [isUsingImportExportSettings] = useRecoilState(isUsingImportExportSettingsAtom);
const [oldPinnedApps, setOldPinnedApps] = useRecoilState(oldPinnedAppsAtom) const [oldPinnedApps, setOldPinnedApps] = useRecoilState(oldPinnedAppsAtom)
const getSavedSettings = useCallback(async (myName, settingsLocalLastUpdated)=> { const getSavedSettings = useCallback(async (myName, settingsLocalLastUpdated)=> {
@ -87,8 +89,9 @@ export const useQortalGetSaveSettings = (myName, isAuthenticated) => {
} }
}, []) }, [])
useEffect(()=> { useEffect(()=> {
if(!myName || !settingsLocalLastUpdated || !isAuthenticated) return if(!myName || !settingsLocalLastUpdated || !isAuthenticated || isUsingImportExportSettings === null) return
if(isUsingImportExportSettings) return
getSavedSettings(myName, settingsLocalLastUpdated) getSavedSettings(myName, settingsLocalLastUpdated)
}, [getSavedSettings, myName, settingsLocalLastUpdated, isAuthenticated]) }, [getSavedSettings, myName, settingsLocalLastUpdated, isAuthenticated, isUsingImportExportSettings])
} }

View File

@ -1,6 +1,6 @@
import React, { useCallback, useEffect } from 'react' import React, { useCallback, useEffect } from 'react'
import { useSetRecoilState } from 'recoil'; import { useSetRecoilState } from 'recoil';
import { settingsLocalLastUpdatedAtom, sortablePinnedAppsAtom } from './atoms/global'; import { isUsingImportExportSettingsAtom, oldPinnedAppsAtom, settingsLocalLastUpdatedAtom, settingsQDNLastUpdatedAtom, sortablePinnedAppsAtom } from './atoms/global';
function fetchFromLocalStorage(key) { function fetchFromLocalStorage(key) {
try { try {
@ -18,7 +18,10 @@ function fetchFromLocalStorage(key) {
export const useRetrieveDataLocalStorage = () => { export const useRetrieveDataLocalStorage = () => {
const setSortablePinnedApps = useSetRecoilState(sortablePinnedAppsAtom); const setSortablePinnedApps = useSetRecoilState(sortablePinnedAppsAtom);
const setSettingsLocalLastUpdated = useSetRecoilState(settingsLocalLastUpdatedAtom); const setSettingsLocalLastUpdated = useSetRecoilState(settingsLocalLastUpdatedAtom);
const setIsUsingImportExportSettings = useSetRecoilState(isUsingImportExportSettingsAtom)
const setSettingsQDNLastUpdated = useSetRecoilState(settingsQDNLastUpdatedAtom);
const setOldPinnedApps = useSetRecoilState(oldPinnedAppsAtom)
const getSortablePinnedApps = useCallback(()=> { const getSortablePinnedApps = useCallback(()=> {
const pinnedAppsLocal = fetchFromLocalStorage('ext_saved_settings') const pinnedAppsLocal = fetchFromLocalStorage('ext_saved_settings')
if(pinnedAppsLocal?.sortablePinnedApps){ if(pinnedAppsLocal?.sortablePinnedApps){
@ -28,10 +31,25 @@ export const useRetrieveDataLocalStorage = () => {
setSettingsLocalLastUpdated(-1) 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(()=> { useEffect(()=> {
getSortablePinnedApps() getSortablePinnedApps()
getSortablePinnedAppsImportExport()
}, [getSortablePinnedApps]) }, [getSortablePinnedApps])
} }