extract image assets for yomitan zip

This commit is contained in:
polv committed 2026-03-21 12:46:54 +07:00
1 parent 79deaf9372
commit 52c25992ab
11 files changed
+114 -52

No files matched your search

Generated
+7
View File
@@ -1981,6 +1981,12 @@ dependencies = [
"pin-project-lite", "pin-project-lite",
] ]
[[package]]
name = "http-range"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21dec9db110f5f872ed9699c3ecf50cf16f423502706ba5c72462e28d3157573"
[[package]] [[package]]
name = "httparse" name = "httparse"
version = "1.10.1" version = "1.10.1"
@@ -4837,6 +4843,7 @@ dependencies = [
"gtk", "gtk",
"heck 0.5.0", "heck 0.5.0",
"http", "http",
"http-range",
"jni", "jni",
"libc", "libc",
"log", "log",
+10 -4
View File
@@ -17,7 +17,7 @@ fn main() -> Result<(), anyhow::Error> {
println!("{:?}", p); 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 println!("zip_dir: {:?}", absolute(zip_dir)); // Relative to workspace root
for entry in for entry in
@@ -31,9 +31,15 @@ fn main() -> Result<(), anyhow::Error> {
println!( println!(
"{:?}", "{:?}",
AppService::import_yomitan_zip_file(&mut writer, &p, "ja", |progress| { AppService::import_yomitan_zip_file(
println!("{:?}", progress); &mut writer,
})?, &p,
&zip_dir.to_path_buf(),
"ja",
|progress| {
println!("{:?}", progress);
}
)?,
); );
} else { } else {
println!("not zip_file: {:?}", p); println!("not zip_file: {:?}", p);
+24 -1
View File
@@ -7,7 +7,8 @@ use std::{
borrow::Cow, borrow::Cow,
collections::HashMap, collections::HashMap,
env::current_dir, env::current_dir,
io::{Cursor, Read}, fs::{File, create_dir_all},
io::{self, Cursor, Read},
path::{Path, PathBuf}, path::{Path, PathBuf},
}; };
use zip::ZipArchive; use zip::ZipArchive;
@@ -437,6 +438,7 @@ impl YomitanWriter {
pub fn import_dictionary_zip_file<Z, Callback>( pub fn import_dictionary_zip_file<Z, Callback>(
&mut self, &mut self,
zip_file: &Z, zip_file: &Z,
asset_dir: &PathBuf,
lang: &str, lang: &str,
progress_callback: Callback, progress_callback: Callback,
) -> anyhow::Result<YomitanZipImportResult, CJDicError> ) -> anyhow::Result<YomitanZipImportResult, CJDicError>
@@ -452,6 +454,27 @@ impl YomitanWriter {
let cursor = Cursor::new(zip_file.bytes()?); let cursor = Cursor::new(zip_file.bytes()?);
let mut archive = ZipArchive::new(cursor)?; 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") { let index_file = match archive.by_name("index.json") {
Ok(mut f) => { Ok(mut f) => {
let mut s = String::new(); let mut s = String::new();
+11 -3
View File
@@ -1,6 +1,6 @@
use std::{ use std::{
collections::{HashMap, HashSet}, collections::{HashMap, HashSet},
path::Path, path::{Path, PathBuf},
}; };
use rusqlite::{Connection, params_from_iter}; use rusqlite::{Connection, params_from_iter};
@@ -166,6 +166,7 @@ impl AppService {
pub fn load_yomitan_zip_dir<Z, LoadCallback, ImportCallback>( pub fn load_yomitan_zip_dir<Z, LoadCallback, ImportCallback>(
&self, &self,
zip_dir: Vec<Z>, zip_dir: Vec<Z>,
asset_dir: &PathBuf,
lang: &str, lang: &str,
load_callback: LoadCallback, load_callback: LoadCallback,
import_callback: ImportCallback, import_callback: ImportCallback,
@@ -246,7 +247,13 @@ impl AppService {
let mut writer = YomitanWriter::new(self.db.dir.clone())?; let mut writer = YomitanWriter::new(self.db.dir.clone())?;
for z in new_dicts.clone().iter() { for z in new_dicts.clone().iter() {
if let Some(zip_file) = dir_zip_map.get(z) { 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<Z, Callback>( pub fn import_yomitan_zip_file<Z, Callback>(
writer: &mut YomitanWriter, // Reuse existing connection writer: &mut YomitanWriter, // Reuse existing connection
zip_file: &Z, zip_file: &Z,
asset_dir: &PathBuf,
lang: &str, lang: &str,
progress_callback: Callback, progress_callback: Callback,
) -> Result<YomitanZipImportResult, CJDicError> ) -> Result<YomitanZipImportResult, CJDicError>
@@ -275,7 +283,7 @@ impl AppService {
Z: ZipSource, Z: ZipSource,
Callback: Fn(YomitanProgress), 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( pub fn remove_yomitan_dictionary(
Binary file not shown.

Before

Width:  |  Height:  |  Size: 254 B

+17 -11
View File
@@ -2,10 +2,12 @@ import { createElement as h } from "react";
export default function Glossary({ export default function Glossary({
glossary, glossary,
yomitanURL,
maxLength, maxLength,
onTermClicked, onTermClicked,
}: { }: {
glossary: any; glossary: any;
yomitanURL: string;
maxLength?: number; maxLength?: number;
onTermClicked: (t: string) => void; onTermClicked: (t: string) => void;
}) { }) {
@@ -29,7 +31,7 @@ export default function Glossary({
// GlossaryImage // GlossaryImage
if ("type" in glossary && glossary.type === "image") { if ("type" in glossary && glossary.type === "image") {
return <GlossaryImage {...glossary} />; return <GlossaryImage img={glossary} yomitanURL={yomitanURL} />;
} }
// GlossaryStructuredContent // GlossaryStructuredContent
@@ -38,8 +40,7 @@ export default function Glossary({
return ( return (
<StructuredContent <StructuredContent
node={sc.content} node={sc.content}
maxLength={maxLength} {...{ yomitanURL, maxLength, onTermClicked }}
onTermClicked={onTermClicked}
/> />
); );
} }
@@ -53,10 +54,10 @@ export default function Glossary({
return null; return null;
} }
function GlossaryImage(img: any) { function GlossaryImage({ img, yomitanURL }: { img: any; yomitanURL: string }) {
return ( return (
<img <img
src={img.path} src={`${yomitanURL || ""}/${img.path}`}
width={img.width} width={img.width}
height={img.height} height={img.height}
title={img.title} title={img.title}
@@ -72,10 +73,12 @@ function GlossaryImage(img: any) {
function StructuredContent({ function StructuredContent({
node, node,
yomitanURL,
maxLength, maxLength,
onTermClicked, onTermClicked,
}: { }: {
node: any; node: any;
yomitanURL: string;
maxLength?: number; maxLength?: number;
onTermClicked: (t: string) => void; onTermClicked: (t: string) => void;
}): any { }): any {
@@ -100,8 +103,7 @@ function StructuredContent({
<StructuredContent <StructuredContent
key={i} key={i}
node={n} node={n}
maxLength={maxLength} {...{ yomitanURL, maxLength, onTermClicked }}
onTermClicked={onTermClicked}
/> />
))} ))}
</> </>
@@ -118,7 +120,7 @@ function StructuredContent({
// Image // Image
if (tag === "img") { if (tag === "img") {
return <GlossaryImage {...node} />; return <GlossaryImage img={node} yomitanURL={yomitanURL} />;
} }
// Link // Link
@@ -145,8 +147,7 @@ function StructuredContent({
{content ? ( {content ? (
<StructuredContent <StructuredContent
node={content} node={content}
maxLength={maxLength} {...{ yomitanURL, maxLength, onTermClicked }}
onTermClicked={onTermClicked}
/> />
) : null} ) : null}
</a> </a>
@@ -159,6 +160,11 @@ function StructuredContent({
return h( return h(
tag, tag,
{ style: block.style, title: block.title, open: block.open }, { style: block.style, title: block.title, open: block.open },
StructuredContent({ node: block.content, maxLength, onTermClicked }), StructuredContent({
node: block.content,
yomitanURL,
maxLength,
onTermClicked,
}),
); );
} }
+4 -4
View File
@@ -79,9 +79,9 @@ function LoadingDialog() {
const dicts: YomitanDownloadDictEntry[] = []; const dicts: YomitanDownloadDictEntry[] = [];
if (lang === "ja") { if (lang === "ja") {
const filename = "Pixiv.zip"; var filename = "PixivLight.zip";
const filepath = `yomitan/${lang}/${filename}`; var filepath = `yomitan/${lang}/${filename}`;
const existing = existing_dicts.find((d) => d.bundle_name === filename); var existing = existing_dicts.find((d) => d.bundle_name === filename);
if (!existing) { if (!existing) {
// Maybe check revision update // Maybe check revision update
dicts.push({ dicts.push({
@@ -89,7 +89,7 @@ function LoadingDialog() {
url: url:
(await ghLatestReleaseURL( (await ghLatestReleaseURL(
"MarvNC/pixiv-yomitan", "MarvNC/pixiv-yomitan",
/^Pixiv_.+\.zip$/, /^PixivLight_.+\.zip$/,
).catch((e) => { ).catch((e) => {
console.error(e); console.error(e);
return ""; return "";
+13 -2
View File
@@ -18,7 +18,8 @@ import {
Stack, Stack,
Switch, Switch,
} from "@chakra-ui/react"; } 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 { readText } from "@tauri-apps/plugin-clipboard-manager";
import Glossary from "../components/Glossary"; import Glossary from "../components/Glossary";
@@ -54,6 +55,13 @@ function SearchPage() {
const [furigana, set_furigana] = useState(""); const [furigana, set_furigana] = useState("");
const [isAutoKana, set_isAutoKana] = useState(true); const [isAutoKana, set_isAutoKana] = useState(true);
const [isScrollEnd, set_isScrollEnd] = useState(false); 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 nSearch = useRef(0);
const searchboxRef = useRef<HTMLInputElement | null>(null); const searchboxRef = useRef<HTMLInputElement | null>(null);
@@ -69,6 +77,9 @@ function SearchPage() {
const intervalId = setInterval(async () => { const intervalId = setInterval(async () => {
const newText = (await readText()).replace(/ /g, ""); const newText = (await readText()).replace(/ /g, "");
if (!/[\p{sc=Han}\p{scx=Hiragana}\p{scx=Katakana}〆]/u.test(newText)) {
return;
}
set_clipboardText((clipboardText) => { set_clipboardText((clipboardText) => {
if (clipboardText !== newText) { if (clipboardText !== newText) {
@@ -411,7 +422,7 @@ function SearchPage() {
<Glossary <Glossary
key={i} key={i}
glossary={g} glossary={g}
onTermClicked={onTermClicked} {...{ yomitanURL, onTermClicked }}
/> />
))} ))}
</div> </div>
+1 -1
View File
@@ -18,7 +18,7 @@ crate-type = ["staticlib", "cdylib", "rlib"]
tauri-build = { version = "2", features = [] } tauri-build = { version = "2", features = [] }
[dependencies] [dependencies]
tauri = { version = "2", features = [] } tauri = { version = "2", features = ["protocol-asset"] }
tauri-plugin-opener = "2" tauri-plugin-opener = "2"
cjdic2-core = { path = "../src-core" } cjdic2-core = { path = "../src-core" }
tauri-plugin-http = "2" tauri-plugin-http = "2"
+22 -25
View File
@@ -34,38 +34,36 @@ pub async fn init_yomitan(
lang: String, lang: String,
state: tauri::State<'_, AppService>, state: tauri::State<'_, AppService>,
) -> Result<(), CJDicError> { ) -> Result<(), CJDicError> {
let app_dir = app let data_dir = app
.path() .path()
.app_data_dir() .app_data_dir()
.map_err(|e| CJDicError::Error(e.to_string()))?; .map_err(|e| CJDicError::Error(e.to_string()))?;
let yomitan_assets = data_dir.join("yomitan");
for d in dicts.iter().clone() { 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()) { if zip_path.exists() && File::open(zip_path).is_ok_and(|f| ZipArchive::new(f).is_ok()) {
continue; continue;
} }
download_url_local( download_url_local(&d.url, &data_dir.clone(), &d.filepath, |progress| {
d.url.clone(), app.emit("download-url-progress", progress).unwrap();
app_dir.clone(), })
d.filepath.clone(),
|progress| {
app.emit("download-url-progress", progress).unwrap();
},
)
.await .await
.map_err(|e| CJDicError::Error(e.to_string()))?; .map_err(|e| CJDicError::Error(e.to_string()))?;
} }
let zip_files: Vec<PathBuf> = dicts let zip_files: Vec<PathBuf> = dicts
.into_iter() .into_iter()
.map(|d| app_dir.join(&d.filepath)) .map(|d| data_dir.join(&d.filepath))
.collect(); .collect();
let update_count = Cell::new(0usize); let update_count = Cell::new(0usize);
state.load_yomitan_zip_dir( state.load_yomitan_zip_dir(
zip_files, zip_files,
&yomitan_assets,
lang.as_str(), lang.as_str(),
|r| { |r| {
update_count.set(update_count.get() + r.new_dicts.len() + r.to_be_removed_dicts.len()); 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() .path()
.app_data_dir() .app_data_dir()
.map_err(|e| CJDicError::Error(e.to_string()))?; .map_err(|e| CJDicError::Error(e.to_string()))?;
let yomitan_assets = data_dir.join("yomitan");
data_dir.push("yomitan"); data_dir.push("yomitan");
data_dir.push(lang); data_dir.push(lang);
data_dir.push(bundle_name); data_dir.push(bundle_name);
@@ -104,7 +105,7 @@ pub async fn import_yomitan_dict(
let mut writer = state.get_yomitan_writer(|r| { let mut writer = state.get_yomitan_writer(|r| {
app.emit("yomitan-import-progress", r).unwrap(); 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(); app.emit("yomitan-import-progress", r).unwrap();
}) })
} }
@@ -151,16 +152,16 @@ struct DownloadProgress {
} }
async fn download_url_local<Callback>( async fn download_url_local<Callback>(
url: String, url: &str,
data_dir: PathBuf, data_dir: &PathBuf,
filepath: String, filepath: &str,
callback: Callback, callback: Callback,
) -> Result<bool, CJDicError> ) -> Result<bool, CJDicError>
where where
Callback: Fn(DownloadProgress), Callback: Fn(DownloadProgress),
{ {
let client = Client::new(); 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_pf = data_dir.join(format!("{}.dl-tmp", filepath));
let temp_path = temp_pf.as_path(); let temp_path = temp_pf.as_path();
@@ -170,7 +171,7 @@ where
.map(|m| m.len()) .map(|m| m.len())
.unwrap_or(0); .unwrap_or(0);
let mut request = client.get(url.as_str()); let mut request = client.get(url);
// Request only the remaining bytes // Request only the remaining bytes
if existing_size > 0 { if existing_size > 0 {
request = request.header("Range", format!("bytes={}-", existing_size)); request = request.header("Range", format!("bytes={}-", existing_size));
@@ -208,8 +209,8 @@ where
downloaded += chunk.len() as u64; downloaded += chunk.len() as u64;
callback(DownloadProgress { callback(DownloadProgress {
url: url.clone(), url: url.to_owned(),
filepath: filepath.clone(), filepath: filepath.to_owned(),
content_length, content_length,
downloaded, downloaded,
}); });
@@ -235,14 +236,10 @@ where
} }
#[tauri::command] #[tauri::command]
pub async fn download_url( pub async fn download_url(app: AppHandle, url: &str, filepath: &str) -> Result<bool, CJDicError> {
app: AppHandle,
url: String,
filepath: String,
) -> Result<bool, CJDicError> {
download_url_local( download_url_local(
url, url,
app.path() &app.path()
.app_data_dir() .app_data_dir()
.map_err(|e| CJDicError::Error(e.to_string()))?, .map_err(|e| CJDicError::Error(e.to_string()))?,
filepath, filepath,
+5 -1
View File
@@ -19,7 +19,11 @@
} }
], ],
"security": { "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": { "bundle": {