From a00efbf5058e4b9d56da85fb74c90b174701a637 Mon Sep 17 00:00:00 2001 From: polv Date: Tue, 10 Mar 2026 01:11:36 +0700 Subject: [PATCH] make starting from zero work on Android + Windows --- Cargo.lock | 1 + package.json | 9 +++-- pnpm-lock.yaml | 62 +++++++++++++++++++++++++++++++ src-core/src/db/search.rs | 3 +- src-core/src/db/yomitan_writer.rs | 44 +++++++++++++++++++--- src-core/src/error.rs | 28 +++++++++----- src-core/src/service.rs | 17 +++++++-- src-core/src/tokenizer.rs | 5 +-- src-tauri/Cargo.toml | 1 + src-tauri/src/lib.rs | 40 +++++++++++--------- 10 files changed, 166 insertions(+), 44 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0a60575..51a5b24 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -645,6 +645,7 @@ dependencies = [ "tauri-plugin-opener", "tokio", "zip", + "zstd", ] [[package]] diff --git a/package.json b/package.json index 56f5037..3a19021 100644 --- a/package.json +++ b/package.json @@ -4,9 +4,9 @@ "version": "0.1.0", "type": "module", "scripts": { - "dev:desktop": "tauri dev", - "dev:mobile": "tauri dev -c src-tauri/tauri.mobile.conf.json", - "dev:android": "tauri android dev", + "dev:desktop": "cross-env RUST_BACKTRACE=1 tauri dev", + "dev:mobile": "cross-env RUST_BACKTRACE=1 tauri dev -c src-tauri/tauri.mobile.conf.json", + "dev:android": "cross-env RUST_BACKTRACE=1 tauri android dev", "build:desktop": "tauri build", "build:android": "tauri android build", "install:android": "adb install -r src-tauri/gen/android/app/build/outputs/apk/universal/release/app-universal-release.apk", @@ -17,6 +17,7 @@ "tauri": "tauri" }, "devDependencies": { - "@tauri-apps/cli": "^2" + "@tauri-apps/cli": "^2", + "cross-env": "^10.1.0" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8f008af..75c4665 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,9 @@ importers: '@tauri-apps/cli': specifier: ^2 version: 2.10.0 + cross-env: + specifier: ^10.1.0 + version: 10.1.0 js-server: dependencies: @@ -256,6 +259,9 @@ packages: '@emotion/weak-memoize@0.4.0': resolution: {integrity: sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==} + '@epic-web/invariant@1.0.0': + resolution: {integrity: sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA==} + '@esbuild/aix-ppc64@0.27.3': resolution: {integrity: sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==} engines: {node: '>=18'} @@ -1100,6 +1106,15 @@ packages: resolution: {integrity: sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==} engines: {node: '>=10'} + cross-env@10.1.0: + resolution: {integrity: sha512-GsYosgnACZTADcmEyJctkJIoqAhHjttw7RsFrVoJNXbsWWqaq6Ym+7kZjq6mS45O0jij6vtiReppKQEtqWy6Dw==} + engines: {node: '>=20'} + hasBin: true + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + csstype@3.2.3: resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} @@ -1238,6 +1253,9 @@ packages: resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} engines: {node: '>= 0.4'} + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -1349,6 +1367,10 @@ packages: resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} engines: {node: '>=8'} + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + path-parse@1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} @@ -1488,6 +1510,14 @@ packages: setprototypeof@1.2.0: resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + sonic-boom@4.2.1: resolution: {integrity: sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==} @@ -1614,6 +1644,11 @@ packages: resolution: {integrity: sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==} engines: {node: '>=18'} + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + ws@8.19.0: resolution: {integrity: sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==} engines: {node: '>=10.0.0'} @@ -1918,6 +1953,8 @@ snapshots: '@emotion/weak-memoize@0.4.0': {} + '@epic-web/invariant@1.0.0': {} + '@esbuild/aix-ppc64@0.27.3': optional: true @@ -2970,6 +3007,17 @@ snapshots: path-type: 4.0.0 yaml: 1.10.2 + cross-env@10.1.0: + dependencies: + '@epic-web/invariant': 1.0.0 + cross-spawn: 7.0.6 + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + csstype@3.2.3: {} debug@4.4.3: @@ -3128,6 +3176,8 @@ snapshots: dependencies: hasown: 2.0.2 + isexe@2.0.0: {} + js-tokens@4.0.0: {} jsesc@3.1.0: {} @@ -3201,6 +3251,8 @@ snapshots: json-parse-even-better-errors: 2.3.1 lines-and-columns: 1.2.4 + path-key@3.1.1: {} + path-parse@1.0.7: {} path-scurry@2.0.2: @@ -3340,6 +3392,12 @@ snapshots: setprototypeof@1.2.0: {} + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + sonic-boom@4.2.1: dependencies: atomic-sleep: 1.0.0 @@ -3420,6 +3478,10 @@ snapshots: tr46: 5.1.1 webidl-conversions: 7.0.0 + which@2.0.2: + dependencies: + isexe: 2.0.0 + ws@8.19.0: {} yallist@3.1.1: {} diff --git a/src-core/src/db/search.rs b/src-core/src/db/search.rs index 8d753eb..202df50 100644 --- a/src-core/src/db/search.rs +++ b/src-core/src/db/search.rs @@ -51,10 +51,11 @@ impl<'a> SearchDatabase<'a> { self.conn.execute_batch(&format!( " DELETE FROM {0}.terms; - DELETE FROM {0}.terms_ft; + DROP TABLE {0}.terms_ft; ", DBSCHEMA[DbChild::Search] ))?; + self.create_schema()?; Ok(()) } diff --git a/src-core/src/db/yomitan_writer.rs b/src-core/src/db/yomitan_writer.rs index 4cfe055..e17ebc7 100644 --- a/src-core/src/db/yomitan_writer.rs +++ b/src-core/src/db/yomitan_writer.rs @@ -1,4 +1,5 @@ use blake3::hash; +use enum_map::EnumMap; use rusqlite::{Connection, OptionalExtension, Result, Transaction, params}; use serde::Serialize; use serde_json::Value; @@ -61,6 +62,7 @@ pub struct YomitanZipImportResult { pub struct YomitanWriter { dir: PathBuf, conn: Connection, + is_db_attached: EnumMap, } impl YomitanWriter { @@ -78,7 +80,15 @@ impl YomitanWriter { ", )?; - Ok(Self { dir, conn }) + Ok(Self { + dir, + conn, + is_db_attached: enum_map! { + DbChild::YomitanGlossary => false, + DbChild::Search => false, + _ => true + }, + }) } pub fn create_schema( @@ -172,8 +182,8 @@ impl YomitanWriter { )?; self.create_materialized_views()?; - self.attach_db(DbChild::YomitanGlossary)?; - self.attach_db(DbChild::Search)?; + self.ensure_db_attached(DbChild::YomitanGlossary)?; + self.ensure_db_attached(DbChild::Search)?; let schema_version = "2026-03-08"; let current_schema_version = { @@ -334,7 +344,11 @@ impl YomitanWriter { Ok(()) } - fn attach_db(&mut self, db_child: DbChild) -> Result<(), CJDicError> { + fn ensure_db_attached(&mut self, db_child: DbChild) -> Result<(), CJDicError> { + if self.is_db_attached[db_child] { + return Ok(()); + } + let dir = self.dir.clone(); let path = if self.dir.is_absolute() { dir @@ -371,6 +385,8 @@ impl YomitanWriter { _ => (), } + self.is_db_attached[db_child] = true; + Ok(()) } @@ -506,7 +522,7 @@ impl YomitanWriter { } } - self.attach_db(DbChild::YomitanGlossary)?; + self.ensure_db_attached(DbChild::YomitanGlossary)?; { let tx = self.conn.transaction()?; @@ -793,12 +809,30 @@ impl YomitanWriter { tokenizer: Tokenizer, progress_callback: impl Fn(YomitanProgress), ) -> Result<(), CJDicError> { + self.ensure_db_attached(DbChild::Search)?; + let mut search_db = SearchDatabase::new(&mut self.conn); + search_db.create_schema()?; search_db.reset_db()?; search_db.regenerate_yomitan("main", tokenizer.clone(), &progress_callback)?; Ok(()) } + + pub fn check_search_db(&mut self) -> Result { + self.ensure_db_attached(DbChild::Search)?; + + let is_filled = self.conn.query_row( + " + SELECT + (SELECT COALESCE(MAX(id), 0) FROM search.terms) >= + (SELECT COALESCE(MAX(id), 0) FROM terms) + ", + [], + |r| r.get::<_, bool>(0), + )?; + Ok(is_filled) + } } fn intern( diff --git a/src-core/src/error.rs b/src-core/src/error.rs index 61517ce..506a088 100644 --- a/src-core/src/error.rs +++ b/src-core/src/error.rs @@ -1,4 +1,5 @@ use std::{ + backtrace::Backtrace, string::FromUtf8Error, sync::{MutexGuard, PoisonError}, }; @@ -51,63 +52,72 @@ pub enum CJDicError { impl From for CJDicError { fn from(e: rusqlite::Error) -> Self { - eprintln!("{e:#}"); + let bt = Backtrace::capture(); + eprintln!("{e:#}\n{bt}"); CJDicError::Database(e.to_string()) } } impl From for CJDicError { fn from(e: serde_json::Error) -> Self { - eprintln!("{e:#}"); + let bt = Backtrace::capture(); + eprintln!("{e:#}\n{bt}"); CJDicError::Serialization(e.to_string()) } } impl From for CJDicError { fn from(e: anyhow::Error) -> Self { - eprintln!("{e:#}"); + let bt = Backtrace::capture(); + eprintln!("{e:#}\n{bt}"); CJDicError::AnyhowError(e.to_string()) } } impl From for CJDicError { fn from(e: std::io::Error) -> Self { - eprintln!("{e:#}"); + let bt = Backtrace::capture(); + eprintln!("{e:#}\n{bt}"); CJDicError::IOError(e.to_string()) } } impl From for CJDicError { fn from(e: ZipError) -> Self { - eprintln!("{e:#}"); + let bt = Backtrace::capture(); + eprintln!("{e:#}\n{bt}"); CJDicError::ZipError(e.to_string()) } } impl From for CJDicError { fn from(e: VibratoError) -> Self { - eprintln!("{e:#}"); + let bt = Backtrace::capture(); + eprintln!("{e:#}\n{bt}"); CJDicError::VibratoError(e.to_string()) } } impl From for CJDicError { fn from(e: FromUtf8Error) -> Self { - eprintln!("{e:#}"); + let bt = Backtrace::capture(); + eprintln!("{e:#}\n{bt}"); CJDicError::FromUtf8Error(e.to_string()) } } impl From>> for CJDicError { fn from(e: PoisonError>) -> Self { - eprintln!("{e:#}"); + let bt = Backtrace::capture(); + eprintln!("{e:#}\n{bt}"); CJDicError::ConnectionMutexGuardError(e.to_string()) } } impl From>> for CJDicError { fn from(e: PoisonError>) -> Self { - eprintln!("{e:#}"); + let bt = Backtrace::capture(); + eprintln!("{e:#}\n{bt}"); CJDicError::TokenizerMutexGuardError(e.to_string()) } } diff --git a/src-core/src/service.rs b/src-core/src/service.rs index 110a1a4..b86d5d4 100644 --- a/src-core/src/service.rs +++ b/src-core/src/service.rs @@ -40,10 +40,10 @@ pub struct AppService { impl AppService { pub fn new( db_dir: impl AsRef, - vibrato_dict: impl AsRef, + vibrato_dic: impl AsRef, ) -> Result { let db = Database::new(db_dir)?; - let tokenizer = Tokenizer::new(vibrato_dict)?; + let tokenizer = Tokenizer::new(vibrato_dic)?; Ok(Self { db, tokenizer }) } @@ -174,6 +174,10 @@ impl AppService { to_be_removed_dicts.push(z.to_string()); } } + } else { + for z in dir_zip_map.keys() { + new_dicts.push(z.to_string()); + } } // Import ordering @@ -202,14 +206,19 @@ impl AppService { { let mut writer = YomitanWriter::new(self.db.dir.clone())?; - for z in new_dicts.clone().iter() { if let Some(zip_file) = dir_zip_map.get(z) { Self::import_yomitan_zip_file(&mut writer, *zip_file, lang, &import_callback)?; } } + } - writer.make_search_db(self.tokenizer.clone(), &import_callback)?; + { + let mut writer = YomitanWriter::new(self.db.dir.clone())?; + + if new_dicts.len() + to_be_removed_dicts.len() > 0 || !writer.check_search_db()? { + writer.make_search_db(self.tokenizer.clone(), &import_callback)?; + } } Ok(LoadYomitanZipDirResult { diff --git a/src-core/src/tokenizer.rs b/src-core/src/tokenizer.rs index 19ab94b..b63b1b2 100644 --- a/src-core/src/tokenizer.rs +++ b/src-core/src/tokenizer.rs @@ -26,9 +26,8 @@ pub struct Tokenizer { } impl Tokenizer { - pub fn new(vibrato_dict: impl AsRef) -> Result { - let file = BufReader::new(File::open(vibrato_dict)?); - let reader = zstd::Decoder::new(file)?; + pub fn new(vibrato_dic: impl AsRef) -> Result { + let reader = BufReader::new(File::open(vibrato_dic)?); let dictionary = vibrato_rkyv::Dictionary::read(reader)?; Ok(Self { ja_tokenizer: vibrato_rkyv::Tokenizer::new(dictionary), diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 6b3d285..715f279 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -29,3 +29,4 @@ serde = "1.0.228" zip = "8.1.0" tokio = { version = "1.50.0", features = ["io-util"] } tauri-plugin-clipboard-manager = "2" +zstd = "0.13.3" diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index a5efa75..2d71c52 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -15,35 +15,39 @@ pub fn run() { .plugin(tauri_plugin_opener::init()) .plugin(tauri_plugin_clipboard_manager::init()) .setup(|app| { - let app_dir = app.path().app_config_dir()?; - create_dir_all(&app_dir)?; + let config_dir = app.path().app_config_dir()?; + create_dir_all(&config_dir)?; - let dict_path = { - // Resolve the resource path (returns asset://localhost/ URI on Android) - let resource_path = app.path().resolve( + let data_dir = app.path().app_data_dir()?; + create_dir_all(&data_dir.join("yomitan/ja"))?; + + let cache_dir = app.path().app_cache_dir()?; + let vibrato_dic_dir = cache_dir.join("vibrato"); + create_dir_all(&vibrato_dic_dir)?; + + println!("{:?}", vibrato_dic_dir); + + let vibrato_dic_path = vibrato_dic_dir.join("system.dic"); + if !vibrato_dic_path.exists() { + let zst_path = app.path().resolve( "resources/mecab-ipadic/system.dic.zst", tauri::path::BaseDirectory::Resource, )?; // Use Tauri's fs abstraction — works on both Android and desktop - let bytes = app.fs().read(&resource_path)?; + let bytes = app.fs().read(&zst_path)?; - // Write to app's cache dir, which IS a real writable filesystem path on Android - let cache_dir = app.path().app_cache_dir()?.join("vibrato"); - create_dir_all(&cache_dir)?; - let dict_path = cache_dir.join("system.dic.zst"); - fs::write(&dict_path, &bytes)?; + let mut decoder = zstd::Decoder::new(bytes.as_slice())?; + let mut out = fs::File::create(&vibrato_dic_path)?; + std::io::copy(&mut decoder, &mut out)?; + } - dict_path - }; + println!("{:?}", vibrato_dic_path); - // db_path is app_dir; - let service = AppService::new(&app_dir, &dict_path)?; + // db_path is at config_dir; + let service = AppService::new(&config_dir, &vibrato_dic_path)?; app.manage(service); - let app_data_dir = app.path().app_data_dir()?; - create_dir_all(&app_data_dir.join("yomitan/ja"))?; - Ok(()) }) .invoke_handler(tauri::generate_handler![