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 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/notepad/hooks/use-notepad.ts b/packages/app/src/components/notepad/hooks/use-notepad.ts index ca4e52b..e9fba7a 100644 --- a/packages/app/src/components/notepad/hooks/use-notepad.ts +++ b/packages/app/src/components/notepad/hooks/use-notepad.ts @@ -113,7 +113,7 @@ export const useNotepad = ({ bookId }: UseNotepadProps = {}) => { const transformNoteForDisplay = useCallback( (note: Note) => ({ id: note.id, - preview: note.content || "", + preview: note.content || note.title || "", createdAt: new Date(note.createdAt), }), [], diff --git a/packages/app/src/components/notepad/note-detail-dialog.tsx b/packages/app/src/components/notepad/note-detail-dialog.tsx index 07d8ebc..45e7eef 100644 --- a/packages/app/src/components/notepad/note-detail-dialog.tsx +++ b/packages/app/src/components/notepad/note-detail-dialog.tsx @@ -1,25 +1,78 @@ -import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog"; +import { QuoteBlock } from "@/components/ui/quote-block"; +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; import { ScrollArea } from "@/components/ui/scroll-area"; +import { Textarea } from "@/components/ui/textarea"; +import { useNotepad } from "./hooks"; import type { Note } from "@/types/note"; import dayjs from "dayjs"; +import { useEffect, useMemo, useState } from "react"; interface NoteDetailDialogProps { note: Note | null; open: boolean; onOpenChange: (open: boolean) => void; + onNoteUpdated?: (note: Note) => void; } -export function NoteDetailDialog({ note, open, onOpenChange }: NoteDetailDialogProps) { +export function NoteDetailDialog({ + note, + open, + onOpenChange, + onNoteUpdated, +}: NoteDetailDialogProps) { + const { handleUpdateNote } = useNotepad({ bookId: note?.bookId }); + const [noteValue, setNoteValue] = useState(note?.content ?? ""); + const [saving, setSaving] = useState(false); + + useEffect(() => { + if (!note || !open) return; + setNoteValue(note.content ?? ""); + }, [note, open]); + + const hasChanges = useMemo(() => { + if (!note) return false; + const originalNote = note.content ?? ""; + return noteValue !== originalNote; + }, [note, noteValue]); + + const handleSave = async () => { + if (!note || !hasChanges) return; + try { + setSaving(true); + const updatedNote = await handleUpdateNote({ + id: note.id, + content: noteValue.trim().length > 0 ? noteValue.trim() : null, + }); + + if (updatedNote) { + onNoteUpdated?.(updatedNote); + } + onOpenChange(false); + } catch (error) { + console.error("更新笔记失败:", error); + } finally { + setSaving(false); + } + }; + if (!note) return null; return ( - {note.title || "无标题"} + 笔记详情 -
+
{dayjs(note.createdAt).format("YYYY-MM-DD HH:mm:ss")}
@@ -33,10 +86,48 @@ export function NoteDetailDialog({ note, open, onOpenChange }: NoteDetailDialogP
-
- {note.content || "暂无内容"} +
+ {note.title && ( +
+ + 引用内容 + + + {note.title} + +
+ )} +
+ + 我的想法 + +