diff --git a/package.json b/package.json
index 5ab2ab5..32fd0f4 100644
--- a/package.json
+++ b/package.json
@@ -6,7 +6,7 @@
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
- "package": "yarn build && web-ext build -s ./dist -a ./output",
+ "package": "yarn build && web-ext build -s ./dist -a ./output --overwrite-dest",
"lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview"
},
@@ -21,6 +21,7 @@
"i18next": "^22.4.15",
"i18next-browser-languagedetector": "^7.0.1",
"i18next-http-backend": "^2.2.0",
+ "lodash.debounce": "^4.0.8",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-i18next": "^12.2.2",
@@ -38,6 +39,7 @@
"@types/classnames": "^2.3.1",
"@types/color": "^3.0.3",
"@types/firefox-webext-browser": "^120.0.4",
+ "@types/lodash.debounce": "^4.0.9",
"@types/react": "^18.0.28",
"@types/react-dom": "^18.0.11",
"@types/uuid": "^9.0.1",
diff --git a/public/manifest.json b/public/manifest.json
index 9d5923f..dc39c21 100644
--- a/public/manifest.json
+++ b/public/manifest.json
@@ -1,7 +1,7 @@
{
"name": "Markdown Home Tab",
"short_name": "Markdown New Tab",
- "version": "0.0.5",
+ "version": "0.0.6",
"description": "Markdown right in your home tab! Paste links, pictures, lists and more. You can also customize colors to match your needs.",
"manifest_version": 2,
"permissions": ["storage"],
diff --git a/src/main.tsx b/src/main.tsx
index dcadee9..31186e7 100644
--- a/src/main.tsx
+++ b/src/main.tsx
@@ -4,16 +4,20 @@ import { Editor } from "~/pages/editor";
import { ThemeProvider } from "./modules/theme/containers/ThemeProvider";
import { SettingsProvider } from "./modules/settings/providers/SettingsProvider";
+import { StorageProvider } from "~/modules/storage/StorageProvider";
import "./i18n";
import "./styles/main.scss";
+import "./utils/seed";
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
-
-
-
-
-
+
+
+
+
+
+
+
);
diff --git a/src/modules/editor/containers/MarkdownEditorContainer/hooks/usePersistedValue.ts b/src/modules/editor/containers/MarkdownEditorContainer/hooks/usePersistedValue.ts
deleted file mode 100644
index 326feee..0000000
--- a/src/modules/editor/containers/MarkdownEditorContainer/hooks/usePersistedValue.ts
+++ /dev/null
@@ -1,26 +0,0 @@
-import { useEffect, useMemo, useState } from "react";
-import { BrowserSyncStorage } from '~/utils/index';
-
-export const usePersistedValue = (
- id: string,
- prefix: string
-) => {
- const [hydrated, setHydrated] = useState(false);
- const storage = useMemo(() => new BrowserSyncStorage(prefix), [prefix]);
- const key = `${prefix}${id}`;
- const [value, setValue] = useState('');
-
- useEffect(() => {
- storage.get(key).then(val => setValue(val ?? '')).finally(() => setHydrated(true));
- }, [key, storage]);
-
- useEffect(() => {
- if (!hydrated) {
- return;
- }
-
- storage.set(key, value);
- }, [key, value, storage, hydrated]);
-
- return { value, setValue, hydrated };
-};
diff --git a/src/modules/editor/containers/MarkdownEditorContainer/index.tsx b/src/modules/editor/containers/MarkdownEditorContainer/index.tsx
index 9a132e3..7dc50e0 100644
--- a/src/modules/editor/containers/MarkdownEditorContainer/index.tsx
+++ b/src/modules/editor/containers/MarkdownEditorContainer/index.tsx
@@ -1,11 +1,10 @@
-import { FC, Suspense, lazy } from "react";
-import { SimpleTextareaEditor } from "../../components/SimpleTextareaEditor";
-import { MarkdownViewer } from "../../components/MarkdownViewer";
-import { usePersistedValue } from "./hooks/usePersistedValue";
-import styles from "./styles.module.scss";
-import { useSettings } from "~/modules/settings/context/SettingsContext";
-import { EmptyViewer } from "../../components/EmptyViewer";
+import { FC, Suspense, useCallback } from "react";
import { EditorWrapper } from "../../components/EditorWrapper";
+import { EmptyViewer } from "../../components/EmptyViewer";
+import { MarkdownViewer } from "../../components/MarkdownViewer";
+import { SimpleTextareaEditor } from "../../components/SimpleTextareaEditor";
+import styles from "./styles.module.scss";
+import { useStorage } from "../../../../modules/storage/StorageContext";
interface MarkdownEditorContainerProps {
id: string;
@@ -14,29 +13,22 @@ interface MarkdownEditorContainerProps {
remove: VoidCallback;
}
-const RichEditor = lazy(() =>
- import("../../components/RemirrorEditor").then((module) => ({
- default: module.RemirrorEditor,
- }))
-);
-
export const MarkdownEditorContainer: FC = ({
id,
locked,
startEditing,
remove,
}) => {
- const {
- settings: { richEditorEnabled },
- } = useSettings();
-
- const { value, setValue, hydrated } = usePersistedValue(
- id,
- "MarkdownEditorContainer"
- );
+ const { panels, setPanel, hydrated } = useStorage();
+ const value = panels[id] ?? "";
const empty = !value.trim();
+ const onChange = useCallback(
+ (val: string) => setPanel(id, val),
+ [id, setPanel]
+ );
+
const viewer = empty ? (
) : (
@@ -45,15 +37,11 @@ export const MarkdownEditorContainer: FC = ({
const editor = (
- {richEditorEnabled ? (
-
- ) : (
-
- )}
+
);
diff --git a/src/modules/layout/components/GridLayout/hooks/useGridLayoutPersistance.ts b/src/modules/layout/components/GridLayout/hooks/useGridLayoutPersistance.ts
index 976534c..50f0fe3 100644
--- a/src/modules/layout/components/GridLayout/hooks/useGridLayoutPersistance.ts
+++ b/src/modules/layout/components/GridLayout/hooks/useGridLayoutPersistance.ts
@@ -1,30 +1,27 @@
-import { DockviewApi, DockviewReadyEvent, SerializedDockview } from "dockview";
+import { DockviewApi, DockviewReadyEvent } from "dockview";
import { useCallback, useEffect, useRef, useState } from "react";
+import { useStorage } from "../../../../../modules/storage/StorageContext";
import { createDefaultLayout } from "../utils/createDefaultLayout";
-import { BrowserSyncStorage } from "~/utils";
-
-const storage = new BrowserSyncStorage();
-const key = 'dockview_persistance_layout';
export const useGridLayoutPersistance = () => {
const api = useRef();
const [hydrated, setHydrated] = useState(false);
+ const { layout, setLayout } = useStorage();
const onReady = (event: DockviewReadyEvent) => {
+ if (hydrated) {
+ return;
+ }
+
api.current = event.api;
- storage.get(key).then(layout => {
- if (!layout) {
- throw new Error("No layout saved, its okay");
- }
-
- event.api.fromJSON(layout);
- }).catch(() => {
+ if (!layout) {
createDefaultLayout(event.api);
-
- }).finally(() => {
- setHydrated(true);
- });
+ return;
+ }
+
+ event.api.fromJSON(layout);
+ setHydrated(true);
};
const persistLayout = useCallback(() => {
@@ -32,8 +29,8 @@ export const useGridLayoutPersistance = () => {
return;
}
- storage.set(key, api.current.toJSON());
- }, []);
+ setLayout(api.current.toJSON());
+ }, [setLayout]);
useEffect(() => {
const onLayoutChange = api.current?.onDidLayoutChange(() => {
diff --git a/src/modules/storage/StorageContext.ts b/src/modules/storage/StorageContext.ts
new file mode 100644
index 0000000..b86c79b
--- /dev/null
+++ b/src/modules/storage/StorageContext.ts
@@ -0,0 +1,13 @@
+import { createContext, useContext } from "react";
+import { SerializedDockview } from "dockview";
+import { noop } from "~/utils/noop";
+
+export const StorageContext = createContext({
+ layout: null as SerializedDockview | null,
+ panels: {} as Record,
+ hydrated: false,
+ setPanel: noop as (uuid: string, content: string) => void,
+ setLayout: noop as (layout: SerializedDockview) => void,
+});
+
+export const useStorage = () => useContext(StorageContext);
diff --git a/src/modules/storage/StorageProvider.tsx b/src/modules/storage/StorageProvider.tsx
new file mode 100644
index 0000000..a2d882d
--- /dev/null
+++ b/src/modules/storage/StorageProvider.tsx
@@ -0,0 +1,67 @@
+import { SerializedDockview } from "dockview";
+import { ReactNode, useCallback, useEffect, useState } from "react";
+import {
+ hydrateLayout,
+ storeLayoutLocally,
+ storePanelLocally,
+} from "~/utils/hydrate";
+import { useDelayedSync } from "./hooks/useDelayedSync";
+import { StorageContext } from "./StorageContext";
+
+const debounceDelay = 500;
+
+export const StorageProvider = ({ children }: { children: ReactNode }) => {
+ const [hydrated, setHydrated] = useState(false);
+ const [layout, setLayoutValue] = useState(null);
+ const [panels, setPanelsValue] = useState>({});
+
+ const { storeLayout, storePanel } = useDelayedSync(debounceDelay);
+
+ const setPanel = useCallback(
+ (uuid: string, value: string) => {
+ setPanelsValue((prev) => ({ ...prev, [uuid]: value }));
+ storePanelLocally(uuid, value);
+ storePanel(uuid, value);
+ },
+ [storePanel]
+ );
+
+ const setLayout = useCallback(
+ (value: SerializedDockview) => {
+ setLayoutValue(value);
+ storeLayoutLocally(value);
+ storeLayout(value);
+ },
+ [storeLayout]
+ );
+
+ useEffect(() => {
+ if (hydrated) {
+ return;
+ }
+
+ hydrateLayout()
+ .then((result) => {
+ if (!result) {
+ return;
+ }
+
+ setLayout(result.layout);
+
+ Object.entries(result.panels).forEach(([uuid, value]) => {
+ setPanel(uuid, value);
+ });
+ })
+ .finally(() => {
+ setHydrated(true);
+ });
+ }, [hydrated, setLayout, setPanel]);
+
+ return (
+
+ {hydrated ? children : null}
+
+ );
+};
diff --git a/src/modules/storage/hooks/useDelayedSync.ts b/src/modules/storage/hooks/useDelayedSync.ts
new file mode 100644
index 0000000..c9b8600
--- /dev/null
+++ b/src/modules/storage/hooks/useDelayedSync.ts
@@ -0,0 +1,38 @@
+import { useCallback, useRef } from "react";
+import { storeLayoutInSync, storePanelInSync } from "~/utils/hydrate";
+import { DebouncedFunc } from "lodash";
+import { SerializedDockview } from "dockview";
+import debounce from "lodash.debounce";
+
+export const useDelayedSync = (debounceDelay: number) => {
+ const layoutTimer = useRef>();
+ const panelTimers = useRef<
+ Record>
+ >({});
+
+ const storeLayout = useCallback(
+ (layout: SerializedDockview) => {
+ if (layoutTimer.current) {
+ layoutTimer.current.cancel();
+ }
+
+ layoutTimer.current = debounce(storeLayoutInSync, debounceDelay);
+ layoutTimer.current(layout);
+ },
+ [debounceDelay]
+ );
+
+ const storePanel = useCallback(
+ (uuid: string, value: string) => {
+ if (panelTimers.current[uuid]) {
+ panelTimers.current[uuid].cancel();
+ }
+
+ panelTimers.current[uuid] = debounce(storePanelInSync, debounceDelay);
+ panelTimers.current[uuid](uuid, value);
+ },
+ [debounceDelay]
+ );
+
+ return { storeLayout, storePanel };
+};
diff --git a/src/utils/hydrate.ts b/src/utils/hydrate.ts
new file mode 100644
index 0000000..1b87090
--- /dev/null
+++ b/src/utils/hydrate.ts
@@ -0,0 +1,127 @@
+import { SerializedDockview } from "dockview";
+import { hasBrowserStorage, hasChromeStorage } from "~/utils/storage";
+
+interface Result {
+ layout: SerializedDockview;
+ panels: Record;
+}
+
+const layoutKey = "dockview_persistance_layout";
+const panelPrefix = "MarkdownEditorContainerMarkdownEditorContainer";
+
+const makePanelKey = (uuid: string) => `${panelPrefix}${uuid}`;
+
+const getFromBrowserStorage = async (): Promise => {
+ const result = await browser.storage.sync.get();
+ const layout = result[layoutKey] as SerializedDockview | undefined;
+
+ if (!layout) {
+ return null;
+ }
+
+ const panels = Object.keys(layout.panels).reduce(
+ (acc, uuid) => ({
+ ...acc,
+ [uuid]: (result[makePanelKey(uuid)] as string) ?? "",
+ }),
+ {} as Record
+ );
+
+ return {
+ layout,
+ panels,
+ };
+};
+
+const getFromChromeStorage = async (): Promise => {
+ const result = await chrome.storage.sync.get();
+ const layout = result[layoutKey] as SerializedDockview | undefined;
+
+ if (!layout) {
+ return null;
+ }
+
+ const panels = Object.keys(layout.panels).reduce(
+ (acc, uuid) => ({
+ ...acc,
+ [uuid]: (result[makePanelKey(uuid)] as string) ?? "",
+ }),
+ {} as Record
+ );
+
+ return {
+ layout,
+ panels,
+ };
+};
+
+const getFromLocalStorage = () => {
+ const rawLayout = localStorage.getItem(layoutKey);
+
+ if (!rawLayout) {
+ return null;
+ }
+
+ const layout = JSON.parse(rawLayout) as SerializedDockview;
+
+ if (!layout.panels) {
+ return null;
+ }
+
+ const panels = Object.keys(layout.panels).reduce(
+ (acc, uuid) => ({
+ ...acc,
+ [uuid]: localStorage.getItem(makePanelKey(uuid)) ?? "",
+ }),
+ {} as Record
+ );
+
+ return {
+ layout,
+ panels,
+ };
+};
+
+export const hydrateLayout = async (): Promise => {
+ const local = getFromLocalStorage();
+
+ if (local) {
+ return local;
+ }
+
+ if (hasBrowserStorage()) {
+ return getFromBrowserStorage();
+ }
+
+ if (hasChromeStorage()) {
+ return getFromChromeStorage();
+ }
+
+ return null;
+};
+
+export const storeLayoutLocally = (layout: SerializedDockview) =>
+ localStorage.setItem(layoutKey, JSON.stringify(layout));
+
+export const storeLayoutInSync = (layout: SerializedDockview) => {
+ if (hasBrowserStorage()) {
+ return browser.storage.sync.set({ [layoutKey]: layout });
+ }
+
+ if (hasChromeStorage()) {
+ return chrome.storage.sync.set({ [layoutKey]: layout });
+ }
+};
+
+export const storePanelLocally = (uuid: string, value: string) =>
+ localStorage.setItem(`${panelPrefix}${uuid}`, value);
+
+export const storePanelInSync = (uuid: string, value: string) => {
+ if (hasBrowserStorage()) {
+ return browser.storage.sync.set({ [`${panelPrefix}${uuid}`]: value });
+ }
+
+ if (hasChromeStorage()) {
+ return chrome.storage.sync.set({ [`${panelPrefix}${uuid}`]: value });
+ }
+};
diff --git a/src/utils/index.ts b/src/utils/index.ts
index 4f44272..8077ba2 100644
--- a/src/utils/index.ts
+++ b/src/utils/index.ts
@@ -1,11 +1,13 @@
+import { hasBrowserStorage, hasChromeStorage } from "./storage";
+
export class BrowserSyncStorage {
constructor(private globalPrefix = "") {}
get engine() {
- if (typeof browser !== 'undefined' && browser?.storage) {
- return "browser"
- } else if (typeof chrome !== 'undefined' && chrome?.storage) {
- return "chrome"
+ if (hasBrowserStorage()) {
+ return "browser";
+ } else if (hasChromeStorage()) {
+ return "chrome";
}
return "local";
@@ -15,20 +17,20 @@ export class BrowserSyncStorage {
set = async (key: string, value: T) => {
switch (this.engine) {
- case 'browser':
+ case "browser":
await browser.storage.sync.set({ [this.makeKey(key)]: value });
return;
- case 'chrome':
+ case "chrome":
await chrome.storage.sync.set({ [this.makeKey(key)]: value });
return;
default:
localStorage.setItem(this.makeKey(key), JSON.stringify(value));
- return
+ return;
}
};
get = async (key: string): Promise => {
- if (this.engine === 'browser') {
+ if (this.engine === "browser") {
const value = await browser.storage.sync
.get([this.makeKey(key)])
.then((result) => result[this.makeKey(key)] as T | undefined);
@@ -36,10 +38,10 @@ export class BrowserSyncStorage {
if (value) {
return value;
}
- } else if (this.engine === 'chrome') {
- const value = await chrome.storage.sync.get(this.makeKey(key)).then(
- (result) => result[this.makeKey(key)] as T | undefined
- );
+ } else if (this.engine === "chrome") {
+ const value = await chrome.storage.sync
+ .get(this.makeKey(key))
+ .then((result) => result[this.makeKey(key)] as T | undefined);
if (value) {
return value;
diff --git a/src/utils/noop.ts b/src/utils/noop.ts
new file mode 100644
index 0000000..3629392
--- /dev/null
+++ b/src/utils/noop.ts
@@ -0,0 +1 @@
+export const noop = () => {};
\ No newline at end of file
diff --git a/src/utils/seed.ts b/src/utils/seed.ts
new file mode 100644
index 0000000..e10e016
--- /dev/null
+++ b/src/utils/seed.ts
@@ -0,0 +1,57 @@
+// eslint-disable-next-line @typescript-eslint/ban-ts-comment
+// @ts-ignore
+window.seedBrowserData = async () => {
+ await browser.storage.sync.clear();
+ await browser.storage.sync.set({
+ "MarkdownEditorContainerMarkdownEditorContainerd87f6e7b-d21e-462e-9f15-a61da9178282":
+ "d123",
+ "MarkdownEditorContainerMarkdownEditorContainer8ea3f663-9c16-4d53-b0cc-25f47adae2b2":
+ "d456",
+ dockview_persistance_layout: {
+ grid: {
+ root: {
+ type: "branch",
+ data: [
+ {
+ type: "leaf",
+ data: {
+ views: ["d87f6e7b-d21e-462e-9f15-a61da9178282"],
+ activeView: "d87f6e7b-d21e-462e-9f15-a61da9178282",
+ id: "1",
+ hideHeader: true,
+ },
+ size: 960,
+ },
+ {
+ type: "leaf",
+ data: {
+ views: ["8ea3f663-9c16-4d53-b0cc-25f47adae2b2"],
+ activeView: "8ea3f663-9c16-4d53-b0cc-25f47adae2b2",
+ id: "2",
+ hideHeader: true,
+ },
+ size: 960,
+ },
+ ],
+ size: 437,
+ },
+ width: 1920,
+ height: 437,
+ orientation: "HORIZONTAL",
+ },
+ panels: {
+ "d87f6e7b-d21e-462e-9f15-a61da9178282": {
+ id: "d87f6e7b-d21e-462e-9f15-a61da9178282",
+ contentComponent: "default",
+ params: { title: "", locked: true },
+ },
+ "8ea3f663-9c16-4d53-b0cc-25f47adae2b2": {
+ id: "8ea3f663-9c16-4d53-b0cc-25f47adae2b2",
+ contentComponent: "default",
+ params: { title: "", locked: true },
+ },
+ },
+ activeGroup: "2",
+ },
+ });
+};
diff --git a/src/utils/storage.ts b/src/utils/storage.ts
new file mode 100644
index 0000000..fea534d
--- /dev/null
+++ b/src/utils/storage.ts
@@ -0,0 +1,5 @@
+export const hasBrowserStorage = () =>
+ typeof browser !== "undefined" && browser?.storage;
+
+export const hasChromeStorage = () =>
+ typeof chrome !== "undefined" && chrome?.storage;
diff --git a/yarn.lock b/yarn.lock
index 5c427d7..5715361 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1961,6 +1961,18 @@
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.11.tgz#d421b6c527a3037f7c84433fd2c4229e016863d3"
integrity sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==
+"@types/lodash.debounce@^4.0.9":
+ version "4.0.9"
+ resolved "https://registry.yarnpkg.com/@types/lodash.debounce/-/lodash.debounce-4.0.9.tgz#0f5f21c507bce7521b5e30e7a24440975ac860a5"
+ integrity sha512-Ma5JcgTREwpLRwMM+XwBR7DaWe96nC38uCBDFKZWbNKD+osjVzdpnUSwBcqCptrp16sSOLBAUb50Car5I0TCsQ==
+ dependencies:
+ "@types/lodash" "*"
+
+"@types/lodash@*":
+ version "4.17.10"
+ resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.17.10.tgz#64f3edf656af2fe59e7278b73d3e62404144a6e6"
+ integrity sha512-YpS0zzoduEhuOWjAotS6A5AVCva7X4lVlYLF0FYHAY9sdraBfnatttHItlWeZdGhuEkf+OzMNg2ZYAx8t+52uQ==
+
"@types/marked@^4.0.2":
version "4.0.8"
resolved "https://registry.yarnpkg.com/@types/marked/-/marked-4.0.8.tgz#b316887ab3499d0a8f4c70b7bd8508f92d477955"
@@ -4965,6 +4977,11 @@ lodash-es@^4.17.15, lodash-es@^4.17.21:
resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.21.tgz#43e626c46e6591b7750beb2b50117390c609e3ee"
integrity sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==
+lodash.debounce@^4.0.8:
+ version "4.0.8"
+ resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af"
+ integrity sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==
+
lodash.merge@^4.6.2:
version "4.6.2"
resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a"