diff --git a/js-server/package.json b/js-server/package.json index 0fae244..25987a1 100644 --- a/js-server/package.json +++ b/js-server/package.json @@ -13,6 +13,7 @@ "dependencies": { "@fastify/mongodb": "^10.0.0", "@fastify/static": "^9.0.0", + "@supabase/supabase-js": "^2.98.0", "adm-zip": "^0.5.16", "dotenv": "^17.3.1", "fastify": "^5.7.4", diff --git a/js-server/scripts/supabase-download.ts b/js-server/scripts/supabase-download.ts new file mode 100644 index 0000000..6885e18 --- /dev/null +++ b/js-server/scripts/supabase-download.ts @@ -0,0 +1,28 @@ +import "dotenv/config"; + +import { createClient } from "@supabase/supabase-js"; + +const supabaseUrl = process.env.VITE_SUPABASE_URL!; +const supabasePublishableKey = process.env.VITE_SUPABASE_PUBLISHABLE_KEY!; + +const supabase = createClient(supabaseUrl, supabasePublishableKey); + +async function main() { + const { data, error } = await supabase.storage.from("yomitan").list("ja"); + if (error) throw error; + if (data) { + console.log(data.map((d) => d.id)); + + const toDownload = data.filter(() => true); + + console.log( + toDownload.map((d) => + supabase.storage.from("yomitan").getPublicUrl(`ja/${d.name}`), + ), + ); + } +} + +if (require.main === module) { + main(); +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3a4a4c2..c565169 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -20,6 +20,9 @@ importers: '@fastify/static': specifier: ^9.0.0 version: 9.0.0 + '@supabase/supabase-js': + specifier: ^2.98.0 + version: 2.98.0 adm-zip: specifier: ^0.5.16 version: 0.5.16 @@ -54,6 +57,9 @@ importers: '@emotion/react': specifier: ^11.14.0 version: 11.14.0(@types/react@19.2.14)(react@19.2.4) + '@supabase/supabase-js': + specifier: ^2.98.0 + version: 2.98.0 '@tauri-apps/api': specifier: ^2 version: 2.10.1 @@ -618,6 +624,30 @@ packages: cpu: [x64] os: [win32] + '@supabase/auth-js@2.98.0': + resolution: {integrity: sha512-GBH361T0peHU91AQNzOlIrjUZw9TZbB9YDRiyFgk/3Kvr3/Z1NWUZ2athWTfHhwNNi8IrW00foyFxQD9IO/Trg==} + engines: {node: '>=20.0.0'} + + '@supabase/functions-js@2.98.0': + resolution: {integrity: sha512-N/xEyiNU5Org+d+PNCpv+TWniAXRzxIURxDYsS/m2I/sfAB/HcM9aM2Dmf5edj5oWb9GxID1OBaZ8NMmPXL+Lg==} + engines: {node: '>=20.0.0'} + + '@supabase/postgrest-js@2.98.0': + resolution: {integrity: sha512-v6e9WeZuJijzUut8HyXu6gMqWFepIbaeaMIm1uKzei4yLg9bC9OtEW9O14LE/9ezqNbSAnSLO5GtOLFdm7Bpkg==} + engines: {node: '>=20.0.0'} + + '@supabase/realtime-js@2.98.0': + resolution: {integrity: sha512-rOWt28uGyFipWOSd+n0WVMr9kUXiWaa7J4hvyLCIHjRFqWm1z9CaaKAoYyfYMC1Exn3WT8WePCgiVhlAtWC2yw==} + engines: {node: '>=20.0.0'} + + '@supabase/storage-js@2.98.0': + resolution: {integrity: sha512-tzr2mG+v7ILSAZSfZMSL9OPyIH4z1ikgQ8EcQTKfMRz4EwmlFt3UnJaGzSOxyvF5b+fc9So7qdSUWTqGgeLokQ==} + engines: {node: '>=20.0.0'} + + '@supabase/supabase-js@2.98.0': + resolution: {integrity: sha512-Ohc97CtInLwZyiSASz7tT9/Abm/vqnIbO9REp+PivVUII8UZsuI3bngRQnYgJdFoOIwvaEII1fX1qy8x0CyNiw==} + engines: {node: '>=20.0.0'} + '@swc/helpers@0.5.18': resolution: {integrity: sha512-TXTnIcNJQEKwThMMqBXsZ4VGAza6bvN4pa41Rkqoio6QBKMvo+5lexeTMScGCIxtzgQJzElcvIltani+adC5PQ==} @@ -733,6 +763,9 @@ packages: '@types/parse-json@4.0.2': resolution: {integrity: sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==} + '@types/phoenix@1.6.7': + resolution: {integrity: sha512-oN9ive//QSBkf19rfDv45M7eZPi0eEXylht2OLEXicu5b4KoQ1OzXIw+xDSGWxSxe1JmepRR/ZH283vsu518/Q==} + '@types/react-dom@19.2.3': resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} peerDependencies: @@ -747,6 +780,9 @@ packages: '@types/whatwg-url@13.0.0': resolution: {integrity: sha512-N8WXpbE6Wgri7KUSvrmQcqrMllKZ9uxkYWMt+mCSGwNc0Hsw9VQTW7ApqI4XNrx6/SaM2QQJCzMPDEXE058s+Q==} + '@types/ws@8.18.1': + resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} + '@vitejs/plugin-react@4.7.0': resolution: {integrity: sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==} engines: {node: ^14.18.0 || >=16.0.0} @@ -1174,6 +1210,10 @@ packages: resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} engines: {node: '>= 0.8'} + iceberg-js@0.8.1: + resolution: {integrity: sha512-1dhVQZXhcHje7798IVM+xoo/1ZdVfzOMIc8/rgVSijRK38EDqOJoGula9N/8ZI5RD8QTxNQtK/Gozpr+qUqRRA==} + engines: {node: '>=20.0.0'} + import-fresh@3.3.1: resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} engines: {node: '>=6'} @@ -1568,6 +1608,18 @@ packages: resolution: {integrity: sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==} engines: {node: '>=18'} + ws@8.19.0: + resolution: {integrity: sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} @@ -2118,6 +2170,44 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.58.0': optional: true + '@supabase/auth-js@2.98.0': + dependencies: + tslib: 2.8.1 + + '@supabase/functions-js@2.98.0': + dependencies: + tslib: 2.8.1 + + '@supabase/postgrest-js@2.98.0': + dependencies: + tslib: 2.8.1 + + '@supabase/realtime-js@2.98.0': + dependencies: + '@types/phoenix': 1.6.7 + '@types/ws': 8.18.1 + tslib: 2.8.1 + ws: 8.19.0 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + '@supabase/storage-js@2.98.0': + dependencies: + iceberg-js: 0.8.1 + tslib: 2.8.1 + + '@supabase/supabase-js@2.98.0': + dependencies: + '@supabase/auth-js': 2.98.0 + '@supabase/functions-js': 2.98.0 + '@supabase/postgrest-js': 2.98.0 + '@supabase/realtime-js': 2.98.0 + '@supabase/storage-js': 2.98.0 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + '@swc/helpers@0.5.18': dependencies: tslib: 2.8.1 @@ -2216,6 +2306,8 @@ snapshots: '@types/parse-json@4.0.2': {} + '@types/phoenix@1.6.7': {} + '@types/react-dom@19.2.3(@types/react@19.2.14)': dependencies: '@types/react': 19.2.14 @@ -2230,6 +2322,10 @@ snapshots: dependencies: '@types/webidl-conversions': 7.0.3 + '@types/ws@8.18.1': + dependencies: + '@types/node': 24.10.13 + '@vitejs/plugin-react@4.7.0(vite@7.3.1(@types/node@24.10.13)(tsx@4.21.0))': dependencies: '@babel/core': 7.29.0 @@ -3005,6 +3101,8 @@ snapshots: statuses: 2.0.2 toidentifier: 1.0.1 + iceberg-js@0.8.1: {} + import-fresh@3.3.1: dependencies: parent-module: 1.0.1 @@ -3312,6 +3410,8 @@ snapshots: tr46: 5.1.1 webidl-conversions: 7.0.0 + ws@8.19.0: {} + yallist@3.1.1: {} yaml@1.10.2: {} diff --git a/src-core/src/service.rs b/src-core/src/service.rs index 26add51..2464172 100644 --- a/src-core/src/service.rs +++ b/src-core/src/service.rs @@ -82,7 +82,7 @@ impl AppService { rusqlite::types::ValueRef::Text(t) => { serde_json::Value::String(String::from_utf8_lossy(t).into()) } - _ => serde_json::Value::Null, + rusqlite::types::ValueRef::Blob(_) => serde_json::Value::Null, }; obj.insert(name.clone(), value); } diff --git a/src-react/package.json b/src-react/package.json index 1a4e20e..64bf575 100644 --- a/src-react/package.json +++ b/src-react/package.json @@ -11,6 +11,7 @@ "dependencies": { "@chakra-ui/react": "^3.33.0", "@emotion/react": "^11.14.0", + "@supabase/supabase-js": "^2.98.0", "@tauri-apps/api": "^2", "@tauri-apps/plugin-fs": "~2.4.5", "@tauri-apps/plugin-http": "~2.5.7", diff --git a/src-react/src/components/LoadingDialog.tsx b/src-react/src/components/LoadingDialog.tsx index 271e829..ca24191 100644 --- a/src-react/src/components/LoadingDialog.tsx +++ b/src-react/src/components/LoadingDialog.tsx @@ -3,6 +3,14 @@ import { useEffect, useRef, useState } from "react"; import { Dialog, Portal } from "@chakra-ui/react"; import { invoke } from "@tauri-apps/api/core"; import { listen } from "@tauri-apps/api/event"; +import { + BaseDirectory, + exists, + readDir, + writeFile, +} from "@tauri-apps/plugin-fs"; + +import { supabase } from "../lib/supabaseClient"; interface LoadYomitanZipDirResult { new_dicts: string[]; @@ -31,7 +39,26 @@ function LoadingDialog() { if (isInit.current) return; isInit.current = true; - invoke("init_yomitan").then(() => { + invoke("init_yomitan").then(async () => { + const { data, error } = await supabase.storage.from("yomitan").list("ja"); + if (error) throw error; + + for (const d of data || []) { + const filepath = `yomitan/ja/${d.name}`; + if ( + await exists(filepath, { + baseDir: BaseDirectory.AppData, + }) + ) + continue; + + const { + data: { publicUrl }, + } = supabase.storage.from("yomitan").getPublicUrl(`ja/${d.name}`); + + // await downloadURL(publicUrl, filepath, (received, total) => {}); + } + setMessages([]); }); @@ -105,3 +132,49 @@ function LoadingDialog() { } export default LoadingDialog; + +async function downloadURL( + url: string, + filepath: string, + progressCallback: (received: number, total: number) => void, +) { + const r = await fetch(url); + if (!r.ok) throw r; + if (!r.body) throw r; + + const contentLength = r.headers.get("Content-Length"); + const total = contentLength ? parseInt(contentLength) : 0; + + let receivedBytes = 0; + + const reader = r.body.getReader(); + const stream = new ReadableStream({ + start(controller) { + function push() { + reader + .read() + .then(({ done, value }) => { + if (done) { + controller.close(); + return; + } + receivedBytes += value.length; + + // Calculate and log progress + if (total) progressCallback(receivedBytes, total); + + controller.enqueue(value); + push(); // Read the next chunk + }) + .catch((error) => { + console.error("Stream reading error:", error); + controller.error(error); + }); + } + + push(); // Start reading the data + }, + }); + + await writeFile(filepath, stream, { baseDir: BaseDirectory.AppData }); +} diff --git a/src-react/src/lib/supabaseClient.ts b/src-react/src/lib/supabaseClient.ts new file mode 100644 index 0000000..646f700 --- /dev/null +++ b/src-react/src/lib/supabaseClient.ts @@ -0,0 +1,6 @@ +import { createClient } from "@supabase/supabase-js"; + +const supabaseUrl = import.meta.env.VITE_SUPABASE_URL; +const supabasePublishableKey = import.meta.env.VITE_SUPABASE_PUBLISHABLE_KEY; + +export const supabase = createClient(supabaseUrl, supabasePublishableKey); diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json index 50eab27..fb651f8 100644 --- a/src-tauri/capabilities/default.json +++ b/src-tauri/capabilities/default.json @@ -16,6 +16,17 @@ } ] }, - "fs:default" + "fs:allow-exists", + "fs:allow-appdata-write", + "fs:allow-appdata-meta", + { + "identifier": "fs:scope", + "allow": [ + "$APPDATA/**/*.zip" + ], + "deny": [ + "$APPDATA/*.db" + ] + } ] } \ No newline at end of file