mirror of
https://github.com/muerwre/markdown-home-tab.git
synced 2025-04-25 00:46:41 +07:00
Compare commits
2 commits
6d00bffbec
...
2e15044c12
Author | SHA1 | Date | |
---|---|---|---|
![]() |
2e15044c12 | ||
![]() |
db911e51e4 |
26 changed files with 467 additions and 157 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -23,3 +23,5 @@ dist-ssr
|
|||
*.sln
|
||||
*.sw?
|
||||
web-ext-artifacts
|
||||
output
|
||||
*.tgz
|
|
@ -22,4 +22,4 @@ yarn dev
|
|||
|
||||
## TO-DO
|
||||
|
||||
- Use Remirror as editor
|
||||
- Use HyperMD editor
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "markdown-home-tab",
|
||||
"private": true,
|
||||
"version": "0.0.4",
|
||||
"version": "0.0.5",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"package": "yarn build && web-ext build -s ./dist",
|
||||
"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",
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "Markdown Home Tab",
|
||||
"short_name": "Markdown New Tab",
|
||||
"version": "0.0.4",
|
||||
"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"],
|
||||
|
|
|
@ -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(
|
||||
<React.StrictMode>
|
||||
<StorageProvider>
|
||||
<SettingsProvider>
|
||||
<ThemeProvider>
|
||||
<Editor />
|
||||
</ThemeProvider>
|
||||
</SettingsProvider>
|
||||
</StorageProvider>
|
||||
</React.StrictMode>
|
||||
);
|
||||
|
|
|
@ -13,7 +13,7 @@ const EmptyViewer: FC<EmptyViewerProps> = ({ startEditing }) => {
|
|||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className={styles.empty} style={style}>
|
||||
<div className={styles.empty} style={style} onDoubleClick={startEditing}>
|
||||
<div className={styles.title}>{t(`Nothing's here yet`)}</div>
|
||||
<div>
|
||||
<Button
|
||||
|
|
|
@ -8,6 +8,8 @@
|
|||
box-sizing: border-box;
|
||||
opacity: 0;
|
||||
transition: opacity 0.25s;
|
||||
user-select: none;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.5;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { FC } from "react";
|
||||
import { FC, useCallback, MouseEvent } from "react";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import { useContainerPaddings } from "~/modules/theme/hooks/useContainerPaddings";
|
||||
import styles from "./styles.module.scss";
|
||||
|
@ -7,20 +7,26 @@ import { useTranslation } from "react-i18next";
|
|||
import remarkGfm from "remark-gfm";
|
||||
import rehypeRaw from "rehype-raw";
|
||||
|
||||
interface ReactMarkdownViewerProps {
|
||||
interface Props {
|
||||
value: string;
|
||||
startEditing: () => void;
|
||||
}
|
||||
|
||||
const ReactMarkdownViewer: FC<ReactMarkdownViewerProps> = ({
|
||||
value,
|
||||
startEditing,
|
||||
}) => {
|
||||
const MarkdownViewer: FC<Props> = ({ value, startEditing }) => {
|
||||
const { t } = useTranslation();
|
||||
const style = useContainerPaddings();
|
||||
|
||||
const onDoubleClick = useCallback(
|
||||
(event: MouseEvent) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
startEditing();
|
||||
},
|
||||
[startEditing]
|
||||
);
|
||||
|
||||
return (
|
||||
<div style={style} className={styles.editor}>
|
||||
<div style={style} className={styles.editor} onDoubleClick={onDoubleClick}>
|
||||
<div className={styles.edit}>
|
||||
<Button
|
||||
size="small"
|
||||
|
@ -41,4 +47,4 @@ const ReactMarkdownViewer: FC<ReactMarkdownViewerProps> = ({
|
|||
);
|
||||
};
|
||||
|
||||
export { ReactMarkdownViewer };
|
||||
export { MarkdownViewer };
|
|
@ -1,37 +1,16 @@
|
|||
import { FC, useCallback } from "react";
|
||||
|
||||
import {
|
||||
EditorComponent,
|
||||
FloatingToolbar,
|
||||
FormattingButtonGroup,
|
||||
HeadingLevelButtonGroup,
|
||||
Remirror,
|
||||
useRemirror,
|
||||
EditorComponent,
|
||||
HeadingLevelButtonGroup,
|
||||
FormattingButtonGroup,
|
||||
} from "@remirror/react";
|
||||
import jsx from "refractor/lang/jsx.js";
|
||||
import typescript from "refractor/lang/typescript.js";
|
||||
import { Extension, ExtensionPriority, RemirrorEventListener } from "remirror";
|
||||
import {
|
||||
BlockquoteExtension,
|
||||
UnderlineExtension,
|
||||
BoldExtension,
|
||||
BulletListExtension,
|
||||
CodeBlockExtension,
|
||||
CodeExtension,
|
||||
HardBreakExtension,
|
||||
HeadingExtension,
|
||||
ItalicExtension,
|
||||
LinkExtension,
|
||||
ListItemExtension,
|
||||
MarkdownExtension,
|
||||
OrderedListExtension,
|
||||
StrikeExtension,
|
||||
TableExtension,
|
||||
TrailingNodeExtension,
|
||||
GapCursorExtension,
|
||||
} from "remirror/extensions";
|
||||
import styles from "./styles.module.scss";
|
||||
import { Extension, RemirrorEventListener } from "remirror";
|
||||
import { useContainerPaddings } from "~/modules/theme/hooks/useContainerPaddings";
|
||||
import styles from "./styles.module.scss";
|
||||
|
||||
interface RemirrorEditorProps {
|
||||
locked: boolean;
|
||||
|
@ -50,7 +29,7 @@ const RemirrorEditor: FC<RemirrorEditorProps> = ({
|
|||
exitMarksOnArrowPress: false,
|
||||
},
|
||||
content: value,
|
||||
stringHandler: "markdown",
|
||||
// stringHandler: "markdown",
|
||||
});
|
||||
|
||||
const onStateChange = useCallback<RemirrorEventListener<Extension>>(
|
||||
|
@ -90,26 +69,26 @@ const RemirrorEditor: FC<RemirrorEditorProps> = ({
|
|||
};
|
||||
|
||||
const extensions = (): Extension[] => [
|
||||
new LinkExtension({ autoLink: true }),
|
||||
new BoldExtension(),
|
||||
new UnderlineExtension(),
|
||||
new StrikeExtension(),
|
||||
new ItalicExtension(),
|
||||
new HeadingExtension(),
|
||||
new BlockquoteExtension(),
|
||||
new BulletListExtension({ enableSpine: false }),
|
||||
new OrderedListExtension(),
|
||||
new ListItemExtension({
|
||||
priority: ExtensionPriority.High,
|
||||
// enableCollapsible: true,
|
||||
}),
|
||||
new CodeExtension(),
|
||||
new CodeBlockExtension({ supportedLanguages: [jsx, typescript] }),
|
||||
new TrailingNodeExtension(),
|
||||
new TableExtension(),
|
||||
new MarkdownExtension({ copyAsMarkdown: true }),
|
||||
new GapCursorExtension(),
|
||||
new HardBreakExtension(),
|
||||
// new LinkExtension({ autoLink: true }),
|
||||
// new BoldExtension(),
|
||||
// new UnderlineExtension(),
|
||||
// new StrikeExtension(),
|
||||
// new ItalicExtension(),
|
||||
// new HeadingExtension(),
|
||||
// new BlockquoteExtension(),
|
||||
// new BulletListExtension({ enableSpine: false }),
|
||||
// new OrderedListExtension(),
|
||||
// new ListItemExtension({
|
||||
// priority: ExtensionPriority.High,
|
||||
// // enableCollapsible: true,
|
||||
// }),
|
||||
// new CodeExtension(),
|
||||
// new CodeBlockExtension({ supportedLanguages: [jsx, typescript] }),
|
||||
// new TrailingNodeExtension(),
|
||||
// new TableExtension(),
|
||||
// new MarkdownExtension({ copyAsMarkdown: true }),
|
||||
// new GapCursorExtension(),
|
||||
// new HardBreakExtension(),
|
||||
];
|
||||
|
||||
export { RemirrorEditor };
|
||||
|
|
|
@ -1,13 +1,15 @@
|
|||
import { ChangeEvent, FC, useCallback, useMemo } from "react";
|
||||
import { ChangeEvent, FC, useCallback, useMemo, KeyboardEvent } from "react";
|
||||
import styles from "./styles.module.scss";
|
||||
import { useTheme } from "~/modules/theme/context/ThemeContext";
|
||||
|
||||
interface ReactMarkdownEditorProps {
|
||||
value: string;
|
||||
onChange: (val: string) => void;
|
||||
save: VoidFunction;
|
||||
}
|
||||
|
||||
const ReactMarkdownEditor: FC<ReactMarkdownEditorProps> = ({
|
||||
const SimpleTextareaEditor: FC<ReactMarkdownEditorProps> = ({
|
||||
save,
|
||||
value,
|
||||
onChange,
|
||||
}) => {
|
||||
|
@ -27,8 +29,22 @@ const ReactMarkdownEditor: FC<ReactMarkdownEditorProps> = ({
|
|||
[paddingHorizontal, paddingVertical]
|
||||
);
|
||||
|
||||
const onKeyDown = useCallback(
|
||||
(event: KeyboardEvent) => {
|
||||
if (event.key === "Enter" && event.ctrlKey) {
|
||||
save();
|
||||
}
|
||||
|
||||
if (event.key === "Escape") {
|
||||
save();
|
||||
}
|
||||
},
|
||||
[save]
|
||||
);
|
||||
|
||||
return (
|
||||
<textarea
|
||||
onKeyDown={onKeyDown}
|
||||
onChange={changeHandler}
|
||||
className={styles.textarea}
|
||||
style={style}
|
||||
|
@ -39,4 +55,4 @@ const ReactMarkdownEditor: FC<ReactMarkdownEditorProps> = ({
|
|||
);
|
||||
};
|
||||
|
||||
export { ReactMarkdownEditor };
|
||||
export { SimpleTextareaEditor };
|
|
@ -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<string>(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 };
|
||||
};
|
|
@ -1,60 +1,52 @@
|
|||
import { FC, Suspense, lazy } from "react";
|
||||
import { ReactMarkdownEditor } from "../../components/ReactMarkdownEditor";
|
||||
import { ReactMarkdownViewer } from "../../components/ReactMarkdownViewer";
|
||||
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;
|
||||
locked: boolean;
|
||||
startEditing: () => void;
|
||||
remove: () => void;
|
||||
startEditing: VoidCallback;
|
||||
remove: VoidCallback;
|
||||
}
|
||||
|
||||
const RichEditor = lazy(() =>
|
||||
import("../../components/RemirrorEditor").then((module) => ({
|
||||
default: module.RemirrorEditor,
|
||||
}))
|
||||
);
|
||||
|
||||
export const MarkdownEditorContainer: FC<MarkdownEditorContainerProps> = ({
|
||||
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 ? (
|
||||
<EmptyViewer startEditing={startEditing} />
|
||||
) : (
|
||||
<ReactMarkdownViewer value={value} startEditing={startEditing} />
|
||||
<MarkdownViewer value={value} startEditing={startEditing} />
|
||||
);
|
||||
|
||||
const editor = (
|
||||
<EditorWrapper save={startEditing} remove={remove}>
|
||||
{richEditorEnabled ? (
|
||||
<RichEditor value={value} onChange={setValue} locked={locked} />
|
||||
) : (
|
||||
<ReactMarkdownEditor value={value} onChange={setValue} />
|
||||
)}
|
||||
<SimpleTextareaEditor
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
save={startEditing}
|
||||
/>
|
||||
</EditorWrapper>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={styles.editor}>
|
||||
<div className={styles.editor} id={id}>
|
||||
{hydrated && <Suspense>{locked ? viewer : editor}</Suspense>}
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -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<DockviewApi>();
|
||||
const [hydrated, setHydrated] = useState(false);
|
||||
const { layout, setLayout } = useStorage();
|
||||
|
||||
const onReady = (event: DockviewReadyEvent) => {
|
||||
if (hydrated) {
|
||||
return;
|
||||
}
|
||||
|
||||
api.current = event.api;
|
||||
|
||||
storage.get<SerializedDockview>(key).then(layout => {
|
||||
if (!layout) {
|
||||
throw new Error("No layout saved, its okay");
|
||||
createDefaultLayout(event.api);
|
||||
return;
|
||||
}
|
||||
|
||||
event.api.fromJSON(layout);
|
||||
}).catch(() => {
|
||||
createDefaultLayout(event.api);
|
||||
|
||||
}).finally(() => {
|
||||
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(() => {
|
||||
|
|
|
@ -44,6 +44,15 @@ const DefaultLayout = ({
|
|||
...panelProps.params,
|
||||
locked: !locked,
|
||||
});
|
||||
|
||||
if (panelProps.params.locked) {
|
||||
setTimeout(() => {
|
||||
document
|
||||
.getElementById(panelProps.api.id)
|
||||
?.querySelector("textarea")
|
||||
?.focus();
|
||||
}, 0);
|
||||
}
|
||||
}, [locked, panelProps.api, panelProps.params]);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
|
@ -3,7 +3,7 @@ import { useTranslation } from "react-i18next";
|
|||
import { RowGroup } from "~/components/containers/RowGroup";
|
||||
import { SettingsRow } from "~/components/containers/SettingsRow";
|
||||
import {
|
||||
ColorSettings,
|
||||
ColorSettings as ColorSettingsValue,
|
||||
SettingsValue,
|
||||
useSettings,
|
||||
} from "~/modules/settings/context/SettingsContext";
|
||||
|
@ -37,7 +37,7 @@ const ColorSettings: FC = () => {
|
|||
);
|
||||
|
||||
const setThemeColors = useCallback(
|
||||
(val: ColorSettings) => {
|
||||
(val: ColorSettingsValue) => {
|
||||
update(fillThemeHeadings(val));
|
||||
},
|
||||
[update]
|
||||
|
|
13
src/modules/storage/StorageContext.ts
Normal file
13
src/modules/storage/StorageContext.ts
Normal file
|
@ -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<string, string>,
|
||||
hydrated: false,
|
||||
setPanel: noop as (uuid: string, content: string) => void,
|
||||
setLayout: noop as (layout: SerializedDockview) => void,
|
||||
});
|
||||
|
||||
export const useStorage = () => useContext(StorageContext);
|
67
src/modules/storage/StorageProvider.tsx
Normal file
67
src/modules/storage/StorageProvider.tsx
Normal file
|
@ -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<SerializedDockview | null>(null);
|
||||
const [panels, setPanelsValue] = useState<Record<string, string>>({});
|
||||
|
||||
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 (
|
||||
<StorageContext.Provider
|
||||
value={{ hydrated, layout, panels, setLayout, setPanel }}
|
||||
>
|
||||
{hydrated ? children : null}
|
||||
</StorageContext.Provider>
|
||||
);
|
||||
};
|
38
src/modules/storage/hooks/useDelayedSync.ts
Normal file
38
src/modules/storage/hooks/useDelayedSync.ts
Normal file
|
@ -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<DebouncedFunc<typeof storeLayoutInSync>>();
|
||||
const panelTimers = useRef<
|
||||
Record<string, DebouncedFunc<typeof storePanelInSync>>
|
||||
>({});
|
||||
|
||||
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 };
|
||||
};
|
127
src/utils/hydrate.ts
Normal file
127
src/utils/hydrate.ts
Normal file
|
@ -0,0 +1,127 @@
|
|||
import { SerializedDockview } from "dockview";
|
||||
import { hasBrowserStorage, hasChromeStorage } from "~/utils/storage";
|
||||
|
||||
interface Result {
|
||||
layout: SerializedDockview;
|
||||
panels: Record<string, string>;
|
||||
}
|
||||
|
||||
const layoutKey = "dockview_persistance_layout";
|
||||
const panelPrefix = "MarkdownEditorContainerMarkdownEditorContainer";
|
||||
|
||||
const makePanelKey = (uuid: string) => `${panelPrefix}${uuid}`;
|
||||
|
||||
const getFromBrowserStorage = async (): Promise<Result | null> => {
|
||||
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<string, string>
|
||||
);
|
||||
|
||||
return {
|
||||
layout,
|
||||
panels,
|
||||
};
|
||||
};
|
||||
|
||||
const getFromChromeStorage = async (): Promise<Result | null> => {
|
||||
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<string, string>
|
||||
);
|
||||
|
||||
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<string, string>
|
||||
);
|
||||
|
||||
return {
|
||||
layout,
|
||||
panels,
|
||||
};
|
||||
};
|
||||
|
||||
export const hydrateLayout = async (): Promise<Result | null> => {
|
||||
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 });
|
||||
}
|
||||
};
|
|
@ -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 <T>(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 <T>(key: string): Promise<T | undefined> => {
|
||||
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;
|
||||
|
|
1
src/utils/noop.ts
Normal file
1
src/utils/noop.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export const noop = () => {};
|
57
src/utils/seed.ts
Normal file
57
src/utils/seed.ts
Normal file
|
@ -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",
|
||||
},
|
||||
});
|
||||
};
|
5
src/utils/storage.ts
Normal file
5
src/utils/storage.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
export const hasBrowserStorage = () =>
|
||||
typeof browser !== "undefined" && browser?.storage;
|
||||
|
||||
export const hasChromeStorage = () =>
|
||||
typeof chrome !== "undefined" && chrome?.storage;
|
17
yarn.lock
17
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"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue