make global storage provider

This commit is contained in:
Fedor Katurov 2024-10-08 14:44:07 +07:00
parent 2ba35b8e2b
commit 3a4e8e7702
9 changed files with 109 additions and 53 deletions

View file

@ -1,7 +1,7 @@
{
"name": "markdown-home-tab",
"private": true,
"version": "0.0.5",
"version": "0.0.6",
"type": "module",
"scripts": {
"dev": "vite",

View file

@ -1,4 +1,5 @@
import { createContext, useContext } from "react";
import { noop } from "~/utils/noop";
export interface ColorSettings {
backgroundColor: string;
@ -26,10 +27,9 @@ export const defaultSettings: SettingsValue = {
export const SettingsContext = createContext({
settings: defaultSettings,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
update: (_: Partial<SettingsValue>) => {},
show: () => {},
hide: () => {},
update: noop as (value: Partial<SettingsValue>) => void,
show: noop,
hide: noop,
});
export const useSettings = () => useContext(SettingsContext);

View file

@ -1,37 +0,0 @@
import { useCallback, useEffect, useState } from "react";
import { SettingsValue } from "~/modules/settings/context/SettingsContext";
import { useDefaultTheme } from "~/modules/theme/hooks/useDefaultTheme";
import { BrowserSyncStorage } from "~/utils";
import { defaultSettings } from "../context/SettingsContext";
const storage = new BrowserSyncStorage('settings');
export const usePersistSettings = () => {
const [hydrated, setHydrated] = useState(false);
const defaultColors = useDefaultTheme();
const [settings, setSettings] = useState({
...defaultSettings, ...defaultColors
});
const update = useCallback((val: Partial<SettingsValue>) => {
if (!hydrated) {
return;
}
setSettings((v) => {
const updatedValue = { ...v, ...val };
storage.set("", updatedValue);
return updatedValue;
});
}, [hydrated]);
useEffect(() => {
storage.get<SettingsValue>("").then(next => {
setSettings(prev => ({ ...prev, ...next }));
}).finally(() => setHydrated(true));
}, [])
return { settings, update, hydrated };
};

View file

@ -0,0 +1,17 @@
import { useMemo } from "react";
import { useStorage } from "~/modules/storage/StorageContext";
import { useDefaultTheme } from "~/modules/theme/hooks/useDefaultTheme";
import { defaultSettings } from "../context/SettingsContext";
export const useSettings = () => {
const defaultColors = useDefaultTheme();
const { settings: storedSettings, setSettings } = useStorage();
const settings = useMemo(
() => ({ ...defaultSettings, ...defaultColors, ...storedSettings }),
[defaultColors, storedSettings]
);
return { settings, update: setSettings };
};

View file

@ -5,23 +5,19 @@ import { ModalPage } from "~/modules/modal/components/ModalPage";
import { Modal } from "~/modules/modal/containers/Modal";
import { SettingsContainer } from "../../containers/SettingsContainer";
import { SettingsContext } from "../../context/SettingsContext";
import { usePersistSettings } from "../../hooks/usePersistSettings";
import { useSettings } from "../../hooks/useSettings";
import styles from "./styles.module.scss";
const SettingsProvider: FC<PropsWithChildren> = ({ children }) => {
const { t } = useTranslation();
const { settings, update, hydrated } = usePersistSettings();
const { settings, update } = useSettings();
const [settingsModalVisible, setSettingsModalVisible] = useState(false);
const show = useCallback(() => setSettingsModalVisible(true), []);
const hide = useCallback(() => setSettingsModalVisible(false), []);
if (!hydrated) {
return null;
}
return (
<SettingsContext.Provider value={{ settings, update, show, hide }}>
{settingsModalVisible && (

View file

@ -1,13 +1,16 @@
import { createContext, useContext } from "react";
import { SerializedDockview } from "dockview";
import { noop } from "~/utils/noop";
import { SettingsValue } from "~/modules/settings/context/SettingsContext";
export const StorageContext = createContext({
layout: null as SerializedDockview | null,
panels: {} as Record<string, string>,
settings: {} as Partial<SettingsValue>,
hydrated: false,
setPanel: noop as (uuid: string, content: string) => void,
setLayout: noop as (layout: SerializedDockview) => void,
setSettings: noop as (layout: Partial<SettingsValue>) => void,
});
export const useStorage = () => useContext(StorageContext);

View file

@ -4,9 +4,11 @@ import {
hydrateLayout,
storeLayoutLocally,
storePanelLocally,
storeSettingsLocally,
} from "~/utils/hydrate";
import { useDelayedSync } from "./hooks/useDelayedSync";
import { StorageContext } from "./StorageContext";
import { SettingsValue } from "~/modules/settings/context/SettingsContext";
const debounceDelay = 500;
@ -14,8 +16,10 @@ export const StorageProvider = ({ children }: { children: ReactNode }) => {
const [hydrated, setHydrated] = useState(false);
const [layout, setLayoutValue] = useState<SerializedDockview | null>(null);
const [panels, setPanelsValue] = useState<Record<string, string>>({});
const [settings, setSettingsValue] = useState<Partial<SettingsValue>>({});
const { storeLayout, storePanel } = useDelayedSync(debounceDelay);
const { storeLayout, storePanel, storeSettings } =
useDelayedSync(debounceDelay);
const setPanel = useCallback(
(uuid: string, value: string) => {
@ -35,6 +39,12 @@ export const StorageProvider = ({ children }: { children: ReactNode }) => {
[storeLayout]
);
const setSettings = useCallback((value: Partial<SettingsValue>) => {
setSettingsValue(value);
storeSettingsLocally(value);
storeSettings(value);
}, []);
useEffect(() => {
if (hydrated) {
return;
@ -47,6 +57,7 @@ export const StorageProvider = ({ children }: { children: ReactNode }) => {
}
setLayout(result.layout);
setSettings(result.settings);
Object.entries(result.panels).forEach(([uuid, value]) => {
setPanel(uuid, value);
@ -55,11 +66,19 @@ export const StorageProvider = ({ children }: { children: ReactNode }) => {
.finally(() => {
setHydrated(true);
});
}, [hydrated, setLayout, setPanel]);
}, [hydrated, setLayout, setPanel, setSettings]);
return (
<StorageContext.Provider
value={{ hydrated, layout, panels, setLayout, setPanel }}
value={{
hydrated,
layout,
panels,
settings,
setLayout,
setPanel,
setSettings,
}}
>
{hydrated ? children : null}
</StorageContext.Provider>

View file

@ -1,11 +1,17 @@
import { useCallback, useRef } from "react";
import { storeLayoutInSync, storePanelInSync } from "~/utils/hydrate";
import {
storeLayoutInSync,
storePanelInSync,
storeSettingsInSync,
} from "~/utils/hydrate";
import { DebouncedFunc } from "lodash";
import { SerializedDockview } from "dockview";
import debounce from "lodash.debounce";
import { SettingsValue } from "~/modules/settings/context/SettingsContext";
export const useDelayedSync = (debounceDelay: number) => {
const layoutTimer = useRef<DebouncedFunc<typeof storeLayoutInSync>>();
const settingsTimer = useRef<DebouncedFunc<typeof storeSettingsInSync>>();
const panelTimers = useRef<
Record<string, DebouncedFunc<typeof storePanelInSync>>
>({});
@ -34,5 +40,16 @@ export const useDelayedSync = (debounceDelay: number) => {
[debounceDelay]
);
return { storeLayout, storePanel };
const storeSettings = useCallback(
(settings: Partial<SettingsValue>) => {
if (settingsTimer.current) {
settingsTimer.current.cancel();
}
settingsTimer.current = debounce(storeSettingsInSync, debounceDelay);
settingsTimer.current(settings);
},
[debounceDelay]
);
return { storeLayout, storePanel, storeSettings };
};

View file

@ -1,12 +1,15 @@
import { SerializedDockview } from "dockview";
import { SettingsValue } from "~/modules/settings/context/SettingsContext";
import { hasBrowserStorage, hasChromeStorage } from "~/utils/storage";
interface Result {
layout: SerializedDockview;
panels: Record<string, string>;
settings: Partial<SettingsValue>;
}
const layoutKey = "dockview_persistance_layout";
const settingsKey = "settings";
const panelPrefix = "MarkdownEditorContainerMarkdownEditorContainer";
const makePanelKey = (uuid: string) => `${panelPrefix}${uuid}`;
@ -27,9 +30,16 @@ const getFromBrowserStorage = async (): Promise<Result | null> => {
{} as Record<string, string>
);
const settings =
typeof result[settingsKey] === "object" &&
Object.keys(result[settingsKey]).length
? result[settingsKey]
: {};
return {
layout,
panels,
settings,
};
};
@ -49,9 +59,16 @@ const getFromChromeStorage = async (): Promise<Result | null> => {
{} as Record<string, string>
);
const settings =
typeof result[settingsKey] === "object" &&
Object.keys(result[settingsKey]).length
? result[settingsKey]
: {};
return {
layout,
panels,
settings,
};
};
@ -76,9 +93,20 @@ const getFromLocalStorage = () => {
{} as Record<string, string>
);
const rawSettings = localStorage.getItem(settingsKey);
const parsedSettings =
rawSettings && (JSON.parse(rawSettings) as Partial<SettingsValue>);
const settings =
parsedSettings &&
typeof parsedSettings === "object" &&
Object.keys(parsedSettings).length
? parsedSettings
: {};
return {
layout,
panels,
settings,
};
};
@ -125,3 +153,16 @@ export const storePanelInSync = (uuid: string, value: string) => {
return chrome.storage.sync.set({ [`${panelPrefix}${uuid}`]: value });
}
};
export const storeSettingsLocally = (settings: Partial<SettingsValue>) =>
localStorage.setItem(settingsKey, JSON.stringify(settings));
export const storeSettingsInSync = (settings: Partial<SettingsValue>) => {
if (hasBrowserStorage()) {
return browser.storage.sync.set({ [settingsKey]: settings });
}
if (hasChromeStorage()) {
return chrome.storage.sync.set({ [settingsKey]: settings });
}
};