@peaske7/readit 0.1.6 → 0.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/biome.json +1 -1
- package/bun.lock +86 -72
- package/package.json +12 -11
- package/src/App.tsx +36 -16
- package/src/cli/index.ts +338 -70
- package/src/components/ActionsMenu.tsx +12 -10
- package/src/components/DocumentViewer/CodeBlock.tsx +131 -54
- package/src/components/DocumentViewer/DocumentViewer.tsx +10 -8
- package/src/components/DocumentViewer/InlineCode.tsx +60 -0
- package/src/components/FloatingTOC.tsx +4 -2
- package/src/components/Header.tsx +3 -1
- package/src/components/InlineEditor.tsx +4 -2
- package/src/components/MarginNote.tsx +17 -8
- package/src/components/RawModal.tsx +9 -7
- package/src/components/ReanchorConfirm.tsx +6 -3
- package/src/components/SettingsModal.tsx +112 -23
- package/src/components/ShortcutCapture.tsx +4 -1
- package/src/components/ShortcutList.tsx +50 -9
- package/src/components/comments/CommentBadge.tsx +7 -1
- package/src/components/comments/CommentInput.tsx +13 -18
- package/src/components/comments/CommentListItem.tsx +15 -5
- package/src/components/comments/CommentManager.tsx +14 -7
- package/src/components/comments/CommentNav.tsx +8 -3
- package/src/contexts/CommentContext.tsx +16 -9
- package/src/contexts/LayoutContext.tsx +17 -5
- package/src/contexts/LocaleContext.tsx +35 -0
- package/src/hooks/useClipboard.ts +11 -8
- package/src/hooks/useDocument.ts +33 -18
- package/src/hooks/useEditorScheme.ts +51 -0
- package/src/hooks/useFontPreference.ts +5 -22
- package/src/hooks/useKeybindings.ts +6 -18
- package/src/hooks/useLocalePreference.ts +42 -0
- package/src/index.css +87 -26
- package/src/lib/editor-links.ts +59 -0
- package/src/lib/highlight/dom.ts +126 -54
- package/src/lib/highlight/highlighter.ts +10 -10
- package/src/lib/i18n/completeness.test.ts +51 -0
- package/src/lib/i18n/en.ts +139 -0
- package/src/lib/i18n/index.ts +3 -0
- package/src/lib/i18n/ja.ts +141 -0
- package/src/lib/i18n/translations.test.ts +39 -0
- package/src/lib/i18n/translations.ts +27 -0
- package/src/lib/i18n/types.ts +145 -0
- package/src/lib/shortcut-registry.ts +1 -1
- package/src/main.tsx +4 -1
- package/src/server/index.ts +197 -124
- package/src/store/index.test.ts +22 -0
- package/src/store/index.ts +24 -4
- package/src/types/index.ts +12 -0
|
@@ -15,6 +15,7 @@ import { generatePrompt } from "../lib/export";
|
|
|
15
15
|
import { truncate } from "../lib/utils";
|
|
16
16
|
import { useAppStore } from "../store";
|
|
17
17
|
import type { Comment, DocumentType } from "../types";
|
|
18
|
+
import { useLocale } from "./LocaleContext";
|
|
18
19
|
|
|
19
20
|
interface CommentContextValue {
|
|
20
21
|
// From useComments
|
|
@@ -108,6 +109,7 @@ export function CommentProvider({
|
|
|
108
109
|
} = useCommentNavigation(sortedComments);
|
|
109
110
|
|
|
110
111
|
const { reanchorTarget, startReanchor, cancelReanchor } = useReanchorMode();
|
|
112
|
+
const { t } = useLocale();
|
|
111
113
|
|
|
112
114
|
// Show comments errors as toast
|
|
113
115
|
useEffect(() => {
|
|
@@ -116,11 +118,14 @@ export function CommentProvider({
|
|
|
116
118
|
}
|
|
117
119
|
}, [commentsError]);
|
|
118
120
|
|
|
119
|
-
const copyCommentRaw = useCallback(
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
121
|
+
const copyCommentRaw = useCallback(
|
|
122
|
+
(comment: Comment) => {
|
|
123
|
+
const raw = `${comment.selectedText}\n\n${comment.comment}`;
|
|
124
|
+
navigator.clipboard.writeText(raw);
|
|
125
|
+
toast.success(t("toast.copied", { text: truncate(comment.comment) }));
|
|
126
|
+
},
|
|
127
|
+
[t],
|
|
128
|
+
);
|
|
124
129
|
|
|
125
130
|
const copyCommentForLLM = useCallback(
|
|
126
131
|
(comment: Comment) => {
|
|
@@ -136,16 +141,18 @@ export function CommentProvider({
|
|
|
136
141
|
});
|
|
137
142
|
|
|
138
143
|
navigator.clipboard.writeText(formatted);
|
|
139
|
-
toast.success(
|
|
144
|
+
toast.success(
|
|
145
|
+
t("toast.copiedForLLM", { text: truncate(comment.comment) }),
|
|
146
|
+
);
|
|
140
147
|
},
|
|
141
|
-
[documentContent, fileName],
|
|
148
|
+
[documentContent, fileName, t],
|
|
142
149
|
);
|
|
143
150
|
|
|
144
151
|
const copyAllForLLM = useCallback(() => {
|
|
145
152
|
const prompt = generatePrompt(comments, fileName);
|
|
146
153
|
navigator.clipboard.writeText(prompt);
|
|
147
|
-
toast.success("
|
|
148
|
-
}, [comments, fileName]);
|
|
154
|
+
toast.success(t("toast.copiedAllComments"));
|
|
155
|
+
}, [comments, fileName, t]);
|
|
149
156
|
|
|
150
157
|
const scrollToHighlight = useCallback(
|
|
151
158
|
(commentId: string) => {
|
|
@@ -1,16 +1,24 @@
|
|
|
1
1
|
import { createContext, type ReactNode, use, useMemo } from "react";
|
|
2
|
+
import { useEditorScheme } from "../hooks/useEditorScheme";
|
|
2
3
|
import { useFontPreference } from "../hooks/useFontPreference";
|
|
3
4
|
import { useKeybindings } from "../hooks/useKeybindings";
|
|
4
5
|
import { useLayoutMode } from "../hooks/useLayoutMode";
|
|
5
6
|
import { useThemePreference } from "../hooks/useThemePreference";
|
|
6
7
|
import type { ShortcutDefinition } from "../lib/shortcut-registry";
|
|
7
|
-
import type {
|
|
8
|
+
import type {
|
|
9
|
+
EditorScheme,
|
|
10
|
+
FontFamily,
|
|
11
|
+
ShortcutBinding,
|
|
12
|
+
ThemeMode,
|
|
13
|
+
} from "../types";
|
|
8
14
|
|
|
9
15
|
interface LayoutContextValue {
|
|
10
16
|
isFullscreen: boolean;
|
|
11
17
|
toggleLayoutMode: () => void;
|
|
12
18
|
fontFamily: FontFamily;
|
|
13
19
|
setFontFamily: (font: FontFamily) => Promise<void>;
|
|
20
|
+
editorScheme: EditorScheme;
|
|
21
|
+
setEditorScheme: (scheme: EditorScheme) => Promise<void>;
|
|
14
22
|
themeMode: ThemeMode;
|
|
15
23
|
setThemeMode: (mode: ThemeMode) => void;
|
|
16
24
|
shortcuts: ShortcutDefinition[];
|
|
@@ -30,20 +38,20 @@ export function useLayoutContext(): LayoutContextValue {
|
|
|
30
38
|
}
|
|
31
39
|
|
|
32
40
|
interface LayoutProviderProps {
|
|
33
|
-
filePath: string;
|
|
34
41
|
children: ReactNode;
|
|
35
42
|
}
|
|
36
43
|
|
|
37
|
-
export function LayoutProvider({
|
|
44
|
+
export function LayoutProvider({ children }: LayoutProviderProps) {
|
|
38
45
|
const { isFullscreen, toggleLayoutMode } = useLayoutMode();
|
|
39
|
-
const { fontFamily, setFontFamily } = useFontPreference(
|
|
46
|
+
const { fontFamily, setFontFamily } = useFontPreference();
|
|
47
|
+
const { editorScheme, setEditorScheme } = useEditorScheme();
|
|
40
48
|
const { themeMode, setThemeMode } = useThemePreference();
|
|
41
49
|
const {
|
|
42
50
|
shortcuts,
|
|
43
51
|
updateBinding,
|
|
44
52
|
toggleEnabled: toggleShortcutEnabled,
|
|
45
53
|
resetToDefaults: resetShortcutsToDefaults,
|
|
46
|
-
} = useKeybindings(
|
|
54
|
+
} = useKeybindings();
|
|
47
55
|
|
|
48
56
|
const value = useMemo<LayoutContextValue>(
|
|
49
57
|
() => ({
|
|
@@ -51,6 +59,8 @@ export function LayoutProvider({ filePath, children }: LayoutProviderProps) {
|
|
|
51
59
|
toggleLayoutMode,
|
|
52
60
|
fontFamily,
|
|
53
61
|
setFontFamily,
|
|
62
|
+
editorScheme,
|
|
63
|
+
setEditorScheme,
|
|
54
64
|
themeMode,
|
|
55
65
|
setThemeMode,
|
|
56
66
|
shortcuts,
|
|
@@ -63,6 +73,8 @@ export function LayoutProvider({ filePath, children }: LayoutProviderProps) {
|
|
|
63
73
|
toggleLayoutMode,
|
|
64
74
|
fontFamily,
|
|
65
75
|
setFontFamily,
|
|
76
|
+
editorScheme,
|
|
77
|
+
setEditorScheme,
|
|
66
78
|
themeMode,
|
|
67
79
|
setThemeMode,
|
|
68
80
|
shortcuts,
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { createContext, type ReactNode, use, useMemo } from "react";
|
|
2
|
+
import { useLocalePreference } from "../hooks/useLocalePreference";
|
|
3
|
+
import { createT, type Locale, type TranslationKey } from "../lib/i18n";
|
|
4
|
+
|
|
5
|
+
interface LocaleContextValue {
|
|
6
|
+
locale: Locale;
|
|
7
|
+
setLocale: (locale: Locale) => void;
|
|
8
|
+
t: (key: TranslationKey, params?: Record<string, string | number>) => string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const LocaleContext = createContext<LocaleContextValue | null>(null);
|
|
12
|
+
|
|
13
|
+
export function useLocale(): LocaleContextValue {
|
|
14
|
+
const value = use(LocaleContext);
|
|
15
|
+
if (!value) {
|
|
16
|
+
throw new Error("useLocale must be used within a LocaleProvider");
|
|
17
|
+
}
|
|
18
|
+
return value;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface LocaleProviderProps {
|
|
22
|
+
children: ReactNode;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function LocaleProvider({ children }: LocaleProviderProps) {
|
|
26
|
+
const { locale, setLocale } = useLocalePreference();
|
|
27
|
+
const t = useMemo(() => createT(locale), [locale]);
|
|
28
|
+
|
|
29
|
+
const value = useMemo<LocaleContextValue>(
|
|
30
|
+
() => ({ locale, setLocale, t }),
|
|
31
|
+
[locale, setLocale, t],
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
return <LocaleContext value={value}>{children}</LocaleContext>;
|
|
35
|
+
}
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
generatePrompt,
|
|
7
7
|
generateRawText,
|
|
8
8
|
} from "../lib/export";
|
|
9
|
+
import type { TranslationKey } from "../lib/i18n";
|
|
9
10
|
import { truncate } from "../lib/utils";
|
|
10
11
|
import type { Comment, Document, Selection } from "../types";
|
|
11
12
|
|
|
@@ -14,6 +15,7 @@ interface UseClipboardParams {
|
|
|
14
15
|
document: Document | undefined;
|
|
15
16
|
selection: Selection | undefined;
|
|
16
17
|
clearSelection: () => void;
|
|
18
|
+
t: (key: TranslationKey, params?: Record<string, string | number>) => string;
|
|
17
19
|
}
|
|
18
20
|
|
|
19
21
|
export function useClipboard({
|
|
@@ -21,21 +23,22 @@ export function useClipboard({
|
|
|
21
23
|
document,
|
|
22
24
|
selection,
|
|
23
25
|
clearSelection,
|
|
26
|
+
t,
|
|
24
27
|
}: UseClipboardParams) {
|
|
25
28
|
// Export handlers
|
|
26
29
|
const copyAll = useCallback(() => {
|
|
27
30
|
if (!document) return;
|
|
28
31
|
const prompt = generatePrompt(comments, document.fileName);
|
|
29
32
|
navigator.clipboard.writeText(prompt);
|
|
30
|
-
toast.success("
|
|
31
|
-
}, [comments, document]);
|
|
33
|
+
toast.success(t("toast.copiedAllComments"));
|
|
34
|
+
}, [comments, document, t]);
|
|
32
35
|
|
|
33
36
|
const copyAllRaw = useCallback(() => {
|
|
34
37
|
if (!document) return;
|
|
35
38
|
const raw = generateRawText(comments);
|
|
36
39
|
navigator.clipboard.writeText(raw);
|
|
37
|
-
toast.success("
|
|
38
|
-
}, [comments, document]);
|
|
40
|
+
toast.success(t("toast.copiedAllRaw"));
|
|
41
|
+
}, [comments, document, t]);
|
|
39
42
|
|
|
40
43
|
const exportJson = useCallback(() => {
|
|
41
44
|
if (!document) return;
|
|
@@ -47,9 +50,9 @@ export function useClipboard({
|
|
|
47
50
|
if (!selection) return;
|
|
48
51
|
|
|
49
52
|
navigator.clipboard.writeText(selection.text);
|
|
50
|
-
toast.success(
|
|
53
|
+
toast.success(t("toast.copied", { text: truncate(selection.text) }));
|
|
51
54
|
clearSelection();
|
|
52
|
-
}, [selection, clearSelection]);
|
|
55
|
+
}, [selection, clearSelection, t]);
|
|
53
56
|
|
|
54
57
|
const copySelectionForLLM = useCallback(() => {
|
|
55
58
|
if (!selection || !document) return;
|
|
@@ -65,9 +68,9 @@ export function useClipboard({
|
|
|
65
68
|
});
|
|
66
69
|
|
|
67
70
|
navigator.clipboard.writeText(formatted);
|
|
68
|
-
toast.success(
|
|
71
|
+
toast.success(t("toast.copiedForLLM", { text: truncate(selection.text) }));
|
|
69
72
|
clearSelection();
|
|
70
|
-
}, [selection, document, clearSelection]);
|
|
73
|
+
}, [selection, document, clearSelection, t]);
|
|
71
74
|
|
|
72
75
|
return {
|
|
73
76
|
copyAll,
|
package/src/hooks/useDocument.ts
CHANGED
|
@@ -10,6 +10,12 @@ interface UseDocumentResult {
|
|
|
10
10
|
reload: () => Promise<void>;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
+
interface DocListItem {
|
|
14
|
+
path: string;
|
|
15
|
+
fileName: string;
|
|
16
|
+
type: Document["type"];
|
|
17
|
+
}
|
|
18
|
+
|
|
13
19
|
/**
|
|
14
20
|
* Manage multi-document loading, lazy content fetching, and live reloading.
|
|
15
21
|
*
|
|
@@ -39,15 +45,21 @@ export function useDocument(): UseDocumentResult {
|
|
|
39
45
|
const data = await res.json();
|
|
40
46
|
|
|
41
47
|
const clean = data.clean || false;
|
|
42
|
-
|
|
43
|
-
appStore.getState().
|
|
44
|
-
content: "", // Content loaded lazily on tab activation
|
|
45
|
-
type: file.type,
|
|
46
|
-
filePath: file.path,
|
|
47
|
-
fileName: file.fileName,
|
|
48
|
-
clean,
|
|
49
|
-
});
|
|
48
|
+
if (data.workingDirectory) {
|
|
49
|
+
appStore.getState().setWorkingDirectory(data.workingDirectory);
|
|
50
50
|
}
|
|
51
|
+
data.files.forEach((file: DocListItem, index: number) => {
|
|
52
|
+
appStore.getState().openDocument(
|
|
53
|
+
{
|
|
54
|
+
content: "", // Content loaded lazily on tab activation
|
|
55
|
+
type: file.type,
|
|
56
|
+
filePath: file.path,
|
|
57
|
+
fileName: file.fileName,
|
|
58
|
+
clean,
|
|
59
|
+
},
|
|
60
|
+
{ active: index === 0 },
|
|
61
|
+
);
|
|
62
|
+
});
|
|
51
63
|
} catch (err) {
|
|
52
64
|
setError(
|
|
53
65
|
err instanceof Error ? err.message : "Failed to load documents",
|
|
@@ -84,23 +96,26 @@ export function useDocument(): UseDocumentResult {
|
|
|
84
96
|
loadContent();
|
|
85
97
|
}, [activeDocumentPath]);
|
|
86
98
|
|
|
87
|
-
// SSE:
|
|
99
|
+
// SSE: register new documents without stealing focus; reload loaded docs on updates
|
|
88
100
|
useEffect(() => {
|
|
89
101
|
const eventSource = new EventSource("/api/document/stream");
|
|
90
102
|
eventSource.onmessage = async (e) => {
|
|
91
103
|
try {
|
|
92
104
|
const data = JSON.parse(e.data);
|
|
93
|
-
if (data.type === "
|
|
94
|
-
appStore.getState().openDocument(
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
105
|
+
if (data.type === "document-added" && data.path) {
|
|
106
|
+
appStore.getState().openDocument(
|
|
107
|
+
{
|
|
108
|
+
content: "", // Lazy-loaded when tab activated
|
|
109
|
+
type: data.fileType,
|
|
110
|
+
filePath: data.path,
|
|
111
|
+
fileName: data.fileName,
|
|
112
|
+
clean: false,
|
|
113
|
+
},
|
|
114
|
+
{ active: false },
|
|
115
|
+
);
|
|
101
116
|
return;
|
|
102
117
|
}
|
|
103
|
-
if (data.type === "
|
|
118
|
+
if (data.type === "document-updated" && data.path) {
|
|
104
119
|
// Only reload if content was previously loaded
|
|
105
120
|
const state = appStore.getState().documents.get(data.path);
|
|
106
121
|
if (!state || !state.document.content) return;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { useCallback, useEffect, useState } from "react";
|
|
2
|
+
import { toast } from "sonner";
|
|
3
|
+
import { type EditorScheme, EditorSchemes } from "../types";
|
|
4
|
+
|
|
5
|
+
interface UseEditorSchemeResult {
|
|
6
|
+
editorScheme: EditorScheme;
|
|
7
|
+
setEditorScheme: (scheme: EditorScheme) => Promise<void>;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function useEditorScheme(): UseEditorSchemeResult {
|
|
11
|
+
const [editorScheme, setEditorSchemeState] = useState<EditorScheme>(
|
|
12
|
+
EditorSchemes.NONE,
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
const fetchSettings = async () => {
|
|
17
|
+
try {
|
|
18
|
+
const response = await fetch("/api/settings");
|
|
19
|
+
if (response.ok) {
|
|
20
|
+
const settings = await response.json();
|
|
21
|
+
setEditorSchemeState(settings.editorScheme || EditorSchemes.NONE);
|
|
22
|
+
}
|
|
23
|
+
} catch (err) {
|
|
24
|
+
console.error("Failed to fetch settings:", err);
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
fetchSettings();
|
|
29
|
+
}, []);
|
|
30
|
+
|
|
31
|
+
const setEditorScheme = useCallback(async (scheme: EditorScheme) => {
|
|
32
|
+
setEditorSchemeState(scheme);
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
const response = await fetch("/api/settings", {
|
|
36
|
+
method: "PUT",
|
|
37
|
+
headers: { "Content-Type": "application/json" },
|
|
38
|
+
body: JSON.stringify({ editorScheme: scheme }),
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
if (!response.ok) {
|
|
42
|
+
throw new Error("Failed to save settings");
|
|
43
|
+
}
|
|
44
|
+
} catch (err) {
|
|
45
|
+
console.error("Failed to save editor scheme:", err);
|
|
46
|
+
toast.error("Failed to save editor scheme");
|
|
47
|
+
}
|
|
48
|
+
}, []);
|
|
49
|
+
|
|
50
|
+
return { editorScheme, setEditorScheme };
|
|
51
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useCallback, useEffect,
|
|
1
|
+
import { useCallback, useEffect, useState } from "react";
|
|
2
2
|
import { toast } from "sonner";
|
|
3
3
|
import { FontFamilies, type FontFamily } from "../types";
|
|
4
4
|
|
|
@@ -8,30 +8,16 @@ interface UseFontPreferenceResult {
|
|
|
8
8
|
isLoading: boolean;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
export function useFontPreference(
|
|
12
|
-
filePath: string | null,
|
|
13
|
-
): UseFontPreferenceResult {
|
|
11
|
+
export function useFontPreference(): UseFontPreferenceResult {
|
|
14
12
|
const [fontFamily, setFontFamilyState] = useState<FontFamily>(
|
|
15
13
|
FontFamilies.SERIF,
|
|
16
14
|
);
|
|
17
15
|
const [isLoading, setIsLoading] = useState(true);
|
|
18
16
|
|
|
19
|
-
const filePathRef = useRef(filePath);
|
|
20
|
-
filePathRef.current = filePath;
|
|
21
|
-
|
|
22
|
-
// Fetch settings when filePath changes
|
|
23
17
|
useEffect(() => {
|
|
24
|
-
if (!filePath) {
|
|
25
|
-
setIsLoading(false);
|
|
26
|
-
return;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
setIsLoading(true);
|
|
30
|
-
|
|
31
18
|
const fetchSettings = async () => {
|
|
32
19
|
try {
|
|
33
|
-
const
|
|
34
|
-
const response = await fetch(`/api/settings${query}`);
|
|
20
|
+
const response = await fetch("/api/settings");
|
|
35
21
|
if (response.ok) {
|
|
36
22
|
const settings = await response.json();
|
|
37
23
|
setFontFamilyState(settings.fontFamily || FontFamilies.SERIF);
|
|
@@ -44,16 +30,13 @@ export function useFontPreference(
|
|
|
44
30
|
};
|
|
45
31
|
|
|
46
32
|
fetchSettings();
|
|
47
|
-
}, [
|
|
33
|
+
}, []);
|
|
48
34
|
|
|
49
35
|
const setFontFamily = useCallback(async (font: FontFamily) => {
|
|
50
|
-
// Optimistic update
|
|
51
36
|
setFontFamilyState(font);
|
|
52
37
|
|
|
53
38
|
try {
|
|
54
|
-
const
|
|
55
|
-
const query = fp ? `?path=${encodeURIComponent(fp)}` : "";
|
|
56
|
-
const response = await fetch(`/api/settings${query}`, {
|
|
39
|
+
const response = await fetch("/api/settings", {
|
|
57
40
|
method: "PUT",
|
|
58
41
|
headers: { "Content-Type": "application/json" },
|
|
59
42
|
body: JSON.stringify({ fontFamily: font }),
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useCallback, useEffect,
|
|
1
|
+
import { useCallback, useEffect, useState } from "react";
|
|
2
2
|
import { toast } from "sonner";
|
|
3
3
|
import {
|
|
4
4
|
resolveShortcuts,
|
|
@@ -14,24 +14,14 @@ interface UseKeybindingsResult {
|
|
|
14
14
|
isLoading: boolean;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
export function useKeybindings(
|
|
17
|
+
export function useKeybindings(): UseKeybindingsResult {
|
|
18
18
|
const [overrides, setOverrides] = useState<KeybindingOverride[]>([]);
|
|
19
19
|
const [isLoading, setIsLoading] = useState(true);
|
|
20
20
|
|
|
21
|
-
const filePathRef = useRef(filePath);
|
|
22
|
-
filePathRef.current = filePath;
|
|
23
|
-
|
|
24
|
-
// Fetch keybindings from settings on mount
|
|
25
21
|
useEffect(() => {
|
|
26
|
-
if (!filePath) {
|
|
27
|
-
setIsLoading(false);
|
|
28
|
-
return;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
22
|
const fetchKeybindings = async () => {
|
|
32
23
|
try {
|
|
33
|
-
const
|
|
34
|
-
const response = await fetch(`/api/settings${query}`);
|
|
24
|
+
const response = await fetch("/api/settings");
|
|
35
25
|
if (response.ok) {
|
|
36
26
|
const settings = await response.json();
|
|
37
27
|
setOverrides(settings.keybindings ?? []);
|
|
@@ -44,20 +34,18 @@ export function useKeybindings(filePath: string | null): UseKeybindingsResult {
|
|
|
44
34
|
};
|
|
45
35
|
|
|
46
36
|
fetchKeybindings();
|
|
47
|
-
}, [
|
|
37
|
+
}, []);
|
|
48
38
|
|
|
49
39
|
const persistOverrides = useCallback(
|
|
50
40
|
async (newOverrides: KeybindingOverride[]) => {
|
|
51
41
|
try {
|
|
52
|
-
const
|
|
53
|
-
const query = fp ? `?path=${encodeURIComponent(fp)}` : "";
|
|
54
|
-
const response = await fetch(`/api/settings${query}`);
|
|
42
|
+
const response = await fetch("/api/settings");
|
|
55
43
|
if (!response.ok) return;
|
|
56
44
|
|
|
57
45
|
const currentSettings = await response.json();
|
|
58
46
|
const updated = { ...currentSettings, keybindings: newOverrides };
|
|
59
47
|
|
|
60
|
-
const putResponse = await fetch(
|
|
48
|
+
const putResponse = await fetch("/api/settings", {
|
|
61
49
|
method: "PUT",
|
|
62
50
|
headers: { "Content-Type": "application/json" },
|
|
63
51
|
body: JSON.stringify(updated),
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { useCallback, useState } from "react";
|
|
2
|
+
import { type Locale, Locales } from "../lib/i18n";
|
|
3
|
+
|
|
4
|
+
const STORAGE_KEY = "readit:locale";
|
|
5
|
+
|
|
6
|
+
function detectLocale(): Locale {
|
|
7
|
+
const browserLang = navigator.language.slice(0, 2).toLowerCase();
|
|
8
|
+
if (browserLang === "ja") return Locales.JA;
|
|
9
|
+
return Locales.EN;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function getStoredLocale(): Locale {
|
|
13
|
+
try {
|
|
14
|
+
const stored = localStorage.getItem(STORAGE_KEY);
|
|
15
|
+
if (stored === Locales.JA || stored === Locales.EN) {
|
|
16
|
+
return stored;
|
|
17
|
+
}
|
|
18
|
+
} catch {
|
|
19
|
+
// localStorage may be unavailable
|
|
20
|
+
}
|
|
21
|
+
return detectLocale();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface UseLocalePreferenceResult {
|
|
25
|
+
locale: Locale;
|
|
26
|
+
setLocale: (locale: Locale) => void;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function useLocalePreference(): UseLocalePreferenceResult {
|
|
30
|
+
const [locale, setLocaleState] = useState<Locale>(getStoredLocale);
|
|
31
|
+
|
|
32
|
+
const setLocale = useCallback((newLocale: Locale) => {
|
|
33
|
+
setLocaleState(newLocale);
|
|
34
|
+
try {
|
|
35
|
+
localStorage.setItem(STORAGE_KEY, newLocale);
|
|
36
|
+
} catch {
|
|
37
|
+
// localStorage may be unavailable
|
|
38
|
+
}
|
|
39
|
+
}, []);
|
|
40
|
+
|
|
41
|
+
return { locale, setLocale };
|
|
42
|
+
}
|