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",
]
[[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",
+10 -4
View File
@@ -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);
+24 -1
View File
@@ -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<Z, Callback>(
&mut self,
zip_file: &Z,
asset_dir: &PathBuf,
lang: &str,
progress_callback: Callback,
) -> anyhow::Result<YomitanZipImportResult, CJDicError>
@@ -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();
+11 -3
View File
@@ -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<Z, LoadCallback, ImportCallback>(
&self,
zip_dir: Vec<Z>,
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<Z, Callback>(
writer: &mut YomitanWriter, // Reuse existing connection
zip_file: &Z,
asset_dir: &PathBuf,
lang: &str,
progress_callback: Callback,
) -> Result<YomitanZipImportResult, CJDicError>
@@ -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(
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({
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 <GlossaryImage {...glossary} />;
return <GlossaryImage img={glossary} yomitanURL={yomitanURL} />;
}
// GlossaryStructuredContent
@@ -38,8 +40,7 @@ export default function Glossary({
return (
<StructuredContent
node={sc.content}
maxLength={maxLength}
onTermClicked={onTermClicked}
{...{ yomitanURL, maxLength, onTermClicked }}
/>
);
}
@@ -53,10 +54,10 @@ export default function Glossary({
return null;
}
function GlossaryImage(img: any) {
function GlossaryImage({ img, yomitanURL }: { img: any; yomitanURL: string }) {
return (
<img
src={img.path}
src={`${yomitanURL || ""}/${img.path}`}
width={img.width}
height={img.height}
title={img.title}
@@ -72,10 +73,12 @@ function GlossaryImage(img: any) {
function StructuredContent({
node,
yomitanURL,
maxLength,
onTermClicked,
}: {
node: any;
yomitanURL: string;
maxLength?: number;
onTermClicked: (t: string) => void;
}): any {
@@ -100,8 +103,7 @@ function StructuredContent({
<StructuredContent
key={i}
node={n}
maxLength={maxLength}
onTermClicked={onTermClicked}
{...{ yomitanURL, maxLength, onTermClicked }}
/>
))}
</>
@@ -118,7 +120,7 @@ function StructuredContent({
// Image
if (tag === "img") {
return <GlossaryImage {...node} />;
return <GlossaryImage img={node} yomitanURL={yomitanURL} />;
}
// Link
@@ -145,8 +147,7 @@ function StructuredContent({
{content ? (
<StructuredContent
node={content}
maxLength={maxLength}
onTermClicked={onTermClicked}
{...{ yomitanURL, maxLength, onTermClicked }}
/>
) : null}
</a>
@@ -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,
}),
);
}
+4 -4
View File
@@ -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 "";
+13 -2
View File
@@ -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<HTMLInputElement | null>(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() {
<Glossary
key={i}
glossary={g}
onTermClicked={onTermClicked}
{...{ yomitanURL, onTermClicked }}
/>
))}
</div>
+1 -1
View File
@@ -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"
+22 -25
View File
@@ -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<PathBuf> = 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<Callback>(
url: String,
data_dir: PathBuf,
filepath: String,
url: &str,
data_dir: &PathBuf,
filepath: &str,
callback: Callback,
) -> Result<bool, CJDicError>
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<bool, CJDicError> {
pub async fn download_url(app: AppHandle, url: &str, filepath: &str) -> Result<bool, CJDicError> {
download_url_local(
url,
app.path()
&app.path()
.app_data_dir()
.map_err(|e| CJDicError::Error(e.to_string()))?,
filepath,
+5 -1
View File
@@ -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": {