import React, { useContext, useEffect, useMemo, useState } from "react"; import { useRecoilState, useSetRecoilState } from "recoil"; import isEqual from "lodash/isEqual"; // Import deep comparison utility import { canSaveSettingToQdnAtom, hasSettingsChangedAtom, isUsingImportExportSettingsAtom, oldPinnedAppsAtom, settingsLocalLastUpdatedAtom, settingsQDNLastUpdatedAtom, sortablePinnedAppsAtom, } from "../../atoms/global"; import { Box, Button, ButtonBase, Popover, Typography } from "@mui/material"; import { objectToBase64 } from "../../qdn/encryption/group-encryption"; import { MyContext } from "../../App"; import { getFee } from "../../background"; import { CustomizedSnackbars } from "../Snackbar/Snackbar"; import { SaveIcon } from "../../assets/svgs/SaveIcon"; 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( settingsQDNLastUpdatedAtom ); const [settingsLocalLastUpdated] = useRecoilState( settingsLocalLastUpdatedAtom ); const setHasSettingsChangedAtom = useSetRecoilState(hasSettingsChangedAtom); const [isUsingImportExportSettings, setIsUsingImportExportSettings] = useRecoilState(isUsingImportExportSettingsAtom); const [canSave] = useRecoilState(canSaveSettingToQdnAtom); const [openSnack, setOpenSnack] = useState(false); const [isLoading, setIsLoading] = useState(false); const [infoSnack, setInfoSnack] = useState(null); const [oldPinnedApps, setOldPinnedApps] = useRecoilState(oldPinnedAppsAtom); const [anchorEl, setAnchorEl] = useState(null); const { show } = useContext(MyContext); const hasChanged = useMemo(() => { const newChanges = { sortablePinnedApps: pinnedApps.map((item) => { return { name: item?.name, service: item?.service, }; }), }; const oldChanges = { sortablePinnedApps: oldPinnedApps.map((item) => { return { name: item?.name, service: item?.service, }; }), }; if (settingsQdnLastUpdated === -100) return false; return ( !isEqual(oldChanges, newChanges) && settingsQdnLastUpdated < settingsLocalLastUpdated ); }, [ oldPinnedApps, pinnedApps, settingsQdnLastUpdated, settingsLocalLastUpdated, ]); useEffect(() => { setHasSettingsChangedAtom(hasChanged); }, [hasChanged]); const saveToQdn = async () => { try { setIsLoading(true); const data64 = await objectToBase64({ sortablePinnedApps: pinnedApps.map((item) => { return { name: item?.name, service: item?.service, }; }), }); const encryptData = await new Promise((res, rej) => { window .sendMessage( "ENCRYPT_DATA", { data64, }, 60000 ) .then((response) => { if (response.error) { rej(response?.message); return; } else { res(response); } }) .catch((error) => { console.error("Failed qortalRequest", error); }); }); if (encryptData && !encryptData?.error) { const fee = await getFee("ARBITRARY"); await show({ message: "Would you like to publish your settings to QDN (encrypted) ?", publishFee: fee.fee + " QORT", }); const response = await new Promise((res, rej) => { window .sendMessage("publishOnQDN", { data: encryptData, identifier: "ext_saved_settings", service: "DOCUMENT_PRIVATE", }) .then((response) => { if (!response?.error) { res(response); return; } rej(response.error); }) .catch((error) => { rej(error.message || "An error occurred"); }); }); if (response?.identifier) { setOldPinnedApps(pinnedApps); setSettingsQdnLastUpdated(Date.now()); setInfoSnack({ type: "success", message: "Sucessfully published to QDN", }); setOpenSnack(true); setAnchorEl(null); } } } catch (error) { setInfoSnack({ type: "error", message: error?.message || "Unable to save to QDN", }); setOpenSnack(true); } finally { setIsLoading(false); } }; const handlePopupClick = (event) => { event.stopPropagation(); // Prevent parent onClick from firing setAnchorEl(event.currentTarget); }; const revertChanges = () => { setPinnedApps(oldPinnedApps); saveToLocalStorage("ext_saved_settings", "sortablePinnedApps", null); setAnchorEl(null); }; return ( <> {isDesktop ? ( ) : ( )} setAnchorEl(null)} // Close popover on click outside anchorOrigin={{ vertical: "bottom", horizontal: "center", }} transformOrigin={{ vertical: "top", horizontal: "center", }} sx={{ width: "300px", maxWidth: "90%", maxHeight: "80%", overflow: "auto", }} > {isUsingImportExportSettings && ( You are using the export/import way of saving settings. )} {!isUsingImportExportSettings && ( {!myName ? ( 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 ); };