diff --git a/Cargo.lock b/Cargo.lock index 51a5b24..fa45cc9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1981,6 +1981,12 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "http-range" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21dec9db110f5f872ed9699c3ecf50cf16f423502706ba5c72462e28d3157573" + [[package]] name = "httparse" version = "1.10.1" @@ -4837,6 +4843,7 @@ dependencies = [ "gtk", "heck 0.5.0", "http", + "http-range", "jni", "libc", "log", diff --git a/src-core/examples/add_yomitan_dict_folder.rs b/src-core/examples/add_yomitan_dict_folder.rs index 256d63f..a4c8043 100644 --- a/src-core/examples/add_yomitan_dict_folder.rs +++ b/src-core/examples/add_yomitan_dict_folder.rs @@ -17,7 +17,7 @@ fn main() -> Result<(), anyhow::Error> { println!("{:?}", p); })?; - let zip_dir = Path::new("src-tauri/resources/yomitan"); + let zip_dir = Path::new("tmp/yomitan"); println!("zip_dir: {:?}", absolute(zip_dir)); // Relative to workspace root for entry in @@ -31,9 +31,15 @@ fn main() -> Result<(), anyhow::Error> { println!( "{:?}", - AppService::import_yomitan_zip_file(&mut writer, &p, "ja", |progress| { - println!("{:?}", progress); - })?, + AppService::import_yomitan_zip_file( + &mut writer, + &p, + &zip_dir.to_path_buf(), + "ja", + |progress| { + println!("{:?}", progress); + } + )?, ); } else { println!("not zip_file: {:?}", p); diff --git a/src-core/src/db/yomitan_writer.rs b/src-core/src/db/yomitan_writer.rs index ef9afad..79b2806 100644 --- a/src-core/src/db/yomitan_writer.rs +++ b/src-core/src/db/yomitan_writer.rs @@ -7,7 +7,8 @@ use std::{ borrow::Cow, collections::HashMap, env::current_dir, - io::{Cursor, Read}, + fs::{File, create_dir_all}, + io::{self, Cursor, Read}, path::{Path, PathBuf}, }; use zip::ZipArchive; @@ -437,6 +438,7 @@ impl YomitanWriter { pub fn import_dictionary_zip_file( &mut self, zip_file: &Z, + asset_dir: &PathBuf, lang: &str, progress_callback: Callback, ) -> anyhow::Result @@ -452,6 +454,27 @@ impl YomitanWriter { let cursor = Cursor::new(zip_file.bytes()?); let mut archive = ZipArchive::new(cursor)?; + for i in 0..archive.len() { + // Metadata — borrow ends before by_index_raw + let (name, is_dir) = { + let f = archive.by_index(i)?; + (f.name().to_owned(), f.is_dir()) + }; + + if !name.starts_with("assets") || is_dir { + continue; + } + + let out_path = asset_dir.join(name); + if let Some(parent) = out_path.parent() { + create_dir_all(parent)?; + } + + let mut entry = archive.by_index(i)?; + let mut out = File::create(&out_path)?; + io::copy(&mut entry, &mut out)?; // writes plain bytes + } + let index_file = match archive.by_name("index.json") { Ok(mut f) => { let mut s = String::new(); diff --git a/src-core/src/service.rs b/src-core/src/service.rs index 7a7de9f..13fe112 100644 --- a/src-core/src/service.rs +++ b/src-core/src/service.rs @@ -1,6 +1,6 @@ use std::{ collections::{HashMap, HashSet}, - path::Path, + path::{Path, PathBuf}, }; use rusqlite::{Connection, params_from_iter}; @@ -166,6 +166,7 @@ impl AppService { pub fn load_yomitan_zip_dir( &self, zip_dir: Vec, + asset_dir: &PathBuf, lang: &str, load_callback: LoadCallback, import_callback: ImportCallback, @@ -246,7 +247,13 @@ 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)?; + Self::import_yomitan_zip_file( + &mut writer, + *zip_file, + &asset_dir, + lang, + &import_callback, + )?; } } } @@ -268,6 +275,7 @@ impl AppService { pub fn import_yomitan_zip_file( writer: &mut YomitanWriter, // Reuse existing connection zip_file: &Z, + asset_dir: &PathBuf, lang: &str, progress_callback: Callback, ) -> Result @@ -275,7 +283,7 @@ impl AppService { Z: ZipSource, Callback: Fn(YomitanProgress), { - Ok(writer.import_dictionary_zip_file(zip_file, lang, progress_callback)?) + Ok(writer.import_dictionary_zip_file(zip_file, asset_dir, lang, progress_callback)?) } pub fn remove_yomitan_dictionary( diff --git a/src-react/public/assets/pixiv-logo.png b/src-react/public/assets/pixiv-logo.png deleted file mode 100644 index 3e9e457..0000000 Binary files a/src-react/public/assets/pixiv-logo.png and /dev/null differ diff --git a/src-react/src/components/Glossary.tsx b/src-react/src/components/Glossary.tsx index 66e538a..21d817b 100644 --- a/src-react/src/components/Glossary.tsx +++ b/src-react/src/components/Glossary.tsx @@ -2,10 +2,12 @@ import { createElement as h } from "react"; export default function Glossary({ glossary, + yomitanURL, maxLength, onTermClicked, }: { glossary: any; + yomitanURL: string; maxLength?: number; onTermClicked: (t: string) => void; }) { @@ -29,7 +31,7 @@ export default function Glossary({ // GlossaryImage if ("type" in glossary && glossary.type === "image") { - return ; + return ; } // GlossaryStructuredContent @@ -38,8 +40,7 @@ export default function Glossary({ return ( ); } @@ -53,10 +54,10 @@ export default function Glossary({ return null; } -function GlossaryImage(img: any) { +function GlossaryImage({ img, yomitanURL }: { img: any; yomitanURL: string }) { return ( void; }): any { @@ -100,8 +103,7 @@ function StructuredContent({ ))} @@ -118,7 +120,7 @@ function StructuredContent({ // Image if (tag === "img") { - return ; + return ; } // Link @@ -145,8 +147,7 @@ function StructuredContent({ {content ? ( ) : null} @@ -159,6 +160,11 @@ function StructuredContent({ return h( tag, { style: block.style, title: block.title, open: block.open }, - StructuredContent({ node: block.content, maxLength, onTermClicked }), + StructuredContent({ + node: block.content, + yomitanURL, + maxLength, + onTermClicked, + }), ); } diff --git a/src-react/src/components/LoadingDialog.tsx b/src-react/src/components/LoadingDialog.tsx index bb78513..5bae483 100644 --- a/src-react/src/components/LoadingDialog.tsx +++ b/src-react/src/components/LoadingDialog.tsx @@ -79,9 +79,9 @@ function LoadingDialog() { const dicts: YomitanDownloadDictEntry[] = []; if (lang === "ja") { - const filename = "Pixiv.zip"; - const filepath = `yomitan/${lang}/${filename}`; - const existing = existing_dicts.find((d) => d.bundle_name === filename); + var filename = "PixivLight.zip"; + var filepath = `yomitan/${lang}/${filename}`; + var existing = existing_dicts.find((d) => d.bundle_name === filename); if (!existing) { // Maybe check revision update dicts.push({ @@ -89,7 +89,7 @@ function LoadingDialog() { url: (await ghLatestReleaseURL( "MarvNC/pixiv-yomitan", - /^Pixiv_.+\.zip$/, + /^PixivLight_.+\.zip$/, ).catch((e) => { console.error(e); return ""; diff --git a/src-react/src/pages/SearchPage.tsx b/src-react/src/pages/SearchPage.tsx index f0a0aaf..9bed28e 100644 --- a/src-react/src/pages/SearchPage.tsx +++ b/src-react/src/pages/SearchPage.tsx @@ -18,7 +18,8 @@ import { Stack, Switch, } from "@chakra-ui/react"; -import { invoke } from "@tauri-apps/api/core"; +import { convertFileSrc, invoke } from "@tauri-apps/api/core"; +import { appDataDir, join } from "@tauri-apps/api/path"; import { readText } from "@tauri-apps/plugin-clipboard-manager"; import Glossary from "../components/Glossary"; @@ -54,6 +55,13 @@ function SearchPage() { const [furigana, set_furigana] = useState(""); const [isAutoKana, set_isAutoKana] = useState(true); const [isScrollEnd, set_isScrollEnd] = useState(false); + const [yomitanURL, set_yomitanURL] = useState(""); + + useEffect(() => { + (async () => { + set_yomitanURL(convertFileSrc(await join(await appDataDir(), "yomitan"))); + })(); + }); const nSearch = useRef(0); const searchboxRef = useRef(null); @@ -69,6 +77,9 @@ function SearchPage() { const intervalId = setInterval(async () => { const newText = (await readText()).replace(/ /g, ""); + if (!/[\p{sc=Han}\p{scx=Hiragana}\p{scx=Katakana}〆]/u.test(newText)) { + return; + } set_clipboardText((clipboardText) => { if (clipboardText !== newText) { @@ -411,7 +422,7 @@ function SearchPage() { ))} diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 715f279..f61e187 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -18,7 +18,7 @@ crate-type = ["staticlib", "cdylib", "rlib"] tauri-build = { version = "2", features = [] } [dependencies] -tauri = { version = "2", features = [] } +tauri = { version = "2", features = ["protocol-asset"] } tauri-plugin-opener = "2" cjdic2-core = { path = "../src-core" } tauri-plugin-http = "2" diff --git a/src-tauri/src/command.rs b/src-tauri/src/command.rs index 18bf7d7..9e0150c 100644 --- a/src-tauri/src/command.rs +++ b/src-tauri/src/command.rs @@ -34,38 +34,36 @@ pub async fn init_yomitan( lang: String, state: tauri::State<'_, AppService>, ) -> Result<(), CJDicError> { - let app_dir = app + let data_dir = app .path() .app_data_dir() .map_err(|e| CJDicError::Error(e.to_string()))?; + let yomitan_assets = data_dir.join("yomitan"); + for d in dicts.iter().clone() { - let zip_path = app_dir.join(&d.filepath); + let zip_path = data_dir.join(&d.filepath); if zip_path.exists() && File::open(zip_path).is_ok_and(|f| ZipArchive::new(f).is_ok()) { continue; } - download_url_local( - d.url.clone(), - app_dir.clone(), - d.filepath.clone(), - |progress| { - app.emit("download-url-progress", progress).unwrap(); - }, - ) + download_url_local(&d.url, &data_dir.clone(), &d.filepath, |progress| { + app.emit("download-url-progress", progress).unwrap(); + }) .await .map_err(|e| CJDicError::Error(e.to_string()))?; } let zip_files: Vec = dicts .into_iter() - .map(|d| app_dir.join(&d.filepath)) + .map(|d| data_dir.join(&d.filepath)) .collect(); let update_count = Cell::new(0usize); state.load_yomitan_zip_dir( zip_files, + &yomitan_assets, lang.as_str(), |r| { update_count.set(update_count.get() + r.new_dicts.len() + r.to_be_removed_dicts.len()); @@ -97,6 +95,9 @@ pub async fn import_yomitan_dict( .path() .app_data_dir() .map_err(|e| CJDicError::Error(e.to_string()))?; + + let yomitan_assets = data_dir.join("yomitan"); + data_dir.push("yomitan"); data_dir.push(lang); data_dir.push(bundle_name); @@ -104,7 +105,7 @@ pub async fn import_yomitan_dict( let mut writer = state.get_yomitan_writer(|r| { app.emit("yomitan-import-progress", r).unwrap(); })?; - AppService::import_yomitan_zip_file(&mut writer, &data_dir, lang, |r| { + AppService::import_yomitan_zip_file(&mut writer, &data_dir, &yomitan_assets, lang, |r| { app.emit("yomitan-import-progress", r).unwrap(); }) } @@ -151,16 +152,16 @@ struct DownloadProgress { } async fn download_url_local( - url: String, - data_dir: PathBuf, - filepath: String, + url: &str, + data_dir: &PathBuf, + filepath: &str, callback: Callback, ) -> Result where Callback: Fn(DownloadProgress), { let client = Client::new(); - let file_pf = data_dir.join(filepath.as_str()); + let file_pf = data_dir.join(filepath); let temp_pf = data_dir.join(format!("{}.dl-tmp", filepath)); let temp_path = temp_pf.as_path(); @@ -170,7 +171,7 @@ where .map(|m| m.len()) .unwrap_or(0); - let mut request = client.get(url.as_str()); + let mut request = client.get(url); // Request only the remaining bytes if existing_size > 0 { request = request.header("Range", format!("bytes={}-", existing_size)); @@ -208,8 +209,8 @@ where downloaded += chunk.len() as u64; callback(DownloadProgress { - url: url.clone(), - filepath: filepath.clone(), + url: url.to_owned(), + filepath: filepath.to_owned(), content_length, downloaded, }); @@ -235,14 +236,10 @@ where } #[tauri::command] -pub async fn download_url( - app: AppHandle, - url: String, - filepath: String, -) -> Result { +pub async fn download_url(app: AppHandle, url: &str, filepath: &str) -> Result { download_url_local( url, - app.path() + &app.path() .app_data_dir() .map_err(|e| CJDicError::Error(e.to_string()))?, filepath, diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 9103d80..6e44f1d 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -19,7 +19,11 @@ } ], "security": { - "csp": null + "csp": "default-src 'self' ipc: http://ipc.localhost; img-src 'self' asset: http://asset.localhost", + "assetProtocol": { + "enable": true, + "scope": ["$APPDATA/yomitan/**"] + } } }, "bundle": {