9 Commits
Author SHA1 Message Date
polv a0b2cf63d1 app icon, and remove android debug via .dev flavor
- debug .dev flavor doesn't work well with RUST-BACKTRACE and webview devtools
- tauri android run already install debug without dev server
2026-03-24 13:25:06 +07:00
polv 079fdd512c try spawn blocking 2026-03-22 21:46:47 +07:00
polv 413376d3f3 remove zipSource 2026-03-22 15:15:26 +07:00
polv 9f88b6d93c remove csp and assetProtocol, escape problem in production that don't appear in dev 2026-03-22 04:21:37 +07:00
polv a26cfceaf4 fix css preserve color images 2026-03-22 02:00:52 +07:00
polv 4b6101b019 quick fix worker auth 2026-03-22 00:53:11 +07:00
polv 904cdd0413 check images and dark mode 2026-03-22 00:41:56 +07:00
polv 978ebfa75f cloudflare worker for zip storage 2026-03-21 22:11:52 +07:00
polv 52c25992ab extract image assets for yomitan zip 2026-03-21 12:46:54 +07:00
103 changed files with 13921 additions and 146 deletions

No files matched your search

+3
View File
@@ -6,3 +6,6 @@ resolver = "3"
lto = true
codegen-units = 1
strip = true
# debug = true # keep symbols
# lto = false # disable LTO
# opt-level = 1 # reduce but don't maximize optimization
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

+3
View File
@@ -15,6 +15,9 @@
{
"path": "src-tauri",
},
{
"path": "storage-worker",
},
],
"settings": {
// "importSorter.generalConfiguration.sortOnBeforeSave": true,
+75
View File
@@ -0,0 +1,75 @@
import sharp from "sharp";
import { spawnSync } from "child_process";
async function resizeAndMakeWhiteTransparent(inputPath, outputPath) {
const whiteThreshold = 250; // treat >= as white
const img = sharp(inputPath).ensureAlpha();
// Get metadata to compute resize dimensions preserving aspect ratio
const meta = await img.metadata();
const origW = meta.width || 0;
const origH = meta.height || 0;
if (!origW || !origH) throw new Error("Unable to read image dimensions");
const targetW = Math.max(origW, origH);
const targetH = targetW;
// Resize and output raw RGBA buffer
const { data, info } = await img
.resize(targetW, targetH, { fit: "fill" })
.raw()
.toBuffer({ resolveWithObject: true });
const { width: w, height: h, channels } = info; // channels should be 4 (RGBA)
if (channels < 3) throw new Error("Unexpected channel count");
// Check if image already has any transparency
let hasTransparency = false;
for (let i = 0; i < data.length; i += channels) {
if (data[i + 3] < 255) {
hasTransparency = true;
break;
}
}
if (!hasTransparency) {
// Modify alpha for near-white pixels
for (let i = 0; i < data.length; i += channels) {
const r = data[i];
const g = data[i + 1];
const b = data[i + 2];
const a = channels === 4 ? data[i + 3] : 255;
if (
a > 0 &&
r >= whiteThreshold &&
g >= whiteThreshold &&
b >= whiteThreshold
) {
if (channels === 4) data[i + 3] = 0;
else {
// expand to RGBA if input lacked alpha
// create a new buffer would be required; throw for simplicity
throw new Error(
"Input image missing alpha channel; ensure ensureAlpha() was applied.",
);
}
}
}
}
// Recreate and save PNG
await sharp(data, { raw: { width: w, height: h, channels } })
.png({ compressionLevel: 9 })
.toFile(outputPath);
}
const [, , input] = process.argv;
if (!input) {
throw new Error(`Input is required: input=${input}`);
}
// Example usage
resizeAndMakeWhiteTransparent(input, "app-icon.png")
.then(() => spawnSync("pnpm tauri icon", { shell: true, stdio: "inherit" }))
.catch((err) => console.error(err));
+8 -7
View File
@@ -5,21 +5,22 @@
"scripts": {
"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",
"log:android": "adb logcat -e \"RustStdoutStderr\"",
"dev:android": "cross-env RUST_BACKTRACE=1 tauri android run",
"dev:android:server": "cross-env RUST_BACKTRACE=1 tauri android dev",
"dev:android:install": "adb install -r src-tauri/gen/android/app/build/outputs/apk/arm64/debug/app-arm64-debug.apk",
"dev:android:build": "tauri android build --debug",
"dev:android:log": "adb logcat -e \"RustStdoutStderr\"",
"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",
"build:android:debug": "tauri android build --debug",
"install:android:debug": "adb install -r src-tauri/gen/android/app/build/outputs/apk/universal/debug/app-universal-debug.apk",
"run:desktop": ".\\target\\release\\cjdic2.exe",
"release": "node release.js",
"dev": "cd src-react && pnpm run dev",
"build": "cd src-react && pnpm run build",
"tauri": "tauri"
"build": "cd src-react && pnpm run build"
},
"devDependencies": {
"@tauri-apps/cli": "^2",
"cross-env": "^10.1.0"
"cross-env": "^10.1.0",
"sharp": "^0.34.5"
}
}
+1045 -5
View File
File diff suppressed because it is too large. Load diff
+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);
+1 -1
View File
@@ -16,7 +16,7 @@ mod yomitan;
pub use yomitan::YomitanRow;
mod yomitan_writer;
pub use yomitan_writer::{YomitanProgress, YomitanWriter, YomitanZipImportResult, ZipSource};
pub use yomitan_writer::{YomitanProgress, YomitanWriter, YomitanZipImportResult};
use crate::{CJDicError, tokenizer::Tokenizer};
+65 -31
View File
@@ -4,10 +4,10 @@ use rusqlite::{Connection, OptionalExtension, Result, Transaction, params};
use serde::Serialize;
use serde_json::Value;
use std::{
borrow::Cow,
collections::HashMap,
env::current_dir,
io::{Cursor, Read},
fs::{File, create_dir_all},
io::{self, Read},
path::{Path, PathBuf},
};
use zip::ZipArchive;
@@ -29,21 +29,6 @@ pub(super) fn normalize_term(s: &str) -> String {
.collect()
}
pub trait ZipSource {
fn file_name(&self) -> &str;
fn bytes(&self) -> std::io::Result<Cow<'_, [u8]>>;
}
impl ZipSource for PathBuf {
fn file_name(&self) -> &str {
Path::file_name(self).and_then(|n| n.to_str()).unwrap_or("")
}
fn bytes(&self) -> std::io::Result<Cow<'_, [u8]>> {
std::fs::read(self).map(Cow::Owned)
}
}
#[derive(Serialize, Debug, Clone)]
pub struct YomitanProgress {
pub message: String,
@@ -112,6 +97,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
@@ -185,7 +172,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
@@ -274,6 +261,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")?
@@ -434,23 +430,57 @@ impl YomitanWriter {
Ok(())
}
pub fn import_dictionary_zip_file<Z, Callback>(
pub fn import_dictionary_zip_file<Callback>(
&mut self,
zip_file: &Z,
zip_file: &PathBuf,
asset_dir: &PathBuf,
lang: &str,
progress_callback: Callback,
) -> anyhow::Result<YomitanZipImportResult, CJDicError>
where
Z: ZipSource,
Callback: Fn(YomitanProgress),
{
let bundle_name = zip_file.file_name();
let bundle_name = zip_file.file_name().and_then(|n| n.to_str()).unwrap_or("");
let _timer = Timer::new(format!("Importing {}", bundle_name));
self.conn.execute_batch("PRAGMA foreign_keys = off")?;
let cursor = Cursor::new(zip_file.bytes()?);
let mut archive = ZipArchive::new(cursor)?;
let file = File::open(&zip_file)?;
let mut archive = ZipArchive::new(file)?;
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) = {
let f = archive.by_index(i)?;
(f.enclosed_name(), f.is_dir())
} {
if is_dir {
continue;
}
if asset_name.parent().is_none() {
continue;
}
match asset_name.extension().and_then(|e| e.to_str()) {
Some("json") => continue,
_ => {}
}
let out_path = asset_dir.join(asset_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
asset_count += 1;
}
}
let index_file = match archive.by_name("index.json") {
Ok(mut f) => {
@@ -459,7 +489,6 @@ impl YomitanWriter {
serde_json::from_str::<Value>(&s)?
}
Err(e) => {
// TODO: proper error handling
eprintln!("{}", e);
return Ok(YomitanZipImportResult {
exists: false,
@@ -502,17 +531,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,
@@ -528,8 +561,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,
@@ -538,6 +571,7 @@ impl YomitanWriter {
index_file.get("description").and_then(Value::as_str),
lang,
bundle_name,
asset_count,
],
)?;
+1 -1
View File
@@ -7,7 +7,7 @@ mod service;
mod tokenizer;
mod util;
pub use db::{YomitanProgress, YomitanRow, YomitanZipImportResult, ZipSource};
pub use db::{YomitanProgress, YomitanRow, YomitanZipImportResult};
pub use error::CJDicError;
pub use service::{AppService, SqlParam, YomitanDictEntry};
pub use tokenizer::TokenizeSegment;
+30 -19
View File
@@ -1,13 +1,13 @@
use std::{
collections::{HashMap, HashSet},
path::Path,
path::{Path, PathBuf},
};
use rusqlite::{Connection, params_from_iter};
use serde::{Deserialize, Serialize};
use crate::{
Timer, ZipSource,
Timer,
db::{
DBFILE, Database, DbChild, YomitanProgress, YomitanRow, YomitanWriter,
YomitanZipImportResult,
@@ -40,6 +40,7 @@ pub enum SqlParam {
Bool(bool),
}
#[derive(Clone)]
pub struct AppService {
db: Database,
tokenizer: Tokenizer,
@@ -163,23 +164,23 @@ impl AppService {
Ok(out)
}
pub fn load_yomitan_zip_dir<Z, LoadCallback, ImportCallback>(
pub fn load_yomitan_zip_dir<LoadCallback, ImportCallback>(
&self,
zip_dir: Vec<Z>,
zip_dir: Vec<PathBuf>,
asset_dir: &PathBuf,
lang: &str,
load_callback: LoadCallback,
import_callback: ImportCallback,
) -> Result<LoadYomitanZipDirResult, CJDicError>
where
Z: ZipSource,
LoadCallback: Fn(LoadYomitanZipDirResult),
ImportCallback: Fn(YomitanProgress),
{
let mut dir_zip_map: HashMap<String, &Z> = HashMap::new();
let mut dir_zip_map: HashMap<String, &PathBuf> = HashMap::new();
let mut zip_dir_ord: HashMap<String, usize> = HashMap::new();
for (i, entry) in zip_dir.iter().enumerate() {
let filename = entry.file_name();
let filename = entry.file_name().and_then(|n| n.to_str()).unwrap_or("");
dir_zip_map.insert(filename.to_string(), entry);
zip_dir_ord.insert(filename.to_string(), i);
}
@@ -187,12 +188,21 @@ impl AppService {
let mut new_dicts: Vec<String> = vec![];
let mut to_be_removed_dicts: Vec<String> = 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<String> = HashSet::new();
@@ -227,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() {
@@ -246,7 +251,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,
)?;
}
}
}
@@ -265,17 +276,17 @@ impl AppService {
})
}
pub fn import_yomitan_zip_file<Z, Callback>(
pub fn import_yomitan_zip_file<Callback>(
writer: &mut YomitanWriter, // Reuse existing connection
zip_file: &Z,
zip_file: &PathBuf,
asset_dir: &PathBuf,
lang: &str,
progress_callback: Callback,
) -> Result<YomitanZipImportResult, CJDicError>
where
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

+14
View File
@@ -44,3 +44,17 @@ html {
height: 100vh;
overflow-y: hidden;
}
html.dark .glossaries {
span {
word-break: keep-all;
}
img[src$=".svg"], img[src^="data:image/svg"] {
filter: invert(1) hue-rotate(180deg);
}
rt {
font-size: 50%;
}
}
+37 -21
View File
@@ -1,4 +1,5 @@
import { createElement as h } from "react";
import { createElement as h, useEffect, useState } from "react";
import { BaseDirectory, readFile } from "@tauri-apps/plugin-fs";
export default function Glossary({
glossary,
@@ -29,18 +30,14 @@ export default function Glossary({
// GlossaryImage
if ("type" in glossary && glossary.type === "image") {
return <GlossaryImage {...glossary} />;
return <GlossaryImage img={glossary} />;
}
// GlossaryStructuredContent
if ("type" in glossary && glossary.type === "structured-content") {
const sc = glossary;
return (
<StructuredContent
node={sc.content}
maxLength={maxLength}
onTermClicked={onTermClicked}
/>
<StructuredContent node={sc.content} {...{ maxLength, onTermClicked }} />
);
}
@@ -53,17 +50,37 @@ export default function Glossary({
return null;
}
function GlossaryImage(img: any) {
function GlossaryImage({ img }: { img: any }) {
const [src, set_src] = useState(img.path);
useEffect(() => {
readFile("yomitan/" + img.path, {
baseDir: BaseDirectory.AppData,
}).then((b) => {
const r = new FileReader();
r.onload = () => {
const dataSrc = r.result as string;
const ext = img.path.split(".").pop();
set_src(
dataSrc.replace(
"data:application/octet-stream",
"data:image/" + (ext === "svg" ? "svg+xml" : ext),
),
);
};
r.readAsDataURL(new File([b], img.path));
});
});
return (
<img
src={img.path}
width={img.width}
height={img.height}
className="glossary-image"
src={src}
title={img.title}
alt={img.alt}
style={{
imageRendering: img.imageRendering || "auto",
width: `${img.width}em`,
width: `${img.width}em`, // unit seems to be em, not px
height: `${img.height}em`,
}}
/>
@@ -100,8 +117,7 @@ function StructuredContent({
<StructuredContent
key={i}
node={n}
maxLength={maxLength}
onTermClicked={onTermClicked}
{...{ maxLength, onTermClicked }}
/>
))}
</>
@@ -118,7 +134,7 @@ function StructuredContent({
// Image
if (tag === "img") {
return <GlossaryImage {...node} />;
return <GlossaryImage img={node} />;
}
// Link
@@ -143,11 +159,7 @@ function StructuredContent({
}}
>
{content ? (
<StructuredContent
node={content}
maxLength={maxLength}
onTermClicked={onTermClicked}
/>
<StructuredContent node={content} {...{ maxLength, onTermClicked }} />
) : null}
</a>
);
@@ -159,6 +171,10 @@ function StructuredContent({
return h(
tag,
{ style: block.style, title: block.title, open: block.open },
StructuredContent({ node: block.content, maxLength, onTermClicked }),
StructuredContent({
node: block.content,
maxLength,
onTermClicked,
}),
);
}
+59 -4
View File
@@ -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;
@@ -79,9 +87,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 +97,7 @@ function LoadingDialog() {
url:
(await ghLatestReleaseURL(
"MarvNC/pixiv-yomitan",
/^Pixiv_.+\.zip$/,
/^PixivLight_.+\.zip$/,
).catch((e) => {
console.error(e);
return "";
@@ -119,6 +127,53 @@ function LoadingDialog() {
});
}
}
const LAST_API_CALL_KEY = "LAST_API_CALL_KEY_storageWorkerURL";
const LAST_API_JSON_KEY = "LAST_API_JSON_KEY_storageWorkerURL";
let entries: YomitanStorageWorkerDictEntry[] = [];
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);
}
}
const prefix = `${lang}/`;
try {
if (!entries.length) {
const { VITE_STORAGE_WORKER_URL, VITE_STORAGE_WORKER_TOKEN } =
import.meta.env;
console.log(`accessing ${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));
}
} catch (e) {
console.error(e);
entries = JSON.parse(lastJSON || "[]");
}
dicts.push(
...entries
.filter((o) => o.key.startsWith(prefix))
.map((o) => ({
filepath: `yomitan/${lang}/${o.key.substring(prefix.length)}`,
url: o.url,
})),
);
}
await invoke("init_yomitan", { dicts, lang });
+5 -5
View File
@@ -69,6 +69,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) {
@@ -401,6 +404,7 @@ function SearchPage() {
</Card.Header>
<Card.Body>
<div
className="glossaries"
style={{
display: "flex",
flexDirection: "column",
@@ -408,11 +412,7 @@ function SearchPage() {
}}
>
{JSON.parse(glossary_json).map((g: any, i: number) => (
<Glossary
key={i}
glossary={g}
onTermClicked={onTermClicked}
/>
<Glossary key={i} glossary={g} {...{ onTermClicked }} />
))}
</div>
</Card.Body>
+11 -1
View File
@@ -8,8 +8,18 @@
"permissions": [
"core:default",
"opener:default",
"http:default",
{
"identifier": "http:default",
"allow": [
{ "url": "https://api.github.com/repos/*/releases/latest" },
{ "url": "https://storage-worker.*.workers.dev" }
]
},
"fs:default",
{
"identifier": "fs:allow-appdata-read",
"allow": [{ "path": "$APPDATA/yomitan/**" }]
},
"clipboard-manager:allow-read-text"
]
}
+1 -1
View File
@@ -41,7 +41,7 @@ android {
}
buildTypes {
getByName("debug") {
applicationIdSuffix = ".dev"
// applicationIdSuffix = ".dev"
manifestPlaceholders["usesCleartextTraffic"] = "true"
isDebuggable = true
isJniDebuggable = true
Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

@@ -1,4 +0,0 @@
<resources>
<string name="app_name">cjdic2 (debug)</string>
<string name="main_activity_title">cjdic2 (debug)</string>
</resources>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.9 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.8 KiB

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.8 KiB

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 163 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 974 B

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 903 B

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.4 KiB

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 85 KiB

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 232 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 961 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 937 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

+46 -40
View File
@@ -34,53 +34,60 @@ 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);
let state = state.inner().clone();
let app_clone = app.clone();
state.load_yomitan_zip_dir(
zip_files,
lang.as_str(),
|r| {
update_count.set(update_count.get() + r.new_dicts.len() + r.to_be_removed_dicts.len());
app.emit("load-yomitan-dir", r).unwrap();
},
|r| {
update_count.set(update_count.get() + 1);
app.emit("yomitan-import-progress", r).unwrap();
},
)?;
let update_count = tokio::task::spawn_blocking(move || {
let update_count = Cell::new(0usize);
if update_count.get() > 0 {
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());
app.emit("load-yomitan-dir", r).unwrap();
},
|r| {
update_count.set(update_count.get() + 1);
app.emit("yomitan-import-progress", r).unwrap();
},
)?;
Ok::<usize, CJDicError>(update_count.get())
})
.await
.map_err(|e| CJDicError::Error(e.to_string()))??;
if update_count > 0 {
println!("Request app restart?",);
// Request app restart anyway
app.request_restart();
app_clone.request_restart();
}
Ok(())
@@ -97,6 +104,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 +114,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 +161,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 +180,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 +218,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 +245,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,
+2 -2
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "cjdic2",
"version": "0.1.2",
"version": "0.1.3",
"identifier": "cc.polv.cjdic2",
"build": {
"beforeDevCommand": "pnpm dev",
@@ -19,7 +19,7 @@
}
],
"security": {
"csp": null
"csp": null
}
},
"bundle": {
+12
View File
@@ -0,0 +1,12 @@
# http://editorconfig.org
root = true
[*]
indent_style = tab
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.yml]
indent_style = space
+167
View File
@@ -0,0 +1,167 @@
# Logs
logs
_.log
npm-debug.log_
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
# Runtime data
pids
_.pid
_.seed
\*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
\*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
\*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
\*.tgz
# Yarn Integrity file
.yarn-integrity
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
.cache
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.\*
# wrangler project
.dev.vars*
!.dev.vars.example
.env*
!.env.example
.wrangler/
+6
View File
@@ -0,0 +1,6 @@
{
"printWidth": 140,
"singleQuote": true,
"semi": true,
"useTabs": true
}
+5
View File
@@ -0,0 +1,5 @@
{
"files.associations": {
"wrangler.json": "jsonc"
}
}
+34
View File
@@ -0,0 +1,34 @@
# Cloudflare Workers
STOP. Your knowledge of Cloudflare Workers APIs and limits may be outdated. Always retrieve current documentation before any Workers, KV, R2, D1, Durable Objects, Queues, Vectorize, AI, or Agents SDK task.
## Docs
- https://developers.cloudflare.com/workers/
- MCP: `https://docs.mcp.cloudflare.com/mcp`
For all limits and quotas, retrieve from the product's `/platform/limits/` page. eg. `/workers/platform/limits`
## Commands
| Command | Purpose |
|---------|---------|
| `npx wrangler dev` | Local development |
| `npx wrangler deploy` | Deploy to Cloudflare |
| `npx wrangler types` | Generate TypeScript types |
Run `wrangler types` after changing bindings in wrangler.jsonc.
## Node.js Compatibility
https://developers.cloudflare.com/workers/runtime-apis/nodejs/
## Errors
- **Error 1102** (CPU/Memory exceeded): Retrieve limits from `/workers/platform/limits/`
- **All errors**: https://developers.cloudflare.com/workers/observability/errors/
## Product Docs
Retrieve API references and limits from:
`/kv/` · `/r2/` · `/d1/` · `/durable-objects/` · `/queues/` · `/vectorize/` · `/workers-ai/` · `/agents/`
+3
View File
@@ -0,0 +1,3 @@
interface Env {
YOMITAN_STORAGE_API_TOKEN: string;
}
+19
View File
@@ -0,0 +1,19 @@
{
"name": "storage-worker",
"version": "0.0.0",
"private": true,
"scripts": {
"deploy": "wrangler deploy",
"dev": "wrangler dev",
"start": "wrangler dev",
"test": "vitest",
"cf-typegen": "wrangler types"
},
"devDependencies": {
"@cloudflare/vitest-pool-workers": "^0.12.4",
"@types/node": "^24.10.13",
"typescript": "^5.5.2",
"vitest": "~3.2.0",
"wrangler": "^4.76.0"
}
}
+50
View File
@@ -0,0 +1,50 @@
/**
* Welcome to Cloudflare Workers! This is your first worker.
*
* - Run `npm run dev` in your terminal to start a development server
* - Open a browser tab at http://localhost:8787/ to see your worker in action
* - Run `npm run deploy` to publish your worker
*
* Bind resources to your worker in `wrangler.jsonc`. After adding bindings, a type definition for the
* `Env` object can be regenerated with `npm run cf-typegen`.
*
* Learn more at https://developers.cloudflare.com/workers/
*/
export default {
async fetch(request, env) {
const url = new URL(request.url);
// List files: GET /files
if (url.pathname === '/files') {
// Auth check first
const auth = request.headers.get("Authorization");
if (auth !== `Bearer ${env.YOMITAN_STORAGE_API_TOKEN}`) {
return new Response("Unauthorized", { status: 401 });
}
const list = await env.BUCKET.list();
const files = list.objects
.filter((o) => o.size)
.map((o) => ({
key: o.key,
size: o.size,
uploaded: o.uploaded,
url: `${url.origin}/download/${o.key}`,
}));
return Response.json(files);
}
// Download: GET /download/filename.zip
if (url.pathname.startsWith('/download/')) {
const key = url.pathname.replace('/download/', '');
const o = await env.BUCKET.get(key);
if (!o || !o.size) return new Response('Not found', { status: 404 });
return new Response(o.body, {
headers: { 'Content-Disposition': `attachment; filename="${key}"` },
});
}
return new Response('Not found', { status: 404 });
},
} satisfies ExportedHandler<Env>;
+3
View File
@@ -0,0 +1,3 @@
declare module 'cloudflare:test' {
interface ProvidedEnv extends Env {}
}
+24
View File
@@ -0,0 +1,24 @@
import { env, createExecutionContext, waitOnExecutionContext, SELF } from 'cloudflare:test';
import { describe, it, expect } from 'vitest';
import worker from '../src/index';
// For now, you'll need to do something like this to get a correctly-typed
// `Request` to pass to `worker.fetch()`.
const IncomingRequest = Request<unknown, IncomingRequestCfProperties>;
describe('Hello World worker', () => {
it('responds with Hello World! (unit style)', async () => {
const request = new IncomingRequest('http://example.com');
// Create an empty context to pass to `worker.fetch()`.
const ctx = createExecutionContext();
const response = await worker.fetch(request, env, ctx);
// Wait for all `Promise`s passed to `ctx.waitUntil()` to settle before running test assertions
await waitOnExecutionContext(ctx);
expect(await response.text()).toMatchInlineSnapshot(`"Hello World!"`);
});
it('responds with Hello World! (integration style)', async () => {
const response = await SELF.fetch('https://example.com');
expect(await response.text()).toMatchInlineSnapshot(`"Hello World!"`);
});
});
+8
View File
@@ -0,0 +1,8 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"types": ["@cloudflare/vitest-pool-workers"]
},
"include": ["./**/*.ts", "../worker-configuration.d.ts"],
"exclude": []
}
+47
View File
@@ -0,0 +1,47 @@
{
"compilerOptions": {
/* Visit https://aka.ms/tsconfig.json to read more about this file */
/* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
"target": "es2024",
/* Specify a set of bundled library declaration files that describe the target runtime environment. */
"lib": ["es2024"],
/* Specify what JSX code is generated. */
"jsx": "react-jsx",
/* Specify what module code is generated. */
"module": "es2022",
/* Specify how TypeScript looks up a file from a given module specifier. */
"moduleResolution": "Bundler",
/* Enable importing .json files */
"resolveJsonModule": true,
/* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */
"allowJs": true,
/* Enable error reporting in type-checked JavaScript files. */
"checkJs": false,
/* Disable emitting files from a compilation. */
"noEmit": true,
/* Ensure that each file can be safely transpiled without relying on other imports. */
"isolatedModules": true,
/* Allow 'import x from y' when a module doesn't have a default export. */
"allowSyntheticDefaultImports": true,
/* Ensure that casing is correct in imports. */
"forceConsistentCasingInFileNames": true,
/* Enable all strict type-checking options. */
"strict": true,
/* Skip type checking all .d.ts files. */
"skipLibCheck": true,
"types": [
"./worker-configuration.d.ts",
"./env.d.ts",
"node"
]
},
"exclude": ["test"],
"include": ["worker-configuration.d.ts", "src/**/*.ts"]
}
Loaded 100 of 103 files, more files were not shown because too many files have changed in this diff. Show more