diff --git a/packages/app-expo/src/lib/platform/expo-platform-service.ts b/packages/app-expo/src/lib/platform/expo-platform-service.ts index 1935cf27..ece4ab6f 100644 --- a/packages/app-expo/src/lib/platform/expo-platform-service.ts +++ b/packages/app-expo/src/lib/platform/expo-platform-service.ts @@ -40,8 +40,16 @@ export class ExpoPlatformService implements IPlatformService { // ---- File system (expo-file-system v55 — File/Directory/Paths API) ---- async readFile(path: string): Promise { - const file = new File(path); - return file.bytes(); + try { + const file = new File(path); + return await file.bytes(); + } catch (err) { + if (!path.startsWith("content://")) throw err; + const base64 = await LegacyFS.readAsStringAsync(path, { + encoding: LegacyFS.EncodingType.Base64, + }); + return base64ToBytes(base64); + } } async writeFile(path: string, data: Uint8Array): Promise { @@ -838,6 +846,15 @@ export class ExpoPlatformService implements IPlatformService { } } +function base64ToBytes(base64: string): Uint8Array { + const binary = atob(base64); + const bytes = new Uint8Array(binary.length); + for (let i = 0; i < binary.length; i++) { + bytes[i] = binary.charCodeAt(i); + } + return bytes; +} + /** Map book file extensions to MIME types for document picker */ function extensionToMime(ext: string): string { const map: Record = { diff --git a/packages/app-expo/src/screens/ReaderScreen.tsx b/packages/app-expo/src/screens/ReaderScreen.tsx index ec06a608..564f3f05 100644 --- a/packages/app-expo/src/screens/ReaderScreen.tsx +++ b/packages/app-expo/src/screens/ReaderScreen.tsx @@ -144,6 +144,7 @@ const NOTE_TOOLTIP_SIDE_PADDING = 12; const NOTE_TOOLTIP_ABOVE_OFFSET = 2; const NOTE_TOOLTIP_BELOW_OFFSET = 8; const NOTE_TOOLTIP_TOP_THRESHOLD = 180; +import { useRubyStore } from "@readany/core/stores/ruby-store"; import { ReaderSettingsPanel } from "./reader/ReaderSettingsPanel"; import { ReaderTOCPanel } from "./reader/ReaderTOCPanel"; import { @@ -159,7 +160,6 @@ import { useReaderSearch } from "./reader/useReaderSearch"; import { useReaderSystemInfo } from "./reader/useReaderSystemInfo"; import { useReaderTTS } from "./reader/useReaderTTS"; import { useVolumeButtonPaging } from "./reader/useVolumeButtonPaging"; -import { useRubyStore } from "@readany/core/stores/ruby-store"; const READER_HTML_ASSET = Asset.fromModule(require("../../assets/reader/reader.html")); const LOCAL_FONT_SERVER_DIR = "readany-fonts"; @@ -333,8 +333,8 @@ export function ReaderScreen({ route, navigation }: Props) { const customFonts = useFontStore((s) => s.fonts); const selectedFontId = useFontStore((s) => s.selectedFontId); const customFontFamily = useMemo(() => { - if (!selectedFontId) return undefined; - return customFonts.find((f) => f.id === selectedFontId)?.fontFamily; + if (!selectedFontId) return ""; + return customFonts.find((f) => f.id === selectedFontId)?.fontFamily ?? ""; }, [customFonts, selectedFontId]); const customFontFaceCSS = useMemo( () => buildCustomFontFaceCSS(customFonts, selectedFontId, fontServerUrl), @@ -571,7 +571,7 @@ export function ReaderScreen({ route, navigation }: Props) { const settings = useSettingsStore.getState().readSettings; const { fonts, selectedFontId: selId } = useFontStore.getState(); const fontCSS = buildCustomFontFaceCSS(fonts, selId, fileServerRef.current); - const fontFamily = selId ? fonts.find((f) => f.id === selId)?.fontFamily : undefined; + const fontFamily = selId ? fonts.find((f) => f.id === selId)?.fontFamily : ""; console.log("[ReaderScreen][Font] selection", { selectedFontId: selId, fontFamily, @@ -586,7 +586,7 @@ export function ReaderScreen({ route, navigation }: Props) { viewMode: settings.viewMode, paginatedLayout: settings.paginatedLayout, customFontFaceCSS: fontCSS, - customFontFamily: fontFamily, + customFontFamily: fontFamily ?? "", }); // Auto-restore ruby annotations if enabled for this book @@ -594,7 +594,9 @@ export function ReaderScreen({ route, navigation }: Props) { if (rubyMode) { void (async () => { try { - const { checkExistingDictMobile, readDictStrings } = await import("@/lib/ruby/dict-service-mobile"); + const { checkExistingDictMobile, readDictStrings } = await import( + "@/lib/ruby/dict-service-mobile" + ); const exists = await checkExistingDictMobile(); if (exists) { const { wordDict, charDict } = await readDictStrings(); @@ -961,7 +963,7 @@ export function ReaderScreen({ route, navigation }: Props) { const currentSettings = useSettingsStore.getState().readSettings; const { fonts, selectedFontId: selId } = useFontStore.getState(); const fontCSS = buildCustomFontFaceCSS(fonts, selId, fileServerRef.current); - const fontFamily = selId ? fonts.find((f) => f.id === selId)?.fontFamily : undefined; + const fontFamily = selId ? fonts.find((f) => f.id === selId)?.fontFamily : ""; // Recompute effective fontSize after every settings change — covers // both stepper changes and toggling followSystemFontScale on/off. const merged = { ...currentSettings, ...updates }; @@ -969,7 +971,7 @@ export function ReaderScreen({ route, navigation }: Props) { ...merged, fontSize: computeEffectiveFontSize(merged.fontSize, merged.followSystemFontScale), customFontFaceCSS: fontCSS, - customFontFamily: fontFamily, + customFontFamily: fontFamily ?? "", }); }, [bridge, updateReadSettings, computeEffectiveFontSize], @@ -1412,8 +1414,9 @@ export function ReaderScreen({ route, navigation }: Props) { const isPanelOpen = showTOC || showSettings || showSearch || showNotebook || showTranslation; const existingSelectionHighlight = selection - ? (highlights.find((highlight) => highlight.bookId === bookId && highlight.cfi === selection.cfi) ?? - null) + ? (highlights.find( + (highlight) => highlight.bookId === bookId && highlight.cfi === selection.cfi, + ) ?? null) : null; const readerTopMargin = !showSearch ? showTopTitleProgress diff --git a/packages/app-expo/src/screens/settings/FontSettingsScreen.tsx b/packages/app-expo/src/screens/settings/FontSettingsScreen.tsx index a0727e62..3bdbf7cd 100644 --- a/packages/app-expo/src/screens/settings/FontSettingsScreen.tsx +++ b/packages/app-expo/src/screens/settings/FontSettingsScreen.tsx @@ -1,35 +1,36 @@ +import { GlobeIcon, LinkIcon, PlusIcon, Trash2Icon, TypeIcon } from "@/components/ui/Icon"; +import { useResponsiveLayout } from "@/hooks/use-responsive-layout"; +import { fontSize, fontWeight, radius, spacing, useColors } from "@/styles/theme"; /** * FontSettingsScreen — custom font management for mobile */ import { - useFontStore, + createCustomFontFamily, generateFontId, saveFontFile, + useFontStore, } from "@readany/core/stores"; -import { useResponsiveLayout } from "@/hooks/use-responsive-layout"; import type { CustomFont } from "@readany/core/types/font"; import { PRESET_FONTS } from "@readany/core/types/font"; -import { getPlatformService } from "@readany/core/services"; +import * as DocumentPicker from "expo-document-picker"; +import * as Font from "expo-font"; +import { useCallback, useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { + ActivityIndicator, Alert, + KeyboardAvoidingView, + Modal, + Platform, ScrollView, StyleSheet, Text, + TextInput, TouchableOpacity, View, - ActivityIndicator, - TextInput, - Modal, - KeyboardAvoidingView, - Platform, } from "react-native"; import { SafeAreaView } from "react-native-safe-area-context"; -import * as DocumentPicker from "expo-document-picker"; -import { useCallback, useState } from "react"; import { SettingsHeader } from "./SettingsHeader"; -import { useColors, fontSize, fontWeight, radius, spacing } from "@/styles/theme"; -import { PlusIcon, Trash2Icon, TypeIcon, LinkIcon, GlobeIcon } from "@/components/ui/Icon"; export default function FontSettingsScreen() { const { t, i18n } = useTranslation(); @@ -60,10 +61,10 @@ export default function FontSettingsScreen() { remoteUrlWoff2: preset.remoteUrlWoff2, remoteUrl: preset.remoteUrl, }; - addFont(font); + addFont(font, { select: true }); Alert.alert( t("fonts.imported", "导入成功"), - t("fonts.importedDesc", "字体 \"{{name}}\" 已添加", { name: font.name }), + t("fonts.importedDesc", '字体 "{{name}}" 已添加', { name: font.name }), ); }, [addFont, i18n.language, t], @@ -72,18 +73,55 @@ export default function FontSettingsScreen() { const [importing, setImporting] = useState(false); const [nameModalVisible, setNameModalVisible] = useState(false); const [urlModalVisible, setUrlModalVisible] = useState(false); - const [pendingFontFile, setPendingFontFile] = useState<{ uri: string; name: string } | null>(null); + const [pendingFontFile, setPendingFontFile] = useState<{ uri: string; name: string } | null>( + null, + ); const [fontNameInput, setFontNameInput] = useState(""); const [remoteUrl, setRemoteUrl] = useState(""); const [remoteUrlWoff2, setRemoteUrlWoff2] = useState(""); const [remoteFontName, setRemoteFontName] = useState(""); + const [loadedPreviewFontIds, setLoadedPreviewFontIds] = useState>(() => new Set()); + + useEffect(() => { + let cancelled = false; + + const loadPreviewFonts = async () => { + const loadedIds = new Set(); + for (const font of fonts) { + if (font.source !== "local" || !font.filePath) continue; + try { + if (!Font.isLoaded(font.fontFamily)) { + await Font.loadAsync({ [font.fontFamily]: font.filePath }); + } + loadedIds.add(font.id); + } catch (err) { + console.warn("[FontSettings] Failed to load font preview:", font.name, err); + } + } + if (!cancelled) setLoadedPreviewFontIds(loadedIds); + }; + + void loadPreviewFonts(); + return () => { + cancelled = true; + }; + }, [fonts]); const handleImport = useCallback(async () => { setImporting(true); try { const result = await DocumentPicker.getDocumentAsync({ - type: ["application/font-sfnt", "application/x-font-ttf", "application/x-font-otf", "font/ttf", "font/otf", "font/woff", "font/woff2", "*/*"], + type: [ + "application/font-sfnt", + "application/x-font-ttf", + "application/x-font-otf", + "font/ttf", + "font/otf", + "font/woff", + "font/woff2", + "*/*", + ], copyToCacheDirectory: true, }); @@ -118,28 +156,30 @@ export default function FontSettingsScreen() { setImporting(true); try { - const { filePath, fileName: savedName, size } = await saveFontFile( - pendingFontFile.uri, - fontNameInput.trim(), - ); + const { + filePath, + fileName: savedName, + size, + } = await saveFontFile(pendingFontFile.uri, fontNameInput.trim(), pendingFontFile.name); - const fontFamily = `Custom-${fontNameInput.trim().replace(/\s+/g, "-")}`; + const id = generateFontId(); const font: CustomFont = { - id: generateFontId(), + id, name: fontNameInput.trim(), fileName: savedName, filePath, - fontFamily, - format: (savedName.split(".").pop()?.toLowerCase() as "ttf" | "otf" | "woff" | "woff2") || "ttf", + fontFamily: createCustomFontFamily(id), + format: + (savedName.split(".").pop()?.toLowerCase() as "ttf" | "otf" | "woff" | "woff2") || "ttf", size, addedAt: Date.now(), source: "local", }; - addFont(font); + addFont(font, { select: true }); Alert.alert( t("fonts.imported", "导入成功"), - t("fonts.importedDesc", "字体 \"{{name}}\" 已导入", { name: fontNameInput.trim() }), + t("fonts.importedDesc", '字体 "{{name}}" 已导入', { name: fontNameInput.trim() }), ); } catch (err) { console.error("[FontSettings] Import error:", err); @@ -165,16 +205,16 @@ export default function FontSettingsScreen() { setImporting(true); try { - const fontFamily = `Custom-${remoteFontName.trim().replace(/\s+/g, "-")}`; + const id = generateFontId(); const url = remoteUrl.trim(); const woff2Url = remoteUrlWoff2.trim(); const format = woff2Url ? "woff2" : url.endsWith(".woff2") ? "woff2" : "woff"; const font: CustomFont = { - id: generateFontId(), + id, name: remoteFontName.trim(), fileName: `remote-${Date.now()}.${format}`, - fontFamily, + fontFamily: createCustomFontFamily(id), format, addedAt: Date.now(), source: "remote", @@ -182,10 +222,10 @@ export default function FontSettingsScreen() { remoteUrlWoff2: woff2Url || undefined, }; - addFont(font); + addFont(font, { select: true }); Alert.alert( t("fonts.imported", "导入成功"), - t("fonts.importedDesc", "字体 \"{{name}}\" 已导入", { name: remoteFontName.trim() }), + t("fonts.importedDesc", '字体 "{{name}}" 已导入', { name: remoteFontName.trim() }), ); } catch (err) { console.error("[FontSettings] Import remote error:", err); @@ -202,10 +242,14 @@ export default function FontSettingsScreen() { (font: CustomFont) => { Alert.alert( t("fonts.deleteTitle", "删除字体"), - t("fonts.deleteConfirm", "确定删除字体 \"{{name}}\" 吗?", { name: font.name }), + t("fonts.deleteConfirm", '确定删除字体 "{{name}}" 吗?', { name: font.name }), [ { text: t("common.cancel", "取消"), style: "cancel" }, - { text: t("common.delete", "删除"), style: "destructive", onPress: () => removeFont(font.id) }, + { + text: t("common.delete", "删除"), + style: "destructive", + onPress: () => removeFont(font.id), + }, ], ); }, @@ -251,7 +295,6 @@ export default function FontSettingsScreen() { ) : ( <> - - {/* Preset fonts */} - {availablePresetFonts.length > 0 && ( - - - {t("fonts.presets", "推荐字体(在线,点击即可添加)")} - - {availablePresetFonts.map((preset) => { - const name = i18n.language === "zh" ? preset.name : preset.nameEn; - const desc = i18n.language === "zh" ? preset.description : preset.descriptionEn; - return ( + {/* Preset fonts */} + {availablePresetFonts.length > 0 && ( + + + {t("fonts.presets", "推荐字体(在线,点击即可添加)")} + + {availablePresetFonts.map((preset) => { + const name = i18n.language === "zh" ? preset.name : preset.nameEn; + const desc = i18n.language === "zh" ? preset.description : preset.descriptionEn; + return ( + + + + + {name} + + + {preset.license} + + + + + {desc} + + + handleAddPreset(preset)} + > + + {t("fonts.add", "添加")} + + + + + ); + })} + + )} + + {fonts.length === 0 ? ( + + + + {t("fonts.empty", "暂无自定义字体")} + + + {t("fonts.emptyHint", "点击上方按钮导入字体文件")} + + + ) : ( + + {fonts.map((font) => ( - {name} - - {preset.license} - + + {font.name} + + {font.source === "remote" && ( + + + + {t("fonts.remote", "在线")} + + + )} + + + + {font.format.toUpperCase()} + + + · + + + {formatSize(font.size)} + - - {desc} - handleAddPreset(preset)} + style={s.deleteBtn} + onPress={() => handleDelete(font)} + hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }} > - - {t("fonts.add", "添加")} - + - - ); - })} - - )} - {fonts.length === 0 ? ( - - - - {t("fonts.empty", "暂无自定义字体")} - - - {t("fonts.emptyHint", "点击上方按钮导入字体文件")} - - - ) : ( - - {fonts.map((font) => ( - - - - - {font.name} - {font.source === "remote" && ( - - - - {t("fonts.remote", "在线")} - - - )} - - - - {font.format.toUpperCase()} - - · - - {formatSize(font.size)} - - - - handleDelete(font)} - hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }} + - - - - - - - {t("fonts.preview", "预览文字:阅读改变世界 The quick brown fox")} - + + {t("fonts.preview", "预览文字:阅读改变世界 The quick brown fox")} + + - - ))} - - )} + ))} + + )} )} @@ -410,7 +499,14 @@ export default function FontSettingsScreen() { {t("fonts.nameFontDesc", "请输入字体的显示名称")} { setNameModalVisible(false); setPendingFontFile(null); setFontNameInput(""); }} + onPress={() => { + setNameModalVisible(false); + setPendingFontFile(null); + setFontNameInput(""); + }} > - {t("common.cancel", "取消")} + + {t("common.cancel", "取消")} + - {t("fonts.import", "导入")} + + {t("fonts.import", "导入")} + @@ -454,18 +558,36 @@ export default function FontSettingsScreen() { {t("fonts.urlHint", "输入字体 CDN 链接")} - {t("fonts.name", "字体名称")} + + {t("fonts.name", "字体名称")} + - {t("fonts.urlWoff2", "WOFF2 链接")} + + {t("fonts.urlWoff2", "WOFF2 链接")} + - {t("fonts.urlWoff", "WOFF 链接 (备选)")} + + {t("fonts.urlWoff", "WOFF 链接 (备选)")} + { setUrlModalVisible(false); setRemoteUrl(""); setRemoteUrlWoff2(""); setRemoteFontName(""); }} + onPress={() => { + setUrlModalVisible(false); + setRemoteUrl(""); + setRemoteUrlWoff2(""); + setRemoteFontName(""); + }} > - {t("common.cancel", "取消")} + + {t("common.cancel", "取消")} + - {t("fonts.import", "导入")} + + {t("fonts.import", "导入")} + @@ -508,7 +648,7 @@ export default function FontSettingsScreen() { ); } -function makeStyles(colors: ReturnType) { +function makeStyles(_colors: ReturnType) { return StyleSheet.create({ container: { flex: 1 }, scroll: { flex: 1 }, @@ -534,8 +674,12 @@ function makeStyles(colors: ReturnType) { buttonRow: { flexDirection: "row", gap: 12, marginBottom: 16 }, presetSection: { gap: 8 }, importBtn: { - flexDirection: "row", alignItems: "center", justifyContent: "center", - gap: 8, borderRadius: radius.lg, paddingVertical: 14, + flexDirection: "row", + alignItems: "center", + justifyContent: "center", + gap: 8, + borderRadius: radius.lg, + paddingVertical: 14, }, importBtnHalf: { flex: 1 }, importBtnText: { fontSize: fontSize.base, fontWeight: fontWeight.medium }, @@ -553,14 +697,33 @@ function makeStyles(colors: ReturnType) { deleteBtn: { padding: 4 }, previewBox: { borderRadius: radius.md, borderWidth: 1, padding: 12 }, previewText: { fontSize: fontSize.sm }, - remoteBadge: { flexDirection: "row", alignItems: "center", gap: 4, paddingHorizontal: 6, paddingVertical: 2, borderRadius: radius.sm }, + remoteBadge: { + flexDirection: "row", + alignItems: "center", + gap: 4, + paddingHorizontal: 6, + paddingVertical: 2, + borderRadius: radius.sm, + }, remoteBadgeText: { fontSize: fontSize.xs, fontWeight: fontWeight.medium }, - modalOverlay: { flex: 1, backgroundColor: "rgba(0,0,0,0.5)", justifyContent: "center", alignItems: "center" }, + modalOverlay: { + flex: 1, + backgroundColor: "rgba(0,0,0,0.5)", + justifyContent: "center", + alignItems: "center", + }, modalContent: { borderRadius: radius.xl, padding: 20, width: "85%", maxWidth: 340 }, modalTitle: { fontSize: fontSize.lg, fontWeight: fontWeight.semibold, marginBottom: 8 }, modalDesc: { fontSize: fontSize.sm, marginBottom: 16 }, inputLabel: { fontSize: fontSize.sm, fontWeight: fontWeight.medium, marginBottom: 4 }, - modalInput: { borderRadius: radius.md, borderWidth: 1, paddingHorizontal: 12, paddingVertical: 10, fontSize: fontSize.base, marginBottom: 12 }, + modalInput: { + borderRadius: radius.md, + borderWidth: 1, + paddingHorizontal: 12, + paddingVertical: 10, + fontSize: fontSize.base, + marginBottom: 12, + }, modalButtons: { flexDirection: "row", gap: 12, marginTop: 4 }, modalBtn: { flex: 1, borderRadius: radius.md, paddingVertical: 10, alignItems: "center" }, modalBtnText: { fontSize: fontSize.base, fontWeight: fontWeight.medium }, diff --git a/packages/app/src/components/layout/AppLayout.tsx b/packages/app/src/components/layout/AppLayout.tsx index e995265a..6cf24ab3 100644 --- a/packages/app/src/components/layout/AppLayout.tsx +++ b/packages/app/src/components/layout/AppLayout.tsx @@ -29,8 +29,8 @@ import SkillsPage from "@/pages/Skills"; import { useAppStore } from "@/stores/app-store"; import { useLibraryStore } from "@/stores/library-store"; import { useReaderStore } from "@/stores/reader-store"; -import { useSettingsStore } from "@readany/core/stores/settings-store"; import { useFontStore } from "@readany/core/stores"; +import { useSettingsStore } from "@readany/core/stores/settings-store"; import { BookOpen } from "lucide-react"; import { useCallback, useEffect, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; @@ -82,31 +82,44 @@ export function AppLayout() { // 2. Inject @font-face for local and direct-URL remote fonts if (customFonts.every((f) => f.remoteCssUrl)) return; - import("@tauri-apps/api/core").then(({ convertFileSrc }) => { - const styleId = "__readany_app_font_faces__"; - let el = document.getElementById(styleId) as HTMLStyleElement | null; - if (!el) { - el = document.createElement("style"); - el.id = styleId; - document.head.appendChild(el); - } - el.textContent = customFonts - .map((f) => { - if (f.remoteCssUrl) return ""; // handled by - if (f.source === "remote") { - const src = f.remoteUrlWoff2 - ? `url('${f.remoteUrlWoff2}') format('woff2')${f.remoteUrl ? `, url('${f.remoteUrl}') format('woff')` : ""}` - : f.remoteUrl ? `url('${f.remoteUrl}') format('woff')` : ""; - return src ? `@font-face { font-family: '${f.fontFamily}'; src: ${src}; font-display: swap; }` : ""; - } - if (!f.filePath) return ""; - const fileUrl = convertFileSrc(f.filePath); - const fmt = f.format === "otf" ? "opentype" : f.format === "woff" ? "woff" : f.format === "woff2" ? "woff2" : "truetype"; - return `@font-face { font-family: '${f.fontFamily}'; src: url('${fileUrl}') format('${fmt}'); }`; - }) - .filter(Boolean) - .join("\n"); - }).catch((err) => console.warn("[Layout] Failed to load custom font faces:", err)); + import("@tauri-apps/api/core") + .then(({ convertFileSrc }) => { + const styleId = "__readany_app_font_faces__"; + let el = document.getElementById(styleId) as HTMLStyleElement | null; + if (!el) { + el = document.createElement("style"); + el.id = styleId; + document.head.appendChild(el); + } + el.textContent = customFonts + .map((f) => { + if (f.remoteCssUrl) return ""; // handled by + if (f.source === "remote") { + const src = f.remoteUrlWoff2 + ? `url('${f.remoteUrlWoff2}') format('woff2')${f.remoteUrl ? `, url('${f.remoteUrl}') format('woff')` : ""}` + : f.remoteUrl + ? `url('${f.remoteUrl}') format('woff')` + : ""; + return src + ? `@font-face { font-family: ${JSON.stringify(f.fontFamily)}; src: ${src}; font-display: swap; }` + : ""; + } + if (!f.filePath) return ""; + const fileUrl = convertFileSrc(f.filePath); + const fmt = + f.format === "otf" + ? "opentype" + : f.format === "woff" + ? "woff" + : f.format === "woff2" + ? "woff2" + : "truetype"; + return `@font-face { font-family: ${JSON.stringify(f.fontFamily)}; src: url('${fileUrl}') format('${fmt}'); }`; + }) + .filter(Boolean) + .join("\n"); + }) + .catch((err) => console.warn("[Layout] Failed to load custom font faces:", err)); }, [customFonts]); const [commandPaletteOpen, setCommandPaletteOpen] = useState(false); @@ -140,10 +153,12 @@ export function AppLayout() { } if (e.key === "F11") { e.preventDefault(); - import("@tauri-apps/api/window").then(({ getCurrentWindow }) => { - const win = getCurrentWindow(); - win.isFullscreen().then((fs) => win.setFullscreen(!fs)); - }).catch((err) => console.warn("[Layout] Failed to toggle fullscreen:", err)); + import("@tauri-apps/api/window") + .then(({ getCurrentWindow }) => { + const win = getCurrentWindow(); + win.isFullscreen().then((fs) => win.setFullscreen(!fs)); + }) + .catch((err) => console.warn("[Layout] Failed to toggle fullscreen:", err)); } }; window.addEventListener("keydown", handleKeyDown, { capture: true }); @@ -327,9 +342,7 @@ export function AppLayout() {
- {!isReaderActive && ( -
- )} + {!isReaderActive &&
} {/* === Home layer (sidebar + content card) === */}
- r.top < min.top ? r : min, - ); + const topmostRect = containerRelativeRects.reduce((min, r) => (r.top < min.top ? r : min)); const bottommostRect = containerRelativeRects.reduce((max, r) => r.bottom > max.bottom ? r : max, ); diff --git a/packages/app/src/components/settings/FontSettings.tsx b/packages/app/src/components/settings/FontSettings.tsx index bd01b0d6..403f77ad 100644 --- a/packages/app/src/components/settings/FontSettings.tsx +++ b/packages/app/src/components/settings/FontSettings.tsx @@ -1,25 +1,21 @@ -/** - * FontSettings — custom font management for desktop - */ -import { useCallback, useState } from "react"; -import { useTranslation } from "react-i18next"; -import { Download, FileText, Globe, Trash2 } from "lucide-react"; +import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; +import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import { - Dialog, - DialogContent, - DialogHeader, - DialogTitle, -} from "@/components/ui/dialog"; -import { Badge } from "@/components/ui/badge"; -import { - useFontStore, + createCustomFontFamily, generateFontId, saveFontFile, + useFontStore, } from "@readany/core/stores"; import type { CustomFont } from "@readany/core/types/font"; import { PRESET_FONTS } from "@readany/core/types/font"; +import { Download, FileText, Globe, Trash2 } from "lucide-react"; +/** + * FontSettings — custom font management for desktop + */ +import { useCallback, useState } from "react"; +import { useTranslation } from "react-i18next"; export function FontSettings() { const { t, i18n } = useTranslation(); @@ -31,7 +27,9 @@ export function FontSettings() { const [importing, setImporting] = useState(false); const [nameModalOpen, setNameModalOpen] = useState(false); const [urlModalOpen, setUrlModalOpen] = useState(false); - const [pendingFontFile, setPendingFontFile] = useState<{ path: string; name: string } | null>(null); + const [pendingFontFile, setPendingFontFile] = useState<{ path: string; name: string } | null>( + null, + ); const [fontNameInput, setFontNameInput] = useState(""); const [remoteUrl, setRemoteUrl] = useState(""); @@ -58,7 +56,7 @@ export function FontSettings() { remoteUrlWoff2: preset.remoteUrlWoff2, remoteUrl: preset.remoteUrl, }; - addFont(font); + addFont(font, { select: true }); }, [addFont, i18n.language], ); @@ -103,27 +101,27 @@ export function FontSettings() { setImporting(true); try { - const { filePath, fileName: savedName, size } = await saveFontFile( - pendingFontFile.path, - fontNameInput.trim(), - ); + const { + filePath, + fileName: savedName, + size, + } = await saveFontFile(pendingFontFile.path, fontNameInput.trim(), pendingFontFile.name); - const fontFamily = `Custom-${fontNameInput.trim().replace(/\s+/g, "-")}`; + const id = generateFontId(); const font: CustomFont = { - id: generateFontId(), + id, name: fontNameInput.trim(), fileName: savedName, filePath, - fontFamily, + fontFamily: createCustomFontFamily(id), format: - (savedName.split(".").pop()?.toLowerCase() as "ttf" | "otf" | "woff" | "woff2") || - "ttf", + (savedName.split(".").pop()?.toLowerCase() as "ttf" | "otf" | "woff" | "woff2") || "ttf", size, addedAt: Date.now(), source: "local", }; - addFont(font); + addFont(font, { select: true }); } catch (err) { console.error("[FontSettings] Import error:", err); } finally { @@ -141,16 +139,16 @@ export function FontSettings() { setImporting(true); try { - const fontFamily = `Custom-${remoteFontName.trim().replace(/\s+/g, "-")}`; + const id = generateFontId(); const url = remoteUrl.trim(); const woff2Url = remoteUrlWoff2.trim(); const format = woff2Url ? "woff2" : url.endsWith(".woff2") ? "woff2" : "woff"; const font: CustomFont = { - id: generateFontId(), + id, name: remoteFontName.trim(), fileName: `remote-${Date.now()}.${format}`, - fontFamily, + fontFamily: createCustomFontFamily(id), format, addedAt: Date.now(), source: "remote", @@ -158,7 +156,7 @@ export function FontSettings() { remoteUrlWoff2: woff2Url || undefined, }; - addFont(font); + addFont(font, { select: true }); } catch (err) { console.error("[FontSettings] Import remote error:", err); } finally { @@ -266,9 +264,7 @@ export function FontSettings() { {fonts.length === 0 ? (
-

- {t("fonts.empty", "暂无自定义字体")} -

+

{t("fonts.empty", "暂无自定义字体")}

{t("fonts.emptyHint", "点击上方按钮导入字体文件")}

@@ -283,7 +279,10 @@ export function FontSettings() {
{font.name} {font.source === "remote" && ( - + {t("fonts.remote", "在线")} @@ -296,10 +295,7 @@ export function FontSettings() {
{/* Preview */}
- + {t("fonts.preview", "预览文字:阅读改变世界 The quick brown fox")}
@@ -327,7 +323,9 @@ export function FontSettings() { {t("fonts.nameFontDesc", "请输入字体的显示名称")}

- + setFontNameInput(e.target.value)} @@ -337,7 +335,14 @@ export function FontSettings() { />
-