diff --git a/src-react/src/App.css b/src-react/src/App.css index cd0395b..8918e13 100644 --- a/src-react/src/App.css +++ b/src-react/src/App.css @@ -38,4 +38,9 @@ body { padding-bottom: env(safe-area-inset-bottom); padding-left: env(safe-area-inset-left); padding-right: env(safe-area-inset-right); +} + +html { + height: 100vh; + overflow-y: hidden; } \ No newline at end of file diff --git a/src-react/src/App.tsx b/src-react/src/App.tsx index 8c0d797..d9f1251 100644 --- a/src-react/src/App.tsx +++ b/src-react/src/App.tsx @@ -7,8 +7,10 @@ import SearchPage from "./pages/SearchPage"; function App() { return ( - - + <> + + + ); } diff --git a/src-react/src/components/Glossary.tsx b/src-react/src/components/Glossary.tsx index 19d8be0..66e538a 100644 --- a/src-react/src/components/Glossary.tsx +++ b/src-react/src/components/Glossary.tsx @@ -2,19 +2,29 @@ import { createElement as h } from "react"; export default function Glossary({ glossary, + maxLength, onTermClicked, }: { glossary: any; + maxLength?: number; onTermClicked: (t: string) => void; }) { + function trimString(s: any) { + if (!maxLength) return s; + if (typeof s === "string") { + return s.substring(0, maxLength); + } + return s; + } + // Plain string if (typeof glossary === "string") { - return glossary; + return trimString(glossary); } // GlossaryText if ("type" in glossary && glossary.type === "text") { - return glossary.text; + return trimString(glossary.text); } // GlossaryImage @@ -26,7 +36,11 @@ export default function Glossary({ if ("type" in glossary && glossary.type === "structured-content") { const sc = glossary; return ( - + ); } @@ -58,22 +72,37 @@ function GlossaryImage(img: any) { function StructuredContent({ node, + maxLength, onTermClicked, }: { node: any; + maxLength?: number; onTermClicked: (t: string) => void; }): any { if (!node) return null; + function trimString(s: any) { + if (!maxLength) return s; + if (typeof s === "string") { + return s.substring(0, maxLength); + } + return s; + } + if (typeof node === "string") { - return node; + return trimString(node); } if (Array.isArray(node)) { return ( <> {...node.map((n, i) => ( - + ))} ); @@ -114,7 +143,11 @@ function StructuredContent({ }} > {content ? ( - + ) : null} ); @@ -126,6 +159,6 @@ function StructuredContent({ return h( tag, { style: block.style, title: block.title, open: block.open }, - StructuredContent({ node: block.content, onTermClicked }), + StructuredContent({ node: block.content, maxLength, onTermClicked }), ); } diff --git a/src-react/src/pages/SearchPage.tsx b/src-react/src/pages/SearchPage.tsx index d1cdb21..9fc9b04 100644 --- a/src-react/src/pages/SearchPage.tsx +++ b/src-react/src/pages/SearchPage.tsx @@ -1,4 +1,10 @@ -import { CompositionEventHandler, useEffect, useRef, useState } from "react"; +import { + CompositionEventHandler, + UIEventHandler, + useEffect, + useRef, + useState, +} from "react"; import { toHiragana } from "wanakana"; import { @@ -65,7 +71,7 @@ function SearchPage() { ); } - async function runSearch() { + async function runSearch(isNew = true) { const ender = /\p{Z}$/u.test(q) ? "" : "*"; const re = /\<(.+?)\>\[(.+?)\]/g; @@ -73,35 +79,56 @@ function SearchPage() { let qTerm = ""; let qReading = ""; - q.trim() - .split(re) - .map((s, i) => { - switch (i % 3) { - case 1: - qTerm += s; - break; - case 2: - qReading += s; - break; - default: - qTerm += s; - qReading += s; - } - }); + let norm_q = q.trim(); + if (isAutoKana) { + norm_q = norm_q.replace(/n$/i, "ん").replace(/[a-z]$/i, ""); + } + + 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; - setEntries( - await invoke("search_yomitan", { - qTerm, - qReading, - limit: 10, - offset: 0, - }), - ); + const result = await invoke("search_yomitan", { + qTerm, + qReading, + limit: 10, + offset: isNew ? 0 : entries.length, + }); + + if (isNew) { + setEntries(result); + } else { + setEntries([...entries, ...result]); + } } + const onEntriesScrollEnd: UIEventHandler = async ({ target }) => { + if (!(target instanceof HTMLElement)) return; + const { scrollHeight, scrollTop } = target; + + if (scrollHeight - 20 > scrollTop + target.getBoundingClientRect().height) + return; + + await runSearch(false); + + setTimeout(() => { + target.scrollTop = scrollTop + 100; + }, 100); + }; + function onSearchboxChange(q: string) { if (isAutoKana) { q = toHiragana(q, { useObsoleteKana: true, IMEMode: true }); @@ -167,7 +194,16 @@ function SearchPage() { ) : null; return ( - +
{ @@ -201,18 +237,20 @@ function SearchPage() {
- + {entries.map( ({ term, reading, dict_title, glossary_json, ...it }, i) => ( -
  • -
    - - {term} - {reading ? ` (${reading})` : ""} 【{dict_title}】 - - +
  • +
    + {term} + {reading ? ` (${reading})` : ""} 【{dict_title}】 {JSON.stringify(it, (_, v) => v || undefined)} @@ -223,8 +261,9 @@ function SearchPage() { whiteSpace: "pre-wrap", }} > - {JSON.parse(glossary_json).map((g: any) => ( + {JSON.parse(glossary_json).map((g: any, i: number) => ( setQ(t + " ")} /> @@ -232,7 +271,7 @@ function SearchPage() {
    -
  • +
  • ), )}