From ff64c035e6ee73b1f0b2da941a1f51cfd6598d74 Mon Sep 17 00:00:00 2001 From: polv Date: Wed, 25 Feb 2026 19:01:55 +0700 Subject: [PATCH] yomitan init on UI --- src-core/src/db.rs | 13 --- src-core/src/db/yomitan_writer.rs | 26 +++-- src-core/src/error.rs | 3 + src-core/src/service.rs | 116 ++++++++++++++++++++- src-react/src/components/LoadingDialog.tsx | 84 +++++++++++++-- src-react/src/pages/SearchPage.tsx | 12 +-- src-tauri/src/command.rs | 31 +++++- src-tauri/src/lib.rs | 6 +- 8 files changed, 245 insertions(+), 46 deletions(-) diff --git a/src-core/src/db.rs b/src-core/src/db.rs index 2abf611..f78acdf 100644 --- a/src-core/src/db.rs +++ b/src-core/src/db.rs @@ -46,19 +46,6 @@ impl Database { }) } - pub fn is_yomitan_dbfile_init(&self) -> Result { - let yomitan_db_path = self.dir.join(YOMITAN_DBFILE); - - if yomitan_db_path.exists() { - return Ok(false); - } - - let conn = Connection::open(yomitan_db_path)?; - let mut stmt = conn.prepare("SELECT 1 FROM dictionaries")?; - - Ok(stmt.exists([])?) - } - pub fn yomitan(&self) -> Result { self.ensure_yomitan_attached()?; Ok(yomitan::YomitanDatabase::new(self.clone())) diff --git a/src-core/src/db/yomitan_writer.rs b/src-core/src/db/yomitan_writer.rs index fe53157..74aedec 100644 --- a/src-core/src/db/yomitan_writer.rs +++ b/src-core/src/db/yomitan_writer.rs @@ -19,7 +19,7 @@ pub(super) fn normalize_term(s: &str) -> String { .collect() } -#[derive(Serialize, Debug)] +#[derive(Serialize, Debug, Clone)] pub struct YomitanZipImportProgress { pub message: String, pub current: u32, @@ -69,6 +69,7 @@ impl YomitanWriter { description TEXT, sort_order INTEGER NOT NULL DEFAULT 0, installed_at TEXT NOT NULL DEFAULT (datetime('now')), + bundle_name TEXT, -- custom lang TEXT NOT NULL, -- custom UNIQUE (title, revision) ); @@ -126,7 +127,7 @@ impl YomitanWriter { self.conn.execute_batch( " - CREATE INDEX IF NOT EXISTS dictionaries_sort_order ON dctionaries (sort_order DESC); + CREATE INDEX IF NOT EXISTS dictionaries_sort_order ON dictionaries (sort_order DESC); CREATE INDEX IF NOT EXISTS dictionaries_lang ON dictionaries (lang); CREATE INDEX IF NOT EXISTS terms_dict_id ON terms (dict_id); @@ -246,7 +247,13 @@ impl YomitanWriter { where Callback: Fn(YomitanZipImportProgress), { - self.create_schema()?; // < 1 ms + let bundle_name = if let Some(fo) = zip_file.file_name() + && let Some(f) = fo.to_str() + { + f + } else { + return Err(CJDicError::FileNameNotFound); + }; let start_time = Instant::now(); self.conn.execute_batch("PRAGMA foreign_keys = off")?; @@ -274,7 +281,7 @@ impl YomitanWriter { let mut steps: u64 = 0; progress_callback(YomitanZipImportProgress { - message: format!("Extracted {}", zip_file.display()), + message: format!("Opening {}", bundle_name), current: 0, total: 0, steps, @@ -327,7 +334,9 @@ impl YomitanWriter { let tx = self.conn.transaction()?; tx.execute( - "INSERT INTO dictionaries (title, revision, author, url, description, lang) VALUES (?1, ?2, ?3, ?4, ?5, ?6)", + "INSERT INTO dictionaries + (title, revision, author, url, description, lang, bundle_name) VALUES + ( ?1, ?2, ?3, ?4, ?5, ?6, ?7)", params![ title, revision, @@ -335,6 +344,7 @@ impl YomitanWriter { index_file.get("url").and_then(Value::as_str), index_file.get("description").and_then(Value::as_str), lang, + bundle_name, ], )?; @@ -391,7 +401,7 @@ impl YomitanWriter { steps += entries.len() as u64; progress_callback(YomitanZipImportProgress { - message: format!("Reading term bank {} of {}", bank_i, n_bank), + message: format!("Reading term banks"), current: bank_i, total: n_bank, steps, @@ -492,7 +502,7 @@ impl YomitanWriter { steps += 1; progress_callback(YomitanZipImportProgress { - message: format!("Reading term meta bank"), + message: format!("Reading term meta banks"), current: 0, total: 0, steps, @@ -529,7 +539,7 @@ impl YomitanWriter { steps += 1; progress_callback(YomitanZipImportProgress { - message: format!("Reading tag bank"), + message: format!("Reading tag banks"), current: 0, total: 0, steps, diff --git a/src-core/src/error.rs b/src-core/src/error.rs index 6017c0e..8e948c7 100644 --- a/src-core/src/error.rs +++ b/src-core/src/error.rs @@ -15,6 +15,9 @@ pub enum CJDicError { #[error("IO error: {0}")] IOError(#[from] std::io::Error), + #[error("File name not found")] + FileNameNotFound, + #[error("Zip error: {0}")] ZipError(#[from] ZipError), diff --git a/src-core/src/service.rs b/src-core/src/service.rs index 4cbe64e..de3c855 100644 --- a/src-core/src/service.rs +++ b/src-core/src/service.rs @@ -1,6 +1,11 @@ -use std::path::{Path, PathBuf}; +use std::{ + collections::HashSet, + fs::read_dir, + path::{Path, PathBuf}, +}; use rusqlite::Connection; +use serde::Serialize; use crate::{ db::{ @@ -10,6 +15,12 @@ use crate::{ error::CJDicError, }; +#[derive(Serialize, Debug, Clone)] +pub struct LoadYomitanZipDirResult { + pub new_dicts: Vec, + pub to_be_removed_dicts: Vec, +} + pub struct AppService { db: Database, } @@ -20,10 +31,6 @@ impl AppService { Ok(Self { db }) } - pub fn is_yomitan_setup_yet(&self) -> Result { - Ok(self.db.is_yomitan_dbfile_init()?) - } - pub fn search_yomitan( &self, q_term: &str, @@ -44,6 +51,105 @@ impl AppService { Ok(writer) } + pub fn load_yomitan_zip_dir( + &self, + zip_dir: PathBuf, + lang: &str, + load_callback: LoadCallback, + import_callback: ImportCallback, + ) -> Result<(), CJDicError> + where + LoadCallback: Fn(LoadYomitanZipDirResult), + ImportCallback: Fn(YomitanZipImportProgress), + { + let mut dir_zip_list: HashSet = HashSet::new(); + + for entry in read_dir(&zip_dir)? { + let e = entry?; + let p = e.path(); + + if p.extension().and_then(|s| s.to_str()) == Some("zip") + && let Some(f) = e.file_name().to_str() + { + dir_zip_list.insert(f.to_string()); + } + } + + { + let conn = Connection::open(self.db.dir.join(YOMITAN_DBFILE))?; + let mut writer = YomitanWriter::new(conn)?; + writer.create_schema()?; + } + + let mut new_dicts: Vec = vec![]; + let mut to_be_removed_dicts: Vec = vec![]; + + { + let conn = Connection::open(self.db.dir.join(YOMITAN_DBFILE))?; + let mut stmt = + conn.prepare("SELECT DISTINCT bundle_name FROM dictionaries WHERE lang = ?1")?; + let mut rows = stmt.query([lang])?; + + let mut db_zip_list: HashSet = HashSet::new(); + while let Some(row) = rows.next()? { + let b_name: String = row.get(0)?; + db_zip_list.insert(b_name); + } + + for z in dir_zip_list.iter() { + if !db_zip_list.contains(z) { + new_dicts.push(z.to_string()); + } + } + + for z in db_zip_list.iter() { + if !dir_zip_list.contains(z) { + to_be_removed_dicts.push(z.to_string()); + } + } + } + + // Import ordering + new_dicts.sort_by_key(|s| { + if s.starts_with("[") + && let Some(end_i) = s.find(']') + { + s[(end_i + 1)..].trim_start().to_string() + } else { + s.to_string() + } + }); + to_be_removed_dicts.sort(); + + load_callback(LoadYomitanZipDirResult { + new_dicts: new_dicts.clone(), + to_be_removed_dicts: to_be_removed_dicts.clone(), + }); + + { + let conn = Connection::open(self.db.dir.join(YOMITAN_DBFILE))?; + for z in to_be_removed_dicts.iter() { + conn.execute("DELETE FROM dictionaries WHERE bundle_name = ?1", [z])?; + } + } + + { + let conn = Connection::open(self.db.dir.join(YOMITAN_DBFILE))?; + let mut writer = YomitanWriter::new(conn)?; + + for z in new_dicts.iter() { + Self::import_yomitan_zip_file( + &mut writer, + zip_dir.join(z), + lang, + &import_callback, + )?; + } + } + + Ok(()) + } + pub fn import_yomitan_zip_file( writer: &mut YomitanWriter, zip_file: PathBuf, diff --git a/src-react/src/components/LoadingDialog.tsx b/src-react/src/components/LoadingDialog.tsx index 7e08ea2..7f61bbe 100644 --- a/src-react/src/components/LoadingDialog.tsx +++ b/src-react/src/components/LoadingDialog.tsx @@ -1,19 +1,81 @@ -import { useEffect, useState } from "react"; +import { useEffect, useRef, useState } from "react"; import { Dialog, Portal } from "@chakra-ui/react"; import { invoke } from "@tauri-apps/api/core"; +import { listen } from "@tauri-apps/api/event"; + +interface LoadYomitanZipDirResult { + new_dicts: string[]; + to_be_removed_dicts: string[]; +} + +interface YomitanZipImportProgress { + message: string; + current: number; + total: number; + steps: number; +} + +interface Progress { + message: string; + current?: number; + total?: number; +} function LoadingDialog() { - const [isLoading, setIsLoading] = useState(false); + const [messages, setMessages] = useState([]); + + const isInit = useRef(false); useEffect(() => { - invoke("is_yomitan_setup_yet").then((is_setup) => - setIsLoading(!is_setup), + if (isInit.current) return; + isInit.current = true; + + invoke("init_yomitan").then(() => { + setMessages([]); + }); + + listen("load-yomitan-dir", ({ payload }) => { + setMessages((messages) => { + if (payload.new_dicts.length > 0) { + messages = [ + ...messages, + { message: `Loading:` }, + ...payload.new_dicts.map((d) => ({ message: `- ${d}` })), + ]; + } + + if (payload.to_be_removed_dicts.length > 0) { + messages = [ + ...messages, + { message: `Removing:` }, + ...payload.to_be_removed_dicts.map((d) => ({ message: `- ${d}` })), + ]; + } + + return messages; + }); + }); + + listen( + "yomitan-import-progress", + ({ payload }) => { + setMessages((messages) => { + const prev = messages[messages.length - 1]; + if (prev?.message === payload.message) { + messages = messages.slice(0, messages.length - 1); + } + + messages = [...messages, payload]; + + return messages; + }); + }, ); }, []); return ( - + 0}> @@ -23,7 +85,17 @@ function LoadingDialog() { - Example text + + {messages.length + ? messages.map((m, i) => ( +

+ {m.total + ? `${m.message} (${m.current} / ${m.total})` + : m.message} +

+ )) + : "Loading..."} +
diff --git a/src-react/src/pages/SearchPage.tsx b/src-react/src/pages/SearchPage.tsx index 89cbf49..d733d9b 100644 --- a/src-react/src/pages/SearchPage.tsx +++ b/src-react/src/pages/SearchPage.tsx @@ -1,4 +1,4 @@ -import { CompositionEventHandler, useEffect, useState } from "react"; +import { CompositionEventHandler, useEffect, useRef, useState } from "react"; import { Box, Button, Card, Group, Input, Stack } from "@chakra-ui/react"; import { invoke } from "@tauri-apps/api/core"; @@ -22,6 +22,7 @@ function SearchPage() { const [entries, setEntries] = useState([]); const [searchTimeout, setSearchTimeout] = useState(0); const [furigana, setFurigana] = useState(""); + const nSearch = useRef(0); const lang = "ja-JP"; @@ -35,19 +36,18 @@ function SearchPage() { return; } - let willRunSearch = runSearch; - if (searchTimeout) { clearTimeout(searchTimeout); - } else { + } else if (nSearch.current === 0) { // If new, just run immediately. No need for throttling. runSearch(); - willRunSearch = async () => {}; } + nSearch.current += 1; + setSearchTimeout( setTimeout(() => { - willRunSearch(); + if (nSearch.current > 1) runSearch(); }, 250), ); } diff --git a/src-tauri/src/command.rs b/src-tauri/src/command.rs index 5cf4291..7ae0f1f 100644 --- a/src-tauri/src/command.rs +++ b/src-tauri/src/command.rs @@ -1,18 +1,41 @@ // Learn more about Tauri commands at https://tauri.app/develop/calling-rust/ use cjdic2_core::{AppService, YomitanRow}; +use tauri::{AppHandle, Emitter, Manager, path::BaseDirectory}; #[tauri::command] -pub fn is_yomitan_setup_yet(state: tauri::State) -> Result { - state.is_yomitan_setup_yet().map_err(|e| e.to_string()) +pub async fn init_yomitan( + app: AppHandle, + state: tauri::State<'_, AppService>, +) -> Result<(), String> { + let zip_dir = app + .path() + .resolve("resources/yomitan", BaseDirectory::Resource) + .map_err(|e| e.to_string())?; + // println!("{:?}", zip_dir.as_path()); + + state + .load_yomitan_zip_dir( + zip_dir, + "ja", + |r| { + app.emit("load-yomitan-dir", r).unwrap(); + }, + |r| { + app.emit("yomitan-import-progress", r).unwrap(); + }, + ) + .map_err(|e| e.to_string())?; + + Ok(()) } #[tauri::command] -pub fn search_yomitan( +pub async fn search_yomitan( q_term: &str, q_reading: &str, limit: u32, offset: u32, - state: tauri::State, + state: tauri::State<'_, AppService>, ) -> Result, String> { state .search_yomitan(q_term, q_reading, limit, offset) diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index b6a32a2..d4e0a84 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -13,6 +13,7 @@ pub fn run() { .setup(|app| { let app_dir = app.path().app_config_dir()?; create_dir_all(&app_dir)?; + // println!("{:?}", app_dir); // db_path is app_dir; let db_dir = app_dir.as_path(); @@ -22,10 +23,7 @@ pub fn run() { Ok(()) }) - .invoke_handler(tauri::generate_handler![ - is_yomitan_setup_yet, - search_yomitan - ]) + .invoke_handler(tauri::generate_handler![init_yomitan, search_yomitan]) .build(tauri::generate_context!()) .expect("error while running tauri application") .run(|_app_handle, event| match event {