From 035ecbb316bf8e021f874ca26c34765786a7487f Mon Sep 17 00:00:00 2001 From: AHpx Date: Sun, 19 Oct 2025 13:26:52 +0800 Subject: [PATCH 01/11] =?UTF-8?q?chore:=20=E6=96=B0=E5=A2=9E=20gitignore?= =?UTF-8?q?=20=E8=A7=84=E5=88=99=E5=BF=BD=E7=95=A5=20serena=20mcp=E7=94=9F?= =?UTF-8?q?=E6=88=90=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 0ab7464..87581f9 100644 --- a/.gitignore +++ b/.gitignore @@ -35,4 +35,6 @@ target/ # Tauri autogenerated files packages/app/src-tauri/plugins/*/permissions/autogenerated/ -packages/app/src-tauri/plugins/*/permissions/schemas/ \ No newline at end of file +packages/app/src-tauri/plugins/*/permissions/schemas/ + +.serena \ No newline at end of file From 321e176ab057e762a8ead08a3404dc5f919cbc07 Mon Sep 17 00:00:00 2001 From: AHpx Date: Sun, 19 Oct 2025 13:59:42 +0800 Subject: [PATCH 02/11] =?UTF-8?q?feat:=20=E8=87=AA=E5=8A=A8=E6=90=9C?= =?UTF-8?q?=E7=B4=A2=E7=B3=BB=E7=BB=9F=E5=B7=B2=E7=BB=8F=E5=AE=89=E8=A3=85?= =?UTF-8?q?=E7=9A=84=E5=AD=97=E4=BD=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/app/src-tauri/Cargo.lock | 27 ++ packages/app/src-tauri/Cargo.toml | 1 + .../app/src-tauri/src/core/fonts/commands.rs | 57 ++++ packages/app/src-tauri/src/lib.rs | 3 +- .../src/components/settings/font-manager.tsx | 292 +++++++++++++++--- .../reader/components/settings-dropdown.tsx | 19 +- packages/app/src/services/constants.ts | 1 + packages/app/src/services/font-service.ts | 16 + packages/app/src/store/font-store.ts | 18 +- packages/app/src/types/settings.ts | 1 + packages/app/src/utils/font.ts | 50 +++ packages/app/vite.config.ts | 16 +- 12 files changed, 439 insertions(+), 62 deletions(-) diff --git a/packages/app/src-tauri/Cargo.lock b/packages/app/src-tauri/Cargo.lock index dd41d73..f4c6ed5 100644 --- a/packages/app/src-tauri/Cargo.lock +++ b/packages/app/src-tauri/Cargo.lock @@ -7,6 +7,7 @@ name = "SageRead" version = "0.1.0" dependencies = [ "chrono", + "fontdb", "futures-util", "jan-utils", "log", @@ -1694,6 +1695,17 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[package]] +name = "fontdb" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "652aa0009b1406e40114685c96ea2c01069e1c035ad6c340999fa08213fad4c5" +dependencies = [ + "log", + "memmap2", + "ttf-parser", +] + [[package]] name = "foreign-types" version = "0.3.2" @@ -3325,6 +3337,15 @@ version = "2.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" +[[package]] +name = "memmap2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83faa42c0a078c393f6b29d5db232d8be22776a891f8f56e5284faee4a20b327" +dependencies = [ + "libc", +] + [[package]] name = "memoffset" version = "0.9.1" @@ -7061,6 +7082,12 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "ttf-parser" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0609f771ad9c6155384897e1df4d948e692667cc0588548b68eb44d052b27633" + [[package]] name = "tungstenite" version = "0.26.2" diff --git a/packages/app/src-tauri/Cargo.toml b/packages/app/src-tauri/Cargo.toml index 5de4382..6e34518 100644 --- a/packages/app/src-tauri/Cargo.toml +++ b/packages/app/src-tauri/Cargo.toml @@ -40,6 +40,7 @@ futures-util = "0.3" tauri-plugin-os = "2" tauri-plugin-shell = "2" tauri-plugin-updater = "2" +fontdb = "0.11" [target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies] tauri-plugin-global-shortcut = "2" diff --git a/packages/app/src-tauri/src/core/fonts/commands.rs b/packages/app/src-tauri/src/core/fonts/commands.rs index 2487a6b..4aeda81 100644 --- a/packages/app/src-tauri/src/core/fonts/commands.rs +++ b/packages/app/src-tauri/src/core/fonts/commands.rs @@ -1,9 +1,17 @@ use serde::{Deserialize, Serialize}; +use std::collections::BTreeMap; use std::fs; use std::path::PathBuf; use tauri::{AppHandle, Emitter, Manager}; use tauri_plugin_shell::ShellExt; +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct SystemFontInfo { + pub family: String, + pub is_monospace: bool, + pub sources: Vec, +} + #[derive(Debug, Serialize, Deserialize, Clone)] pub struct FontInfo { pub name: String, @@ -177,3 +185,52 @@ pub async fn upload_font_data( Ok(result) } + +fn map_font_source(source: &fontdb::Source) -> Option { + #[allow(unreachable_patterns)] + match source { + fontdb::Source::File(path) => Some(path.to_string_lossy().to_string()), + fontdb::Source::SharedFile(path, _) => Some(path.to_string_lossy().to_string()), + _ => None, + } +} + +#[tauri::command] +pub async fn list_system_fonts() -> Result, String> { + tauri::async_runtime::spawn_blocking(|| { + let mut db = fontdb::Database::new(); + db.load_system_fonts(); + + let mut families: BTreeMap = BTreeMap::new(); + + for face in db.faces() { + let is_monospace = face.monospaced; + let source_path = map_font_source(&face.source); + + for (name, _) in &face.families { + let entry = families.entry(name.to_string()).or_insert_with(|| SystemFontInfo { + family: name.to_string(), + is_monospace, + sources: Vec::new(), + }); + + if is_monospace { + entry.is_monospace = true; + } + + if let Some(path) = &source_path { + if !entry.sources.contains(path) { + entry.sources.push(path.clone()); + } + } + } + } + + let mut fonts: Vec = families.into_values().collect(); + fonts.sort_by(|a, b| a.family.to_lowercase().cmp(&b.family.to_lowercase())); + + Ok(fonts) + }) + .await + .map_err(|e| format!("获取系统字体失败: {e}"))? +} diff --git a/packages/app/src-tauri/src/lib.rs b/packages/app/src-tauri/src/lib.rs index e5d23b4..b722ce6 100644 --- a/packages/app/src-tauri/src/lib.rs +++ b/packages/app/src-tauri/src/lib.rs @@ -24,7 +24,7 @@ use crate::core::{ update_reading_session, }, database, - fonts::commands::{upload_and_convert_font, upload_font_data}, + fonts::commands::{list_system_fonts, upload_and_convert_font, upload_font_data}, llama::commands::{ delete_local_model, download_llama_server, download_model_file, ensure_llamacpp_directories, get_app_data_dir, get_llamacpp_backend_path, greet, @@ -165,6 +165,7 @@ pub fn run() { // fonts upload_and_convert_font, upload_font_data, + list_system_fonts, // llama greet, get_app_data_dir, diff --git a/packages/app/src/components/settings/font-manager.tsx b/packages/app/src/components/settings/font-manager.tsx index f073b7b..832cbaf 100644 --- a/packages/app/src/components/settings/font-manager.tsx +++ b/packages/app/src/components/settings/font-manager.tsx @@ -1,15 +1,48 @@ import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; +import { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectLabel, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; import { useFontUpload } from "@/hooks/use-font-upload"; -import { type FontInfo, deleteFont, downloadFont, setFontMetadata } from "@/services/font-service"; +import { + type FontInfo, + type SystemFontInfo, + deleteFont, + downloadFont, + setFontMetadata, +} from "@/services/font-service"; +import { DEFAULT_BOOK_FONT } from "@/services/constants"; +import { useAppSettingsStore } from "@/store/app-settings-store"; import { useFontStore } from "@/store/font-store"; +import { applyUiFont } from "@/utils/font"; import clsx from "clsx"; import { Loader2, SquarePen, Trash2, Upload } from "lucide-react"; -import { useEffect, useState } from "react"; +import { useEffect, useMemo, useState } from "react"; import { toast } from "sonner"; +const SYSTEM_DEFAULT_VALUE = "__system__"; + +const formatPreviewStyle = (family: string) => ({ + fontFamily: family, +}); + +interface FontOption { + id: string; + label: string; + family: string; + source: "system" | "custom"; + info?: SystemFontInfo | FontInfo; +} + export default function FontManager() { - const { fonts, isLoading, loadFonts, refreshFonts } = useFontStore(); + const { settings, setSettings } = useAppSettingsStore(); + const { fonts, systemFonts, isLoading, isSystemLoading, loadFonts, refreshFonts, loadSystemFonts } = useFontStore(); const [downloadUrl, setDownloadUrl] = useState(""); const [isDownloading, setIsDownloading] = useState(false); const [editingFont, setEditingFont] = useState(null); @@ -17,7 +50,8 @@ export default function FontManager() { useEffect(() => { loadFonts(); - }, [loadFonts]); + loadSystemFonts(); + }, [loadFonts, loadSystemFonts]); const { isDragOver, isUploading, handleDragOver, handleDragLeave, handleDrop, triggerFileSelect } = useFontUpload(refreshFonts); @@ -78,60 +112,220 @@ export default function FontManager() { setEditDisplayName(""); }; + const systemFontOptions = useMemo( + () => + systemFonts.map((font) => ({ + id: `system-${font.family}`, + label: font.family, + family: font.family, + source: "system" as const, + info: font, + })), + [systemFonts], + ); + + const customFontOptions = useMemo( + () => + fonts.map((font) => { + const family = font.fontFamily || font.name; + return { + id: `custom-${font.filename}`, + label: font.displayName || font.name, + family, + source: "custom" as const, + info: font, + }; + }), + [fonts], + ); + + const readerFontOptions = useMemo( + () => [...systemFontOptions, ...customFontOptions], + [systemFontOptions, customFontOptions], + ); + + const uiFontValue = settings.uiFontFamily?.trim() || SYSTEM_DEFAULT_VALUE; + + const currentReaderFont = settings.globalViewSettings?.serifFont?.trim(); + const readerFontValue = + readerFontOptions.find((opt) => opt.family === currentReaderFont)?.family || SYSTEM_DEFAULT_VALUE; + + const updateSettings = (updater: (current: typeof settings) => typeof settings) => { + const { settings: currentSettings } = useAppSettingsStore.getState(); + const updated = updater(currentSettings); + setSettings(updated); + return updated; + }; + + const handleUiFontChange = (value: string) => { + const selectedFont = value === SYSTEM_DEFAULT_VALUE ? "" : value; + updateSettings((current) => ({ + ...current, + uiFontFamily: selectedFont, + })); + applyUiFont(selectedFont || undefined); + }; + + const handleReaderFontChange = (value: string) => { + const targetFont = + value === SYSTEM_DEFAULT_VALUE + ? DEFAULT_BOOK_FONT.serifFont + : readerFontOptions.find((opt) => opt.family === value)?.family || DEFAULT_BOOK_FONT.serifFont; + + const updated = updateSettings((current) => ({ + ...current, + globalViewSettings: { + ...current.globalViewSettings, + serifFont: targetFont, + sansSerifFont: targetFont, + defaultCJKFont: targetFont, + }, + })); + + if (updated.globalViewSettings.overrideFont) { + toast.success(`阅读字体已切换为 ${targetFont}`); + } + }; + return (
-
+

字体管理

-
+
+

应用字体

+

选择界面和系统使用的字体

+
+ {isSystemLoading ? ( +
+ +
+ ) : ( + )} - onDragOver={handleDragOver} - onDragLeave={handleDragLeave} - onDrop={handleDrop} - > - -

拖拽 .woff2 或 .ttf 字体文件到此处

-
-
- setDownloadUrl(e.target.value)} - onKeyDown={(e) => { - if (e.key === "Enter") { - handleDownload(); - } - }} - /> - +
+
+

阅读默认字体

+

设置阅读器使用的默认字体

+
+ {isSystemLoading && readerFontOptions.length === 0 ? ( +
+ +
+ ) : ( + + )} +
+ +
+
+

安装自定义字体

+

拖拽或粘贴字体文件,支持 .woff2 / .ttf

+
+
+ +

拖拽 .woff2 或 .ttf 字体文件到此处

+ +
+ +
+ setDownloadUrl(e.target.value)} + onKeyDown={(e) => { + if (e.key === "Enter") { + handleDownload(); + } + }} + /> + +

已安装的字体

{isLoading ? ( -
+
) : fonts.length === 0 ? ( -
+
暂无已安装的字体
) : ( -
+
{fonts.map((font) => ( -
+
{editingFont === font.filename ? (
) : ( <> -
-

{font.name}

+
+

{font.displayName || font.name}

{font.fontFamily ? `Font Family: ${font.fontFamily}` : ""}

-
-
+
-
+
+
)} diff --git a/packages/app/src/pages/reader/components/settings-dropdown.tsx b/packages/app/src/pages/reader/components/settings-dropdown.tsx index 518fccb..d7d59dd 100644 --- a/packages/app/src/pages/reader/components/settings-dropdown.tsx +++ b/packages/app/src/pages/reader/components/settings-dropdown.tsx @@ -24,7 +24,7 @@ const SettingsDropdown = () => { const { settings, setSettings } = useAppSettingsStore(); const openDropdown = useReaderStore((state) => state.openDropdown); const setOpenDropdown = useReaderStore((state) => state.setOpenDropdown)!; - const { fonts: customFontList, loadFonts } = useFontStore(); + const { fonts: customFontList, systemFonts, loadFonts, loadSystemFonts } = useFontStore(); const globalViewSettings = settings.globalViewSettings; const view = store.getState().view; @@ -45,11 +45,24 @@ const SettingsDropdown = () => { [customFontList], ); - const allFonts = useMemo(() => [...CURATED_FONTS, ...customFonts], [customFonts]); + const systemFontOptions = useMemo( + () => + systemFonts.map((font) => ({ + id: `system-${font.family}`, + name: font.family, + serif: font.family, + sansSerif: font.family, + cjk: font.family, + })), + [systemFonts], + ); + + const allFonts = useMemo(() => [...CURATED_FONTS, ...systemFontOptions, ...customFonts], [customFonts, systemFontOptions]); useEffect(() => { loadFonts(); - }, [loadFonts]); + loadSystemFonts(); + }, [loadFonts, loadSystemFonts]); useEffect(() => { const currentFontExists = allFonts.some( diff --git a/packages/app/src/services/constants.ts b/packages/app/src/services/constants.ts index 4483118..45e4183 100644 --- a/packages/app/src/services/constants.ts +++ b/packages/app/src/services/constants.ts @@ -47,6 +47,7 @@ export const DEFAULT_SYSTEM_SETTINGS: Partial = { lastSyncedAtBooks: 0, lastSyncedAtConfigs: 0, lastSyncedAtNotes: 0, + uiFontFamily: "", }; export const DEFAULT_READSETTINGS: ReadSettings = { diff --git a/packages/app/src/services/font-service.ts b/packages/app/src/services/font-service.ts index 36604a5..c95fc3d 100644 --- a/packages/app/src/services/font-service.ts +++ b/packages/app/src/services/font-service.ts @@ -22,6 +22,12 @@ export interface FontMetadata { type FontMetadataMap = Record; +export interface SystemFontInfo { + family: string; + isMonospace: boolean; + sources: string[]; +} + const FONTS_DIR = "fonts"; const FONTS_META_FILE = "fonts-meta.json"; @@ -250,3 +256,13 @@ export async function uploadFontData(file: File): Promise { throw error; } } + +export async function listSystemFonts(): Promise { + try { + const fonts = await invoke("list_system_fonts"); + return fonts; + } catch (error) { + console.error("[FontService] Failed to list system fonts:", error); + return []; + } +} diff --git a/packages/app/src/store/font-store.ts b/packages/app/src/store/font-store.ts index ad56666..d11a903 100644 --- a/packages/app/src/store/font-store.ts +++ b/packages/app/src/store/font-store.ts @@ -1,16 +1,21 @@ -import { type FontInfo, listFonts } from "@/services/font-service"; +import { type FontInfo, type SystemFontInfo, listFonts, listSystemFonts } from "@/services/font-service"; import { create } from "zustand"; interface FontStoreState { fonts: FontInfo[]; + systemFonts: SystemFontInfo[]; isLoading: boolean; + isSystemLoading: boolean; loadFonts: () => Promise; refreshFonts: () => Promise; + loadSystemFonts: () => Promise; } export const useFontStore = create((set) => ({ fonts: [], + systemFonts: [], isLoading: false, + isSystemLoading: false, loadFonts: async () => { set({ isLoading: true }); @@ -27,4 +32,15 @@ export const useFontStore = create((set) => ({ const fontList = await listFonts(); set({ fonts: fontList }); }, + + loadSystemFonts: async () => { + set({ isSystemLoading: true }); + try { + const systemFonts = await listSystemFonts(); + set({ systemFonts, isSystemLoading: false }); + } catch (error) { + console.error("[FontStore] Failed to load system fonts:", error); + set({ systemFonts: [], isSystemLoading: false }); + } + }, })); diff --git a/packages/app/src/types/settings.ts b/packages/app/src/types/settings.ts index 80592b6..0367045 100644 --- a/packages/app/src/types/settings.ts +++ b/packages/app/src/types/settings.ts @@ -45,4 +45,5 @@ export interface SystemSettings { globalReadSettings: ReadSettings; globalViewSettings: ViewSettings; + uiFontFamily?: string; } diff --git a/packages/app/src/utils/font.ts b/packages/app/src/utils/font.ts index 93267d6..9173c35 100644 --- a/packages/app/src/utils/font.ts +++ b/packages/app/src/utils/font.ts @@ -1,9 +1,57 @@ import { listFonts } from "@/services/font-service"; +import { useAppSettingsStore } from "@/store/app-settings-store"; import { convertFileSrc } from "@tauri-apps/api/core"; import { appDataDir, resourceDir } from "@tauri-apps/api/path"; import { isCJKEnv } from "./misc"; let cachedBuiltInFontUrl: string | null = null; +const SYSTEM_FONT_FALLBACK = + 'system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji"'; +const DEFAULT_SERIF_FALLBACK = + '"Geist", "Geist Fallback", ui-serif, Georgia, Cambria, "Times New Roman", Times, serif'; + +const quoteIfNeeded = (font: string) => { + if (!font) return font; + return /\s/.test(font) && !/^".*"$/.test(font) ? `"${font}"` : font; +}; + +export const applyUiFont = (fontFamily?: string | null) => { + if (typeof document === "undefined") return; + const root = document.documentElement; + const sanitized = fontFamily?.trim(); + + if (!sanitized) { + root.style.removeProperty("--font-sans"); + root.style.removeProperty("--font-serif"); + return; + } + + const cssFont = quoteIfNeeded(sanitized); + const computedStyle = getComputedStyle(root); + const fallbackSans = computedStyle.getPropertyValue("--font-sans").trim() || SYSTEM_FONT_FALLBACK; + const fallbackSerif = computedStyle.getPropertyValue("--font-serif").trim() || DEFAULT_SERIF_FALLBACK; + + root.style.setProperty("--font-sans", `${cssFont}, ${fallbackSans}`); + root.style.setProperty("--font-serif", `${cssFont}, ${fallbackSerif}`); +}; + +export const applyUiFontFromSettings = () => { + try { + const { settings } = useAppSettingsStore.getState(); + applyUiFont(settings.uiFontFamily); + } catch (error) { + console.error("[Font] Failed to apply UI font from settings:", error); + } +}; + +if (typeof window !== "undefined") { + useAppSettingsStore.subscribe( + (state) => state.settings.uiFontFamily, + (fontFamily) => { + applyUiFont(fontFamily); + }, + ); +} const getBuiltInFontUrl = async (): Promise => { if (cachedBuiltInFontUrl) { @@ -110,6 +158,8 @@ export const mountFontsToMainApp = async () => { style.textContent = builtInFontFaces; document.head.appendChild(style); } + + applyUiFontFromSettings(); } catch (error) { console.error("[Font] Failed to load fonts to main app:", error); } diff --git a/packages/app/vite.config.ts b/packages/app/vite.config.ts index 42ed285..89aacaf 100644 --- a/packages/app/vite.config.ts +++ b/packages/app/vite.config.ts @@ -3,7 +3,7 @@ import tailwindcss from "@tailwindcss/vite"; import react from "@vitejs/plugin-react"; import { defineConfig } from "vite"; -const host = process.env.TAURI_DEV_HOST; +const host = process.env.TAURI_DEV_HOST || "127.0.0.1"; // https://vite.dev/config/ export default defineConfig(async () => ({ @@ -24,14 +24,12 @@ export default defineConfig(async () => ({ server: { port: 1420, strictPort: true, - host: host || false, - hmr: host - ? { - protocol: "ws", - host, - port: 1421, - } - : undefined, + host, + hmr: { + protocol: "ws", + host, + port: 1421, + }, watch: { // 3. tell Vite to ignore watching `src-tauri` ignored: ["**/src-tauri/**"], From e5b55c8842dc8e805c76375e1749e90439145ff2 Mon Sep 17 00:00:00 2001 From: AHpx Date: Sun, 19 Oct 2025 14:07:03 +0800 Subject: [PATCH 03/11] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E5=AD=97?= =?UTF-8?q?=E4=BD=93=E6=97=A0=E6=B3=95=E6=AD=A3=E5=B8=B8=E5=BA=94=E7=94=A8?= =?UTF-8?q?=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/settings/font-manager.tsx | 94 +++++++++++++++---- packages/app/src/index.css | 4 +- 2 files changed, 79 insertions(+), 19 deletions(-) diff --git a/packages/app/src/components/settings/font-manager.tsx b/packages/app/src/components/settings/font-manager.tsx index 832cbaf..39ee6dc 100644 --- a/packages/app/src/components/settings/font-manager.tsx +++ b/packages/app/src/components/settings/font-manager.tsx @@ -6,6 +6,7 @@ import { SelectGroup, SelectItem, SelectLabel, + SelectSeparator, SelectTrigger, SelectValue, } from "@/components/ui/select"; @@ -47,6 +48,8 @@ export default function FontManager() { const [isDownloading, setIsDownloading] = useState(false); const [editingFont, setEditingFont] = useState(null); const [editDisplayName, setEditDisplayName] = useState(""); + const [uiFontQuery, setUiFontQuery] = useState(""); + const [readerFontQuery, setReaderFontQuery] = useState(""); useEffect(() => { loadFonts(); @@ -144,6 +147,24 @@ export default function FontManager() { [systemFontOptions, customFontOptions], ); + const filteredSystemFonts = useMemo(() => { + if (!uiFontQuery.trim()) return systemFontOptions; + const lowered = uiFontQuery.trim().toLowerCase(); + return systemFontOptions.filter((font) => font.label.toLowerCase().includes(lowered)); + }, [systemFontOptions, uiFontQuery]); + + const readerFilteredSystemFonts = useMemo(() => { + if (!readerFontQuery.trim()) return systemFontOptions; + const lowered = readerFontQuery.trim().toLowerCase(); + return systemFontOptions.filter((font) => font.label.toLowerCase().includes(lowered)); + }, [systemFontOptions, readerFontQuery]); + + const readerFilteredCustomFonts = useMemo(() => { + if (!readerFontQuery.trim()) return customFontOptions; + const lowered = readerFontQuery.trim().toLowerCase(); + return customFontOptions.filter((font) => font.label.toLowerCase().includes(lowered)); + }, [customFontOptions, readerFontQuery]); + const uiFontValue = settings.uiFontFamily?.trim() || SYSTEM_DEFAULT_VALUE; const currentReaderFont = settings.globalViewSettings?.serifFont?.trim(); @@ -159,6 +180,7 @@ export default function FontManager() { const handleUiFontChange = (value: string) => { const selectedFont = value === SYSTEM_DEFAULT_VALUE ? "" : value; + setUiFontQuery(""); updateSettings((current) => ({ ...current, uiFontFamily: selectedFont, @@ -171,6 +193,7 @@ export default function FontManager() { value === SYSTEM_DEFAULT_VALUE ? DEFAULT_BOOK_FONT.serifFont : readerFontOptions.find((opt) => opt.family === value)?.family || DEFAULT_BOOK_FONT.serifFont; + setReaderFontQuery(""); const updated = updateSettings((current) => ({ ...current, @@ -178,6 +201,7 @@ export default function FontManager() { ...current.globalViewSettings, serifFont: targetFont, sansSerifFont: targetFont, + monospaceFont: targetFont, defaultCJKFont: targetFont, }, })); @@ -209,15 +233,29 @@ export default function FontManager() { +
+ setUiFontQuery(event.target.value)} + placeholder="搜索字体..." + className="h-8" + onKeyDown={(event) => event.stopPropagation()} + onPointerDown={(event) => event.stopPropagation()} + /> +
系统默认 - {systemFontOptions.map((font) => ( - - - {font.label} - - - ))} + {filteredSystemFonts.length > 0 ? ( + filteredSystemFonts.map((font) => ( + + + {font.label} + + + )) + ) : ( +
未找到匹配字体
+ )}
@@ -241,22 +279,41 @@ export default function FontManager() { +
+ setReaderFontQuery(event.target.value)} + placeholder="搜索字体..." + className="h-8" + onKeyDown={(event) => event.stopPropagation()} + onPointerDown={(event) => event.stopPropagation()} + /> +
系统默认 - 系统字体 - {systemFontOptions.map((font) => ( - - - {font.label} - - - ))} - {customFontOptions.length > 0 ? ( + {readerFilteredSystemFonts.length > 0 ? ( + <> + + 系统字体 + + {readerFilteredSystemFonts.map((font) => ( + + + {font.label} + + + ))} + + ) : null} + {readerFilteredCustomFonts.length > 0 && readerFilteredSystemFonts.length > 0 ? ( + + ) : null} + {readerFilteredCustomFonts.length > 0 ? ( <> 已安装字体 - {customFontOptions.map((font) => ( + {readerFilteredCustomFonts.map((font) => ( {font.label} @@ -265,6 +322,9 @@ export default function FontManager() { ))} ) : null} + {readerFilteredSystemFonts.length === 0 && readerFilteredCustomFonts.length === 0 ? ( +
未找到匹配字体
+ ) : null}
diff --git a/packages/app/src/index.css b/packages/app/src/index.css index 98dbf53..9516a04 100644 --- a/packages/app/src/index.css +++ b/packages/app/src/index.css @@ -124,7 +124,7 @@ foliate-view { -webkit-text-size-adjust: 100%; font-feature-settings: normal; -webkit-tap-highlight-color: transparent; - font-family: system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji"; + font-family: var(--font-sans); font-variation-settings: normal; } @@ -340,4 +340,4 @@ foliate-view { 0 0 3px 0 rgba(0, 0, 0, 0.10), 1px 1px 3px 0 rgba(0, 0, 0, 0.15); } -} \ No newline at end of file +} From 14e2b2439a2e3b2c78de5f52d9787c0bdd3fd3d4 Mon Sep 17 00:00:00 2001 From: AHpx Date: Sun, 19 Oct 2025 14:19:18 +0800 Subject: [PATCH 04/11] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0=E5=AF=B9?= =?UTF-8?q?=E5=AD=97=E4=BD=93=E7=B2=97=E7=BB=86=E3=80=81=E5=A4=A7=E5=B0=8F?= =?UTF-8?q?=E7=9A=84=E8=AE=BE=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/settings/font-manager.tsx | 154 +++++++++++++++++- packages/app/src/index.css | 2 + packages/app/src/services/constants.ts | 2 + packages/app/src/themes/default.css | 3 +- packages/app/src/types/settings.ts | 2 + packages/app/src/utils/font.ts | 56 +++++-- 6 files changed, 204 insertions(+), 15 deletions(-) diff --git a/packages/app/src/components/settings/font-manager.tsx b/packages/app/src/components/settings/font-manager.tsx index 39ee6dc..e1063a8 100644 --- a/packages/app/src/components/settings/font-manager.tsx +++ b/packages/app/src/components/settings/font-manager.tsx @@ -28,6 +28,18 @@ import { useEffect, useMemo, useState } from "react"; import { toast } from "sonner"; const SYSTEM_DEFAULT_VALUE = "__system__"; +const FONT_WEIGHT_OPTIONS = [300, 400, 500, 600, 700]; +const FONT_WEIGHT_LABELS: Record = { + 300: "Light", + 400: "Regular", + 500: "Medium", + 600: "SemiBold", + 700: "Bold", +}; +const UI_FONT_SIZE_OPTIONS = [12, 14, 16, 18, 20]; +const READER_FONT_SIZE_OPTIONS = [12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 32]; + +const getWeightLabel = (weight: number) => FONT_WEIGHT_LABELS[weight] ?? String(weight); const formatPreviewStyle = (family: string) => ({ fontFamily: family, @@ -166,10 +178,32 @@ export default function FontManager() { }, [customFontOptions, readerFontQuery]); const uiFontValue = settings.uiFontFamily?.trim() || SYSTEM_DEFAULT_VALUE; + const uiFontSize = settings.uiFontSize ?? 14; + const uiFontWeight = settings.uiFontWeight ?? 400; const currentReaderFont = settings.globalViewSettings?.serifFont?.trim(); const readerFontValue = readerFontOptions.find((opt) => opt.family === currentReaderFont)?.family || SYSTEM_DEFAULT_VALUE; + const readerFontSize = settings.globalViewSettings?.defaultFontSize ?? DEFAULT_BOOK_FONT.defaultFontSize; + const readerFontWeight = settings.globalViewSettings?.fontWeight ?? DEFAULT_BOOK_FONT.fontWeight; + const effectiveUiFontSizes = useMemo(() => { + if (UI_FONT_SIZE_OPTIONS.includes(uiFontSize)) { + return UI_FONT_SIZE_OPTIONS; + } + return Array.from(new Set([...UI_FONT_SIZE_OPTIONS, uiFontSize])).sort((a, b) => a - b); + }, [uiFontSize]); + const effectiveReaderFontSizes = useMemo(() => { + if (READER_FONT_SIZE_OPTIONS.includes(readerFontSize)) { + return READER_FONT_SIZE_OPTIONS; + } + return Array.from(new Set([...READER_FONT_SIZE_OPTIONS, readerFontSize])).sort((a, b) => a - b); + }, [readerFontSize]); + const effectiveFontWeights = useMemo(() => { + const weights = new Set(FONT_WEIGHT_OPTIONS); + weights.add(uiFontWeight); + weights.add(readerFontWeight); + return Array.from(weights).sort((a, b) => a - b); + }, [uiFontWeight, readerFontWeight]); const updateSettings = (updater: (current: typeof settings) => typeof settings) => { const { settings: currentSettings } = useAppSettingsStore.getState(); @@ -181,11 +215,11 @@ export default function FontManager() { const handleUiFontChange = (value: string) => { const selectedFont = value === SYSTEM_DEFAULT_VALUE ? "" : value; setUiFontQuery(""); - updateSettings((current) => ({ + const updated = updateSettings((current) => ({ ...current, uiFontFamily: selectedFont, })); - applyUiFont(selectedFont || undefined); + applyUiFont(updated.uiFontFamily, updated.uiFontSize, updated.uiFontWeight); }; const handleReaderFontChange = (value: string) => { @@ -211,6 +245,50 @@ export default function FontManager() { } }; + const handleUiFontSizeSelect = (value: string) => { + const numeric = Number.parseInt(value, 10); + if (Number.isNaN(numeric)) return; + const updated = updateSettings((current) => ({ + ...current, + uiFontSize: numeric, + })); + applyUiFont(updated.uiFontFamily, updated.uiFontSize, updated.uiFontWeight); + }; + + const handleUiFontWeightSelect = (value: string) => { + const numeric = Number.parseInt(value, 10); + if (Number.isNaN(numeric)) return; + const updated = updateSettings((current) => ({ + ...current, + uiFontWeight: numeric, + })); + applyUiFont(updated.uiFontFamily, updated.uiFontSize, updated.uiFontWeight); + }; + + const handleReaderFontSizeSelect = (value: string) => { + const numeric = Number.parseInt(value, 10); + if (Number.isNaN(numeric)) return; + updateSettings((current) => ({ + ...current, + globalViewSettings: { + ...current.globalViewSettings, + defaultFontSize: numeric, + }, + })); + }; + + const handleReaderFontWeightSelect = (value: string) => { + const numeric = Number.parseInt(value, 10); + if (Number.isNaN(numeric)) return; + updateSettings((current) => ({ + ...current, + globalViewSettings: { + ...current.globalViewSettings, + fontWeight: numeric, + }, + })); + }; + return (
@@ -260,6 +338,42 @@ export default function FontManager() { )} +
+
+

字体大小

+ +
+
+

字体粗细

+ +
+
@@ -329,6 +443,42 @@ export default function FontManager() { )} +
+
+

字体大小

+ +
+
+

字体粗细

+ +
+
diff --git a/packages/app/src/index.css b/packages/app/src/index.css index 9516a04..f74c623 100644 --- a/packages/app/src/index.css +++ b/packages/app/src/index.css @@ -125,6 +125,8 @@ foliate-view { font-feature-settings: normal; -webkit-tap-highlight-color: transparent; font-family: var(--font-sans); + font-size: var(--app-font-size, 14px); + font-weight: var(--app-font-weight, 400); font-variation-settings: normal; } diff --git a/packages/app/src/services/constants.ts b/packages/app/src/services/constants.ts index 45e4183..9fe966d 100644 --- a/packages/app/src/services/constants.ts +++ b/packages/app/src/services/constants.ts @@ -48,6 +48,8 @@ export const DEFAULT_SYSTEM_SETTINGS: Partial = { lastSyncedAtConfigs: 0, lastSyncedAtNotes: 0, uiFontFamily: "", + uiFontSize: 14, + uiFontWeight: 400, }; export const DEFAULT_READSETTINGS: ReadSettings = { diff --git a/packages/app/src/themes/default.css b/packages/app/src/themes/default.css index ca022d9..c6d41d8 100644 --- a/packages/app/src/themes/default.css +++ b/packages/app/src/themes/default.css @@ -34,6 +34,8 @@ --font-sans: 'Geist', 'Geist Fallback', ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'; --font-serif: "Geist", "Geist Fallback", ui-serif, Georgia, Cambria, "Times New Roman", Times, serif; --font-mono: "Geist Mono", "Geist Mono Fallback", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; + --app-font-size: 14px; + --app-font-weight: 400; --radius: 0.625rem; @@ -90,4 +92,3 @@ --shadow-xl: 0 1px 3px 0px oklch(0.00 0 0 / 0.10), 0 8px 10px -1px oklch(0.00 0 0 / 0.10); --shadow-2xl: 0 1px 3px 0px oklch(0.00 0 0 / 0.25); } - diff --git a/packages/app/src/types/settings.ts b/packages/app/src/types/settings.ts index 0367045..ac885b1 100644 --- a/packages/app/src/types/settings.ts +++ b/packages/app/src/types/settings.ts @@ -46,4 +46,6 @@ export interface SystemSettings { globalReadSettings: ReadSettings; globalViewSettings: ViewSettings; uiFontFamily?: string; + uiFontSize?: number; + uiFontWeight?: number; } diff --git a/packages/app/src/utils/font.ts b/packages/app/src/utils/font.ts index 9173c35..1ecb303 100644 --- a/packages/app/src/utils/font.ts +++ b/packages/app/src/utils/font.ts @@ -15,7 +15,21 @@ const quoteIfNeeded = (font: string) => { return /\s/.test(font) && !/^".*"$/.test(font) ? `"${font}"` : font; }; -export const applyUiFont = (fontFamily?: string | null) => { +const normalizeFontSize = (value?: number | null) => { + if (typeof value !== "number" || Number.isNaN(value) || value <= 0) { + return null; + } + return `${value}px`; +}; + +const normalizeFontWeight = (value?: number | null) => { + if (typeof value !== "number" || Number.isNaN(value) || value <= 0) { + return null; + } + return `${Math.round(value)}`; +}; + +export const applyUiFont = (fontFamily?: string | null, fontSize?: number | null, fontWeight?: number | null) => { if (typeof document === "undefined") return; const root = document.documentElement; const sanitized = fontFamily?.trim(); @@ -23,22 +37,36 @@ export const applyUiFont = (fontFamily?: string | null) => { if (!sanitized) { root.style.removeProperty("--font-sans"); root.style.removeProperty("--font-serif"); - return; + } else { + const cssFont = quoteIfNeeded(sanitized); + const computedStyle = getComputedStyle(root); + const fallbackSans = computedStyle.getPropertyValue("--font-sans").trim() || SYSTEM_FONT_FALLBACK; + const fallbackSerif = computedStyle.getPropertyValue("--font-serif").trim() || DEFAULT_SERIF_FALLBACK; + + root.style.setProperty("--font-sans", `${cssFont}, ${fallbackSans}`); + root.style.setProperty("--font-serif", `${cssFont}, ${fallbackSerif}`); } - const cssFont = quoteIfNeeded(sanitized); - const computedStyle = getComputedStyle(root); - const fallbackSans = computedStyle.getPropertyValue("--font-sans").trim() || SYSTEM_FONT_FALLBACK; - const fallbackSerif = computedStyle.getPropertyValue("--font-serif").trim() || DEFAULT_SERIF_FALLBACK; + const normalizedSize = normalizeFontSize(fontSize); + const normalizedWeight = normalizeFontWeight(fontWeight); + + if (normalizedSize) { + root.style.setProperty("--app-font-size", normalizedSize); + } else { + root.style.removeProperty("--app-font-size"); + } - root.style.setProperty("--font-sans", `${cssFont}, ${fallbackSans}`); - root.style.setProperty("--font-serif", `${cssFont}, ${fallbackSerif}`); + if (normalizedWeight) { + root.style.setProperty("--app-font-weight", normalizedWeight); + } else { + root.style.removeProperty("--app-font-weight"); + } }; export const applyUiFontFromSettings = () => { try { const { settings } = useAppSettingsStore.getState(); - applyUiFont(settings.uiFontFamily); + applyUiFont(settings.uiFontFamily, settings.uiFontSize, settings.uiFontWeight); } catch (error) { console.error("[Font] Failed to apply UI font from settings:", error); } @@ -46,9 +74,13 @@ export const applyUiFontFromSettings = () => { if (typeof window !== "undefined") { useAppSettingsStore.subscribe( - (state) => state.settings.uiFontFamily, - (fontFamily) => { - applyUiFont(fontFamily); + (state) => ({ + fontFamily: state.settings.uiFontFamily, + fontSize: state.settings.uiFontSize, + fontWeight: state.settings.uiFontWeight, + }), + ({ fontFamily, fontSize, fontWeight }) => { + applyUiFont(fontFamily, fontSize, fontWeight); }, ); } From 48241d0fac2b46a5392063dcf745a4790ca5b477 Mon Sep 17 00:00:00 2001 From: AHpx Date: Sun, 19 Oct 2025 15:03:05 +0800 Subject: [PATCH 05/11] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E4=BA=86?= =?UTF-8?q?=E5=88=87=E6=8D=A2=E5=AD=97=E4=BD=93=E9=85=8D=E7=BD=AE=E4=B9=8B?= =?UTF-8?q?=E5=90=8E=E7=9A=84=E6=8C=81=E4=B9=85=E5=8C=96=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/settings/font-manager.tsx | 34 +++++++-------- .../foliate-viewer-manager.ts | 7 ++- .../reader/hooks/use-foliate-viewer/index.ts | 15 +++++-- packages/app/src/utils/font.ts | 43 +++++++++++++------ 4 files changed, 63 insertions(+), 36 deletions(-) diff --git a/packages/app/src/components/settings/font-manager.tsx b/packages/app/src/components/settings/font-manager.tsx index e1063a8..fced299 100644 --- a/packages/app/src/components/settings/font-manager.tsx +++ b/packages/app/src/components/settings/font-manager.tsx @@ -11,14 +11,8 @@ import { SelectValue, } from "@/components/ui/select"; import { useFontUpload } from "@/hooks/use-font-upload"; -import { - type FontInfo, - type SystemFontInfo, - deleteFont, - downloadFont, - setFontMetadata, -} from "@/services/font-service"; import { DEFAULT_BOOK_FONT } from "@/services/constants"; +import { type FontInfo, type SystemFontInfo, deleteFont, downloadFont, setFontMetadata } from "@/services/font-service"; import { useAppSettingsStore } from "@/store/app-settings-store"; import { useFontStore } from "@/store/font-store"; import { applyUiFont } from "@/utils/font"; @@ -296,7 +290,7 @@ export default function FontManager() {
-

应用字体

+

应用字体

选择界面和系统使用的字体

{isSystemLoading ? ( @@ -332,7 +326,9 @@ export default function FontManager() { )) ) : ( -
未找到匹配字体
+
+ 未找到匹配字体 +
)} @@ -340,7 +336,7 @@ export default function FontManager() { )}
-

字体大小

+

字体大小

-

字体粗细

+

字体粗细

@@ -462,7 +460,7 @@ export default function FontManager() {
-

字体粗细

+

字体粗细

handleKeyDown(item.action, event)} + /> +
+ {isOverridden && ( + + )} +
+
+
+ ); + })} +
+
+

Application

diff --git a/packages/app/src/pages/reader/components/annotator/annotation-popup.tsx b/packages/app/src/pages/reader/components/annotator/annotation-popup.tsx index 7197871..2cf2cc2 100644 --- a/packages/app/src/pages/reader/components/annotator/annotation-popup.tsx +++ b/packages/app/src/pages/reader/components/annotator/annotation-popup.tsx @@ -7,10 +7,17 @@ import React, { useEffect, useMemo, useState } from "react"; import HighlightOptions from "./highlight-options"; import PopupButton from "./popup-button"; +interface AnnotationMenuButton { + label: string | undefined; + Icon: React.ElementType; + onClick: () => void; + shortcut?: string; +} + interface AnnotationPopupProps { dir: "ltr" | "rtl"; isVertical: boolean; - buttons: Array<{ label: string | undefined; Icon: React.ElementType; onClick: () => void }>; + buttons: AnnotationMenuButton[]; position: Position; trianglePosition: Position; highlightOptionsVisible: boolean; @@ -166,7 +173,13 @@ const AnnotationPopup: React.FC = ({ > {buttons.map((button, index) => ( - + {index === 2 && ( { }, [showAnnotPopup, showAskAIPopup]); const selectionAnnotated = selection?.annotated; - const buttons = [ - { label: "复制", Icon: FiCopy, onClick: handleCopy }, - { label: "解释", Icon: FiHelpCircle, onClick: handleExplain }, - { label: "询问AI", Icon: FiMessageCircle, onClick: handleAskAI }, - { - label: undefined, - Icon: selectionAnnotated ? RiDeleteBinLine : PiHighlighterFill, - onClick: handleHighlight, - }, - { label: undefined, Icon: NotebookPen, onClick: addNote }, - ]; + const readerShortcuts = useMemo( + () => ({ ...DEFAULT_READER_SHORTCUTS, ...(settings.readerShortcuts ?? {}) }), + [settings.readerShortcuts], + ); + + const buttons = useMemo( + () => [ + { label: "复制", Icon: FiCopy, onClick: handleCopy, shortcut: readerShortcuts.copy }, + { label: "解释", Icon: FiHelpCircle, onClick: handleExplain, shortcut: readerShortcuts.explain }, + { label: "询问AI", Icon: FiMessageCircle, onClick: handleAskAI, shortcut: readerShortcuts.askAI }, + { + label: undefined, + Icon: selectionAnnotated ? RiDeleteBinLine : PiHighlighterFill, + onClick: handleHighlight, + shortcut: readerShortcuts.toggleHighlight, + }, + { label: undefined, Icon: NotebookPen, onClick: addNote, shortcut: readerShortcuts.addNote }, + ], + [ + readerShortcuts.copy, + readerShortcuts.explain, + readerShortcuts.askAI, + readerShortcuts.toggleHighlight, + readerShortcuts.addNote, + handleCopy, + handleExplain, + handleAskAI, + handleHighlight, + addNote, + selectionAnnotated, + ], + ); + + useEffect(() => { + if (!showAnnotPopup || showAskAIPopup) return; + if (!buttons.some((button) => button.shortcut)) return; + + const handleShortcut = (event: KeyboardEvent) => { + if (event.defaultPrevented) return; + if (event.altKey || event.ctrlKey || event.metaKey) return; + + const activeElement = document.activeElement as HTMLElement | null; + if (activeElement) { + const tagName = activeElement.tagName; + if ( + tagName === "INPUT" || + tagName === "TEXTAREA" || + tagName === "SELECT" || + activeElement.isContentEditable + ) { + return; + } + } + + const key = event.key.toLowerCase(); + const matchedButton = buttons.find( + (button) => button.shortcut && button.shortcut.toLowerCase() === key, + ); + + if (!matchedButton) return; + + event.preventDefault(); + matchedButton.onClick(); + }; + + window.addEventListener("keydown", handleShortcut); + return () => window.removeEventListener("keydown", handleShortcut); + }, [buttons, showAnnotPopup, showAskAIPopup]); return (
diff --git a/packages/app/src/pages/reader/components/annotator/popup-button.tsx b/packages/app/src/pages/reader/components/annotator/popup-button.tsx index 49db033..6059a04 100644 --- a/packages/app/src/pages/reader/components/annotator/popup-button.tsx +++ b/packages/app/src/pages/reader/components/annotator/popup-button.tsx @@ -5,9 +5,10 @@ interface PopupButtonProps { Icon: React.ElementType; onClick: () => void; isVertical?: boolean; + shortcutKey?: string; } -const PopupButton: React.FC = ({ label, Icon, onClick, isVertical = false }) => { +const PopupButton: React.FC = ({ label, Icon, onClick, isVertical = false, shortcutKey }) => { const handleClick = () => { onClick(); }; @@ -22,6 +23,11 @@ const PopupButton: React.FC = ({ label, Icon, onClick, isVerti > {label && {label}} + {shortcutKey && ( + + {shortcutKey} + + )}
); diff --git a/packages/app/src/services/constants.ts b/packages/app/src/services/constants.ts index 9fe966d..515b5c4 100644 --- a/packages/app/src/services/constants.ts +++ b/packages/app/src/services/constants.ts @@ -10,7 +10,7 @@ import type { ViewConfig, ViewSettings, } from "@/types/book"; -import type { ReadSettings, SystemSettings } from "@/types/settings"; +import type { ReadSettings, ReaderShortcutConfig, SystemSettings } from "@/types/settings"; import type { UserDailyTranslationQuota, UserStorageQuota } from "@/types/user"; import { getDefaultMaxBlockSize, getDefaultMaxInlineSize } from "@/utils/config"; import { stubTranslation as _ } from "@/utils/misc"; @@ -27,6 +27,14 @@ export const BOOK_UNGROUPED_ID = ""; export const SUPPORTED_IMAGE_EXTS = ["png", "jpg", "jpeg"]; export const IMAGE_ACCEPT_FORMATS = SUPPORTED_IMAGE_EXTS.map((ext) => `.${ext}`).join(", "); +export const DEFAULT_READER_SHORTCUTS: ReaderShortcutConfig = { + copy: "c", + explain: "e", + askAI: "a", + toggleHighlight: "h", + addNote: "n", +}; + export const DEFAULT_SYSTEM_SETTINGS: Partial = { keepLogin: false, autoUpload: true, @@ -50,6 +58,7 @@ export const DEFAULT_SYSTEM_SETTINGS: Partial = { uiFontFamily: "", uiFontSize: 14, uiFontWeight: 400, + readerShortcuts: { ...DEFAULT_READER_SHORTCUTS }, }; export const DEFAULT_READSETTINGS: ReadSettings = { diff --git a/packages/app/src/store/app-settings-store.ts b/packages/app/src/store/app-settings-store.ts index efaa904..65e5043 100644 --- a/packages/app/src/store/app-settings-store.ts +++ b/packages/app/src/store/app-settings-store.ts @@ -5,6 +5,7 @@ import { DEFAULT_BOOK_LAYOUT, DEFAULT_BOOK_STYLE, DEFAULT_CJK_VIEW_SETTINGS, + DEFAULT_READER_SHORTCUTS, DEFAULT_READSETTINGS, DEFAULT_SYSTEM_SETTINGS, DEFAULT_VIEW_CONFIG, @@ -38,9 +39,16 @@ export const useAppSettingsStore = create()( ...(isCJKEnv() ? DEFAULT_CJK_VIEW_SETTINGS : {}), ...DEFAULT_VIEW_CONFIG, }, + readerShortcuts: { ...DEFAULT_READER_SHORTCUTS }, } as SystemSettings, toggleSettingsDialog: () => set((state) => ({ isSettingsDialogOpen: !state.isSettingsDialogOpen })), - setSettings: (settings: SystemSettings) => set({ settings }), + setSettings: (settings: SystemSettings) => + set({ + settings: { + ...settings, + readerShortcuts: { ...DEFAULT_READER_SHORTCUTS, ...(settings.readerShortcuts ?? {}) }, + }, + }), }), { name: tauriStorageKey.appSettings, diff --git a/packages/app/src/types/settings.ts b/packages/app/src/types/settings.ts index ac885b1..0f9532e 100644 --- a/packages/app/src/types/settings.ts +++ b/packages/app/src/types/settings.ts @@ -1,6 +1,10 @@ import type { CustomTheme } from "@/styles/themes"; import type { HighlightColor, HighlightStyle, ViewSettings } from "./book"; +export type ReaderShortcutAction = "copy" | "explain" | "askAI" | "toggleHighlight" | "addNote"; + +export type ReaderShortcutConfig = Record; + export type LibraryViewModeType = "grid" | "list"; export type LibrarySortByType = "title" | "author" | "updated" | "created" | "size" | "format"; export type LibraryCoverFitType = "crop" | "fit"; @@ -45,6 +49,7 @@ export interface SystemSettings { globalReadSettings: ReadSettings; globalViewSettings: ViewSettings; + readerShortcuts: ReaderShortcutConfig; uiFontFamily?: string; uiFontSize?: number; uiFontWeight?: number; From 25923b168382cc60caebe576f6873343f9297e54 Mon Sep 17 00:00:00 2001 From: AHpx Date: Sun, 19 Oct 2025 15:37:18 +0800 Subject: [PATCH 07/11] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E5=BF=AB?= =?UTF-8?q?=E6=8D=B7=E9=94=AE=E4=B8=8D=E5=93=8D=E5=BA=94=E7=9A=84=E9=97=AE?= =?UTF-8?q?=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../reader/components/annotator/index.tsx | 29 ++++++++++++++++--- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/packages/app/src/pages/reader/components/annotator/index.tsx b/packages/app/src/pages/reader/components/annotator/index.tsx index f3985ed..3b9617f 100644 --- a/packages/app/src/pages/reader/components/annotator/index.tsx +++ b/packages/app/src/pages/reader/components/annotator/index.tsx @@ -153,7 +153,9 @@ const Annotator: React.FC = () => { if (event.defaultPrevented) return; if (event.altKey || event.ctrlKey || event.metaKey) return; - const activeElement = document.activeElement as HTMLElement | null; + const eventTarget = event.target as Node | null; + const activeDocument = eventTarget?.ownerDocument ?? document; + const activeElement = activeDocument.activeElement as HTMLElement | null; if (activeElement) { const tagName = activeElement.tagName; if ( @@ -177,9 +179,28 @@ const Annotator: React.FC = () => { matchedButton.onClick(); }; - window.addEventListener("keydown", handleShortcut); - return () => window.removeEventListener("keydown", handleShortcut); - }, [buttons, showAnnotPopup, showAskAIPopup]); + const listeners: Array<() => void> = []; + const attachedTargets = new Set(); + const addListener = (target: Window | Document | null | undefined) => { + if (!target || attachedTargets.has(target)) return; + attachedTargets.add(target); + target.addEventListener("keydown", handleShortcut); + listeners.push(() => target.removeEventListener("keydown", handleShortcut)); + }; + + addListener(window); + addListener(document); + + const ownerDocument = selection?.range?.startContainer?.ownerDocument ?? null; + addListener(ownerDocument); + if (ownerDocument?.defaultView && ownerDocument.defaultView !== window) { + addListener(ownerDocument.defaultView); + } + + return () => { + listeners.forEach((dispose) => dispose()); + }; + }, [buttons, selection?.range, showAnnotPopup, showAskAIPopup]); return (
From fabee1838ed73502ad96c29a35a22c95b5363987 Mon Sep 17 00:00:00 2001 From: AHpx Date: Sun, 19 Oct 2025 16:29:15 +0800 Subject: [PATCH 08/11] =?UTF-8?q?style:=20=E5=88=A0=E9=99=A4=E5=95=8A?= =?UTF-8?q?=E4=BA=86=20popup=20=E7=9A=84=E6=96=87=E5=AD=97label?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pages/reader/components/annotator/annotation-popup.tsx | 2 +- .../src/pages/reader/components/annotator/popup-button.tsx | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/app/src/pages/reader/components/annotator/annotation-popup.tsx b/packages/app/src/pages/reader/components/annotator/annotation-popup.tsx index 2cf2cc2..b59fde8 100644 --- a/packages/app/src/pages/reader/components/annotator/annotation-popup.tsx +++ b/packages/app/src/pages/reader/components/annotator/annotation-popup.tsx @@ -8,7 +8,7 @@ import HighlightOptions from "./highlight-options"; import PopupButton from "./popup-button"; interface AnnotationMenuButton { - label: string | undefined; + label?: string; Icon: React.ElementType; onClick: () => void; shortcut?: string; diff --git a/packages/app/src/pages/reader/components/annotator/popup-button.tsx b/packages/app/src/pages/reader/components/annotator/popup-button.tsx index 6059a04..cc36830 100644 --- a/packages/app/src/pages/reader/components/annotator/popup-button.tsx +++ b/packages/app/src/pages/reader/components/annotator/popup-button.tsx @@ -1,7 +1,7 @@ import type React from "react"; interface PopupButtonProps { - label: string | undefined; + label?: string; Icon: React.ElementType; onClick: () => void; isVertical?: boolean; @@ -17,12 +17,13 @@ const PopupButton: React.FC = ({ label, Icon, onClick, isVerti