18 Commits
Author SHA1 Message Date
polv 0d20f18634 remove next tauri from git 2026-03-29 18:25:22 +07:00
polv 7168acd1c4 scaffold next tauri for sqlx and first run fix 2026-03-29 09:15:54 +07:00
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
polv 79deaf9372 fix fts prefix ~ 2026-03-16 12:44:12 +07:00
polv f0cafa450d full text search and UI button 2026-03-16 00:05:25 +07:00
polv afacdc0856 load next scroll faster in search 2026-03-15 19:38:11 +07:00
polv 7033e70ddf add alternate app icon 2026-03-15 19:21:52 +07:00
polv 96e36e1d4c furigana format is now <>{} and add input message for glob 2026-03-15 14:27:34 +07:00
polv b1ef987f9d router and enable back button 2026-03-15 11:49:20 +07:00
polv 41e207afad fix scroll and wanakana 2026-03-10 23:40:06 +07:00
99 changed files with 14033 additions and 567 deletions

No files matched your search

+2 -1
View File
@@ -1,2 +1,3 @@
# flutter_app/** linguist-vendored # flutter_app/** linguist-vendored
# src-tauri/gen/** linguist-vendored src-tauri/gen/android/** linguist-vendored
cjdic2-next/src-tauri/gen/android/** linguist-vendored
Generated
+488 -325
View File
File diff suppressed because it is too large. Load diff
+4
View File
@@ -1,8 +1,12 @@
[workspace] [workspace]
members = ["src-core", "src-tauri"] members = ["src-core", "src-tauri"]
# exclude = ["cjdic2-next/src-tauri"]
resolver = "3" resolver = "3"
[profile.release] [profile.release]
lto = true lto = true
codegen-units = 1 codegen-units = 1
strip = true 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": "src-tauri",
}, },
{
"path": "storage-worker",
},
], ],
"settings": { "settings": {
// "importSorter.generalConfiguration.sortOnBeforeSave": true, // "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 -8
View File
@@ -1,26 +1,26 @@
{ {
"name": "cjdic2", "name": "cjdic2",
"private": true, "private": true,
"version": "0.1.1",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev:desktop": "cross-env RUST_BACKTRACE=1 tauri 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: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", "dev:android": "cross-env RUST_BACKTRACE=1 tauri android run",
"log:android": "adb logcat -e \"RustStdoutStderr\"", "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:desktop": "tauri build",
"build:android": "tauri android 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", "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", "run:desktop": ".\\target\\release\\cjdic2.exe",
"release": "node release.js", "release": "node release.js",
"dev": "cd src-react && pnpm run dev", "dev": "cd src-react && pnpm run dev",
"build": "cd src-react && pnpm run build", "build": "cd src-react && pnpm run build"
"tauri": "tauri"
}, },
"devDependencies": { "devDependencies": {
"@tauri-apps/cli": "^2", "@tauri-apps/cli": "^2",
"cross-env": "^10.1.0" "cross-env": "^10.1.0",
"sharp": "^0.34.5"
} }
} }
+336 -5
View File
@@ -14,6 +14,9 @@ importers:
cross-env: cross-env:
specifier: ^10.1.0 specifier: ^10.1.0
version: 10.1.0 version: 10.1.0
sharp:
specifier: ^0.34.5
version: 0.34.5
js-server: js-server:
dependencies: dependencies:
@@ -90,6 +93,9 @@ importers:
react-icons: react-icons:
specifier: ^5.5.0 specifier: ^5.5.0
version: 5.5.0(react@19.2.4) version: 5.5.0(react@19.2.4)
react-router:
specifier: ^7.13.1
version: 7.13.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
wanakana: wanakana:
specifier: ^5.3.1 specifier: ^5.3.1
version: 5.3.1 version: 5.3.1
@@ -105,13 +111,13 @@ importers:
version: 19.2.3(@types/react@19.2.14) version: 19.2.3(@types/react@19.2.14)
'@vitejs/plugin-react': '@vitejs/plugin-react':
specifier: ^4.6.0 specifier: ^4.6.0
version: 4.7.0(vite@7.3.1(@types/node@24.10.13)(tsx@4.21.0)) version: 4.7.0(vite@7.3.1(@types/node@24.10.13)(tsx@4.21.0)(yaml@2.8.3))
typescript: typescript:
specifier: ~5.8.3 specifier: ~5.8.3
version: 5.8.3 version: 5.8.3
vite: vite:
specifier: ^7.0.4 specifier: ^7.0.4
version: 7.3.1(@types/node@24.10.13)(tsx@4.21.0) version: 7.3.1(@types/node@24.10.13)(tsx@4.21.0)(yaml@2.8.3)
packages: packages:
@@ -215,6 +221,9 @@ packages:
react: '>=18' react: '>=18'
react-dom: '>=18' react-dom: '>=18'
'@emnapi/runtime@1.9.1':
resolution: {integrity: sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA==}
'@emotion/babel-plugin@11.13.5': '@emotion/babel-plugin@11.13.5':
resolution: {integrity: sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==} resolution: {integrity: sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==}
@@ -457,6 +466,159 @@ packages:
'@floating-ui/utils@0.2.10': '@floating-ui/utils@0.2.10':
resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==} resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==}
'@img/colour@1.1.0':
resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==}
engines: {node: '>=18'}
'@img/sharp-darwin-arm64@0.34.5':
resolution: {integrity: sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm64]
os: [darwin]
'@img/sharp-darwin-x64@0.34.5':
resolution: {integrity: sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64]
os: [darwin]
'@img/sharp-libvips-darwin-arm64@1.2.4':
resolution: {integrity: sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==}
cpu: [arm64]
os: [darwin]
'@img/sharp-libvips-darwin-x64@1.2.4':
resolution: {integrity: sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==}
cpu: [x64]
os: [darwin]
'@img/sharp-libvips-linux-arm64@1.2.4':
resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-arm@1.2.4':
resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==}
cpu: [arm]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-ppc64@1.2.4':
resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-riscv64@1.2.4':
resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-s390x@1.2.4':
resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-x64@1.2.4':
resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==}
cpu: [x64]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linuxmusl-arm64@1.2.4':
resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==}
cpu: [arm64]
os: [linux]
libc: [musl]
'@img/sharp-libvips-linuxmusl-x64@1.2.4':
resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==}
cpu: [x64]
os: [linux]
libc: [musl]
'@img/sharp-linux-arm64@0.34.5':
resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@img/sharp-linux-arm@0.34.5':
resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm]
os: [linux]
libc: [glibc]
'@img/sharp-linux-ppc64@0.34.5':
resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@img/sharp-linux-riscv64@0.34.5':
resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@img/sharp-linux-s390x@0.34.5':
resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@img/sharp-linux-x64@0.34.5':
resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64]
os: [linux]
libc: [glibc]
'@img/sharp-linuxmusl-arm64@0.34.5':
resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm64]
os: [linux]
libc: [musl]
'@img/sharp-linuxmusl-x64@0.34.5':
resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64]
os: [linux]
libc: [musl]
'@img/sharp-wasm32@0.34.5':
resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [wasm32]
'@img/sharp-win32-arm64@0.34.5':
resolution: {integrity: sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm64]
os: [win32]
'@img/sharp-win32-ia32@0.34.5':
resolution: {integrity: sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [ia32]
os: [win32]
'@img/sharp-win32-x64@0.34.5':
resolution: {integrity: sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64]
os: [win32]
'@internationalized/date@3.11.0': '@internationalized/date@3.11.0':
resolution: {integrity: sha512-BOx5huLAWhicM9/ZFs84CzP+V3gBW6vlpM02yzsdYC7TGlZJX1OJiEEHcSayF00Z+3jLlm4w79amvSt6RqKN3Q==} resolution: {integrity: sha512-BOx5huLAWhicM9/ZFs84CzP+V3gBW6vlpM02yzsdYC7TGlZJX1OJiEEHcSayF00Z+3jLlm4w79amvSt6RqKN3Q==}
@@ -1135,6 +1297,10 @@ packages:
resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==}
engines: {node: '>=6'} engines: {node: '>=6'}
detect-libc@2.1.2:
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
engines: {node: '>=8'}
dotenv@17.3.1: dotenv@17.3.1:
resolution: {integrity: sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA==} resolution: {integrity: sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA==}
engines: {node: '>=12'} engines: {node: '>=12'}
@@ -1442,6 +1608,16 @@ packages:
resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==} resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
react-router@7.13.1:
resolution: {integrity: sha512-td+xP4X2/6BJvZoX6xw++A2DdEi++YypA69bJUV5oVvqf6/9/9nNlD70YO1e9d3MyamJEBQFEzk6mbfDYbqrSA==}
engines: {node: '>=20.0.0'}
peerDependencies:
react: '>=18'
react-dom: '>=18'
peerDependenciesMeta:
react-dom:
optional: true
react@19.2.4: react@19.2.4:
resolution: {integrity: sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==} resolution: {integrity: sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
@@ -1510,6 +1686,10 @@ packages:
setprototypeof@1.2.0: setprototypeof@1.2.0:
resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==}
sharp@0.34.5:
resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
shebang-command@2.0.0: shebang-command@2.0.0:
resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
engines: {node: '>=8'} engines: {node: '>=8'}
@@ -1668,6 +1848,11 @@ packages:
resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==} resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==}
engines: {node: '>= 6'} engines: {node: '>= 6'}
yaml@2.8.3:
resolution: {integrity: sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==}
engines: {node: '>= 14.6'}
hasBin: true
zustand@5.0.11: zustand@5.0.11:
resolution: {integrity: sha512-fdZY+dk7zn/vbWNCYmzZULHRrss0jx5pPFiOuMZ/5HJN6Yv3u+1Wswy/4MpZEkEGhtNH+pwxZB8OKgUBPzYAGg==} resolution: {integrity: sha512-fdZY+dk7zn/vbWNCYmzZULHRrss0jx5pPFiOuMZ/5HJN6Yv3u+1Wswy/4MpZEkEGhtNH+pwxZB8OKgUBPzYAGg==}
engines: {node: '>=12.20.0'} engines: {node: '>=12.20.0'}
@@ -1885,6 +2070,11 @@ snapshots:
react: 19.2.4 react: 19.2.4
react-dom: 19.2.4(react@19.2.4) react-dom: 19.2.4(react@19.2.4)
'@emnapi/runtime@1.9.1':
dependencies:
tslib: 2.8.1
optional: true
'@emotion/babel-plugin@11.13.5': '@emotion/babel-plugin@11.13.5':
dependencies: dependencies:
'@babel/helper-module-imports': 7.28.6 '@babel/helper-module-imports': 7.28.6
@@ -2099,6 +2289,102 @@ snapshots:
'@floating-ui/utils@0.2.10': {} '@floating-ui/utils@0.2.10': {}
'@img/colour@1.1.0': {}
'@img/sharp-darwin-arm64@0.34.5':
optionalDependencies:
'@img/sharp-libvips-darwin-arm64': 1.2.4
optional: true
'@img/sharp-darwin-x64@0.34.5':
optionalDependencies:
'@img/sharp-libvips-darwin-x64': 1.2.4
optional: true
'@img/sharp-libvips-darwin-arm64@1.2.4':
optional: true
'@img/sharp-libvips-darwin-x64@1.2.4':
optional: true
'@img/sharp-libvips-linux-arm64@1.2.4':
optional: true
'@img/sharp-libvips-linux-arm@1.2.4':
optional: true
'@img/sharp-libvips-linux-ppc64@1.2.4':
optional: true
'@img/sharp-libvips-linux-riscv64@1.2.4':
optional: true
'@img/sharp-libvips-linux-s390x@1.2.4':
optional: true
'@img/sharp-libvips-linux-x64@1.2.4':
optional: true
'@img/sharp-libvips-linuxmusl-arm64@1.2.4':
optional: true
'@img/sharp-libvips-linuxmusl-x64@1.2.4':
optional: true
'@img/sharp-linux-arm64@0.34.5':
optionalDependencies:
'@img/sharp-libvips-linux-arm64': 1.2.4
optional: true
'@img/sharp-linux-arm@0.34.5':
optionalDependencies:
'@img/sharp-libvips-linux-arm': 1.2.4
optional: true
'@img/sharp-linux-ppc64@0.34.5':
optionalDependencies:
'@img/sharp-libvips-linux-ppc64': 1.2.4
optional: true
'@img/sharp-linux-riscv64@0.34.5':
optionalDependencies:
'@img/sharp-libvips-linux-riscv64': 1.2.4
optional: true
'@img/sharp-linux-s390x@0.34.5':
optionalDependencies:
'@img/sharp-libvips-linux-s390x': 1.2.4
optional: true
'@img/sharp-linux-x64@0.34.5':
optionalDependencies:
'@img/sharp-libvips-linux-x64': 1.2.4
optional: true
'@img/sharp-linuxmusl-arm64@0.34.5':
optionalDependencies:
'@img/sharp-libvips-linuxmusl-arm64': 1.2.4
optional: true
'@img/sharp-linuxmusl-x64@0.34.5':
optionalDependencies:
'@img/sharp-libvips-linuxmusl-x64': 1.2.4
optional: true
'@img/sharp-wasm32@0.34.5':
dependencies:
'@emnapi/runtime': 1.9.1
optional: true
'@img/sharp-win32-arm64@0.34.5':
optional: true
'@img/sharp-win32-ia32@0.34.5':
optional: true
'@img/sharp-win32-x64@0.34.5':
optional: true
'@internationalized/date@3.11.0': '@internationalized/date@3.11.0':
dependencies: dependencies:
'@swc/helpers': 0.5.18 '@swc/helpers': 0.5.18
@@ -2373,7 +2659,7 @@ snapshots:
dependencies: dependencies:
'@types/node': 24.10.13 '@types/node': 24.10.13
'@vitejs/plugin-react@4.7.0(vite@7.3.1(@types/node@24.10.13)(tsx@4.21.0))': '@vitejs/plugin-react@4.7.0(vite@7.3.1(@types/node@24.10.13)(tsx@4.21.0)(yaml@2.8.3))':
dependencies: dependencies:
'@babel/core': 7.29.0 '@babel/core': 7.29.0
'@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.29.0) '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.29.0)
@@ -2381,7 +2667,7 @@ snapshots:
'@rolldown/pluginutils': 1.0.0-beta.27 '@rolldown/pluginutils': 1.0.0-beta.27
'@types/babel__core': 7.20.5 '@types/babel__core': 7.20.5
react-refresh: 0.17.0 react-refresh: 0.17.0
vite: 7.3.1(@types/node@24.10.13)(tsx@4.21.0) vite: 7.3.1(@types/node@24.10.13)(tsx@4.21.0)(yaml@2.8.3)
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
@@ -3028,6 +3314,8 @@ snapshots:
dequal@2.0.3: {} dequal@2.0.3: {}
detect-libc@2.1.2: {}
dotenv@17.3.1: {} dotenv@17.3.1: {}
electron-to-chromium@1.5.302: {} electron-to-chromium@1.5.302: {}
@@ -3321,6 +3609,14 @@ snapshots:
react-refresh@0.17.0: {} react-refresh@0.17.0: {}
react-router@7.13.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
dependencies:
cookie: 1.1.1
react: 19.2.4
set-cookie-parser: 2.7.2
optionalDependencies:
react-dom: 19.2.4(react@19.2.4)
react@19.2.4: {} react@19.2.4: {}
real-require@0.2.0: {} real-require@0.2.0: {}
@@ -3392,6 +3688,37 @@ snapshots:
setprototypeof@1.2.0: {} setprototypeof@1.2.0: {}
sharp@0.34.5:
dependencies:
'@img/colour': 1.1.0
detect-libc: 2.1.2
semver: 7.7.4
optionalDependencies:
'@img/sharp-darwin-arm64': 0.34.5
'@img/sharp-darwin-x64': 0.34.5
'@img/sharp-libvips-darwin-arm64': 1.2.4
'@img/sharp-libvips-darwin-x64': 1.2.4
'@img/sharp-libvips-linux-arm': 1.2.4
'@img/sharp-libvips-linux-arm64': 1.2.4
'@img/sharp-libvips-linux-ppc64': 1.2.4
'@img/sharp-libvips-linux-riscv64': 1.2.4
'@img/sharp-libvips-linux-s390x': 1.2.4
'@img/sharp-libvips-linux-x64': 1.2.4
'@img/sharp-libvips-linuxmusl-arm64': 1.2.4
'@img/sharp-libvips-linuxmusl-x64': 1.2.4
'@img/sharp-linux-arm': 0.34.5
'@img/sharp-linux-arm64': 0.34.5
'@img/sharp-linux-ppc64': 0.34.5
'@img/sharp-linux-riscv64': 0.34.5
'@img/sharp-linux-s390x': 0.34.5
'@img/sharp-linux-x64': 0.34.5
'@img/sharp-linuxmusl-arm64': 0.34.5
'@img/sharp-linuxmusl-x64': 0.34.5
'@img/sharp-wasm32': 0.34.5
'@img/sharp-win32-arm64': 0.34.5
'@img/sharp-win32-ia32': 0.34.5
'@img/sharp-win32-x64': 0.34.5
shebang-command@2.0.0: shebang-command@2.0.0:
dependencies: dependencies:
shebang-regex: 3.0.0 shebang-regex: 3.0.0
@@ -3456,7 +3783,7 @@ snapshots:
uqr@0.1.2: {} uqr@0.1.2: {}
vite@7.3.1(@types/node@24.10.13)(tsx@4.21.0): vite@7.3.1(@types/node@24.10.13)(tsx@4.21.0)(yaml@2.8.3):
dependencies: dependencies:
esbuild: 0.27.3 esbuild: 0.27.3
fdir: 6.5.0(picomatch@4.0.3) fdir: 6.5.0(picomatch@4.0.3)
@@ -3468,6 +3795,7 @@ snapshots:
'@types/node': 24.10.13 '@types/node': 24.10.13
fsevents: 2.3.3 fsevents: 2.3.3
tsx: 4.21.0 tsx: 4.21.0
yaml: 2.8.3
wanakana@5.3.1: {} wanakana@5.3.1: {}
@@ -3488,6 +3816,9 @@ snapshots:
yaml@1.10.2: {} yaml@1.10.2: {}
yaml@2.8.3:
optional: true
zustand@5.0.11(@types/react@19.2.14)(react@19.2.4): zustand@5.0.11(@types/react@19.2.14)(react@19.2.4):
optionalDependencies: optionalDependencies:
'@types/react': 19.2.14 '@types/react': 19.2.14
+1
View File
@@ -1,6 +1,7 @@
packages: packages:
- js-server - js-server
- src-react - src-react
# - cjdic2-next
onlyBuiltDependencies: onlyBuiltDependencies:
- esbuild - esbuild
+9 -3
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(
&mut writer,
&p,
&zip_dir.to_path_buf(),
"ja",
|progress| {
println!("{:?}", progress); println!("{:?}", progress);
})?, }
)?,
); );
} else { } else {
println!("not zip_file: {:?}", p); println!("not zip_file: {:?}", p);
+9 -4
View File
@@ -16,15 +16,16 @@ mod yomitan;
pub use yomitan::YomitanRow; pub use yomitan::YomitanRow;
mod yomitan_writer; mod yomitan_writer;
pub use yomitan_writer::{YomitanProgress, YomitanWriter, YomitanZipImportResult, ZipSource}; pub use yomitan_writer::{YomitanProgress, YomitanWriter, YomitanZipImportResult};
use crate::CJDicError; use crate::{CJDicError, tokenizer::Tokenizer};
#[derive(Clone)] #[derive(Clone)]
pub(crate) struct Database { pub(crate) struct Database {
pub(crate) conn: Arc<Mutex<Connection>>, pub(crate) conn: Arc<Mutex<Connection>>,
pub(crate) dir: PathBuf, pub(crate) dir: PathBuf,
is_db_attached: EnumMap<DbChild, Arc<AtomicBool>>, is_db_attached: EnumMap<DbChild, Arc<AtomicBool>>,
tokenizer: Tokenizer,
} }
#[derive(Debug, Enum, Clone, Copy)] #[derive(Debug, Enum, Clone, Copy)]
@@ -52,7 +53,7 @@ pub(crate) static DBSCHEMA: LazyLock<EnumMap<DbChild, &str>> = LazyLock::new(||
}); });
impl Database { impl Database {
pub fn new(db_dir: impl AsRef<Path>) -> Result<Self, CJDicError> { pub fn new(db_dir: impl AsRef<Path>, tokenizer: Tokenizer) -> Result<Self, CJDicError> {
let dir = db_dir.as_ref().to_path_buf(); let dir = db_dir.as_ref().to_path_buf();
let conn = Connection::open(dir.join(USER_DBFILE))?; let conn = Connection::open(dir.join(USER_DBFILE))?;
@@ -73,6 +74,7 @@ impl Database {
DbChild::YomitanGlossary => Arc::new(AtomicBool::new(false)), DbChild::YomitanGlossary => Arc::new(AtomicBool::new(false)),
DbChild::Search => Arc::new(AtomicBool::new(false)), DbChild::Search => Arc::new(AtomicBool::new(false)),
}, },
tokenizer,
}) })
} }
@@ -80,7 +82,10 @@ impl Database {
self.ensure_db_attached(DbChild::Yomitan)?; self.ensure_db_attached(DbChild::Yomitan)?;
self.ensure_db_attached(DbChild::YomitanGlossary)?; self.ensure_db_attached(DbChild::YomitanGlossary)?;
self.ensure_db_attached(DbChild::Search)?; self.ensure_db_attached(DbChild::Search)?;
Ok(yomitan::YomitanDatabase::new(self.clone())) Ok(yomitan::YomitanDatabase::new(
self.clone(),
self.tokenizer.clone(),
))
} }
fn ensure_db_attached(&self, db_child: DbChild) -> Result<(), CJDicError> { fn ensure_db_attached(&self, db_child: DbChild) -> Result<(), CJDicError> {
+1 -26
View File
@@ -1,5 +1,3 @@
use std::collections::HashSet;
use rusqlite::{Connection, Transaction, params}; use rusqlite::{Connection, Transaction, params};
use wana_kana::utils::katakana_to_hiragana; use wana_kana::utils::katakana_to_hiragana;
@@ -114,8 +112,6 @@ impl<'a> SearchDatabase<'a> {
DBSCHEMA[DbChild::Search] DBSCHEMA[DbChild::Search]
))?; ))?;
let exclude_pos =
HashSet::from(["助詞", "助動詞", "記号", "接続詞"].map(|p| p.to_string()));
let mut stmt_write_ft = tx.prepare(&format!( let mut stmt_write_ft = tx.prepare(&format!(
"INSERT INTO {}.terms_ft (rowid, term) VALUES (?1, ?2)", "INSERT INTO {}.terms_ft (rowid, term) VALUES (?1, ?2)",
DBSCHEMA[DbChild::Search] DBSCHEMA[DbChild::Search]
@@ -130,28 +126,7 @@ impl<'a> SearchDatabase<'a> {
max_score max_score
])?; ])?;
let mut segmented: Vec<String> = vec![]; stmt_write_ft.execute(params![id, tokenizer.ja_normalize(term).join(" ")])?;
for t in tokenizer.tokenize(term) {
let mut base = t.surface;
let mut pos = None;
for (i, dt) in t.details.iter().enumerate() {
match i {
0 => base = dt.to_string(),
6 => pos = Some(dt),
_ => {}
}
}
if let Some(p) = pos {
if exclude_pos.contains(p.as_str()) {
continue;
}
}
segmented.push(base);
}
stmt_write_ft.execute(params![id, segmented.join(" ")])?;
if i % 10_000 == 0 { if i % 10_000 == 0 {
progress_callback(YomitanProgress { progress_callback(YomitanProgress {
+63 -22
View File
@@ -5,6 +5,7 @@ use zstd::bulk::Decompressor;
use crate::{ use crate::{
CJDicError, Timer, CJDicError, Timer,
db::{Database, yomitan_writer::normalize_term}, db::{Database, yomitan_writer::normalize_term},
tokenizer::Tokenizer,
}; };
#[derive(Serialize, Debug, Clone)] #[derive(Serialize, Debug, Clone)]
@@ -15,7 +16,6 @@ pub struct YomitanRow {
rules: String, rules: String,
score: i64, score: i64,
glossary_json: String, glossary_json: String,
glossary_b: Vec<u8>,
sequence: Option<i64>, sequence: Option<i64>,
term_tags: String, term_tags: String,
dict_title: String, dict_title: String,
@@ -23,11 +23,12 @@ pub struct YomitanRow {
pub struct YomitanDatabase { pub struct YomitanDatabase {
db: Database, db: Database,
tokenizer: Tokenizer,
} }
impl YomitanDatabase { impl YomitanDatabase {
pub(crate) fn new(db: Database) -> Self { pub(crate) fn new(db: Database, tokenizer: Tokenizer) -> Self {
Self { db } Self { db, tokenizer }
} }
fn decompressor(&self) -> Result<Decompressor<'_>, CJDicError> { fn decompressor(&self) -> Result<Decompressor<'_>, CJDicError> {
@@ -66,8 +67,10 @@ impl YomitanDatabase {
// max decompressed: 90KB (compression ratio on largest: ~2.5x) // max decompressed: 90KB (compression ratio on largest: ~2.5x)
const MAX_DECOMPRESSED_SIZE: usize = 512 * 1024; // 512KB, ~5x headroom const MAX_DECOMPRESSED_SIZE: usize = 512 * 1024; // 512KB, ~5x headroom
let and_or = if q_term == q_reading { "OR" } else { "AND" }; let is_fts = q_term.starts_with('~');
let q_term = if is_fts {
normalize_term(q_term)
} else {
let mut new_t = String::new(); let mut new_t = String::new();
let mut new_seg = String::new(); let mut new_seg = String::new();
@@ -83,21 +86,58 @@ impl YomitanDatabase {
} }
new_t.push_str(&normalize_term(&new_seg)); new_t.push_str(&normalize_term(&new_seg));
let q_term_norm = new_t; new_t
};
let eq1 = if q_term_norm.len() > 0 { "GLOB" } else { "=" }; let and_or = if q_term == q_reading { "OR" } else { "AND" };
let eq2 = if q_reading.len() > 0 { "GLOB" } else { "=" };
let conn = self.db.conn.lock()?; let conn = self.db.conn.lock()?;
let mut stmt = conn.prepare(&format!( conn.execute_batch(
" "
WITH s AS ( CREATE TEMP TABLE IF NOT EXISTS search_ids (term_id INTEGER);
SELECT * FROM search.terms DELETE FROM search_ids;
",
)?;
if is_fts {
let q = self.tokenizer.ja_normalize(q_term.to_string()).join(" ");
println!("fts: {q_term} => {q}");
if q.len() > 0 {
conn.execute(
&format!(
"
INSERT INTO search_ids (term_id)
SELECT rowid FROM search.terms_ft
WHERE term MATCH ?1
ORDER BY rank
LIMIT ?2 OFFSET ?3
"
),
params![q, limit, offset],
)?;
}
} else {
let eq1 = if q_term.len() > 0 { "GLOB" } else { "=" };
let eq2 = if q_reading.len() > 0 { "GLOB" } else { "=" };
conn.execute(
&format!(
"
INSERT INTO search_ids (term_id)
SELECT id FROM search.terms
WHERE term {eq1} ?1 {and_or} reading {eq2} ?2 WHERE term {eq1} ?1 {and_or} reading {eq2} ?2
ORDER BY max_score DESC ORDER BY max_score DESC
LIMIT ?3 OFFSET ?4 LIMIT ?3 OFFSET ?4
) "
),
params![q_term, q_reading, limit, offset],
)?;
}
let mut stmt = conn.prepare(&format!(
"
SELECT SELECT
t.term, t.term,
t.reading, t.reading,
@@ -108,8 +148,8 @@ impl YomitanDatabase {
t.sequence, t.sequence,
COALESCE(tt.tags, '') AS term_tags, COALESCE(tt.tags, '') AS term_tags,
d.title AS dict_title d.title AS dict_title
FROM s FROM search_ids s
JOIN yomitan.terms t ON t.id = s.id JOIN yomitan.terms t ON t.id = s.term_id
LEFT JOIN yomitan.dictionaries d ON d.id = t.dict_id LEFT JOIN yomitan.dictionaries d ON d.id = t.dict_id
LEFT JOIN yomitan.def_tag_sets dt ON dt.id = t.def_tags_id LEFT JOIN yomitan.def_tag_sets dt ON dt.id = t.def_tags_id
LEFT JOIN yomitan.rule_sets r ON r.id = t.rules_id LEFT JOIN yomitan.rule_sets r ON r.id = t.rules_id
@@ -117,27 +157,28 @@ impl YomitanDatabase {
LEFT JOIN glossary.glossaries g ON g.id = t.glossary_id LEFT JOIN glossary.glossaries g ON g.id = t.glossary_id
" "
))?; ))?;
let rows = stmt.query_map(params![q_term_norm, q_reading, limit, offset], |r| { let rows = stmt.query_map([], |r| {
Ok(YomitanRow { Ok((
YomitanRow {
term: r.get(0)?, term: r.get(0)?,
reading: r.get(1)?, reading: r.get(1)?,
def_tags: r.get(2)?, def_tags: r.get(2)?,
rules: r.get(3)?, rules: r.get(3)?,
score: r.get(4)?, score: r.get(4)?,
glossary_json: String::new(), glossary_json: String::from("[]"), // a valid JSON array
glossary_b: r.get(5)?,
sequence: r.get(6)?, sequence: r.get(6)?,
term_tags: r.get(7)?, term_tags: r.get(7)?,
dict_title: r.get(8)?, dict_title: r.get(8)?,
}) },
r.get::<_, Vec<u8>>(5)?,
))
})?; })?;
let mut out = Vec::new(); let mut out = Vec::new();
for row in rows { for row in rows {
let mut r = row?; let (mut r, glossary_b) = row?;
r.glossary_json = r.glossary_json =
String::from_utf8(decompressor.decompress(&r.glossary_b, MAX_DECOMPRESSED_SIZE)?)?; String::from_utf8(decompressor.decompress(&glossary_b, MAX_DECOMPRESSED_SIZE)?)?;
r.glossary_b = vec![];
out.push(r); out.push(r);
} }
+65 -31
View File
@@ -4,10 +4,10 @@ use rusqlite::{Connection, OptionalExtension, Result, Transaction, params};
use serde::Serialize; use serde::Serialize;
use serde_json::Value; use serde_json::Value;
use std::{ use std::{
borrow::Cow,
collections::HashMap, collections::HashMap,
env::current_dir, env::current_dir,
io::{Cursor, Read}, fs::{File, create_dir_all},
io::{self, Read},
path::{Path, PathBuf}, path::{Path, PathBuf},
}; };
use zip::ZipArchive; use zip::ZipArchive;
@@ -29,21 +29,6 @@ pub(super) fn normalize_term(s: &str) -> String {
.collect() .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)] #[derive(Serialize, Debug, Clone)]
pub struct YomitanProgress { pub struct YomitanProgress {
pub message: String, pub message: String,
@@ -112,6 +97,8 @@ impl YomitanWriter {
installed_at TEXT NOT NULL DEFAULT (datetime('now')), installed_at TEXT NOT NULL DEFAULT (datetime('now')),
bundle_name TEXT, -- custom bundle_name TEXT, -- custom
lang TEXT NOT NULL, -- custom lang TEXT NOT NULL, -- custom
asset_count INTEGER, -- custom
source TEXT, -- custom
UNIQUE (title, revision) UNIQUE (title, revision)
); );
CREATE TABLE IF NOT EXISTS glossaries ( -- not used. moved to a new file 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::YomitanGlossary)?;
self.ensure_db_attached(DbChild::Search)?; self.ensure_db_attached(DbChild::Search)?;
let schema_version = "2026-03-08"; let schema_version = "2026-03-21";
let current_schema_version = { let current_schema_version = {
let mut stmt = self let mut stmt = self
.conn .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 { if v < schema_version {
self.conn self.conn
.prepare("UPDATE schema_meta SET value = ?2 WHERE key = ?1")? .prepare("UPDATE schema_meta SET value = ?2 WHERE key = ?1")?
@@ -434,23 +430,57 @@ impl YomitanWriter {
Ok(()) Ok(())
} }
pub fn import_dictionary_zip_file<Z, Callback>( pub fn import_dictionary_zip_file<Callback>(
&mut self, &mut self,
zip_file: &Z, zip_file: &PathBuf,
asset_dir: &PathBuf,
lang: &str, lang: &str,
progress_callback: Callback, progress_callback: Callback,
) -> anyhow::Result<YomitanZipImportResult, CJDicError> ) -> anyhow::Result<YomitanZipImportResult, CJDicError>
where where
Z: ZipSource,
Callback: Fn(YomitanProgress), 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)); let _timer = Timer::new(format!("Importing {}", bundle_name));
self.conn.execute_batch("PRAGMA foreign_keys = off")?; self.conn.execute_batch("PRAGMA foreign_keys = off")?;
let cursor = Cursor::new(zip_file.bytes()?); let file = File::open(&zip_file)?;
let mut archive = ZipArchive::new(cursor)?; 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") { let index_file = match archive.by_name("index.json") {
Ok(mut f) => { Ok(mut f) => {
@@ -459,7 +489,6 @@ impl YomitanWriter {
serde_json::from_str::<Value>(&s)? serde_json::from_str::<Value>(&s)?
} }
Err(e) => { Err(e) => {
// TODO: proper error handling
eprintln!("{}", e); eprintln!("{}", e);
return Ok(YomitanZipImportResult { return Ok(YomitanZipImportResult {
exists: false, exists: false,
@@ -502,17 +531,21 @@ impl YomitanWriter {
.to_string(); .to_string();
{ {
let title: &str = &title; // Skip if already installed or older revision
// Skip if already installed
let mut stmt = self let mut stmt = self
.conn .conn
.prepare("SELECT (revision < ?2) FROM dictionaries WHERE title = ?1")?; .prepare("SELECT (revision < ?2) FROM dictionaries WHERE bundle_name = ?1")?;
let mut rows = stmt.query(params![title, revision])?; let mut rows = stmt.query(params![bundle_name, revision])?;
if let Some(row) = rows.next()? { if let Some(row) = rows.next()? {
let is_outdated: bool = row.get(0)?; let is_outdated: bool = row.get(0)?;
if is_outdated { if is_outdated {
self.drop_dictionary(bundle_name, lang)?; self.drop_dictionary(bundle_name, lang)?;
} else { } else {
self.conn.execute(
"UPDATE dictionaries SET asset_count = ?2 WHERE bundle_name = ?1",
params![bundle_name, asset_count],
)?;
return Ok(YomitanZipImportResult { return Ok(YomitanZipImportResult {
exists: true, exists: true,
load: false, load: false,
@@ -528,8 +561,8 @@ impl YomitanWriter {
let tx = self.conn.transaction()?; let tx = self.conn.transaction()?;
tx.execute( tx.execute(
"INSERT INTO dictionaries "INSERT INTO dictionaries
(title, revision, author, url, description, lang, bundle_name) VALUES (title, revision, author, url, description, lang, bundle_name, asset_count) VALUES
( ?1, ?2, ?3, ?4, ?5, ?6, ?7)", ( ?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)",
params![ params![
title, title,
revision, revision,
@@ -538,6 +571,7 @@ impl YomitanWriter {
index_file.get("description").and_then(Value::as_str), index_file.get("description").and_then(Value::as_str),
lang, lang,
bundle_name, bundle_name,
asset_count,
], ],
)?; )?;
+2 -2
View File
@@ -7,9 +7,9 @@ mod service;
mod tokenizer; mod tokenizer;
mod util; mod util;
pub use db::{YomitanProgress, YomitanRow, YomitanZipImportResult, ZipSource}; pub use db::{YomitanProgress, YomitanRow, YomitanZipImportResult};
pub use error::CJDicError; pub use error::CJDicError;
pub use service::{AppService, SqlParam}; pub use service::{AppService, SqlParam, YomitanDictEntry};
pub use tokenizer::TokenizeSegment; pub use tokenizer::TokenizeSegment;
pub use util::Timer; pub use util::Timer;
+69 -20
View File
@@ -1,13 +1,13 @@
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};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::{ use crate::{
Timer, ZipSource, Timer,
db::{ db::{
DBFILE, Database, DbChild, YomitanProgress, YomitanRow, YomitanWriter, DBFILE, Database, DbChild, YomitanProgress, YomitanRow, YomitanWriter,
YomitanZipImportResult, YomitanZipImportResult,
@@ -16,6 +16,14 @@ use crate::{
tokenizer::{TokenizeSegment, Tokenizer}, tokenizer::{TokenizeSegment, Tokenizer},
}; };
#[derive(Serialize, Debug)]
pub struct YomitanDictEntry {
pub title: String,
pub bundle_name: String,
pub revision: String,
pub lang: String,
}
#[derive(Serialize, Debug, Clone)] #[derive(Serialize, Debug, Clone)]
pub struct LoadYomitanZipDirResult { pub struct LoadYomitanZipDirResult {
pub new_dicts: Vec<String>, pub new_dicts: Vec<String>,
@@ -32,6 +40,7 @@ pub enum SqlParam {
Bool(bool), Bool(bool),
} }
#[derive(Clone)]
pub struct AppService { pub struct AppService {
db: Database, db: Database,
tokenizer: Tokenizer, tokenizer: Tokenizer,
@@ -42,8 +51,9 @@ impl AppService {
db_dir: impl AsRef<Path>, db_dir: impl AsRef<Path>,
vibrato_dic: impl AsRef<Path>, vibrato_dic: impl AsRef<Path>,
) -> Result<Self, CJDicError> { ) -> Result<Self, CJDicError> {
let db = Database::new(db_dir)?;
let tokenizer = Tokenizer::new(vibrato_dic)?; let tokenizer = Tokenizer::new(vibrato_dic)?;
let db = Database::new(db_dir, tokenizer.clone())?;
Ok(Self { db, tokenizer }) Ok(Self { db, tokenizer })
} }
@@ -125,23 +135,52 @@ impl AppService {
Ok(writer) Ok(writer)
} }
pub fn load_yomitan_zip_dir<Z, LoadCallback, ImportCallback>( pub fn list_yomitan_dict(&self) -> Result<Vec<YomitanDictEntry>, CJDicError> {
let db_path = self.db.dir.join(DBFILE[DbChild::Yomitan]);
let mut out = vec![];
if db_path.exists() {
let conn = Connection::open(&db_path)?;
let mut stmt = conn.prepare(
"
SELECT title, bundle_name, revision, lang
FROM dictionaries
WHERE bundle_name IS NOT NULL",
)?;
let rows = stmt.query_map([], |r| {
Ok(YomitanDictEntry {
title: r.get(0)?,
bundle_name: r.get(1)?,
revision: r.get(2)?,
lang: r.get(3)?,
})
})?;
for r in rows {
out.push(r?);
}
}
Ok(out)
}
pub fn load_yomitan_zip_dir<LoadCallback, ImportCallback>(
&self, &self,
zip_dir: Vec<Z>, zip_dir: Vec<PathBuf>,
asset_dir: &PathBuf,
lang: &str, lang: &str,
load_callback: LoadCallback, load_callback: LoadCallback,
import_callback: ImportCallback, import_callback: ImportCallback,
) -> Result<LoadYomitanZipDirResult, CJDicError> ) -> Result<LoadYomitanZipDirResult, CJDicError>
where where
Z: ZipSource,
LoadCallback: Fn(LoadYomitanZipDirResult), LoadCallback: Fn(LoadYomitanZipDirResult),
ImportCallback: Fn(YomitanProgress), 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(); let mut zip_dir_ord: HashMap<String, usize> = HashMap::new();
for (i, entry) in zip_dir.iter().enumerate() { 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); dir_zip_map.insert(filename.to_string(), entry);
zip_dir_ord.insert(filename.to_string(), i); zip_dir_ord.insert(filename.to_string(), i);
} }
@@ -149,12 +188,21 @@ impl AppService {
let mut new_dicts: Vec<String> = vec![]; let mut new_dicts: Vec<String> = vec![];
let mut to_be_removed_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]); let db_path = self.db.dir.join(DBFILE[DbChild::Yomitan]);
if db_path.exists() { if db_path.exists() {
let conn = Connection::open(&db_path)?; let conn = Connection::open(&db_path)?;
let mut stmt = let mut stmt = conn.prepare(
conn.prepare("SELECT DISTINCT bundle_name FROM dictionaries WHERE lang = ?1")?; "
SELECT DISTINCT bundle_name FROM dictionaries
WHERE lang = ?1 AND asset_count IS NOT NULL
",
)?;
let mut rows = stmt.query([lang])?; let mut rows = stmt.query([lang])?;
let mut db_zip_list: HashSet<String> = HashSet::new(); let mut db_zip_list: HashSet<String> = HashSet::new();
@@ -189,11 +237,6 @@ impl AppService {
to_be_removed_dicts: to_be_removed_dicts.clone(), 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)?; let conn = Connection::open(&db_path)?;
for z in to_be_removed_dicts.clone().iter() { for z in to_be_removed_dicts.clone().iter() {
@@ -208,7 +251,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,
)?;
} }
} }
} }
@@ -227,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 writer: &mut YomitanWriter, // Reuse existing connection
zip_file: &Z, zip_file: &PathBuf,
asset_dir: &PathBuf,
lang: &str, lang: &str,
progress_callback: Callback, progress_callback: Callback,
) -> Result<YomitanZipImportResult, CJDicError> ) -> Result<YomitanZipImportResult, CJDicError>
where where
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(
+35 -1
View File
@@ -1,4 +1,4 @@
use std::{fs::File, io::BufReader, path::Path}; use std::{collections::HashSet, fs::File, io::BufReader, path::Path};
use serde::Serialize; use serde::Serialize;
@@ -25,6 +25,7 @@ pub struct TokenizeSegment {
#[derive(Clone)] #[derive(Clone)]
pub struct Tokenizer { pub struct Tokenizer {
ja_tokenizer: vibrato_rkyv::Tokenizer, ja_tokenizer: vibrato_rkyv::Tokenizer,
ja_exclude_pos: HashSet<String>,
} }
impl Tokenizer { impl Tokenizer {
@@ -33,6 +34,9 @@ impl Tokenizer {
let dictionary = vibrato_rkyv::Dictionary::read(reader)?; let dictionary = vibrato_rkyv::Dictionary::read(reader)?;
Ok(Self { Ok(Self {
ja_tokenizer: vibrato_rkyv::Tokenizer::new(dictionary), ja_tokenizer: vibrato_rkyv::Tokenizer::new(dictionary),
ja_exclude_pos: HashSet::from(
["助詞", "助動詞", "記号", "接続詞"].map(|p| p.to_string()),
),
}) })
} }
@@ -56,4 +60,34 @@ impl Tokenizer {
output output
} }
pub fn ja_normalize(&self, text: String) -> Vec<String> {
let mut segmented: Vec<String> = vec![];
for t in self.ja_tokenize(text) {
let mut surface = t.surface;
let mut pos = None;
for (i, dt) in t.details.iter().enumerate() {
match i {
0 => pos = Some(dt),
6 => {
let base = dt.to_string();
if base != "*" {
surface = base
}
}
_ => {}
}
}
if let Some(p) = pos {
if self.ja_exclude_pos.contains(p.as_str()) {
continue;
}
}
segmented.push(surface);
}
segmented
}
} }
+1
View File
@@ -21,6 +21,7 @@
"react": "^19.1.0", "react": "^19.1.0",
"react-dom": "^19.1.0", "react-dom": "^19.1.0",
"react-icons": "^5.5.0", "react-icons": "^5.5.0",
"react-router": "^7.13.1",
"wanakana": "^5.3.1", "wanakana": "^5.3.1",
"zustand": "^5.0.11" "zustand": "^5.0.11"
}, },
Binary file not shown.

Before

Width:  |  Height:  |  Size: 254 B

+1 -1
View File
@@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg> <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="512" height="512" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

+14
View File
@@ -44,3 +44,17 @@ html {
height: 100vh; height: 100vh;
overflow-y: hidden; 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%;
}
}
+15 -2
View File
@@ -1,14 +1,27 @@
import "./App.css"; import "./App.css";
import { createBrowserRouter } from "react-router";
import { RouterProvider } from "react-router/dom";
import LoadingDialog from "./components/LoadingDialog"; import LoadingDialog from "./components/LoadingDialog";
import { Provider } from "./components/ui/provider"; import { Provider } from "./components/ui/provider";
import SearchPage from "./pages/SearchPage";
const router = createBrowserRouter([
{
path: "/",
lazy: async () => {
return {
Component: (await import("./pages/SearchPage")).default,
};
},
},
]);
function App() { function App() {
return ( return (
<Provider> <Provider>
<> <>
<SearchPage /> <RouterProvider router={router} />
<LoadingDialog /> <LoadingDialog />
</> </>
</Provider> </Provider>
+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({ export default function Glossary({
glossary, glossary,
@@ -29,18 +30,14 @@ 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} />;
} }
// GlossaryStructuredContent // GlossaryStructuredContent
if ("type" in glossary && glossary.type === "structured-content") { if ("type" in glossary && glossary.type === "structured-content") {
const sc = glossary; const sc = glossary;
return ( return (
<StructuredContent <StructuredContent node={sc.content} {...{ maxLength, onTermClicked }} />
node={sc.content}
maxLength={maxLength}
onTermClicked={onTermClicked}
/>
); );
} }
@@ -53,17 +50,37 @@ export default function Glossary({
return null; 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 ( return (
<img <img
src={img.path} className="glossary-image"
width={img.width} src={src}
height={img.height}
title={img.title} title={img.title}
alt={img.alt} alt={img.alt}
style={{ style={{
imageRendering: img.imageRendering || "auto", imageRendering: img.imageRendering || "auto",
width: `${img.width}em`, width: `${img.width}em`, // unit seems to be em, not px
height: `${img.height}em`, height: `${img.height}em`,
}} }}
/> />
@@ -100,8 +117,7 @@ function StructuredContent({
<StructuredContent <StructuredContent
key={i} key={i}
node={n} node={n}
maxLength={maxLength} {...{ maxLength, onTermClicked }}
onTermClicked={onTermClicked}
/> />
))} ))}
</> </>
@@ -118,7 +134,7 @@ function StructuredContent({
// Image // Image
if (tag === "img") { if (tag === "img") {
return <GlossaryImage {...node} />; return <GlossaryImage img={node} />;
} }
// Link // Link
@@ -143,11 +159,7 @@ function StructuredContent({
}} }}
> >
{content ? ( {content ? (
<StructuredContent <StructuredContent node={content} {...{ maxLength, onTermClicked }} />
node={content}
maxLength={maxLength}
onTermClicked={onTermClicked}
/>
) : null} ) : null}
</a> </a>
); );
@@ -159,6 +171,10 @@ 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,
maxLength,
onTermClicked,
}),
); );
} }
+81 -10
View File
@@ -3,10 +3,25 @@ import { useEffect, useRef, useState } from "react";
import { Dialog, Portal } from "@chakra-ui/react"; import { Dialog, Portal } from "@chakra-ui/react";
import { invoke } from "@tauri-apps/api/core"; import { invoke } from "@tauri-apps/api/core";
import { listen } from "@tauri-apps/api/event"; import { listen } from "@tauri-apps/api/event";
import { fetch } from "@tauri-apps/plugin-http";
import { supabase } from "../lib/supabaseClient"; import { supabase } from "../lib/supabaseClient";
interface YomitanDictEntry { interface YomitanDictEntry {
title: string;
bundle_name: string;
revision: string;
lang: string;
}
interface YomitanStorageWorkerDictEntry {
key: string;
size: number;
uploaded: string;
url: string;
}
interface YomitanDownloadDictEntry {
url: string; url: string;
filepath: string; filepath: string;
} }
@@ -38,7 +53,7 @@ interface Progress {
} }
function LoadingDialog() { function LoadingDialog() {
const [messages, setMessages] = useState<Progress[]>([]); const [messages, set_messages] = useState<Progress[]>([]);
const isInit = useRef(false); const isInit = useRef(false);
@@ -66,24 +81,33 @@ function LoadingDialog() {
} }
}); });
async function downloadAssets(lang = "ja") { async function downloadAssets(lang = "ja") {
const dicts: YomitanDictEntry[] = []; const existing_dicts =
await invoke<YomitanDictEntry[]>("list_yomitan_dict");
const dicts: YomitanDownloadDictEntry[] = [];
if (lang === "ja") { if (lang === "ja") {
{ var filename = "PixivLight.zip";
const filename = "Pixiv.zip"; var filepath = `yomitan/${lang}/${filename}`;
const filepath = `yomitan/${lang}/${filename}`; var existing = existing_dicts.find((d) => d.bundle_name === filename);
if (!existing) {
// Maybe check revision update
dicts.push({ dicts.push({
filepath, filepath,
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 "";
})) || "", })) || "",
}); });
} else {
dicts.push({
filepath,
url: "",
});
} }
{ {
@@ -103,14 +127,61 @@ 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 }); await invoke("init_yomitan", { dicts, lang });
setMessages([]); set_messages([]);
} }
listen<LoadYomitanZipDirResult>("load-yomitan-dir", ({ payload }) => { listen<LoadYomitanZipDirResult>("load-yomitan-dir", ({ payload }) => {
setMessages((messages) => { set_messages((messages) => {
if (payload.new_dicts.length > 0) { if (payload.new_dicts.length > 0) {
messages = [ messages = [
...messages, ...messages,
@@ -149,7 +220,7 @@ function LoadingDialog() {
}, []); }, []);
function updateProgress(payload: Progress) { function updateProgress(payload: Progress) {
setMessages((messages) => { set_messages((messages) => {
const prev = messages[messages.length - 1]; const prev = messages[messages.length - 1];
if (prev?.message === payload.message) { if (prev?.message === payload.message) {
messages = messages.slice(0, messages.length - 1); messages = messages.slice(0, messages.length - 1);
+138 -52
View File
@@ -5,13 +5,14 @@ import {
useRef, useRef,
useState, useState,
} from "react"; } from "react";
import { toKana } from "wanakana"; import { useSearchParams } from "react-router";
import { toHiragana, toKana } from "wanakana";
import { import {
Box, Box,
Card, Card,
CloseButton, CloseButton,
Group, Field,
Input, Input,
InputGroup, InputGroup,
Stack, Stack,
@@ -35,14 +36,28 @@ interface Entry {
} }
function SearchPage() { function SearchPage() {
const [q, setQ] = useState(""); const [searchParams, set_searchParams] = useSearchParams();
const [entries, setEntries] = useState<Entry[]>([]);
const [searchTimeout, setSearchTimeout] = useState(0); useEffect(() => {
const [furigana, setFurigana] = useState(""); let q = searchParams.get("q") || "";
if (q) set_q(q);
}, [searchParams]);
function searchParams_setQ(q: string) {
if (searchParams.get("q") === q) return;
set_searchParams({ q });
}
const [q, set_q] = useState("");
const [entries, set_entries] = useState<Entry[]>([]);
const [searchTimeout, set_searchTimeout] = useState(0);
const [furigana, set_furigana] = useState("");
const [isAutoKana, set_isAutoKana] = useState(true); const [isAutoKana, set_isAutoKana] = useState(true);
const [isScrollEnd, set_isScrollEnd] = useState(false);
const nSearch = useRef(0); const nSearch = useRef(0);
const searchboxRef = useRef<HTMLInputElement | null>(null); const searchboxRef = useRef<HTMLInputElement | null>(null);
const resultScrollRef = useRef<HTMLElement | null>(null);
const lang = "ja-JP"; const lang = "ja-JP";
@@ -54,10 +69,13 @@ 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) {
setQ(newText + " "); searchParams_setQ(newText + " ");
return newText; return newText;
} }
return clipboardText; return clipboardText;
@@ -73,7 +91,7 @@ function SearchPage() {
function trySearch() { function trySearch() {
if (!q.trim()) { if (!q.trim()) {
setEntries([]); set_entries([]);
return; return;
} }
@@ -86,7 +104,7 @@ function SearchPage() {
nSearch.current += 1; nSearch.current += 1;
setSearchTimeout( set_searchTimeout(
setTimeout(() => { setTimeout(() => {
if (nSearch.current > 1) runSearch(); if (nSearch.current > 1) runSearch();
}, 250), }, 250),
@@ -100,11 +118,15 @@ function SearchPage() {
if (isAutoKana) { if (isAutoKana) {
norm_q = norm_q.replace(/n$/i, "ん").replace(/[a-z]$/i, ""); norm_q = norm_q.replace(/n$/i, "ん").replace(/[a-z]$/i, "");
} }
norm_q = norm_q.replace(/*/g, "*").replace(/?/g, "?").replace(/^〜/, "~");
if (!norm_q) return; if (!norm_q) return;
const ender = /\p{Z}$/u.test(q) ? "" : "*"; const ender =
/\p{Z}$/u.test(q) || [..."*?[]"].some((c) => q.includes(c)) ? "" : "*";
const re = /\<(.+?)\>\[(.+?)\]/g; const re = /\<(.+?)\>{(.+?)}/g;
let qTerm = ""; let qTerm = "";
let qReading = ""; let qReading = "";
@@ -134,9 +156,12 @@ function SearchPage() {
}); });
if (isNew) { if (isNew) {
if (!result.length && !isFromSplit && /^[^ ]{3,} $/.test(q)) { const [s1, prefix = "", s2 = ""] = q.split(/^([\*\?\[\]\~]*)/);
const text = s1 || s2;
if (!result.length && !isFromSplit && /^[^ ]{3,} $/.test(text)) {
const segs = await invoke<{ surface: string }[]>("tokenize", { const segs = await invoke<{ surface: string }[]>("tokenize", {
text: q, text,
}); });
if (segs.length > 1) { if (segs.length > 1) {
const newQ = const newQ =
@@ -145,14 +170,19 @@ function SearchPage() {
segs.map((s) => s.surface).join("") + segs.map((s) => s.surface).join("") +
" "; " ";
setQ(newQ); searchParams_setQ(prefix + newQ);
return; return;
} }
} }
setEntries(result); if (resultScrollRef.current) {
resultScrollRef.current.scrollTop = 0;
set_isScrollEnd(false);
}
set_entries(result);
} else { } else {
setEntries([...entries, ...result]); set_entries([...entries, ...result]);
} }
return result; return result;
@@ -162,30 +192,31 @@ function SearchPage() {
if (!(target instanceof HTMLElement)) return; if (!(target instanceof HTMLElement)) return;
const { scrollHeight, scrollTop } = target; const { scrollHeight, scrollTop } = target;
if (scrollHeight - 20 > scrollTop + target.getBoundingClientRect().height) if (
isScrollEnd ||
scrollHeight - window.innerHeight / 2 >
scrollTop + target.getBoundingClientRect().height
)
return; return;
const results = await runSearch(false); const results = await runSearch(false);
if (!results?.length) { if (!results?.length) {
target.style.paddingBottom = "50vh"; target.style.paddingBottom = "50vh";
set_isScrollEnd(true);
return; return;
} }
setTimeout(() => {
target.scrollTop = scrollTop + 100;
}, 100);
}; };
function onSearchboxChange(q: string) { function onSearchboxChange(q: string) {
if (isAutoKana) { if (isAutoKana) {
q = toKana( q = toKana(q, {
q, useObsoleteKana: true,
{ useObsoleteKana: true, IMEMode: true }, IMEMode: true,
Object.fromEntries("<>[]".split("").map((c) => [c, c])), customKanaMapping: Object.fromEntries([..."<>{}"].map((c) => [c, c])),
); });
} }
setQ(q); set_q(q);
} }
const FURIGANA_REGEX = /^[\p{scx=Hiragana}\p{scx=Katakana}]+$/u; const FURIGANA_REGEX = /^[\p{scx=Hiragana}\p{scx=Katakana}]+$/u;
@@ -195,20 +226,23 @@ function SearchPage() {
data: compositionData, data: compositionData,
}) => { }) => {
if (FURIGANA_REGEX.test(compositionData)) { if (FURIGANA_REGEX.test(compositionData)) {
setFurigana(compositionData); set_furigana(compositionData);
} }
}; };
const addFurigana: CompositionEventHandler = ({ data: compositionData }) => { const addFurigana: CompositionEventHandler = ({
data: compositionData,
target,
}) => {
const cleanedFuri = [...furigana.replace(/n/g, "ん")] const cleanedFuri = [...furigana.replace(/n/g, "ん")]
.map((c) => katakanaToHiragana(c)) .map((c) => toHiragana(c, { passRomaji: true }))
.join(""); .join("");
let parts = compositionData.split(KANJI_REGEX); let parts = compositionData.split(KANJI_REGEX);
if (parts.length === 1) return; if (parts.length === 1) return;
const hiraganaParts = parts.map((p) => const hiraganaParts = parts.map((p) =>
[...p].map((c) => katakanaToHiragana(c)).join(""), [...p].map((c) => toHiragana(c, { passRomaji: true })).join(""),
); );
const regex = new RegExp( const regex = new RegExp(
`^${hiraganaParts.map((p, idx) => `(${idx & 1 ? ".+" : p})`).join("")}$`, `^${hiraganaParts.map((p, idx) => `(${idx & 1 ? ".+" : p})`).join("")}$`,
@@ -221,29 +255,47 @@ function SearchPage() {
rt.shift(); rt.shift();
const markup = parts const markup = parts
.map((p, idx) => (idx & 1 ? `<${p}>[${rt[idx]}]` : p)) .map((p, idx) => (idx & 1 ? `<${p}>{${rt[idx]}}` : p))
.join(""); .join("");
setQ(markup);
};
function katakanaToHiragana(k: string) { set_q((q) => {
let c = k.charCodeAt(0); if (!(target instanceof HTMLInputElement)) return markup;
return c >= 12449 && c <= 12531
? String.fromCharCode(k.charCodeAt(0) - 96) let { selectionStart } = target;
: k; selectionStart = selectionStart || 0;
} if (selectionStart < compositionData.length) return markup;
return (
q.substring(0, selectionStart - compositionData.length) +
markup +
q.substring(selectionStart)
);
});
};
const clearButton = q.trim() ? ( const clearButton = q.trim() ? (
<CloseButton <CloseButton
size={"xs"} size={"xs"}
me={-2} me={-2}
onClick={() => { onClick={() => {
setQ(""); searchParams_setQ(q);
searchParams_setQ("");
set_q("");
searchboxRef.current ? searchboxRef.current.focus() : null; searchboxRef.current ? searchboxRef.current.focus() : null;
}} }}
/> />
) : null; ) : null;
function onTermClicked(t: string) {
searchParams_setQ(q);
searchParams_setQ(t + " ");
}
function JSONdumpClean(o: Record<string, any>) {
const p = Object.entries(o).filter(([, v]) => v || v === 0);
return p.length ? JSON.stringify(Object.fromEntries(p), null, 1) : null;
}
return ( return (
<Stack <Stack
style={{ style={{
@@ -261,7 +313,7 @@ function SearchPage() {
e.preventDefault(); e.preventDefault();
}} }}
> >
<Group attached w="full"> <Field.Root w="full">
<InputGroup endElement={clearButton}> <InputGroup endElement={clearButton}>
<Input <Input
id="greet-input" id="greet-input"
@@ -276,8 +328,24 @@ function SearchPage() {
placeholder="Search..." placeholder="Search..."
/> />
</InputGroup> </InputGroup>
{/* <Button type="submit">Search</Button> */} {q.includes(" ") ? (
</Group> <Field.HelperText>
Space (" ") to mark word boundary for search
</Field.HelperText>
) : null}
{[..."*?[]"].some((c) => q.includes(c)) ? (
<Field.HelperText>
<code>{"*?[]"}</code> can be used as
<a
href="https://en.wikipedia.org/wiki/Glob_(programming)#Syntax"
target="_blank"
rel="noopener noreferrer"
>
glob
</a>
</Field.HelperText>
) : null}
</Field.Root>
<div style={{ marginTop: "0.5em", display: "flex", gap: "1em" }}> <div style={{ marginTop: "0.5em", display: "flex", gap: "1em" }}>
<Switch.Root <Switch.Root
checked={isAutoKana} checked={isAutoKana}
@@ -298,6 +366,7 @@ function SearchPage() {
</div> </div>
</form> </form>
<Box <Box
ref={resultScrollRef}
as={"ol"} as={"ol"}
listStyleType={"number"} listStyleType={"number"}
style={{ overflowY: "scroll", scrollbarWidth: "none" }} style={{ overflowY: "scroll", scrollbarWidth: "none" }}
@@ -308,13 +377,34 @@ function SearchPage() {
<li key={i} lang={lang} style={{ marginLeft: "1.5em" }}> <li key={i} lang={lang} style={{ marginLeft: "1.5em" }}>
<div> <div>
<Card.Root> <Card.Root>
<Card.Header> <Card.Header display={"block"}>
<a
onClick={() =>
onTermClicked(
reading && reading !== term
? `<${term}>{${reading}}`
: term,
)
}
>
{term} {term}
{reading ? ` (${reading})` : ""} 【{dict_title}】 </a>
{JSON.stringify(it, (_, v) => v || undefined)} {reading && reading !== term ? (
<>
{"("}
<a onClick={() => onTermClicked(reading)}>{reading}</a>
{")"}
</>
) : null}
<button onClick={() => onTermClicked("~" + term)}>
🔗
</button>
<span>【{dict_title}】</span>
<span>{JSONdumpClean(it)}</span>
</Card.Header> </Card.Header>
<Card.Body> <Card.Body>
<div <div
className="glossaries"
style={{ style={{
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
@@ -322,11 +412,7 @@ function SearchPage() {
}} }}
> >
{JSON.parse(glossary_json).map((g: any, i: number) => ( {JSON.parse(glossary_json).map((g: any, i: number) => (
<Glossary <Glossary key={i} glossary={g} {...{ onTermClicked }} />
key={i}
glossary={g}
onTermClicked={(t) => setQ(t + " ")}
/>
))} ))}
</div> </div>
</Card.Body> </Card.Body>
+3
View File
@@ -29,4 +29,7 @@ export default defineConfig(async () => ({
ignored: ["**/src-tauri/**"], ignored: ["**/src-tauri/**"],
}, },
}, },
build: {
chunkSizeWarningLimit: 1024, // default = 500 kB
},
})); }));
+11 -1
View File
@@ -8,8 +8,18 @@
"permissions": [ "permissions": [
"core:default", "core:default",
"opener: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", "fs:default",
{
"identifier": "fs:allow-appdata-read",
"allow": [{ "path": "$APPDATA/yomitan/**" }]
},
"clipboard-manager:allow-read-text" "clipboard-manager:allow-read-text"
] ]
} }
+1
View File
@@ -9,3 +9,4 @@
/src/main/assets/**/*.zst /src/main/assets/**/*.zst
/src/main/assets/**/*.db /src/main/assets/**/*.db
/src/main/assets/**/manifest.json /src/main/assets/**/manifest.json
!/src/debug
+1 -1
View File
@@ -41,7 +41,7 @@ android {
} }
buildTypes { buildTypes {
getByName("debug") { getByName("debug") {
applicationIdSuffix = ".dev" // applicationIdSuffix = ".dev"
manifestPlaceholders["usesCleartextTraffic"] = "true" manifestPlaceholders["usesCleartextTraffic"] = "true"
isDebuggable = true isDebuggable = true
isJniDebuggable = true isJniDebuggable = true
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
<background android:drawable="@color/ic_launcher_background"/>
</adaptive-icon>
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

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#fff</color>
</resources>
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

+44 -30
View File
@@ -5,7 +5,8 @@ use std::{
}; };
use cjdic2_core::{ use cjdic2_core::{
AppService, CJDicError, SqlParam, TokenizeSegment, YomitanRow, YomitanZipImportResult, AppService, CJDicError, SqlParam, TokenizeSegment, YomitanDictEntry, YomitanRow,
YomitanZipImportResult,
}; };
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tauri::{AppHandle, Emitter, Manager}; use tauri::{AppHandle, Emitter, Manager};
@@ -13,8 +14,15 @@ use tauri_plugin_http::reqwest::Client;
use tokio::io::AsyncWriteExt; use tokio::io::AsyncWriteExt;
use zip::ZipArchive; use zip::ZipArchive;
#[tauri::command]
pub async fn list_yomitan_dict(
state: tauri::State<'_, AppService>,
) -> Result<Vec<YomitanDictEntry>, CJDicError> {
state.list_yomitan_dict()
}
#[derive(Deserialize)] #[derive(Deserialize)]
pub struct YomitanDictEntry { pub struct YomitanDownloadDictEntry {
url: String, url: String,
filepath: String, filepath: String,
} }
@@ -22,45 +30,48 @@ pub struct YomitanDictEntry {
#[tauri::command] #[tauri::command]
pub async fn init_yomitan( pub async fn init_yomitan(
app: AppHandle, app: AppHandle,
dicts: Vec<YomitanDictEntry>, dicts: Vec<YomitanDownloadDictEntry>,
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_dir.clone(),
d.filepath.clone(),
|progress| {
app.emit("download-url-progress", progress).unwrap(); 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 state = state.inner().clone();
let app_clone = app.clone();
let update_count = tokio::task::spawn_blocking(move || {
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());
app.emit("load-yomitan-dir", r).unwrap(); app.emit("load-yomitan-dir", r).unwrap();
}, },
|r| { |r| {
@@ -68,11 +79,15 @@ pub async fn init_yomitan(
app.emit("yomitan-import-progress", r).unwrap(); 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.get() > 0 { if update_count > 0 {
println!("Request app restart?",); println!("Request app restart?",);
// Request app restart anyway // Request app restart anyway
app.request_restart(); app_clone.request_restart();
} }
Ok(()) Ok(())
@@ -89,6 +104,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);
@@ -96,7 +114,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();
}) })
} }
@@ -143,16 +161,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();
@@ -162,7 +180,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));
@@ -200,8 +218,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,
}); });
@@ -227,14 +245,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,
+1
View File
@@ -47,6 +47,7 @@ pub fn run() {
Ok(()) Ok(())
}) })
.invoke_handler(tauri::generate_handler![ .invoke_handler(tauri::generate_handler![
list_yomitan_dict,
init_yomitan, init_yomitan,
import_yomitan_dict, import_yomitan_dict,
remove_yomitan_dict, remove_yomitan_dict,
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"$schema": "https://schema.tauri.app/config/2", "$schema": "https://schema.tauri.app/config/2",
"productName": "cjdic2", "productName": "cjdic2",
"version": "0.1.1", "version": "0.1.3",
"identifier": "cc.polv.cjdic2", "identifier": "cc.polv.cjdic2",
"build": { "build": {
"beforeDevCommand": "pnpm dev", "beforeDevCommand": "pnpm dev",
+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"]
}
+11
View File
@@ -0,0 +1,11 @@
import { defineWorkersConfig } from '@cloudflare/vitest-pool-workers/config';
export default defineWorkersConfig({
test: {
poolOptions: {
workers: {
wrangler: { configPath: './wrangler.jsonc' },
},
},
},
});
File diff suppressed because it is too large. Load diff
+48
View File
@@ -0,0 +1,48 @@
/**
* For more details on how to configure Wrangler, refer to:
* https://developers.cloudflare.com/workers/wrangler/configuration/
*/
{
"$schema": "node_modules/wrangler/config-schema.json",
"name": "storage-worker",
"main": "src/index.ts",
"compatibility_date": "2025-09-27",
"observability": {
"enabled": true,
},
"compatibility_flags": ["nodejs_compat"],
/**
* Smart Placement
* https://developers.cloudflare.com/workers/configuration/smart-placement/#smart-placement
*/
// "placement": { "mode": "smart" }
/**
* Bindings
* Bindings allow your Worker to interact with resources on the Cloudflare Developer Platform, including
* databases, object storage, AI inference, real-time communication and more.
* https://developers.cloudflare.com/workers/runtime-apis/bindings/
*/
"r2_buckets": [
{
"binding": "BUCKET",
"bucket_name": "yomitan",
},
],
/**
* Environment Variables
* https://developers.cloudflare.com/workers/wrangler/configuration/#environment-variables
* Note: Use secrets to store sensitive data.
* https://developers.cloudflare.com/workers/configuration/secrets/
*/
// "vars": { "MY_VARIABLE": "production_value" }
/**
* Static Assets
* https://developers.cloudflare.com/workers/static-assets/binding/
*/
// "assets": { "directory": "./public/", "binding": "ASSETS" }
/**
* Service Bindings (communicate between multiple Workers)
* https://developers.cloudflare.com/workers/wrangler/configuration/#service-bindings
*/
// "services": [ { "binding": "MY_SERVICE", "service": "my-service" } ]
}