import { useContext, useEffect, useMemo, useState } from 'react'; import isEqual from 'lodash/isEqual'; // TODO Import deep comparison utility import { canSaveSettingToQdnAtom, hasSettingsChangedAtom, isUsingImportExportSettingsAtom, oldPinnedAppsAtom, settingsLocalLastUpdatedAtom, settingsQDNLastUpdatedAtom, sortablePinnedAppsAtom, } from '../../atoms/global'; import { Box, Button, ButtonBase, Popover, Typography, useTheme, } 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/Icons/SaveIcon'; import { IconWrapper } from '../Desktop/DesktopFooter'; import { Spacer } from '../../common/Spacer'; import { LoadingButton } from '@mui/lab'; import { saveToLocalStorage } from '../Apps/AppsNavBarDesktop'; import { decryptData, encryptData } from '../../qortalRequests/get'; import { saveFileToDiskGeneric } from '../../utils/generateWallet/generateWallet'; import { base64ToUint8Array, uint8ArrayToObject, } from '../../backgroundFunctions/encryption'; import { useTranslation } from 'react-i18next'; import { useAtom, useSetAtom } from 'jotai'; 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] = useAtom(sortablePinnedAppsAtom); const [settingsQdnLastUpdated, setSettingsQdnLastUpdated] = useAtom( settingsQDNLastUpdatedAtom ); const [settingsLocalLastUpdated] = useAtom(settingsLocalLastUpdatedAtom); const setHasSettingsChangedAtom = useSetAtom(hasSettingsChangedAtom); const [isUsingImportExportSettings, setIsUsingImportExportSettings] = useAtom( isUsingImportExportSettingsAtom ); const [openSnack, setOpenSnack] = useState(false); const [isLoading, setIsLoading] = useState(false); const [infoSnack, setInfoSnack] = useState(null); const [oldPinnedApps, setOldPinnedApps] = useAtom(oldPinnedAppsAtom); const [anchorEl, setAnchorEl] = useState(null); const { show } = useContext(MyContext); const theme = useTheme(); const { t } = useTranslation(['core']); 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: t('core:message.generic.publish_qnd', { postProcess: 'capitalizeFirst', }), publishFee: fee.fee + ' QORT', }); const response = await new Promise((res, rej) => { window .sendMessage('publishOnQDN', { data: encryptData, identifier: 'ext_saved_settings', service: 'DOCUMENT_PRIVATE', uploadType: 'base64', }) .then((response) => { if (!response?.error) { res(response); return; } rej(response.error); }) .catch((error) => { rej( error.message || t('core:message.error.generic', { postProcess: 'capitalizeFirst', }) ); }); }); if (response?.identifier) { setOldPinnedApps(pinnedApps); setSettingsQdnLastUpdated(Date.now()); setInfoSnack({ type: 'success', message: t('core:message.success.published_qdn', { postProcess: 'capitalizeFirst', }), }); setOpenSnack(true); setAnchorEl(null); } } } catch (error) { setInfoSnack({ type: 'error', message: error?.message || t('core:message.error.save_qdn', { postProcess: 'capitalizeFirst', }), }); 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 && ( {t('core:message.generic.settings', { postProcess: 'capitalizeFirst', })} {' '} )} {!isUsingImportExportSettings && ( {!myName ? ( {t('core:message.generic.register_name', { postProcess: 'capitalizeFirst', })} ) : ( <> {hasChanged && ( {t('core:message.generic.unsaved_changes', { postProcess: 'capitalizeFirst', })} {t('core:action.save_qdn', { postProcess: 'capitalizeFirst', })} {!isNaN(settingsQdnLastUpdated) && settingsQdnLastUpdated > 0 && ( <> {t('core:message.question.reset_qdn', { postProcess: 'capitalizeFirst', })} {t('core:message.generic.revert_qdn', { postProcess: 'capitalizeFirst', })} )} {!isNaN(settingsQdnLastUpdated) && settingsQdnLastUpdated === 0 && ( <> {' '} {t('core:message.question.reset_pinned', { postProcess: 'capitalizeFirst', })} {t('core:message.generic.revert_default', { postProcess: 'capitalizeFirst', })} )} )} {!isNaN(settingsQdnLastUpdated) && settingsQdnLastUpdated === -100 && isUsingImportExportSettings !== true && ( {t('core:message.question.overwrite_changes', { postProcess: 'capitalizeFirst', })} {t('core:message.generic.overwrite_qdn', { postProcess: 'capitalizeFirst', })} )} {!hasChanged && ( {t('core:message.generic.no_pinned_changes', { postProcess: 'capitalizeFirst', })} )} )} )} { 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); } }} > {t('core:action.import', { postProcess: 'capitalizeFirst', })} { 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); } }} > {t('core:action.export', { postProcess: 'capitalizeFirst', })} ); };