mirror of
https://github.com/muerwre/markdown-home-tab.git
synced 2025-05-11 00:36:40 +07:00
Compare commits
No commits in common. "2e15044c120c3c3924b64be7805144ee749d3c3b" and "6d00bffbec4c50641fb2fda8635fa4ba107f66ad" have entirely different histories.
2e15044c12
...
6d00bffbec
26 changed files with 157 additions and 467 deletions
.gitignoreREADME.mdpackage.json
public
src
main.tsx
yarn.lockmodules
editor
components
EmptyViewer
ReactMarkdownEditor
ReactMarkdownViewer
RemirrorEditor
containers/MarkdownEditorContainer
layout/components/GridLayout
settings/containers/ColorSettings
storage
utils
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -23,5 +23,3 @@ dist-ssr
|
|||
*.sln
|
||||
*.sw?
|
||||
web-ext-artifacts
|
||||
output
|
||||
*.tgz
|
|
@ -22,4 +22,4 @@ yarn dev
|
|||
|
||||
## TO-DO
|
||||
|
||||
- Use HyperMD editor
|
||||
- Use Remirror as editor
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "markdown-home-tab",
|
||||
"private": true,
|
||||
"version": "0.0.5",
|
||||
"version": "0.0.4",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"package": "yarn build && web-ext build -s ./dist -a ./output --overwrite-dest",
|
||||
"package": "yarn build && web-ext build -s ./dist",
|
||||
"lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
|
@ -21,7 +21,6 @@
|
|||
"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",
|
||||
|
@ -39,7 +38,6 @@
|
|||
"@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.6",
|
||||
"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"],
|
||||
|
|
|
@ -4,20 +4,16 @@ 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} onDoubleClick={startEditing}>
|
||||
<div className={styles.empty} style={style}>
|
||||
<div className={styles.title}>{t(`Nothing's here yet`)}</div>
|
||||
<div>
|
||||
<Button
|
||||
|
|
|
@ -8,8 +8,6 @@
|
|||
box-sizing: border-box;
|
||||
opacity: 0;
|
||||
transition: opacity 0.25s;
|
||||
user-select: none;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.5;
|
||||
|
|
|
@ -1,15 +1,13 @@
|
|||
import { ChangeEvent, FC, useCallback, useMemo, KeyboardEvent } from "react";
|
||||
import { ChangeEvent, FC, useCallback, useMemo } 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 SimpleTextareaEditor: FC<ReactMarkdownEditorProps> = ({
|
||||
save,
|
||||
const ReactMarkdownEditor: FC<ReactMarkdownEditorProps> = ({
|
||||
value,
|
||||
onChange,
|
||||
}) => {
|
||||
|
@ -29,22 +27,8 @@ const SimpleTextareaEditor: 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}
|
||||
|
@ -55,4 +39,4 @@ const SimpleTextareaEditor: FC<ReactMarkdownEditorProps> = ({
|
|||
);
|
||||
};
|
||||
|
||||
export { SimpleTextareaEditor };
|
||||
export { ReactMarkdownEditor };
|
|
@ -1,4 +1,4 @@
|
|||
import { FC, useCallback, MouseEvent } from "react";
|
||||
import { FC } from "react";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import { useContainerPaddings } from "~/modules/theme/hooks/useContainerPaddings";
|
||||
import styles from "./styles.module.scss";
|
||||
|
@ -7,26 +7,20 @@ import { useTranslation } from "react-i18next";
|
|||
import remarkGfm from "remark-gfm";
|
||||
import rehypeRaw from "rehype-raw";
|
||||
|
||||
interface Props {
|
||||
interface ReactMarkdownViewerProps {
|
||||
value: string;
|
||||
startEditing: () => void;
|
||||
}
|
||||
|
||||
const MarkdownViewer: FC<Props> = ({ value, startEditing }) => {
|
||||
const ReactMarkdownViewer: FC<ReactMarkdownViewerProps> = ({
|
||||
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} onDoubleClick={onDoubleClick}>
|
||||
<div style={style} className={styles.editor}>
|
||||
<div className={styles.edit}>
|
||||
<Button
|
||||
size="small"
|
||||
|
@ -47,4 +41,4 @@ const MarkdownViewer: FC<Props> = ({ value, startEditing }) => {
|
|||
);
|
||||
};
|
||||
|
||||
export { MarkdownViewer };
|
||||
export { ReactMarkdownViewer };
|
|
@ -1,16 +1,37 @@
|
|||
import { FC, useCallback } from "react";
|
||||
|
||||
import {
|
||||
EditorComponent,
|
||||
FloatingToolbar,
|
||||
FormattingButtonGroup,
|
||||
HeadingLevelButtonGroup,
|
||||
Remirror,
|
||||
useRemirror,
|
||||
EditorComponent,
|
||||
HeadingLevelButtonGroup,
|
||||
FormattingButtonGroup,
|
||||
} from "@remirror/react";
|
||||
import { Extension, RemirrorEventListener } from "remirror";
|
||||
import { useContainerPaddings } from "~/modules/theme/hooks/useContainerPaddings";
|
||||
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 { useContainerPaddings } from "~/modules/theme/hooks/useContainerPaddings";
|
||||
|
||||
interface RemirrorEditorProps {
|
||||
locked: boolean;
|
||||
|
@ -29,7 +50,7 @@ const RemirrorEditor: FC<RemirrorEditorProps> = ({
|
|||
exitMarksOnArrowPress: false,
|
||||
},
|
||||
content: value,
|
||||
// stringHandler: "markdown",
|
||||
stringHandler: "markdown",
|
||||
});
|
||||
|
||||
const onStateChange = useCallback<RemirrorEventListener<Extension>>(
|
||||
|
@ -69,26 +90,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 };
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
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,52 +1,60 @@
|
|||
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 { 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 { useStorage } from "../../../../modules/storage/StorageContext";
|
||||
import { useSettings } from "~/modules/settings/context/SettingsContext";
|
||||
import { EmptyViewer } from "../../components/EmptyViewer";
|
||||
import { EditorWrapper } from "../../components/EditorWrapper";
|
||||
|
||||
interface MarkdownEditorContainerProps {
|
||||
id: string;
|
||||
locked: boolean;
|
||||
startEditing: VoidCallback;
|
||||
remove: VoidCallback;
|
||||
startEditing: () => void;
|
||||
remove: () => void;
|
||||
}
|
||||
|
||||
const RichEditor = lazy(() =>
|
||||
import("../../components/RemirrorEditor").then((module) => ({
|
||||
default: module.RemirrorEditor,
|
||||
}))
|
||||
);
|
||||
|
||||
export const MarkdownEditorContainer: FC<MarkdownEditorContainerProps> = ({
|
||||
id,
|
||||
locked,
|
||||
startEditing,
|
||||
remove,
|
||||
}) => {
|
||||
const { panels, setPanel, hydrated } = useStorage();
|
||||
const {
|
||||
settings: { richEditorEnabled },
|
||||
} = useSettings();
|
||||
|
||||
const value = panels[id] ?? "";
|
||||
const empty = !value.trim();
|
||||
|
||||
const onChange = useCallback(
|
||||
(val: string) => setPanel(id, val),
|
||||
[id, setPanel]
|
||||
const { value, setValue, hydrated } = usePersistedValue(
|
||||
id,
|
||||
"MarkdownEditorContainer"
|
||||
);
|
||||
|
||||
const empty = !value.trim();
|
||||
|
||||
const viewer = empty ? (
|
||||
<EmptyViewer startEditing={startEditing} />
|
||||
) : (
|
||||
<MarkdownViewer value={value} startEditing={startEditing} />
|
||||
<ReactMarkdownViewer value={value} startEditing={startEditing} />
|
||||
);
|
||||
|
||||
const editor = (
|
||||
<EditorWrapper save={startEditing} remove={remove}>
|
||||
<SimpleTextareaEditor
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
save={startEditing}
|
||||
/>
|
||||
{richEditorEnabled ? (
|
||||
<RichEditor value={value} onChange={setValue} locked={locked} />
|
||||
) : (
|
||||
<ReactMarkdownEditor value={value} onChange={setValue} />
|
||||
)}
|
||||
</EditorWrapper>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={styles.editor} id={id}>
|
||||
<div className={styles.editor}>
|
||||
{hydrated && <Suspense>{locked ? viewer : editor}</Suspense>}
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -1,27 +1,30 @@
|
|||
import { DockviewApi, DockviewReadyEvent } from "dockview";
|
||||
import { DockviewApi, DockviewReadyEvent, SerializedDockview } 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) {
|
||||
createDefaultLayout(event.api);
|
||||
return;
|
||||
throw new Error("No layout saved, its okay");
|
||||
}
|
||||
|
||||
event.api.fromJSON(layout);
|
||||
}).catch(() => {
|
||||
createDefaultLayout(event.api);
|
||||
|
||||
}).finally(() => {
|
||||
setHydrated(true);
|
||||
});
|
||||
};
|
||||
|
||||
const persistLayout = useCallback(() => {
|
||||
|
@ -29,8 +32,8 @@ export const useGridLayoutPersistance = () => {
|
|||
return;
|
||||
}
|
||||
|
||||
setLayout(api.current.toJSON());
|
||||
}, [setLayout]);
|
||||
storage.set(key, api.current.toJSON());
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const onLayoutChange = api.current?.onDidLayoutChange(() => {
|
||||
|
|
|
@ -44,15 +44,6 @@ 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 as ColorSettingsValue,
|
||||
ColorSettings,
|
||||
SettingsValue,
|
||||
useSettings,
|
||||
} from "~/modules/settings/context/SettingsContext";
|
||||
|
@ -37,7 +37,7 @@ const ColorSettings: FC = () => {
|
|||
);
|
||||
|
||||
const setThemeColors = useCallback(
|
||||
(val: ColorSettingsValue) => {
|
||||
(val: ColorSettings) => {
|
||||
update(fillThemeHeadings(val));
|
||||
},
|
||||
[update]
|
||||
|
|
|
@ -1,13 +0,0 @@
|
|||
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);
|
|
@ -1,67 +0,0 @@
|
|||
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>
|
||||
);
|
||||
};
|
|
@ -1,38 +0,0 @@
|
|||
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 };
|
||||
};
|
|
@ -1,127 +0,0 @@
|
|||
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,13 +1,11 @@
|
|||
import { hasBrowserStorage, hasChromeStorage } from "./storage";
|
||||
|
||||
export class BrowserSyncStorage {
|
||||
constructor(private globalPrefix = "") {}
|
||||
|
||||
get engine() {
|
||||
if (hasBrowserStorage()) {
|
||||
return "browser";
|
||||
} else if (hasChromeStorage()) {
|
||||
return "chrome";
|
||||
if (typeof browser !== 'undefined' && browser?.storage) {
|
||||
return "browser"
|
||||
} else if (typeof chrome !== 'undefined' && chrome?.storage) {
|
||||
return "chrome"
|
||||
}
|
||||
|
||||
return "local";
|
||||
|
@ -17,20 +15,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);
|
||||
|
@ -38,10 +36,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 +0,0 @@
|
|||
export const noop = () => {};
|
|
@ -1,57 +0,0 @@
|
|||
// 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",
|
||||
},
|
||||
});
|
||||
};
|
|
@ -1,5 +0,0 @@
|
|||
export const hasBrowserStorage = () =>
|
||||
typeof browser !== "undefined" && browser?.storage;
|
||||
|
||||
export const hasChromeStorage = () =>
|
||||
typeof chrome !== "undefined" && chrome?.storage;
|
17
yarn.lock
17
yarn.lock
|
@ -1961,18 +1961,6 @@
|
|||
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"
|
||||
|
@ -4977,11 +4965,6 @@ 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