diff --git a/.gitignore b/.gitignore index f483f4e..ee070ae 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,4 @@ dist-ssr *.njsproj *.sln *.sw? +web-ext-artifacts \ No newline at end of file diff --git a/.tool-versions b/.tool-versions deleted file mode 100644 index b00f598..0000000 --- a/.tool-versions +++ /dev/null @@ -1 +0,0 @@ -nodejs 18.0.0 diff --git a/README.md b/README.md index fca0d63..19b9cb9 100644 --- a/README.md +++ b/README.md @@ -6,10 +6,9 @@ Markdown Editor for new tab made with [Milkdown](https://milkdown.dev/) and ## Installation ``` +yarn global add web-ext yarn -yarn build -cd ./dist -web-ext build +yarn package ``` ## Development diff --git a/package.json b/package.json index 5efca03..68e3cff 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,12 @@ { "name": "markdown-home-tab", "private": true, - "version": "0.0.3", + "version": "0.0.4", "type": "module", "scripts": { "dev": "vite", "build": "tsc && vite build", - "package": "yarn build && cd dist && web-ext build", + "package": "yarn build && web-ext build -s ./dist", "lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0", "preview": "vite preview" }, @@ -34,8 +34,10 @@ "web-ext": "^7.6.1" }, "devDependencies": { + "@types/chrome": "^0.0.273", "@types/classnames": "^2.3.1", "@types/color": "^3.0.3", + "@types/firefox-webext-browser": "^120.0.4", "@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 2dc7245..f9a9b92 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.3", + "version": "0.0.4", "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/modules/editor/containers/MarkdownEditorContainer/hooks/usePersistedValue.ts b/src/modules/editor/containers/MarkdownEditorContainer/hooks/usePersistedValue.ts index 1806cdf..326feee 100644 --- a/src/modules/editor/containers/MarkdownEditorContainer/hooks/usePersistedValue.ts +++ b/src/modules/editor/containers/MarkdownEditorContainer/hooks/usePersistedValue.ts @@ -1,35 +1,26 @@ -import { useEffect, useState } from "react"; - -const safelyGetStringValue = (key: string) => { - try { - return localStorage.getItem(key) ?? ""; - } catch (error) { - console.warn(error); - return ""; - } -}; -const safelySetStringValue = (key: string, value: string) => { - try { - return localStorage.setItem(key, value); - } catch (error) { - console.warn(error); - } -}; +import { useEffect, useMemo, useState } from "react"; +import { BrowserSyncStorage } from '~/utils/index'; export const usePersistedValue = ( id: string, prefix: string -): [string, (val: string) => void] => { +) => { + const [hydrated, setHydrated] = useState(false); + const storage = useMemo(() => new BrowserSyncStorage(prefix), [prefix]); const key = `${prefix}${id}`; - const [value, setValue] = useState(safelyGetStringValue(key)); + const [value, setValue] = useState(''); useEffect(() => { - setValue(safelyGetStringValue(key)); - }, [id, key]); + storage.get(key).then(val => setValue(val ?? '')).finally(() => setHydrated(true)); + }, [key, storage]); useEffect(() => { - safelySetStringValue(key, value); - }, [key, value]); + if (!hydrated) { + return; + } - return [value, setValue]; + 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 8735a94..85e351d 100644 --- a/src/modules/editor/containers/MarkdownEditorContainer/index.tsx +++ b/src/modules/editor/containers/MarkdownEditorContainer/index.tsx @@ -30,7 +30,10 @@ export const MarkdownEditorContainer: FC = ({ settings: { richEditorEnabled }, } = useSettings(); - const [value, setValue] = usePersistedValue(id, "MarkdownEditorContainer"); + const { value, setValue, hydrated } = usePersistedValue( + id, + "MarkdownEditorContainer" + ); const empty = !value.trim(); @@ -52,7 +55,7 @@ export const MarkdownEditorContainer: FC = ({ return (
- {locked ? viewer : editor} + {hydrated && {locked ? viewer : editor}}
); }; diff --git a/src/modules/layout/components/GridLayout/hooks/useGridLayoutPersistance.ts b/src/modules/layout/components/GridLayout/hooks/useGridLayoutPersistance.ts index e478f3f..976534c 100644 --- a/src/modules/layout/components/GridLayout/hooks/useGridLayoutPersistance.ts +++ b/src/modules/layout/components/GridLayout/hooks/useGridLayoutPersistance.ts @@ -1,27 +1,30 @@ -import { DockviewApi, DockviewReadyEvent } from "dockview"; -import { useCallback, useEffect, useRef } from "react"; +import { DockviewApi, DockviewReadyEvent, SerializedDockview } from "dockview"; +import { useCallback, useEffect, useRef, useState } from "react"; 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 onReady = (event: DockviewReadyEvent) => { api.current = event.api; - const layoutString = localStorage.getItem("dockview_persistance_layout"); + storage.get(key).then(layout => { + if (!layout) { + throw new Error("No layout saved, its okay"); + } - if (!layoutString) { - createDefaultLayout(event.api); - return; - } - - try { - const layout = JSON.parse(layoutString); event.api.fromJSON(layout); - } catch (err) { - console.log(err); + }).catch(() => { createDefaultLayout(event.api); - } + + }).finally(() => { + setHydrated(true); + }); }; const persistLayout = useCallback(() => { @@ -29,18 +32,12 @@ export const useGridLayoutPersistance = () => { return; } - const layout = api.current.toJSON(); - - localStorage.setItem("dockview_persistance_layout", JSON.stringify(layout)); + storage.set(key, api.current.toJSON()); }, []); useEffect(() => { - if (!api.current) { - return; - } - - const onLayoutChange = api.current.onDidLayoutChange(() => { - if (!api.current) { + const onLayoutChange = api.current?.onDidLayoutChange(() => { + if (!api.current || !hydrated) { return; } @@ -51,11 +48,8 @@ export const useGridLayoutPersistance = () => { persistLayout(); }); - return () => { - onLayoutChange.dispose(); - }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [persistLayout, api.current]); + return onLayoutChange?.dispose; + }, [persistLayout, hydrated]); - return { api, onReady, persistLayout }; + return { api, onReady, persistLayout, hydrated }; }; diff --git a/src/modules/settings/hooks/usePersistSettings.ts b/src/modules/settings/hooks/usePersistSettings.ts index d119262..18ba095 100644 --- a/src/modules/settings/hooks/usePersistSettings.ts +++ b/src/modules/settings/hooks/usePersistSettings.ts @@ -1,33 +1,37 @@ -import { useCallback, useState } from "react"; +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 getLocalStorageSettings = (defaultValue: SettingsValue) => { - try { - const raw = localStorage.getItem("settings"); - const parsed = JSON.parse(raw ?? ""); - - return parsed ? { ...defaultValue, ...parsed } : defaultValue; - } catch (error) { - return defaultValue; - } -}; +const storage = new BrowserSyncStorage('settings'); export const usePersistSettings = () => { + const [hydrated, setHydrated] = useState(false); + const defaultColors = useDefaultTheme(); - const [settings, setSettings] = useState( - getLocalStorageSettings({ ...defaultSettings, ...defaultColors }) - ); + const [settings, setSettings] = useState({ + ...defaultSettings, ...defaultColors + }); const update = useCallback((val: Partial) => { + if (!hydrated) { + return; + } + setSettings((v) => { const updatedValue = { ...v, ...val }; - localStorage.setItem("settings", JSON.stringify(updatedValue)); + storage.set("", updatedValue); return updatedValue; }); - }, []); + }, [hydrated]); - return { settings, update }; + useEffect(() => { + storage.get("").then(next => { + setSettings(prev => ({ ...prev, ...next })); + }).finally(() => setHydrated(true)); + }, []) + + return { settings, update, hydrated }; }; diff --git a/src/modules/settings/providers/SettingsProvider/index.tsx b/src/modules/settings/providers/SettingsProvider/index.tsx index 156b5c8..e663575 100644 --- a/src/modules/settings/providers/SettingsProvider/index.tsx +++ b/src/modules/settings/providers/SettingsProvider/index.tsx @@ -11,13 +11,17 @@ import styles from "./styles.module.scss"; const SettingsProvider: FC = ({ children }) => { const { t } = useTranslation(); - const { settings, update } = usePersistSettings(); + const { settings, update, hydrated } = usePersistSettings(); const [settingsModalVisible, setSettingsModalVisible] = useState(false); const show = useCallback(() => setSettingsModalVisible(true), []); const hide = useCallback(() => setSettingsModalVisible(false), []); + if (!hydrated) { + return null; + } + return ( {settingsModalVisible && ( diff --git a/src/modules/theme/hooks/useThemeColors.ts b/src/modules/theme/hooks/useThemeColors.ts index f91acb2..6a70bec 100644 --- a/src/modules/theme/hooks/useThemeColors.ts +++ b/src/modules/theme/hooks/useThemeColors.ts @@ -13,8 +13,6 @@ export const useThemeColors = (settings: ColorSettings) => { const code = new Color(settings.codeColor); - console.log(code.hex()); - // Backgrounds and text document.body.style.setProperty("--color-background", background.hex()); document.body.style.setProperty("--color-code-text", code.hex()); diff --git a/src/utils/index.ts b/src/utils/index.ts new file mode 100644 index 0000000..4f44272 --- /dev/null +++ b/src/utils/index.ts @@ -0,0 +1,57 @@ +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" + } + + return "local"; + } + + makeKey = (key: string) => `${this.globalPrefix}${key}`; + + set = async (key: string, value: T) => { + switch (this.engine) { + case 'browser': + await browser.storage.sync.set({ [this.makeKey(key)]: value }); + return; + case 'chrome': + await chrome.storage.sync.set({ [this.makeKey(key)]: value }); + return; + default: + localStorage.setItem(this.makeKey(key), JSON.stringify(value)); + return + } + }; + + get = async (key: string): Promise => { + if (this.engine === 'browser') { + const value = await browser.storage.sync + .get([this.makeKey(key)]) + .then((result) => result[this.makeKey(key)] as T | undefined); + + 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 + ); + + if (value) { + return value; + } + } + + try { + const value = localStorage.getItem(this.makeKey(key)); + return value ? (JSON.parse(value) as T) : undefined; + } catch (e) { + console.log(e); + return undefined; + } + }; +} diff --git a/tsconfig.json b/tsconfig.json index b66c74e..3784c2e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -27,6 +27,6 @@ }, "types": ["./src/vite-env.d.ts", "./src/i18next.d.ts"] }, - "include": ["src", "public/locales"], + "include": ["src", "public/locales", "./node_modules/@types/**/*"], "references": [{ "path": "./tsconfig.node.json" }] } diff --git a/yarn.lock b/yarn.lock index 7039ff5..5c427d7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1859,6 +1859,14 @@ dependencies: defer-to-connect "^2.0.1" +"@types/chrome@^0.0.273": + version "0.0.273" + resolved "https://registry.yarnpkg.com/@types/chrome/-/chrome-0.0.273.tgz#986f5090de34cdd8ca467431e9f9c3c12e951407" + integrity sha512-6Wp4GO07GLvti13Rf/RpYG+0COSJDOLE4iq3g1+whn1SNGUVnv6vbXqSa/WFbuVpvN1lcBLiZ40+gSeWmKb+eA== + dependencies: + "@types/filesystem" "*" + "@types/har-format" "*" + "@types/classnames@^2.3.1": version "2.3.1" resolved "https://registry.yarnpkg.com/@types/classnames/-/classnames-2.3.1.tgz#3c2467aa0f1a93f1f021e3b9bcf938bd5dfdc0dd" @@ -1909,6 +1917,28 @@ resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.1.tgz#aa22750962f3bf0e79d753d3cc067f010c95f194" integrity sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA== +"@types/filesystem@*": + version "0.0.36" + resolved "https://registry.yarnpkg.com/@types/filesystem/-/filesystem-0.0.36.tgz#7227c2d76bfed1b21819db310816c7821d303857" + integrity sha512-vPDXOZuannb9FZdxgHnqSwAG/jvdGM8Wq+6N4D/d80z+D4HWH+bItqsZaVRQykAn6WEVeEkLm2oQigyHtgb0RA== + dependencies: + "@types/filewriter" "*" + +"@types/filewriter@*": + version "0.0.33" + resolved "https://registry.yarnpkg.com/@types/filewriter/-/filewriter-0.0.33.tgz#d9d611db9d9cd99ae4e458de420eeb64ad604ea8" + integrity sha512-xFU8ZXTw4gd358lb2jw25nxY9QAgqn2+bKKjKOYfNCzN4DKCFetK7sPtrlpg66Ywe3vWY9FNxprZawAh9wfJ3g== + +"@types/firefox-webext-browser@^120.0.4": + version "120.0.4" + resolved "https://registry.yarnpkg.com/@types/firefox-webext-browser/-/firefox-webext-browser-120.0.4.tgz#27eead781051b2e681a344dd2983735faadd4343" + integrity sha512-lBrpf08xhiZBigrtdQfUaqX1UauwZ+skbFiL8u2Tdra/rklkKadYmIzTwkNZSWtuZ7OKpFqbE2HHfDoFqvZf6w== + +"@types/har-format@*": + version "1.2.16" + resolved "https://registry.yarnpkg.com/@types/har-format/-/har-format-1.2.16.tgz#b71ede8681400cc08b3685f061c31e416cf94944" + integrity sha512-fluxdy7ryD3MV6h8pTfTYpy/xQzCFC7m89nOH9y94cNqJ1mDIDPut7MnRHI3F6qRmh/cT2fUjG1MLdCNb4hE9A== + "@types/hast@^2.0.0": version "2.3.4" resolved "https://registry.yarnpkg.com/@types/hast/-/hast-2.3.4.tgz#8aa5ef92c117d20d974a82bdfb6a648b08c0bafc"