change save indicator

This commit is contained in:
PhilReact 2024-10-21 04:46:45 +03:00
parent 082c9c11cf
commit 7091e0b536
8 changed files with 270 additions and 54 deletions

View File

@ -9,4 +9,14 @@ export const sortablePinnedAppsAtom = atom({
export const canSaveSettingToQdnAtom = atom({
key: 'canSaveSettingToQdnAtom',
default: false,
});
export const settingsQDNLastUpdatedAtom = atom({
key: 'settingsQDNLastUpdatedAtom',
default: -100,
});
export const settingsLocalLastUpdatedAtom = atom({
key: 'settingsLocalLastUpdatedAtom',
default: 0,
});

View File

@ -12,28 +12,52 @@ import { executeEvent, subscribeToEvent, unsubscribeFromEvent } from "../../util
import TabComponent from "./TabComponent";
import PushPinIcon from '@mui/icons-material/PushPin';
import RefreshIcon from '@mui/icons-material/Refresh';
import { useRecoilState } from "recoil";
import { sortablePinnedAppsAtom } from "../../atoms/global";
import { useRecoilState, useSetRecoilState } from "recoil";
import { settingsLocalLastUpdatedAtom, sortablePinnedAppsAtom } from "../../atoms/global";
export function saveToLocalStorage(key, value) {
export function saveToLocalStorage(key, subKey, newValue) {
try {
const serializedValue = JSON.stringify(value);
localStorage.setItem(key, serializedValue);
console.log(`Data saved to localStorage with key: ${key}`);
// Fetch existing data
const existingData = localStorage.getItem(key);
let combinedData = {};
if (existingData) {
// Parse the existing data
const parsedData = JSON.parse(existingData);
// Merge with the new data under the subKey
combinedData = {
...parsedData,
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 = {
timestamp: Date.now(), // Set the initial root timestamp
[subKey]: newValue
};
}
// Save combined data back to localStorage
const serializedValue = JSON.stringify(combinedData);
localStorage.setItem(key, serializedValue);
console.log(`Data saved to localStorage with key: ${key} and subKey: ${subKey}`);
} catch (error) {
console.error('Error saving to localStorage:', error);
console.error('Error saving to localStorage:', error);
}
}
export const AppsNavBar = () => {
const [tabs, setTabs] = useState([])
const [selectedTab, setSelectedTab] = useState([])
const [selectedTab, setSelectedTab] = useState(null)
const [isNewTabWindow, setIsNewTabWindow] = useState(false)
const tabsRef = useRef(null);
const [anchorEl, setAnchorEl] = useState(null);
const open = Boolean(anchorEl);
const [sortablePinnedApps, setSortablePinnedApps] = useRecoilState(sortablePinnedAppsAtom);
const setSettingsLocalLastUpdated = useSetRecoilState(settingsLocalLastUpdatedAtom);
const handleClick = (event) => {
setAnchorEl(event.currentTarget);
@ -59,7 +83,7 @@ export const AppsNavBar = () => {
const {tabs, selectedTab, isNewTabWindow} = e.detail?.data;
setTabs([...tabs])
setSelectedTab({...selectedTab})
setSelectedTab(!selectedTab ? nulll : {...selectedTab})
setIsNewTabWindow(isNewTabWindow)
};
@ -71,6 +95,8 @@ export const AppsNavBar = () => {
};
}, []);
console.log('selectedTab', selectedTab)
const isSelectedAppPinned = !!sortablePinnedApps?.find((item)=> item?.name === selectedTab?.name && item?.service === selectedTab?.service)
return (
<AppsNavBarParent>
@ -115,6 +141,7 @@ export const AppsNavBar = () => {
gap: '10px'
}}>
<ButtonBase onClick={()=> {
setSelectedTab(null)
executeEvent("newTabWindow", {
});
}}>
@ -186,10 +213,11 @@ export const AppsNavBar = () => {
}];
}
saveToLocalStorage('sortablePinnedApps', updatedApps)
saveToLocalStorage('ext_saved_settings', 'sortablePinnedApps', updatedApps)
return updatedApps;
});
setSettingsLocalLastUpdated(Date.now())
handleClose();
}}
>

View File

@ -7,9 +7,10 @@ import { Avatar, ButtonBase } from '@mui/material';
import { AppCircle, AppCircleContainer, AppCircleLabel } from './Apps-styles';
import { getBaseApiReact } from '../../App';
import { executeEvent } from '../../utils/events';
import { sortablePinnedAppsAtom } from '../../atoms/global';
import { useRecoilState } from 'recoil';
import { settingsLocalLastUpdatedAtom, sortablePinnedAppsAtom } from '../../atoms/global';
import { useRecoilState, useSetRecoilState } from 'recoil';
import { saveToLocalStorage } from './AppsNavBar';
import { ContextMenuPinnedApps } from '../ContextMenuPinnedApps';
const SortableItem = ({ id, name, app }) => {
const { attributes, listeners, setNodeRef, transform, transition } = useSortable({ id });
@ -27,6 +28,7 @@ const SortableItem = ({ id, name, app }) => {
};
return (
<ContextMenuPinnedApps app={app}>
<ButtonBase
ref={setNodeRef} {...attributes} {...listeners}
sx={{
@ -75,11 +77,13 @@ const SortableItem = ({ id, name, app }) => {
</AppCircleLabel>
</AppCircleContainer>
</ButtonBase>
</ContextMenuPinnedApps>
);
};
export const SortablePinnedApps = ({ myWebsite, myApp, availableQapps = [] }) => {
const [pinnedApps, setPinnedApps] = useRecoilState(sortablePinnedAppsAtom);
const setSettingsLocalLastUpdated = useSetRecoilState(settingsLocalLastUpdatedAtom);
const transformPinnedApps = useMemo(()=> {
console.log({myWebsite, myApp, availableQapps, pinnedApps})
@ -149,8 +153,8 @@ export const SortablePinnedApps = ({ myWebsite, myApp, availableQapps = [] }) =
const newOrder = arrayMove(transformPinnedApps, oldIndex, newIndex);
setPinnedApps(newOrder);
saveToLocalStorage('sortablePinnedApps', newOrder)
saveToLocalStorage('ext_saved_settings','sortablePinnedApps', newOrder)
setSettingsLocalLastUpdated(Date.now())
}
};
return (

View File

@ -0,0 +1,136 @@
import React, { useState, useRef } from 'react';
import { ListItemIcon, Menu, MenuItem, Typography, styled } from '@mui/material';
import PushPinIcon from '@mui/icons-material/PushPin';
import { saveToLocalStorage } from './Apps/AppsNavBar';
import { useRecoilState } from 'recoil';
import { sortablePinnedAppsAtom } from '../atoms/global';
const CustomStyledMenu = styled(Menu)(({ theme }) => ({
'& .MuiPaper-root': {
backgroundColor: '#f9f9f9',
borderRadius: '12px',
padding: theme.spacing(1),
boxShadow: '0 5px 15px rgba(0, 0, 0, 0.2)',
},
'& .MuiMenuItem-root': {
fontSize: '14px',
color: '#444',
transition: '0.3s background-color',
'&:hover': {
backgroundColor: '#f0f0f0',
},
},
}));
export const ContextMenuPinnedApps = ({ children, app, setEnableDrag }) => {
const [menuPosition, setMenuPosition] = useState(null);
const longPressTimeout = useRef(null);
const maxHoldTimeout = useRef(null);
const preventClick = useRef(false);
const startTouchPosition = useRef({ x: 0, y: 0 }); // Track initial touch position
const [sortablePinnedApps, setSortablePinnedApps] = useRecoilState(sortablePinnedAppsAtom);
const handleContextMenu = (event) => {
event.preventDefault();
event.stopPropagation();
preventClick.current = true;
setMenuPosition({
mouseX: event.clientX,
mouseY: event.clientY,
});
};
const handleTouchStart = (event) => {
const { clientX, clientY } = event.touches[0];
startTouchPosition.current = { x: clientX, y: clientY };
longPressTimeout.current = setTimeout(() => {
preventClick.current = true;
setEnableDrag(false);
event.stopPropagation();
setMenuPosition({
mouseX: clientX,
mouseY: clientY,
});
}, 500);
// Set a maximum hold duration (e.g., 1.5 seconds)
maxHoldTimeout.current = setTimeout(() => {
clearTimeout(longPressTimeout.current);
}, 1500);
};
const handleTouchMove = (event) => {
const { clientX, clientY } = event.touches[0];
const { x, y } = startTouchPosition.current;
// Determine if the touch has moved beyond a small threshold (e.g., 10px)
const movedEnough = Math.abs(clientX - x) > 10 || Math.abs(clientY - y) > 10;
if (movedEnough) {
clearTimeout(longPressTimeout.current);
clearTimeout(maxHoldTimeout.current);
}
};
const handleTouchEnd = (event) => {
clearTimeout(longPressTimeout.current);
clearTimeout(maxHoldTimeout.current);
setEnableDrag(true);
if (preventClick.current) {
event.preventDefault();
event.stopPropagation();
preventClick.current = false;
}
};
const handleClose = (e) => {
e.preventDefault();
e.stopPropagation();
setMenuPosition(null);
};
return (
<div
onContextMenu={handleContextMenu}
onTouchStart={handleTouchStart}
onTouchMove={handleTouchMove}
onTouchEnd={handleTouchEnd}
style={{ touchAction: 'none' }}
>
{children}
<CustomStyledMenu
disableAutoFocusItem
open={!!menuPosition}
onClose={handleClose}
anchorReference="anchorPosition"
anchorPosition={
menuPosition
? { top: menuPosition.mouseY, left: menuPosition.mouseX }
: undefined
}
onClick={(e) => {
e.stopPropagation();
}}
>
<MenuItem onClick={(e) => {
handleClose(e);
setSortablePinnedApps((prev) => {
const updatedApps = prev.filter(
(item) => !(item?.name === app?.name && item?.service === app?.service)
);
saveToLocalStorage('ext_saved_settings', 'sortablePinnedApps', updatedApps);
return updatedApps;
});
}}>
<ListItemIcon sx={{ minWidth: '32px' }}>
<PushPinIcon fontSize="small" />
</ListItemIcon>
<Typography variant="inherit" sx={{ fontSize: '14px' }}>
Unpin app
</Typography>
</MenuItem>
</CustomStyledMenu>
</div>
);
};

View File

@ -239,16 +239,22 @@ const Header = ({
}}
>
{/* Left Home Icon */}
<IconButton
edge="start"
color="inherit"
aria-label="home"
<Box
sx={{
display: "flex",
alignItems: "center",
gap: "18px",
width: "75px",
}}
>
<ButtonBase
onClick={goToHome}
// onClick={onHomeClick}
>
<HomeIcon color="rgba(145, 145, 147, 1)" />
</IconButton>
</ButtonBase>
</Box>
{/* Center Title */}
<Typography
variant="h6"
@ -261,18 +267,26 @@ const Header = ({
>
QORTAL
</Typography>
<Box
sx={{
display: "flex",
alignItems: "center",
gap: "30px",
width: "75px",
justifyContent: "flex-end",
}}
>
{/* Right Logout Icon */}
<IconButton
<Save />
<ButtonBase
onClick={logoutFunc}
edge="end"
color="inherit"
aria-label="logout"
// onClick={onLogoutClick}
>
<LogoutIcon color="rgba(145, 145, 147, 1)" />
</IconButton>
</ButtonBase>
</Box>
</Toolbar>
</AppBar>

View File

@ -1,7 +1,7 @@
import React, { useContext, useMemo, useState } from 'react'
import { useRecoilState } from 'recoil';
import isEqual from 'lodash/isEqual'; // Import deep comparison utility
import { canSaveSettingToQdnAtom, sortablePinnedAppsAtom } from '../../atoms/global';
import { canSaveSettingToQdnAtom, settingsLocalLastUpdatedAtom, settingsQDNLastUpdatedAtom, sortablePinnedAppsAtom } from '../../atoms/global';
import { ButtonBase } from '@mui/material';
import { objectToBase64 } from '../../qdn/encryption/group-encryption';
import { MyContext } from '../../App';
@ -10,12 +10,15 @@ import { CustomizedSnackbars } from '../Snackbar/Snackbar';
import { SaveIcon } from '../../assets/svgs/SaveIcon';
export const Save = () => {
const [pinnedApps, setPinnedApps] = useRecoilState(sortablePinnedAppsAtom);
const [canSave, _] = useRecoilState(canSaveSettingToQdnAtom);
const [settingsQdnLastUpdated, setSettingsQdnLastUpdated] = useRecoilState(settingsQDNLastUpdatedAtom);
const [settingsLocalLastUpdated] = useRecoilState(settingsLocalLastUpdatedAtom);
const [canSave] = useRecoilState(canSaveSettingToQdnAtom);
const [openSnack, setOpenSnack] = useState(false);
const [isLoading, setIsLoading] = useState(false)
const [infoSnack, setInfoSnack] = useState(null);
const [oldPinnedApps, setOldPinnedApps] = useState(pinnedApps)
console.log('oldpin', {oldPinnedApps, pinnedApps})
console.log('oldpin', {oldPinnedApps, pinnedApps}, settingsQdnLastUpdated, settingsLocalLastUpdated, settingsQdnLastUpdated < settingsLocalLastUpdated,)
const { show } = useContext(MyContext);
const hasChanged = useMemo(()=> {
@ -35,14 +38,21 @@ export const Save = () => {
}
})
}
return !isEqual(oldChanges, newChanges)
}, [oldPinnedApps, pinnedApps])
console.log('!isEqual(oldChanges, newChanges)', !isEqual(oldChanges, newChanges))
if(settingsQdnLastUpdated === -100) return false
return !isEqual(oldChanges, newChanges) || settingsQdnLastUpdated < settingsLocalLastUpdated
}, [oldPinnedApps, pinnedApps, settingsQdnLastUpdated, settingsLocalLastUpdated])
const saveToQdn = async ()=> {
try {
setIsLoading(true)
const data64 = await objectToBase64({
sortablePinnedApps: pinnedApps
sortablePinnedApps: pinnedApps.map((item)=> {
return {
name: item?.name,
service: item?.service
}
})
})
const encryptData = await new Promise((res, rej) => {
chrome?.runtime?.sendMessage(
@ -95,6 +105,7 @@ export const Save = () => {
console.log('saved', response)
if(response?.identifier){
setOldPinnedApps(pinnedApps)
setSettingsQdnLastUpdated(Date.now())
setInfoSnack({
type: "success",
message:
@ -115,11 +126,12 @@ export const Save = () => {
setIsLoading(false)
}
}
console.log('settingsQdnLastUpdated', settingsQdnLastUpdated)
return (
<>
<ButtonBase onClick={saveToQdn} disabled={!hasChanged || !canSave || isLoading}>
<ButtonBase onClick={saveToQdn} disabled={!hasChanged || !canSave || isLoading || settingsQdnLastUpdated === -100}>
<SaveIcon
color={(hasChanged && !isLoading) ? '#5EB049' : '#8F8F91'}
color={settingsQdnLastUpdated === -100 ? '#8F8F91' : (hasChanged && !isLoading) ? '#5EB049' : '#8F8F91'}
/>
</ButtonBase>
<CustomizedSnackbars

View File

@ -1,6 +1,6 @@
import React, { useCallback, useEffect } from 'react'
import { useSetRecoilState } from 'recoil';
import { canSaveSettingToQdnAtom, sortablePinnedAppsAtom } from './atoms/global';
import { useRecoilState, useSetRecoilState } from 'recoil';
import { canSaveSettingToQdnAtom, settingsLocalLastUpdatedAtom, settingsQDNLastUpdatedAtom, sortablePinnedAppsAtom } from './atoms/global';
import { getArbitraryEndpointReact, getBaseApiReact } from './App';
import { decryptResource } from './components/Group/Group';
import { base64ToUint8Array, uint8ArrayToObject } from './backgroundFunctions/encryption';
@ -28,12 +28,13 @@ const getPublishRecord = async (myName) => {
}
const publishData = await response.json();
if(publishData?.length > 0) return true
if(publishData?.length > 0) return {hasPublishRecord: false, timestamp: publishData[0]?.updated || publishData[0].created}
return false
return {hasPublishRecord: false}
};
const getPublish = async (myName) => {
let data
try {
let data
const res = await fetch(
`${getBaseApiReact()}/arbitrary/DOCUMENT_PRIVATE/${myName}/ext_saved_settings?encoding=base64`
);
@ -47,22 +48,32 @@ const getPublishRecord = async (myName) => {
const dataint8Array = base64ToUint8Array(decryptedKey.data);
const decryptedKeyToObject = uint8ArrayToObject(dataint8Array);
return decryptedKeyToObject
} catch (error) {
return null
}
};
export const useQortalGetSaveSettings = (myName) => {
const setSortablePinnedApps = useSetRecoilState(sortablePinnedAppsAtom);
const setCanSave = useSetRecoilState(canSaveSettingToQdnAtom);
const getSavedSettings = useCallback(async (myName)=> {
const setSettingsQDNLastUpdated = useSetRecoilState(settingsQDNLastUpdatedAtom);
const [settingsLocalLastUpdated] = useRecoilState(settingsLocalLastUpdatedAtom);
const getSavedSettings = useCallback(async (myName, settingsLocalLastUpdated)=> {
try {
const hasPublishRecord = await getPublishRecord(myName)
const {hasPublishRecord, timestamp} = await getPublishRecord(myName)
if(hasPublishRecord){
const settings = await getPublish(myName)
if(settings?.sortablePinnedApps){
fetchFromLocalStorage('sortablePinnedApps', settings.sortablePinnedApps)
if(settings?.sortablePinnedApps && timestamp > settingsLocalLastUpdated){
setSortablePinnedApps(settings.sortablePinnedApps)
setSettingsQDNLastUpdated(timestamp || 0)
}
if(!settings){
// set -100 to indicate that it couldn't fetch the publish
setSettingsQDNLastUpdated(-100)
}
} else {
setSettingsQDNLastUpdated( 0)
}
setCanSave(true)
} catch (error) {
@ -70,8 +81,8 @@ export const useQortalGetSaveSettings = (myName) => {
}
}, [])
useEffect(()=> {
if(!myName) return
getSavedSettings(myName)
}, [getSavedSettings, myName])
if(!myName || !settingsLocalLastUpdated) return
getSavedSettings(myName, settingsLocalLastUpdated)
}, [getSavedSettings, myName, settingsLocalLastUpdated])
}

View File

@ -1,6 +1,6 @@
import React, { useCallback, useEffect } from 'react'
import { useSetRecoilState } from 'recoil';
import { sortablePinnedAppsAtom } from './atoms/global';
import { settingsLocalLastUpdatedAtom, sortablePinnedAppsAtom } from './atoms/global';
function fetchFromLocalStorage(key) {
try {
@ -18,13 +18,14 @@ function fetchFromLocalStorage(key) {
export const useRetrieveDataLocalStorage = () => {
const setSortablePinnedApps = useSetRecoilState(sortablePinnedAppsAtom);
const setSettingsLocalLastUpdated = useSetRecoilState(settingsLocalLastUpdatedAtom);
const getSortablePinnedApps = useCallback(()=> {
const pinnedAppsLocal = fetchFromLocalStorage('sortablePinnedApps')
if(pinnedAppsLocal){
setSortablePinnedApps(pinnedAppsLocal)
const pinnedAppsLocal = fetchFromLocalStorage('ext_saved_settings')
if(pinnedAppsLocal?.sortablePinnedApps){
setSortablePinnedApps(pinnedAppsLocal?.sortablePinnedApps)
}
setSettingsLocalLastUpdated(pinnedAppsLocal?.timestamp || -1)
}, [])
useEffect(()=> {