diff --git a/src-core/src/db/yomitan_writer.rs b/src-core/src/db/yomitan_writer.rs index ecc0183..5ca42fa 100644 --- a/src-core/src/db/yomitan_writer.rs +++ b/src-core/src/db/yomitan_writer.rs @@ -113,6 +113,8 @@ impl YomitanWriter { installed_at TEXT NOT NULL DEFAULT (datetime('now')), bundle_name TEXT, -- custom lang TEXT NOT NULL, -- custom + asset_count INTEGER, -- custom + source TEXT, -- custom UNIQUE (title, revision) ); CREATE TABLE IF NOT EXISTS glossaries ( -- not used. moved to a new file @@ -186,7 +188,7 @@ impl YomitanWriter { self.ensure_db_attached(DbChild::YomitanGlossary)?; self.ensure_db_attached(DbChild::Search)?; - let schema_version = "2026-03-08"; + let schema_version = "2026-03-21"; let current_schema_version = { let mut stmt = self .conn @@ -275,6 +277,15 @@ impl YomitanWriter { )?; } + if v < "2026-03-21" { + self.conn.execute_batch( + " + ALTER TABLE dictionaries ADD COLUMN asset_count INTEGER; + ALTER TABLE dictionaries ADD COLUMN source TEXT; + ", + )?; + } + if v < schema_version { self.conn .prepare("UPDATE schema_meta SET value = ?2 WHERE key = ?1")? @@ -454,6 +465,8 @@ impl YomitanWriter { let cursor = Cursor::new(zip_file.bytes()?); let mut archive = ZipArchive::new(cursor)?; + let mut asset_count = 0; + for i in 0..archive.len() { // Metadata — borrow ends before by_index_raw if let (Some(asset_name), is_dir) = { @@ -481,6 +494,8 @@ impl YomitanWriter { let mut entry = archive.by_index(i)?; let mut out = File::create(&out_path)?; io::copy(&mut entry, &mut out)?; // writes plain bytes + + asset_count += 1; } } @@ -491,7 +506,6 @@ impl YomitanWriter { serde_json::from_str::(&s)? } Err(e) => { - // TODO: proper error handling eprintln!("{}", e); return Ok(YomitanZipImportResult { exists: false, @@ -534,17 +548,21 @@ impl YomitanWriter { .to_string(); { - let title: &str = &title; - // Skip if already installed + // Skip if already installed or older revision let mut stmt = self .conn - .prepare("SELECT (revision < ?2) FROM dictionaries WHERE title = ?1")?; - let mut rows = stmt.query(params![title, revision])?; + .prepare("SELECT (revision < ?2) FROM dictionaries WHERE bundle_name = ?1")?; + let mut rows = stmt.query(params![bundle_name, revision])?; if let Some(row) = rows.next()? { let is_outdated: bool = row.get(0)?; if is_outdated { self.drop_dictionary(bundle_name, lang)?; } else { + self.conn.execute( + "UPDATE dictionaries SET asset_count = ?2 WHERE bundle_name = ?1", + params![bundle_name, asset_count], + )?; + return Ok(YomitanZipImportResult { exists: true, load: false, @@ -560,8 +578,8 @@ impl YomitanWriter { let tx = self.conn.transaction()?; tx.execute( "INSERT INTO dictionaries - (title, revision, author, url, description, lang, bundle_name) VALUES - ( ?1, ?2, ?3, ?4, ?5, ?6, ?7)", + (title, revision, author, url, description, lang, bundle_name, asset_count) VALUES + ( ?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)", params![ title, revision, @@ -570,6 +588,7 @@ impl YomitanWriter { index_file.get("description").and_then(Value::as_str), lang, bundle_name, + asset_count, ], )?; diff --git a/src-core/src/service.rs b/src-core/src/service.rs index 13fe112..c36dc5b 100644 --- a/src-core/src/service.rs +++ b/src-core/src/service.rs @@ -188,12 +188,21 @@ impl AppService { let mut new_dicts: Vec = vec![]; let mut to_be_removed_dicts: Vec = vec![]; + { + let mut writer = YomitanWriter::new(self.db.dir.clone())?; + writer.create_schema(&import_callback)?; + } + let db_path = self.db.dir.join(DBFILE[DbChild::Yomitan]); if db_path.exists() { let conn = Connection::open(&db_path)?; - let mut stmt = - conn.prepare("SELECT DISTINCT bundle_name FROM dictionaries WHERE lang = ?1")?; + let mut stmt = conn.prepare( + " + SELECT DISTINCT bundle_name FROM dictionaries + WHERE lang = ?1 AND asset_count IS NOT NULL + ", + )?; let mut rows = stmt.query([lang])?; let mut db_zip_list: HashSet = HashSet::new(); @@ -228,11 +237,6 @@ impl AppService { to_be_removed_dicts: to_be_removed_dicts.clone(), }); - { - let mut writer = YomitanWriter::new(self.db.dir.clone())?; - writer.create_schema(&import_callback)?; - } - { let conn = Connection::open(&db_path)?; for z in to_be_removed_dicts.clone().iter() { diff --git a/src-react/src/App.css b/src-react/src/App.css index 8918e13..141354c 100644 --- a/src-react/src/App.css +++ b/src-react/src/App.css @@ -43,4 +43,14 @@ body { html { height: 100vh; overflow-y: hidden; +} + +html.dark .glossaries { + span { + word-break: keep-all; + } + + img { + filter: invert(1) hue-rotate(180deg); + } } \ No newline at end of file diff --git a/src-react/src/components/Glossary.tsx b/src-react/src/components/Glossary.tsx index 21d817b..b5d7323 100644 --- a/src-react/src/components/Glossary.tsx +++ b/src-react/src/components/Glossary.tsx @@ -57,14 +57,13 @@ export default function Glossary({ function GlossaryImage({ img, yomitanURL }: { img: any; yomitanURL: string }) { return ( {img.alt} diff --git a/src-react/src/components/LoadingDialog.tsx b/src-react/src/components/LoadingDialog.tsx index 5bae483..94973b6 100644 --- a/src-react/src/components/LoadingDialog.tsx +++ b/src-react/src/components/LoadingDialog.tsx @@ -3,6 +3,7 @@ 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"; +import { fetch } from "@tauri-apps/plugin-http"; import { supabase } from "../lib/supabaseClient"; @@ -13,6 +14,13 @@ interface YomitanDictEntry { lang: string; } +interface YomitanStorageWorkerDictEntry { + key: string; + size: number; + uploaded: string; + url: string; +} + interface YomitanDownloadDictEntry { url: string; filepath: string; @@ -119,6 +127,52 @@ function LoadingDialog() { }); } } + + const { VITE_STORAGE_WORKER_URL, VITE_STORAGE_WORKER_TOKEN } = + import.meta.env; + + try { + const prefix = `${lang}/`; + + let entries: YomitanStorageWorkerDictEntry[] = []; + + const LAST_API_CALL_KEY = "LAST_API_CALL_KEY_storageWorkerURL"; + const LAST_API_JSON_KEY = "LAST_API_JSON_KEY_storageWorkerURL"; + + const lastCall = localStorage.getItem(LAST_API_CALL_KEY); + const lastJSON = localStorage.getItem(LAST_API_JSON_KEY); + + if (lastJSON && lastCall) { + const d = new Date(lastCall); + d.setDate(d.getDate() + 1); + if (d > new Date()) { + entries = JSON.parse(lastJSON); + } + } + if (!entries.length) { + console.log(`visiting ${VITE_STORAGE_WORKER_URL}`); + + entries = await fetch(VITE_STORAGE_WORKER_URL + "/files", { + headers: { + Authorization: `Bearer ${VITE_STORAGE_WORKER_TOKEN}`, + }, + }).then((r) => r.json()); + + localStorage.setItem(LAST_API_CALL_KEY, new Date().toISOString()); + localStorage.setItem(LAST_API_JSON_KEY, JSON.stringify(entries)); + } + + dicts.push( + ...entries + .filter((o) => o.key.startsWith(prefix)) + .map((o) => ({ + filepath: `yomitan/${lang}/${o.key.substring(prefix.length)}`, + url: o.url, + })), + ); + } catch (e) { + console.error(e); + } } await invoke("init_yomitan", { dicts, lang }); diff --git a/src-react/src/pages/SearchPage.tsx b/src-react/src/pages/SearchPage.tsx index 9bed28e..c61f656 100644 --- a/src-react/src/pages/SearchPage.tsx +++ b/src-react/src/pages/SearchPage.tsx @@ -412,6 +412,7 @@ function SearchPage() {