2025-04-06 22:00:38 +02:00

573 lines
18 KiB
TypeScript

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 (
<>
<ButtonBase
onClick={handlePopupClick}
disabled={
// !hasChanged ||
// !canSave ||
isLoading
// settingsQdnLastUpdated === -100
}
>
{isDesktop ? (
<IconWrapper
disableWidth={disableWidth}
label="Save"
selected={false}
>
<SaveIcon
color={hasChanged && !isLoading ? "#5EB049" : undefined}
/>
</IconWrapper>
) : (
<SaveIcon color={hasChanged && !isLoading ? "#5EB049" : undefined} />
)}
</ButtonBase>
<Popover
open={!!anchorEl}
anchorEl={anchorEl}
onClose={() => 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 && (
<Box
sx={{
padding: "15px",
display: "flex",
flexDirection: "column",
gap: 1,
width: "100%",
}}
>
<Box
sx={{
width: "100%",
display: "flex",
flexDirection: "column",
alignItems: "center",
}}
>
<Typography
sx={{
fontSize: "14px",
}}
>
You are using the export/import way of saving settings.
</Typography>
<Spacer height="40px" />
<Button
size="small"
onClick={() => {
saveToLocalStorage(
"ext_saved_settings_import_export",
"sortablePinnedApps",
null,
true
);
setIsUsingImportExportSettings(false);
}}
variant="contained"
sx={{
backgroundColor: "var(--danger)",
color: "black",
fontWeight: "bold",
opacity: 0.7,
"&:hover": {
backgroundColor: "var(--danger)",
color: "black",
opacity: 1,
},
}}
>
Use QDN saving
</Button>
</Box>
</Box>
)}
{!isUsingImportExportSettings && (
<Box
sx={{
padding: "15px",
display: "flex",
flexDirection: "column",
gap: 1,
width: "100%",
}}
>
{!myName ? (
<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={{
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>
)}
<Box
sx={{
padding: "15px",
display: "flex",
flexDirection: "column",
gap: 1,
width: "100%",
}}
>
<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>
<CustomizedSnackbars
duration={3500}
open={openSnack}
setOpen={setOpenSnack}
info={infoSnack}
setInfo={setInfoSnack}
/>
</>
);
};