diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 75c4665..e49a96d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -90,6 +90,9 @@ importers: react-icons: specifier: ^5.5.0 version: 5.5.0(react@19.2.4) + react-router: + specifier: ^7.13.1 + version: 7.13.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) wanakana: specifier: ^5.3.1 version: 5.3.1 @@ -1442,6 +1445,16 @@ packages: resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==} engines: {node: '>=0.10.0'} + react-router@7.13.1: + resolution: {integrity: sha512-td+xP4X2/6BJvZoX6xw++A2DdEi++YypA69bJUV5oVvqf6/9/9nNlD70YO1e9d3MyamJEBQFEzk6mbfDYbqrSA==} + engines: {node: '>=20.0.0'} + peerDependencies: + react: '>=18' + react-dom: '>=18' + peerDependenciesMeta: + react-dom: + optional: true + react@19.2.4: resolution: {integrity: sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==} engines: {node: '>=0.10.0'} @@ -3321,6 +3334,14 @@ snapshots: react-refresh@0.17.0: {} + react-router@7.13.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4): + dependencies: + cookie: 1.1.1 + react: 19.2.4 + set-cookie-parser: 2.7.2 + optionalDependencies: + react-dom: 19.2.4(react@19.2.4) + react@19.2.4: {} real-require@0.2.0: {} diff --git a/src-core/src/db/yomitan.rs b/src-core/src/db/yomitan.rs index 0c9b610..fa6b4d0 100644 --- a/src-core/src/db/yomitan.rs +++ b/src-core/src/db/yomitan.rs @@ -15,7 +15,6 @@ pub struct YomitanRow { rules: String, score: i64, glossary_json: String, - glossary_b: Vec, sequence: Option, term_tags: String, dict_title: String, @@ -118,26 +117,27 @@ impl YomitanDatabase { " ))?; let rows = stmt.query_map(params![q_term_norm, q_reading, limit, offset], |r| { - Ok(YomitanRow { - term: r.get(0)?, - reading: r.get(1)?, - def_tags: r.get(2)?, - rules: r.get(3)?, - score: r.get(4)?, - glossary_json: String::new(), - glossary_b: r.get(5)?, - sequence: r.get(6)?, - term_tags: r.get(7)?, - dict_title: r.get(8)?, - }) + Ok(( + YomitanRow { + term: r.get(0)?, + reading: r.get(1)?, + def_tags: r.get(2)?, + rules: r.get(3)?, + score: r.get(4)?, + glossary_json: String::from("[]"), // a valid JSON array + sequence: r.get(6)?, + term_tags: r.get(7)?, + dict_title: r.get(8)?, + }, + r.get::<_, Vec>(5)?, + )) })?; let mut out = Vec::new(); for row in rows { - let mut r = row?; + let (mut r, glossary_b) = row?; r.glossary_json = - String::from_utf8(decompressor.decompress(&r.glossary_b, MAX_DECOMPRESSED_SIZE)?)?; - r.glossary_b = vec![]; + String::from_utf8(decompressor.decompress(&glossary_b, MAX_DECOMPRESSED_SIZE)?)?; out.push(r); } diff --git a/src-react/package.json b/src-react/package.json index 8010e43..5963a90 100644 --- a/src-react/package.json +++ b/src-react/package.json @@ -21,6 +21,7 @@ "react": "^19.1.0", "react-dom": "^19.1.0", "react-icons": "^5.5.0", + "react-router": "^7.13.1", "wanakana": "^5.3.1", "zustand": "^5.0.11" }, diff --git a/src-react/src/App.tsx b/src-react/src/App.tsx index d9f1251..7d75110 100644 --- a/src-react/src/App.tsx +++ b/src-react/src/App.tsx @@ -1,14 +1,27 @@ import "./App.css"; +import { createBrowserRouter } from "react-router"; +import { RouterProvider } from "react-router/dom"; + import LoadingDialog from "./components/LoadingDialog"; import { Provider } from "./components/ui/provider"; -import SearchPage from "./pages/SearchPage"; + +const router = createBrowserRouter([ + { + path: "/", + lazy: async () => { + return { + Component: (await import("./pages/SearchPage")).default, + }; + }, + }, +]); function App() { return ( <> - + diff --git a/src-react/src/pages/SearchPage.tsx b/src-react/src/pages/SearchPage.tsx index 25f9627..7ed0351 100644 --- a/src-react/src/pages/SearchPage.tsx +++ b/src-react/src/pages/SearchPage.tsx @@ -5,6 +5,7 @@ import { useRef, useState, } from "react"; +import { useSearchParams } from "react-router"; import { toKana } from "wanakana"; import { @@ -35,16 +36,27 @@ interface Entry { } function SearchPage() { - const [q, setQ] = useState(""); - const [entries, setEntries] = useState([]); - const [searchTimeout, setSearchTimeout] = useState(0); - const [furigana, setFurigana] = useState(""); + const [searchParams, set_searchParams] = useSearchParams(); + + useEffect(() => { + let q = searchParams.get("q") || ""; + if (q) set_q(q); + }, [searchParams]); + + function searchParams_setQ(q: string) { + if (searchParams.get("q") === q) return; + set_searchParams({ q }); + } + + const [q, set_q] = useState(""); + const [entries, set_entries] = useState([]); + const [searchTimeout, set_searchTimeout] = useState(0); + const [furigana, set_furigana] = useState(""); const [isAutoKana, set_isAutoKana] = useState(true); const nSearch = useRef(0); const searchboxRef = useRef(null); const resultScrollRef = useRef(null); - const prevNormQ = useRef(""); const lang = "ja-JP"; @@ -59,7 +71,7 @@ function SearchPage() { set_clipboardText((clipboardText) => { if (clipboardText !== newText) { - setQ(newText + " "); + searchParams_setQ(newText + " "); return newText; } return clipboardText; @@ -75,7 +87,7 @@ function SearchPage() { function trySearch() { if (!q.trim()) { - setEntries([]); + set_entries([]); return; } @@ -88,7 +100,7 @@ function SearchPage() { nSearch.current += 1; - setSearchTimeout( + set_searchTimeout( setTimeout(() => { if (nSearch.current > 1) runSearch(); }, 250), @@ -103,8 +115,6 @@ function SearchPage() { norm_q = norm_q.replace(/n$/i, "ん").replace(/[a-z]$/i, ""); } if (!norm_q) return; - if (isNew && prevNormQ.current === norm_q) return; - prevNormQ.current = norm_q; const ender = /\p{Z}$/u.test(q) ? "" : "*"; @@ -149,7 +159,7 @@ function SearchPage() { segs.map((s) => s.surface).join("") + " "; - setQ(newQ); + searchParams_setQ(newQ); return; } } @@ -158,9 +168,9 @@ function SearchPage() { resultScrollRef.current.scrollTop = 0; } - setEntries(result); + set_entries(result); } else { - setEntries([...entries, ...result]); + set_entries([...entries, ...result]); } return result; @@ -195,7 +205,7 @@ function SearchPage() { }); } - setQ(q); + set_q(q); } const FURIGANA_REGEX = /^[\p{scx=Hiragana}\p{scx=Katakana}]+$/u; @@ -205,7 +215,7 @@ function SearchPage() { data: compositionData, }) => { if (FURIGANA_REGEX.test(compositionData)) { - setFurigana(compositionData); + set_furigana(compositionData); } }; @@ -237,7 +247,7 @@ function SearchPage() { .map((p, idx) => (idx & 1 ? `<${p}>[${rt[idx]}]` : p)) .join(""); - setQ((q) => { + set_q((q) => { if (!(target instanceof HTMLInputElement)) return markup; let { selectionStart } = target; @@ -264,12 +274,24 @@ function SearchPage() { size={"xs"} me={-2} onClick={() => { - setQ(""); + searchParams_setQ(q); + searchParams_setQ(""); + set_q(""); searchboxRef.current ? searchboxRef.current.focus() : null; }} /> ) : null; + function onTermClicked(t: string) { + searchParams_setQ(q); + searchParams_setQ(t + " "); + } + + function JSONdumpClean(o: Record) { + const p = Object.entries(o).filter(([, v]) => v || v === 0); + return p.length ? JSON.stringify(Object.fromEntries(p), null, 1) : null; + } + return (
- - {term} - {reading ? ` (${reading})` : ""} 【{dict_title}】 - {JSON.stringify(it, (_, v) => v || undefined)} + + onTermClicked(term + " ")}>{term} + {reading && reading !== term ? ( + <> + {"("} + onTermClicked(reading + " ")}> + {reading} + + {")"} + + ) : null} + 【{dict_title}】{JSONdumpClean(it)}
setQ(t + " ")} + onTermClicked={onTermClicked} /> ))}