import { CompositionEventHandler, UIEventHandler, useEffect, useRef, useState, } from "react"; import { toKana } from "wanakana"; import { Box, Card, CloseButton, Group, Input, InputGroup, Stack, Switch, } from "@chakra-ui/react"; import { invoke } from "@tauri-apps/api/core"; import { readText } from "@tauri-apps/plugin-clipboard-manager"; import Glossary from "../components/Glossary"; interface Entry { term: string; reading: string; glossary_json: string; def_tags: string; rules: string; score: number; sequence: number; term_tags: string; dict_title: string; } function SearchPage() { const [q, setQ] = useState(""); const [entries, setEntries] = useState([]); const [searchTimeout, setSearchTimeout] = useState(0); const [furigana, setFurigana] = useState(""); const [isAutoKana, set_isAutoKana] = useState(true); const nSearch = useRef(0); const searchboxRef = useRef(null); const lang = "ja-JP"; const [isMonitorClipboard, set_isMonitorclipboard] = useState(false); const [, set_clipboardText] = useState(""); useEffect(() => { if (!isMonitorClipboard) return; const intervalId = setInterval(async () => { const newText = await readText(); set_clipboardText((clipboardText) => { if (clipboardText !== newText) { setQ(newText + " "); return newText; } return clipboardText; }); }, 500); return () => clearInterval(intervalId); }, [isMonitorClipboard]); useEffect(() => { trySearch(); }, [q]); function trySearch() { if (!q.trim()) { setEntries([]); return; } if (searchTimeout) { clearTimeout(searchTimeout); } else if (nSearch.current === 0) { // If new, just run immediately. No need for throttling. runSearch(); } nSearch.current += 1; setSearchTimeout( setTimeout(() => { if (nSearch.current > 1) runSearch(); }, 250), ); } async function runSearch(isNew = true) { let norm_q = q.trim(); if (isAutoKana) { norm_q = norm_q.replace(/n$/i, "ん").replace(/[a-z]$/i, ""); } if (!norm_q) return; const ender = /\p{Z}$/u.test(q) ? "" : "*"; const re = /\<(.+?)\>\[(.+?)\]/g; let qTerm = ""; let qReading = ""; norm_q.split(re).map((s, i) => { switch (i % 3) { case 1: qTerm += s; break; case 2: qReading += s; break; default: qTerm += s; qReading += s; } }); qTerm += ender; qReading += ender; const result = await invoke("search_yomitan", { qTerm, qReading, limit: 10, offset: isNew ? 0 : entries.length, }); if (isNew) { setEntries(result); } else { setEntries([...entries, ...result]); } return result; } const onEntriesScrollEnd: UIEventHandler = async ({ target }) => { if (!(target instanceof HTMLElement)) return; const { scrollHeight, scrollTop } = target; if (scrollHeight - 20 > scrollTop + target.getBoundingClientRect().height) return; const results = await runSearch(false); if (!results?.length) { target.style.paddingBottom = "50vh"; return; } setTimeout(() => { target.scrollTop = scrollTop + 100; }, 100); }; function onSearchboxChange(q: string) { if (isAutoKana) { q = toKana(q, { useObsoleteKana: true, IMEMode: true }); } setQ(q); } const FURIGANA_REGEX = /^[\p{scx=Hiragana}\p{scx=Katakana}]+$/u; const KANJI_REGEX = /([\p{sc=Han}\p{N}々〆ヵヶ]+)/u; const updateFurigana: CompositionEventHandler = ({ data: compositionData, }) => { if (FURIGANA_REGEX.test(compositionData)) { setFurigana(compositionData); } }; const addFurigana: CompositionEventHandler = ({ data: compositionData }) => { const cleanedFuri = [...furigana.replace(/n/g, "ん")] .map((c) => katakanaToHiragana(c)) .join(""); let parts = compositionData.split(KANJI_REGEX); if (parts.length === 1) return; const hiraganaParts = parts.map((p) => [...p].map((c) => katakanaToHiragana(c)).join(""), ); const regex = new RegExp( `^${hiraganaParts.map((p, idx) => `(${idx & 1 ? ".+" : p})`).join("")}$`, ); let rt: (string | null)[] = furigana.match(regex) || []; if (!rt.length) { parts = [compositionData]; rt = [null, cleanedFuri]; } rt.shift(); const markup = parts .map((p, idx) => (idx & 1 ? `<${p}>[${rt[idx]}]` : p)) .join(""); setQ(markup); }; function katakanaToHiragana(k: string) { let c = k.charCodeAt(0); return c >= 12449 && c <= 12531 ? String.fromCharCode(k.charCodeAt(0) - 96) : k; } const clearButton = q.trim() ? ( { setQ(""); searchboxRef.current ? searchboxRef.current.focus() : null; }} /> ) : null; return (
{ e.preventDefault(); }} > onSearchboxChange(e.currentTarget.value)} onCompositionUpdate={updateFurigana} onCompositionEnd={addFurigana} autoComplete="off" spellCheck={false} placeholder="Search..." /> {/* */}
set_isAutoKana(d.checked)} > Auto-convert Kana set_isMonitorclipboard(d.checked)} > Monitor clipboard
{entries.map( ({ term, reading, dict_title, glossary_json, ...it }, i) => (
  • {term} {reading ? ` (${reading})` : ""} 【{dict_title}】 {JSON.stringify(it, (_, v) => v || undefined)}
    {JSON.parse(glossary_json).map((g: any, i: number) => ( setQ(t + " ")} /> ))}
  • ), )}
    ); } export default SearchPage;