From f288acaa839c57d3dae63be5f5aac72c26ff388f Mon Sep 17 00:00:00 2001 From: Tonho Date: Mon, 13 Apr 2026 15:39:10 -0300 Subject: [PATCH] feat: add version bumping script and i18n key generator --- .github/workflows/release.yml | 36 ++- browser-extension/chrome/popup/popup.css | 1 + browser-extension/chrome/popup/popup.html | 14 +- browser-extension/chrome/popup/popup.js | 14 + browser-extension/chrome/src/background.js | 5 + browser-extension/chrome/src/media-sniffer.js | 35 ++- .../chrome/src/open-app-toggle.js | 18 ++ browser-extension/firefox/popup/popup.css | 1 + browser-extension/firefox/popup/popup.html | 14 +- browser-extension/firefox/popup/popup.js | 14 + browser-extension/firefox/src/background.js | 5 + .../firefox/src/media-sniffer.js | 35 ++- .../firefox/src/open-app-toggle.js | 18 ++ browser-extension/sync.mjs | 102 ++++++ package.json | 11 +- pnpm-lock.yaml | 295 ++++++++++++++++-- scripts/bump-version.js | 197 ++++++++++++ scripts/generate-i18n-keys.js | 111 +++++++ src/app.css | 6 + src/app.d.ts | 6 + src/app.html | 2 +- src/lib/plugin-invoke.test.ts | 58 ++++ vite.config.js | 81 +++-- vitest.config.ts | 22 ++ 24 files changed, 1032 insertions(+), 69 deletions(-) create mode 100644 browser-extension/chrome/src/open-app-toggle.js create mode 100644 browser-extension/firefox/src/open-app-toggle.js create mode 100644 browser-extension/sync.mjs create mode 100644 scripts/bump-version.js create mode 100644 scripts/generate-i18n-keys.js create mode 100644 src/app.d.ts create mode 100644 src/lib/plugin-invoke.test.ts create mode 100644 vitest.config.ts diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index bbf34050..4f3786ad 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -145,9 +145,6 @@ jobs: --arg d "$PUB_DATE" \ '{version: $v, notes: "", pub_date: $d, platforms: {}}') - ASSETS=$(gh release view "$TAG" --repo "$REPO" --json assets -q '.assets[].name') - TMPDIR=$(mktemp -d) - declare -A PLATFORM_MAP=( ["aarch64.app.tar.gz.sig"]="darwin-aarch64" ["x64.app.tar.gz.sig"]="darwin-x86_64" @@ -156,13 +153,44 @@ jobs: ["x64-setup.exe.sig"]="windows-x86_64" ) + ASSETS="" + for attempt in $(seq 1 10); do + ASSETS=$(gh release view "$TAG" --repo "$REPO" --json assets -q '.assets[].name' || true) + MISSING=0 + for SUFFIX in "${!PLATFORM_MAP[@]}"; do + if ! echo "$ASSETS" | grep -q "${SUFFIX}$"; then + MISSING=$((MISSING + 1)) + fi + done + if [ "$MISSING" -eq 0 ]; then + echo "All expected .sig assets are visible (attempt $attempt)." + break + fi + echo "Attempt $attempt: $MISSING .sig asset(s) not yet visible, retrying in 30s..." + sleep 30 + done + + TMPDIR=$(mktemp -d) + for SUFFIX in "${!PLATFORM_MAP[@]}"; do PLATFORM="${PLATFORM_MAP[$SUFFIX]}" SIG_FILE=$(echo "$ASSETS" | grep "${SUFFIX}$" || true) if [ -n "$SIG_FILE" ]; then BUNDLE="${SIG_FILE%.sig}" - gh release download "$TAG" --repo "$REPO" --pattern "$SIG_FILE" --dir "$TMPDIR" --clobber + DOWNLOADED=0 + for dl_attempt in $(seq 1 10); do + if gh release download "$TAG" --repo "$REPO" --pattern "$SIG_FILE" --dir "$TMPDIR" --clobber; then + DOWNLOADED=1 + break + fi + echo "Download attempt $dl_attempt for $SIG_FILE failed, retrying in 30s..." + sleep 30 + done + if [ "$DOWNLOADED" -ne 1 ]; then + echo "Failed to download $SIG_FILE after 10 attempts" >&2 + exit 1 + fi SIG_CONTENT=$(cat "$TMPDIR/$SIG_FILE") URL="https://github.com/${REPO}/releases/download/${TAG}/${BUNDLE}" diff --git a/browser-extension/chrome/popup/popup.css b/browser-extension/chrome/popup/popup.css index 1238be8d..c0a8403f 100644 --- a/browser-extension/chrome/popup/popup.css +++ b/browser-extension/chrome/popup/popup.css @@ -37,6 +37,7 @@ body { } .header-left { display: flex; align-items: center; gap: 8px; } +.header-toggles { display: flex; align-items: center; gap: 10px; } .logo { width: 16px; height: 16px; } .title { font-weight: 600; font-size: 14px; } diff --git a/browser-extension/chrome/popup/popup.html b/browser-extension/chrome/popup/popup.html index 5a78bd4d..a283c1a7 100644 --- a/browser-extension/chrome/popup/popup.html +++ b/browser-extension/chrome/popup/popup.html @@ -12,10 +12,16 @@ OmniGet - +
+ + +
diff --git a/browser-extension/chrome/popup/popup.js b/browser-extension/chrome/popup/popup.js index 3531e13f..7dff38f3 100644 --- a/browser-extension/chrome/popup/popup.js +++ b/browser-extension/chrome/popup/popup.js @@ -1,3 +1,5 @@ +import { loadOpenAppState, isOpenAppEnabled, setOpenAppEnabled } from "../src/open-app-toggle.js"; + const APP_URL = "https://github.com/tonhowtf/omniget/releases/latest"; const SVG = { @@ -11,15 +13,24 @@ const SVG = { let currentData = null; let pageTitle = ""; +let pageThumbnail = ""; async function init() { const toggle = document.getElementById("sniffer-toggle"); + const openAppToggle = document.getElementById("open-app-toggle"); try { const [tab] = await chrome.tabs.query({ active: true, currentWindow: true }); pageTitle = tab?.title || ""; + pageThumbnail = tab?.favIconUrl || ""; } catch {} + await loadOpenAppState(); + openAppToggle.checked = isOpenAppEnabled(); + openAppToggle.addEventListener("change", () => { + setOpenAppEnabled(openAppToggle.checked); + }); + chrome.runtime.sendMessage({ type: "getDetectedMedia" }, (response) => { if (!response) return; currentData = response; @@ -309,9 +320,11 @@ function sendToApp(url, platform, mediaEntry) { const msg = { type: "sendToOmniGet", url, platform }; if (pageTitle) msg.title = pageTitle; + if (pageThumbnail) msg.thumbnail = pageThumbnail; if (currentData?.tabUrl) msg.referer = currentData.tabUrl; if (mediaEntry?.mediaType) msg.mediaType = mediaEntry.mediaType; if (mediaEntry?.contentType) msg.contentType = mediaEntry.contentType; + msg.openApp = isOpenAppEnabled(); if (mediaEntry?.requestHeaders) { const skip = ["host", "connection", "accept-encoding", "sec-ch-ua", "sec-ch-ua-mobile", "sec-ch-ua-platform", "sec-fetch-dest", "sec-fetch-mode", "sec-fetch-site", "upgrade-insecure-requests"]; @@ -344,6 +357,7 @@ async function sendBatch(groups) { if (currentData?.tabUrl) msg.referer = currentData.tabUrl; if (group.master.mediaType) msg.mediaType = group.master.mediaType; if (group.master.contentType) msg.contentType = group.master.contentType; + msg.openApp = isOpenAppEnabled(); try { await new Promise((resolve) => { diff --git a/browser-extension/chrome/src/background.js b/browser-extension/chrome/src/background.js index b7b85a5e..6dd6f519 100644 --- a/browser-extension/chrome/src/background.js +++ b/browser-extension/chrome/src/background.js @@ -156,9 +156,11 @@ async function handleSendToApp(msg) { const platform = msg.platform || "generic"; let pageTitle = ""; + let pageThumbnail = ""; try { const [tab] = await chrome.tabs.query({ active: true, currentWindow: true }); pageTitle = tab?.title || ""; + pageThumbnail = tab?.favIconUrl || ""; } catch {} let cookies = null; @@ -204,9 +206,12 @@ async function handleSendToApp(msg) { if (msg.referer) message.referer = msg.referer; if (msg.title) message.title = msg.title; else if (pageTitle) message.title = pageTitle; + if (msg.thumbnail) message.thumbnail = msg.thumbnail; + else if (pageThumbnail) message.thumbnail = pageThumbnail; if (msg.mediaType) message.mediaType = msg.mediaType; if (msg.contentType) message.contentType = msg.contentType; if (msg.headers) message.headers = msg.headers; + if (typeof msg.openApp === "boolean") message.openApp = msg.openApp; message.pageUrl = msg.referer || ""; message.userAgent = navigator.userAgent; diff --git a/browser-extension/chrome/src/media-sniffer.js b/browser-extension/chrome/src/media-sniffer.js index f275f05d..ea49c6b5 100644 --- a/browser-extension/chrome/src/media-sniffer.js +++ b/browser-extension/chrome/src/media-sniffer.js @@ -71,6 +71,18 @@ const PLATFORM_CDN_HOSTS = [ "tiktokcdn-us.com", ]; +const BLOCKED_PATH_PATTERNS = [ + /\/analytics(\/|\?|$)/i, + /\/pixel(\/|\?|\.|$)/i, + /\/collect(\/|\?|$)/i, + /\/beacon(\/|\?|$)/i, + /\/telemetry(\/|\?|$)/i, + /\/track(\/|\?|$|er)/i, + /1x1\.gif$/i, + /\/spacer\.gif$/i, + /\/transparent\.gif$/i, +]; + const MIN_CONTENT_LENGTH = 50 * 1024; const detectedMedia = new Map(); @@ -96,6 +108,14 @@ function isBlockedHost(url) { } catch { return false; } } +function isBlockedPath(url) { + try { + const u = new URL(url); + const target = u.pathname + u.search; + return BLOCKED_PATH_PATTERNS.some(rx => rx.test(target)); + } catch { return false; } +} + function isMediaByExtension(url) { try { const path = new URL(url).pathname.toLowerCase(); @@ -168,16 +188,18 @@ function isPlatformCdnFragment(url, contentType, contentLength) { } catch { return false; } } +const storageArea = chrome.storage.session ?? chrome.storage.local; + function persistMedia(tabId) { const media = detectedMedia.get(tabId); if (!media || media.size === 0) return; const arr = Array.from(media.entries()); - chrome.storage.session.set({ [`media_${tabId}`]: arr }).catch(() => {}); + storageArea.set({ [`media_${tabId}`]: arr }).catch(() => {}); } export async function restoreMedia() { try { - const data = await chrome.storage.session.get(null); + const data = await storageArea.get(null); for (const [key, value] of Object.entries(data)) { if (!key.startsWith("media_")) continue; const tabId = parseInt(key.replace("media_", "")); @@ -214,7 +236,10 @@ export function registerSnifferListeners(onMediaDetected) { if (details.statusCode < 200 || details.statusCode >= 300) return; const url = details.url; - if (isBlockedHost(url)) return; + if (isBlockedHost(url) || isBlockedPath(url)) { + pendingRequests.delete(details.requestId); + return; + } const contentType = getContentType(details.responseHeaders); const contentLength = getContentLength(details.responseHeaders); @@ -280,13 +305,13 @@ export function registerSnifferListeners(onMediaDetected) { chrome.tabs.onRemoved.addListener((tabId) => { detectedMedia.delete(tabId); - chrome.storage.session.remove(`media_${tabId}`).catch(() => {}); + storageArea.remove(`media_${tabId}`).catch(() => {}); }); chrome.tabs.onUpdated.addListener((tabId, changeInfo) => { if (changeInfo.url) { detectedMedia.delete(tabId); - chrome.storage.session.remove(`media_${tabId}`).catch(() => {}); + storageArea.remove(`media_${tabId}`).catch(() => {}); } }); } diff --git a/browser-extension/chrome/src/open-app-toggle.js b/browser-extension/chrome/src/open-app-toggle.js new file mode 100644 index 00000000..6b279245 --- /dev/null +++ b/browser-extension/chrome/src/open-app-toggle.js @@ -0,0 +1,18 @@ +const STORAGE_KEY = "omniget_open_app_on_download"; + +let openApp = true; + +export async function loadOpenAppState() { + const result = await chrome.storage.local.get(STORAGE_KEY); + openApp = result[STORAGE_KEY] !== false; + return openApp; +} + +export function isOpenAppEnabled() { + return openApp; +} + +export async function setOpenAppEnabled(value) { + openApp = value; + await chrome.storage.local.set({ [STORAGE_KEY]: value }); +} diff --git a/browser-extension/firefox/popup/popup.css b/browser-extension/firefox/popup/popup.css index 1238be8d..c0a8403f 100644 --- a/browser-extension/firefox/popup/popup.css +++ b/browser-extension/firefox/popup/popup.css @@ -37,6 +37,7 @@ body { } .header-left { display: flex; align-items: center; gap: 8px; } +.header-toggles { display: flex; align-items: center; gap: 10px; } .logo { width: 16px; height: 16px; } .title { font-weight: 600; font-size: 14px; } diff --git a/browser-extension/firefox/popup/popup.html b/browser-extension/firefox/popup/popup.html index 5a78bd4d..a283c1a7 100644 --- a/browser-extension/firefox/popup/popup.html +++ b/browser-extension/firefox/popup/popup.html @@ -12,10 +12,16 @@ OmniGet
- +
+ + +
diff --git a/browser-extension/firefox/popup/popup.js b/browser-extension/firefox/popup/popup.js index 3531e13f..7dff38f3 100644 --- a/browser-extension/firefox/popup/popup.js +++ b/browser-extension/firefox/popup/popup.js @@ -1,3 +1,5 @@ +import { loadOpenAppState, isOpenAppEnabled, setOpenAppEnabled } from "../src/open-app-toggle.js"; + const APP_URL = "https://github.com/tonhowtf/omniget/releases/latest"; const SVG = { @@ -11,15 +13,24 @@ const SVG = { let currentData = null; let pageTitle = ""; +let pageThumbnail = ""; async function init() { const toggle = document.getElementById("sniffer-toggle"); + const openAppToggle = document.getElementById("open-app-toggle"); try { const [tab] = await chrome.tabs.query({ active: true, currentWindow: true }); pageTitle = tab?.title || ""; + pageThumbnail = tab?.favIconUrl || ""; } catch {} + await loadOpenAppState(); + openAppToggle.checked = isOpenAppEnabled(); + openAppToggle.addEventListener("change", () => { + setOpenAppEnabled(openAppToggle.checked); + }); + chrome.runtime.sendMessage({ type: "getDetectedMedia" }, (response) => { if (!response) return; currentData = response; @@ -309,9 +320,11 @@ function sendToApp(url, platform, mediaEntry) { const msg = { type: "sendToOmniGet", url, platform }; if (pageTitle) msg.title = pageTitle; + if (pageThumbnail) msg.thumbnail = pageThumbnail; if (currentData?.tabUrl) msg.referer = currentData.tabUrl; if (mediaEntry?.mediaType) msg.mediaType = mediaEntry.mediaType; if (mediaEntry?.contentType) msg.contentType = mediaEntry.contentType; + msg.openApp = isOpenAppEnabled(); if (mediaEntry?.requestHeaders) { const skip = ["host", "connection", "accept-encoding", "sec-ch-ua", "sec-ch-ua-mobile", "sec-ch-ua-platform", "sec-fetch-dest", "sec-fetch-mode", "sec-fetch-site", "upgrade-insecure-requests"]; @@ -344,6 +357,7 @@ async function sendBatch(groups) { if (currentData?.tabUrl) msg.referer = currentData.tabUrl; if (group.master.mediaType) msg.mediaType = group.master.mediaType; if (group.master.contentType) msg.contentType = group.master.contentType; + msg.openApp = isOpenAppEnabled(); try { await new Promise((resolve) => { diff --git a/browser-extension/firefox/src/background.js b/browser-extension/firefox/src/background.js index b7b85a5e..6dd6f519 100644 --- a/browser-extension/firefox/src/background.js +++ b/browser-extension/firefox/src/background.js @@ -156,9 +156,11 @@ async function handleSendToApp(msg) { const platform = msg.platform || "generic"; let pageTitle = ""; + let pageThumbnail = ""; try { const [tab] = await chrome.tabs.query({ active: true, currentWindow: true }); pageTitle = tab?.title || ""; + pageThumbnail = tab?.favIconUrl || ""; } catch {} let cookies = null; @@ -204,9 +206,12 @@ async function handleSendToApp(msg) { if (msg.referer) message.referer = msg.referer; if (msg.title) message.title = msg.title; else if (pageTitle) message.title = pageTitle; + if (msg.thumbnail) message.thumbnail = msg.thumbnail; + else if (pageThumbnail) message.thumbnail = pageThumbnail; if (msg.mediaType) message.mediaType = msg.mediaType; if (msg.contentType) message.contentType = msg.contentType; if (msg.headers) message.headers = msg.headers; + if (typeof msg.openApp === "boolean") message.openApp = msg.openApp; message.pageUrl = msg.referer || ""; message.userAgent = navigator.userAgent; diff --git a/browser-extension/firefox/src/media-sniffer.js b/browser-extension/firefox/src/media-sniffer.js index f343ae40..ea49c6b5 100644 --- a/browser-extension/firefox/src/media-sniffer.js +++ b/browser-extension/firefox/src/media-sniffer.js @@ -71,6 +71,18 @@ const PLATFORM_CDN_HOSTS = [ "tiktokcdn-us.com", ]; +const BLOCKED_PATH_PATTERNS = [ + /\/analytics(\/|\?|$)/i, + /\/pixel(\/|\?|\.|$)/i, + /\/collect(\/|\?|$)/i, + /\/beacon(\/|\?|$)/i, + /\/telemetry(\/|\?|$)/i, + /\/track(\/|\?|$|er)/i, + /1x1\.gif$/i, + /\/spacer\.gif$/i, + /\/transparent\.gif$/i, +]; + const MIN_CONTENT_LENGTH = 50 * 1024; const detectedMedia = new Map(); @@ -96,6 +108,14 @@ function isBlockedHost(url) { } catch { return false; } } +function isBlockedPath(url) { + try { + const u = new URL(url); + const target = u.pathname + u.search; + return BLOCKED_PATH_PATTERNS.some(rx => rx.test(target)); + } catch { return false; } +} + function isMediaByExtension(url) { try { const path = new URL(url).pathname.toLowerCase(); @@ -168,16 +188,18 @@ function isPlatformCdnFragment(url, contentType, contentLength) { } catch { return false; } } +const storageArea = chrome.storage.session ?? chrome.storage.local; + function persistMedia(tabId) { const media = detectedMedia.get(tabId); if (!media || media.size === 0) return; const arr = Array.from(media.entries()); - chrome.storage.local.set({ [`media_${tabId}`]: arr }).catch(() => {}); + storageArea.set({ [`media_${tabId}`]: arr }).catch(() => {}); } export async function restoreMedia() { try { - const data = await chrome.storage.local.get(null); + const data = await storageArea.get(null); for (const [key, value] of Object.entries(data)) { if (!key.startsWith("media_")) continue; const tabId = parseInt(key.replace("media_", "")); @@ -214,7 +236,10 @@ export function registerSnifferListeners(onMediaDetected) { if (details.statusCode < 200 || details.statusCode >= 300) return; const url = details.url; - if (isBlockedHost(url)) return; + if (isBlockedHost(url) || isBlockedPath(url)) { + pendingRequests.delete(details.requestId); + return; + } const contentType = getContentType(details.responseHeaders); const contentLength = getContentLength(details.responseHeaders); @@ -280,13 +305,13 @@ export function registerSnifferListeners(onMediaDetected) { chrome.tabs.onRemoved.addListener((tabId) => { detectedMedia.delete(tabId); - chrome.storage.local.remove(`media_${tabId}`).catch(() => {}); + storageArea.remove(`media_${tabId}`).catch(() => {}); }); chrome.tabs.onUpdated.addListener((tabId, changeInfo) => { if (changeInfo.url) { detectedMedia.delete(tabId); - chrome.storage.local.remove(`media_${tabId}`).catch(() => {}); + storageArea.remove(`media_${tabId}`).catch(() => {}); } }); } diff --git a/browser-extension/firefox/src/open-app-toggle.js b/browser-extension/firefox/src/open-app-toggle.js new file mode 100644 index 00000000..6b279245 --- /dev/null +++ b/browser-extension/firefox/src/open-app-toggle.js @@ -0,0 +1,18 @@ +const STORAGE_KEY = "omniget_open_app_on_download"; + +let openApp = true; + +export async function loadOpenAppState() { + const result = await chrome.storage.local.get(STORAGE_KEY); + openApp = result[STORAGE_KEY] !== false; + return openApp; +} + +export function isOpenAppEnabled() { + return openApp; +} + +export async function setOpenAppEnabled(value) { + openApp = value; + await chrome.storage.local.set({ [STORAGE_KEY]: value }); +} diff --git a/browser-extension/sync.mjs b/browser-extension/sync.mjs new file mode 100644 index 00000000..226ecb2b --- /dev/null +++ b/browser-extension/sync.mjs @@ -0,0 +1,102 @@ +#!/usr/bin/env node + +import fs from "fs"; +import path from "path"; +import { fileURLToPath } from "url"; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const CHROME = path.join(__dirname, "chrome"); +const FIREFOX = path.join(__dirname, "firefox"); + +const SHARED = [ + "src/action-click.js", + "src/action-feedback.js", + "src/action-title.js", + "src/background.js", + "src/context-menu.js", + "src/cookies.js", + "src/detect.js", + "src/media-sniffer.js", + "src/open-app-toggle.js", + "src/sniffer-toggle.js", + "popup/popup.js", + "popup/popup.css", + "popup/popup.html", + "pages/error.html", + "pages/error.css", + "pages/error.js", + "pages/error-content.js", +]; + +const SHARED_DIRS = ["_locales", "icons"]; + +function copyFile(rel) { + const from = path.join(CHROME, rel); + const to = path.join(FIREFOX, rel); + if (!fs.existsSync(from)) { + console.warn(`[skip] missing in chrome: ${rel}`); + return { changed: false, same: false, skipped: true }; + } + fs.mkdirSync(path.dirname(to), { recursive: true }); + const a = fs.readFileSync(from); + if (fs.existsSync(to)) { + const b = fs.readFileSync(to); + if (a.equals(b)) return { changed: false, same: true, skipped: false }; + } + fs.writeFileSync(to, a); + return { changed: true, same: false, skipped: false }; +} + +function copyDirRecursive(relDir) { + const from = path.join(CHROME, relDir); + const to = path.join(FIREFOX, relDir); + if (!fs.existsSync(from)) return { changed: 0, same: 0 }; + let changed = 0; + let same = 0; + const walk = (srcDir, dstDir) => { + fs.mkdirSync(dstDir, { recursive: true }); + for (const entry of fs.readdirSync(srcDir, { withFileTypes: true })) { + const srcPath = path.join(srcDir, entry.name); + const dstPath = path.join(dstDir, entry.name); + if (entry.isDirectory()) { + walk(srcPath, dstPath); + } else if (entry.isFile()) { + const a = fs.readFileSync(srcPath); + if (fs.existsSync(dstPath)) { + const b = fs.readFileSync(dstPath); + if (a.equals(b)) { + same++; + continue; + } + } + fs.writeFileSync(dstPath, a); + changed++; + } + } + }; + walk(from, to); + return { changed, same }; +} + +let totalChanged = 0; +let totalSame = 0; + +for (const rel of SHARED) { + const r = copyFile(rel); + if (r.changed) { + console.log(`[copy] ${rel}`); + totalChanged++; + } else if (r.same) { + totalSame++; + } +} + +for (const rel of SHARED_DIRS) { + const r = copyDirRecursive(rel); + if (r.changed > 0) console.log(`[copy] ${rel}/ (${r.changed} file(s))`); + totalChanged += r.changed; + totalSame += r.same; +} + +console.log(`\nSynced chrome → firefox: ${totalChanged} changed, ${totalSame} already identical.`); +console.log(`Note: manifest.json is intentionally per-browser and not synced.`); diff --git a/package.json b/package.json index 82af792e..f3b0cbf1 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,12 @@ "preview": "vite preview", "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", - "tauri": "tauri" + "tauri": "tauri", + "version:set": "node scripts/bump-version.js", + "generate:i18n-keys": "node scripts/generate-i18n-keys.js", + "extension:sync": "node browser-extension/sync.mjs", + "test": "vitest run", + "test:watch": "vitest" }, "license": "GPL-3.0", "dependencies": { @@ -29,9 +34,11 @@ "@sveltejs/kit": "^2.9.0", "@sveltejs/vite-plugin-svelte": "^5.0.0", "@tauri-apps/cli": "^2", + "@types/node": "^25.6.0", "svelte": "^5.0.0", "svelte-check": "^4.0.0", "typescript": "~5.6.2", - "vite": "^6.0.3" + "vite": "^6.0.3", + "vitest": "^4.1.4" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1be6625f..a0dc5ca8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -38,16 +38,19 @@ importers: devDependencies: '@sveltejs/adapter-static': specifier: ^3.0.6 - version: 3.0.10(@sveltejs/kit@2.51.0(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.51.0)(vite@6.4.1))(svelte@5.51.0)(typescript@5.6.3)(vite@6.4.1)) + version: 3.0.10(@sveltejs/kit@2.51.0(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.51.0)(vite@6.4.1(@types/node@25.6.0)))(svelte@5.51.0)(typescript@5.6.3)(vite@6.4.1(@types/node@25.6.0))) '@sveltejs/kit': specifier: ^2.9.0 - version: 2.51.0(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.51.0)(vite@6.4.1))(svelte@5.51.0)(typescript@5.6.3)(vite@6.4.1) + version: 2.51.0(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.51.0)(vite@6.4.1(@types/node@25.6.0)))(svelte@5.51.0)(typescript@5.6.3)(vite@6.4.1(@types/node@25.6.0)) '@sveltejs/vite-plugin-svelte': specifier: ^5.0.0 - version: 5.1.1(svelte@5.51.0)(vite@6.4.1) + version: 5.1.1(svelte@5.51.0)(vite@6.4.1(@types/node@25.6.0)) '@tauri-apps/cli': specifier: ^2 version: 2.10.0 + '@types/node': + specifier: ^25.6.0 + version: 25.6.0 svelte: specifier: ^5.0.0 version: 5.51.0 @@ -59,7 +62,10 @@ importers: version: 5.6.3 vite: specifier: ^6.0.3 - version: 6.4.1 + version: 6.4.1(@types/node@25.6.0) + vitest: + specifier: ^4.1.4 + version: 4.1.4(@types/node@25.6.0)(vite@6.4.1(@types/node@25.6.0)) packages: @@ -528,15 +534,53 @@ packages: '@tauri-apps/plugin-updater@2.10.0': resolution: {integrity: sha512-ljN8jPlnT0aSn8ecYhuBib84alxfMx6Hc8vJSKMJyzGbTPFZAC44T2I1QNFZssgWKrAlofvJqCC6Rr472JWfkQ==} + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + '@types/cookie@0.6.0': resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==} + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + '@types/node@25.6.0': + resolution: {integrity: sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==} + '@types/trusted-types@2.0.7': resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} + '@vitest/expect@4.1.4': + resolution: {integrity: sha512-iPBpra+VDuXmBFI3FMKHSFXp3Gx5HfmSCE8X67Dn+bwephCnQCaB7qWK2ldHa+8ncN8hJU8VTMcxjPpyMkUjww==} + + '@vitest/mocker@4.1.4': + resolution: {integrity: sha512-R9HTZBhW6yCSGbGQnDnH3QHfJxokKN4KB+Yvk9Q1le7eQNYwiCyKxmLmurSpFy6BzJanSLuEUDrD+j97Q+ZLPg==} + peerDependencies: + msw: ^2.4.9 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@4.1.4': + resolution: {integrity: sha512-ddmDHU0gjEUyEVLxtZa7xamrpIefdEETu3nZjWtHeZX4QxqJ7tRxSteHVXJOcr8jhiLoGAhkK4WJ3WqBpjx42A==} + + '@vitest/runner@4.1.4': + resolution: {integrity: sha512-xTp7VZ5aXP5ZJrn15UtJUWlx6qXLnGtF6jNxHepdPHpMfz/aVPx+htHtgcAL2mDXJgKhpoo2e9/hVJsIeFbytQ==} + + '@vitest/snapshot@4.1.4': + resolution: {integrity: sha512-MCjCFgaS8aZz+m5nTcEcgk/xhWv0rEH4Yl53PPlMXOZ1/Ka2VcZU6CJ+MgYCZbcJvzGhQRjVrGQNZqkGPttIKw==} + + '@vitest/spy@4.1.4': + resolution: {integrity: sha512-XxNdAsKW7C+FLydqFJLb5KhJtl3PGCMmYwFRfhvIgxJvLSXhhVI1zM8f1qD3Zg7RCjTSzDVyct6sghs9UEgBEQ==} + + '@vitest/utils@4.1.4': + resolution: {integrity: sha512-13QMT+eysM5uVGa1rG4kegGYNp6cnQcsTc67ELFbhNLQO+vgsygtYJx2khvdt4gVQqSSpC/KT5FZZxUpP3Oatw==} + acorn@8.15.0: resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} engines: {node: '>=0.4.0'} @@ -546,10 +590,18 @@ packages: resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} engines: {node: '>= 0.4'} + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + axobject-query@4.1.0: resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} engines: {node: '>= 0.4'} + chai@6.2.2: + resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} + engines: {node: '>=18'} + chokidar@4.0.3: resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} engines: {node: '>= 14.16.0'} @@ -558,6 +610,9 @@ packages: resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} engines: {node: '>=6'} + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + cookie@0.6.0: resolution: {integrity: sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==} engines: {node: '>= 0.6'} @@ -578,6 +633,9 @@ packages: devalue@5.6.2: resolution: {integrity: sha512-nPRkjWzzDQlsejL1WVifk5rvcFi/y1onBRxjaFMjZeR9mFpqu2gmAZ9xUB9/IEanEP/vBtGeGganC/GO1fmufg==} + es-module-lexer@2.0.0: + resolution: {integrity: sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==} + esbuild@0.25.12: resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==} engines: {node: '>=18'} @@ -589,6 +647,13 @@ packages: esrap@2.2.3: resolution: {integrity: sha512-8fOS+GIGCQZl/ZIlhl59htOlms6U8NvX6ZYgYHpRU/b6tVSh3uHkOHZikl3D4cMbYM0JlpBe+p/BkZEi8J9XIQ==} + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + expect-type@1.3.0: + resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} + engines: {node: '>=12.0.0'} + fdir@6.5.0: resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} engines: {node: '>=12.0.0'} @@ -632,6 +697,12 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + obug@2.1.1: + resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -659,6 +730,9 @@ packages: set-cookie-parser@3.0.1: resolution: {integrity: sha512-n7Z7dXZhJbwuAHhNzkTti6Aw9QDDjZtm3JTpTGATIdNzdQz5GuFs22w90BcvF4INfnrL5xrX3oGsuqO5Dx3A1Q==} + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + sirv@3.0.2: resolution: {integrity: sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==} engines: {node: '>=18'} @@ -667,6 +741,12 @@ packages: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + std-env@4.0.0: + resolution: {integrity: sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ==} + svelte-check@4.4.0: resolution: {integrity: sha512-gB3FdEPb8tPO3Y7Dzc6d/Pm/KrXAhK+0Fk+LkcysVtupvAh6Y/IrBCEZNupq57oh0hcwlxCUamu/rq7GtvfSEg==} engines: {node: '>= 18.0.0'} @@ -684,10 +764,21 @@ packages: peerDependencies: svelte: '>=3.49.0' + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@1.1.1: + resolution: {integrity: sha512-VKS/ZaQhhkKFMANmAOhhXVoIfBXblQxGX1myCQ2faQrfmobMftXeJPcZGp0gS07ocvGJWDLZGyOZDadDBqYIJg==} + engines: {node: '>=18'} + tinyglobby@0.2.15: resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} engines: {node: '>=12.0.0'} + tinyrainbow@3.1.0: + resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==} + engines: {node: '>=14.0.0'} + totalist@3.0.1: resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} engines: {node: '>=6'} @@ -697,6 +788,9 @@ packages: engines: {node: '>=14.17'} hasBin: true + undici-types@7.19.2: + resolution: {integrity: sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==} + vite@6.4.1: resolution: {integrity: sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} @@ -745,6 +839,52 @@ packages: vite: optional: true + vitest@4.1.4: + resolution: {integrity: sha512-tFuJqTxKb8AvfyqMfnavXdzfy3h3sWZRWwfluGbkeR7n0HUev+FmNgZ8SDrRBTVrVCjgH5cA21qGbCffMNtWvg==} + engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@opentelemetry/api': ^1.9.0 + '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 + '@vitest/browser-playwright': 4.1.4 + '@vitest/browser-preview': 4.1.4 + '@vitest/browser-webdriverio': 4.1.4 + '@vitest/coverage-istanbul': 4.1.4 + '@vitest/coverage-v8': 4.1.4 + '@vitest/ui': 4.1.4 + happy-dom: '*' + jsdom: '*' + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@opentelemetry/api': + optional: true + '@types/node': + optional: true + '@vitest/browser-playwright': + optional: true + '@vitest/browser-preview': + optional: true + '@vitest/browser-webdriverio': + optional: true + '@vitest/coverage-istanbul': + optional: true + '@vitest/coverage-v8': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + zimmerframe@1.1.4: resolution: {integrity: sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==} @@ -934,15 +1074,15 @@ snapshots: dependencies: acorn: 8.15.0 - '@sveltejs/adapter-static@3.0.10(@sveltejs/kit@2.51.0(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.51.0)(vite@6.4.1))(svelte@5.51.0)(typescript@5.6.3)(vite@6.4.1))': + '@sveltejs/adapter-static@3.0.10(@sveltejs/kit@2.51.0(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.51.0)(vite@6.4.1(@types/node@25.6.0)))(svelte@5.51.0)(typescript@5.6.3)(vite@6.4.1(@types/node@25.6.0)))': dependencies: - '@sveltejs/kit': 2.51.0(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.51.0)(vite@6.4.1))(svelte@5.51.0)(typescript@5.6.3)(vite@6.4.1) + '@sveltejs/kit': 2.51.0(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.51.0)(vite@6.4.1(@types/node@25.6.0)))(svelte@5.51.0)(typescript@5.6.3)(vite@6.4.1(@types/node@25.6.0)) - '@sveltejs/kit@2.51.0(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.51.0)(vite@6.4.1))(svelte@5.51.0)(typescript@5.6.3)(vite@6.4.1)': + '@sveltejs/kit@2.51.0(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.51.0)(vite@6.4.1(@types/node@25.6.0)))(svelte@5.51.0)(typescript@5.6.3)(vite@6.4.1(@types/node@25.6.0))': dependencies: '@standard-schema/spec': 1.1.0 '@sveltejs/acorn-typescript': 1.0.9(acorn@8.15.0) - '@sveltejs/vite-plugin-svelte': 5.1.1(svelte@5.51.0)(vite@6.4.1) + '@sveltejs/vite-plugin-svelte': 5.1.1(svelte@5.51.0)(vite@6.4.1(@types/node@25.6.0)) '@types/cookie': 0.6.0 acorn: 8.15.0 cookie: 0.6.0 @@ -955,29 +1095,29 @@ snapshots: set-cookie-parser: 3.0.1 sirv: 3.0.2 svelte: 5.51.0 - vite: 6.4.1 + vite: 6.4.1(@types/node@25.6.0) optionalDependencies: typescript: 5.6.3 - '@sveltejs/vite-plugin-svelte-inspector@4.0.1(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.51.0)(vite@6.4.1))(svelte@5.51.0)(vite@6.4.1)': + '@sveltejs/vite-plugin-svelte-inspector@4.0.1(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.51.0)(vite@6.4.1(@types/node@25.6.0)))(svelte@5.51.0)(vite@6.4.1(@types/node@25.6.0))': dependencies: - '@sveltejs/vite-plugin-svelte': 5.1.1(svelte@5.51.0)(vite@6.4.1) + '@sveltejs/vite-plugin-svelte': 5.1.1(svelte@5.51.0)(vite@6.4.1(@types/node@25.6.0)) debug: 4.4.3 svelte: 5.51.0 - vite: 6.4.1 + vite: 6.4.1(@types/node@25.6.0) transitivePeerDependencies: - supports-color - '@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.51.0)(vite@6.4.1)': + '@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.51.0)(vite@6.4.1(@types/node@25.6.0))': dependencies: - '@sveltejs/vite-plugin-svelte-inspector': 4.0.1(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.51.0)(vite@6.4.1))(svelte@5.51.0)(vite@6.4.1) + '@sveltejs/vite-plugin-svelte-inspector': 4.0.1(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.51.0)(vite@6.4.1(@types/node@25.6.0)))(svelte@5.51.0)(vite@6.4.1(@types/node@25.6.0)) debug: 4.4.3 deepmerge: 4.3.1 kleur: 4.1.5 magic-string: 0.30.21 svelte: 5.51.0 - vite: 6.4.1 - vitefu: 1.1.1(vite@6.4.1) + vite: 6.4.1(@types/node@25.6.0) + vitefu: 1.1.1(vite@6.4.1(@types/node@25.6.0)) transitivePeerDependencies: - supports-color @@ -1056,24 +1196,82 @@ snapshots: dependencies: '@tauri-apps/api': 2.10.1 + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + '@types/cookie@0.6.0': {} + '@types/deep-eql@4.0.2': {} + '@types/estree@1.0.8': {} + '@types/node@25.6.0': + dependencies: + undici-types: 7.19.2 + '@types/trusted-types@2.0.7': {} + '@vitest/expect@4.1.4': + dependencies: + '@standard-schema/spec': 1.1.0 + '@types/chai': 5.2.3 + '@vitest/spy': 4.1.4 + '@vitest/utils': 4.1.4 + chai: 6.2.2 + tinyrainbow: 3.1.0 + + '@vitest/mocker@4.1.4(vite@6.4.1(@types/node@25.6.0))': + dependencies: + '@vitest/spy': 4.1.4 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 6.4.1(@types/node@25.6.0) + + '@vitest/pretty-format@4.1.4': + dependencies: + tinyrainbow: 3.1.0 + + '@vitest/runner@4.1.4': + dependencies: + '@vitest/utils': 4.1.4 + pathe: 2.0.3 + + '@vitest/snapshot@4.1.4': + dependencies: + '@vitest/pretty-format': 4.1.4 + '@vitest/utils': 4.1.4 + magic-string: 0.30.21 + pathe: 2.0.3 + + '@vitest/spy@4.1.4': {} + + '@vitest/utils@4.1.4': + dependencies: + '@vitest/pretty-format': 4.1.4 + convert-source-map: 2.0.0 + tinyrainbow: 3.1.0 + acorn@8.15.0: {} aria-query@5.3.2: {} + assertion-error@2.0.1: {} + axobject-query@4.1.0: {} + chai@6.2.2: {} + chokidar@4.0.3: dependencies: readdirp: 4.1.2 clsx@2.1.1: {} + convert-source-map@2.0.0: {} + cookie@0.6.0: {} debug@4.4.3: @@ -1084,6 +1282,8 @@ snapshots: devalue@5.6.2: {} + es-module-lexer@2.0.0: {} + esbuild@0.25.12: optionalDependencies: '@esbuild/aix-ppc64': 0.25.12 @@ -1119,6 +1319,12 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.8 + + expect-type@1.3.0: {} + fdir@6.5.0(picomatch@4.0.3): optionalDependencies: picomatch: 4.0.3 @@ -1146,6 +1352,10 @@ snapshots: nanoid@3.3.11: {} + obug@2.1.1: {} + + pathe@2.0.3: {} + picocolors@1.1.1: {} picomatch@4.0.3: {} @@ -1195,6 +1405,8 @@ snapshots: set-cookie-parser@3.0.1: {} + siginfo@2.0.0: {} + sirv@3.0.2: dependencies: '@polka/url': 1.0.0-next.29 @@ -1203,6 +1415,10 @@ snapshots: source-map-js@1.2.1: {} + stackback@0.0.2: {} + + std-env@4.0.0: {} + svelte-check@4.4.0(picomatch@4.0.3)(svelte@5.51.0)(typescript@5.6.3): dependencies: '@jridgewell/trace-mapping': 0.3.31 @@ -1240,16 +1456,24 @@ snapshots: '@sveltekit-i18n/parser-default': 1.1.1 svelte: 5.51.0 + tinybench@2.9.0: {} + + tinyexec@1.1.1: {} + tinyglobby@0.2.15: dependencies: fdir: 6.5.0(picomatch@4.0.3) picomatch: 4.0.3 + tinyrainbow@3.1.0: {} + totalist@3.0.1: {} typescript@5.6.3: {} - vite@6.4.1: + undici-types@7.19.2: {} + + vite@6.4.1(@types/node@25.6.0): dependencies: esbuild: 0.25.12 fdir: 6.5.0(picomatch@4.0.3) @@ -1258,10 +1482,43 @@ snapshots: rollup: 4.57.1 tinyglobby: 0.2.15 optionalDependencies: + '@types/node': 25.6.0 fsevents: 2.3.3 - vitefu@1.1.1(vite@6.4.1): + vitefu@1.1.1(vite@6.4.1(@types/node@25.6.0)): optionalDependencies: - vite: 6.4.1 + vite: 6.4.1(@types/node@25.6.0) + + vitest@4.1.4(@types/node@25.6.0)(vite@6.4.1(@types/node@25.6.0)): + dependencies: + '@vitest/expect': 4.1.4 + '@vitest/mocker': 4.1.4(vite@6.4.1(@types/node@25.6.0)) + '@vitest/pretty-format': 4.1.4 + '@vitest/runner': 4.1.4 + '@vitest/snapshot': 4.1.4 + '@vitest/spy': 4.1.4 + '@vitest/utils': 4.1.4 + es-module-lexer: 2.0.0 + expect-type: 1.3.0 + magic-string: 0.30.21 + obug: 2.1.1 + pathe: 2.0.3 + picomatch: 4.0.3 + std-env: 4.0.0 + tinybench: 2.9.0 + tinyexec: 1.1.1 + tinyglobby: 0.2.15 + tinyrainbow: 3.1.0 + vite: 6.4.1(@types/node@25.6.0) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 25.6.0 + transitivePeerDependencies: + - msw + + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 zimmerframe@1.1.4: {} diff --git a/scripts/bump-version.js b/scripts/bump-version.js new file mode 100644 index 00000000..ae40adbe --- /dev/null +++ b/scripts/bump-version.js @@ -0,0 +1,197 @@ +#!/usr/bin/env node + +import fs from "fs"; +import path from "path"; +import { fileURLToPath } from "url"; +import { execSync } from "child_process"; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); +const arg = process.argv[2]; + +if (!arg) { + console.error("Usage: node scripts/bump-version.js "); + process.exit(1); +} + +function readCurrentVersion() { + const pkg = JSON.parse(fs.readFileSync(path.join(root, "package.json"), "utf8")); + return pkg.version; +} + +function bump(current, kind) { + const match = current.match(/^(\d+)\.(\d+)\.(\d+)(?:-([\w.]+))?$/); + if (!match) { + console.error(`Cannot parse current version: ${current}`); + process.exit(1); + } + let [, maj, min, patch] = match; + maj = Number(maj); + min = Number(min); + patch = Number(patch); + if (kind === "major") { + maj += 1; + min = 0; + patch = 0; + } else if (kind === "minor") { + min += 1; + patch = 0; + } else if (kind === "patch") { + patch += 1; + } else { + console.error(`Unknown bump kind: ${kind}`); + process.exit(1); + } + return `${maj}.${min}.${patch}`; +} + +const current = readCurrentVersion(); +let version; +if (["major", "minor", "patch"].includes(arg)) { + version = bump(current, arg); +} else if (/^\d+\.\d+\.\d+(-[\w.]+)?$/.test(arg)) { + version = arg; +} else { + console.error(`Invalid version: ${arg}`); + console.error("Expected: major | minor | patch | X.Y.Z[-suffix]"); + process.exit(1); +} + +console.log(`Bumping ${current} → ${version}`); + +const changed = []; + +function writeJson(filePath, mutator) { + const raw = fs.readFileSync(filePath, "utf8"); + const data = JSON.parse(raw); + mutator(data); + const out = JSON.stringify(data, null, 2) + (raw.endsWith("\n") ? "\n" : ""); + fs.writeFileSync(filePath, out); + changed.push(path.relative(root, filePath)); +} + +function writeText(filePath, mutator) { + const original = fs.readFileSync(filePath, "utf8"); + const updated = mutator(original); + if (updated !== original) { + fs.writeFileSync(filePath, updated); + changed.push(path.relative(root, filePath)); + } +} + +writeJson(path.join(root, "package.json"), (p) => { + p.version = version; +}); + +const pkgLockPath = path.join(root, "package-lock.json"); +if (fs.existsSync(pkgLockPath)) { + writeJson(pkgLockPath, (p) => { + p.version = version; + if (p.packages && p.packages[""]) { + p.packages[""].version = version; + } + }); +} + +const cargoTomlPath = path.join(root, "src-tauri", "Cargo.toml"); +writeText(cargoTomlPath, (content) => { + const lines = content.split("\n"); + let inPackage = false; + let replaced = false; + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + const sectionMatch = line.match(/^\[([^\]]+)\]\s*$/); + if (sectionMatch) { + inPackage = sectionMatch[1].trim() === "package"; + continue; + } + if (inPackage && /^version\s*=\s*"[^"]+"/.test(line)) { + lines[i] = line.replace(/^version\s*=\s*"[^"]+"/, `version = "${version}"`); + replaced = true; + break; + } + } + if (!replaced) { + console.error("Failed to update version in Cargo.toml"); + process.exit(1); + } + return lines.join("\n"); +}); + +const cargoLockPath = path.join(root, "src-tauri", "Cargo.lock"); +if (fs.existsSync(cargoLockPath)) { + writeText(cargoLockPath, (content) => { + return content.replace( + /(name = "omniget"\nversion = ")([^"]+)(")/, + (_, a, _b, c) => `${a}${version}${c}` + ); + }); +} + +const tauriConfPath = path.join(root, "src-tauri", "tauri.conf.json"); +writeJson(tauriConfPath, (conf) => { + conf.version = version; +}); + +const metainfoPath = path.join(root, "flatpak", "wtf.tonho.omniget.metainfo.xml"); +writeText(metainfoPath, (content) => { + let updated = content.replace( + /(raw\.githubusercontent\.com\/tonhowtf\/omniget\/v)(\d+\.\d+\.\d+(?:-[\w.]+)?)(\/)/g, + (_, a, _b, c) => `${a}${version}${c}` + ); + return updated; +}); + +const changelogStorePath = path.join(root, "src", "lib", "stores", "changelog-store.svelte.ts"); +if (fs.existsSync(changelogStorePath)) { + writeText(changelogStorePath, (content) => + content.replace( + /(currentVersion\s*=\s*")[^"]+(";)/g, + (_, a, c) => `${a}${version}${c}` + ) + ); +} + +const aboutProjectPath = path.join(root, "src", "routes", "about", "project", "+page.svelte"); +if (fs.existsSync(aboutProjectPath)) { + writeText(aboutProjectPath, (content) => + content.replace( + /(const\s+APP_VERSION\s*=\s*")[^"]+(";)/, + (_, a, c) => `${a}${version}${c}` + ) + ); +} + +if (changed.length === 0) { + console.error("No files changed — aborting."); + process.exit(1); +} + +console.log("Updated files:"); +for (const file of changed) { + console.log(` - ${file}`); +} + +function hasBinary(name) { + const probe = process.platform === "win32" ? "where" : "which"; + try { + execSync(`${probe} ${name}`, { stdio: "ignore" }); + return true; + } catch { + return false; + } +} + +const metainfoAbs = path.join(root, "flatpak", "wtf.tonho.omniget.metainfo.xml"); +if (hasBinary("appstreamcli")) { + try { + execSync(`appstreamcli validate "${metainfoAbs}"`, { stdio: "inherit" }); + console.log("appstreamcli validate: OK"); + } catch { + console.error("appstreamcli validate failed — aborting."); + process.exit(1); + } +} else { + console.warn("appstreamcli not found in PATH — skipping metainfo validation."); +} + +console.log(`v${version}`); diff --git a/scripts/generate-i18n-keys.js b/scripts/generate-i18n-keys.js new file mode 100644 index 00000000..8d7d9b32 --- /dev/null +++ b/scripts/generate-i18n-keys.js @@ -0,0 +1,111 @@ +#!/usr/bin/env node + +import fs from "fs"; +import path from "path"; +import { fileURLToPath } from "url"; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const i18nDir = path.join(__dirname, "..", "src", "lib", "i18n"); +const enPath = path.join(i18nDir, "en.json"); +const outputFile = path.join(i18nDir, "keys.ts"); + +function extractKeys(obj, prefix = "") { + const keys = []; + for (const [key, value] of Object.entries(obj)) { + const fullKey = prefix ? `${prefix}.${key}` : key; + if (typeof value === "object" && value !== null && !Array.isArray(value)) { + keys.push(...extractKeys(value, fullKey)); + } else { + keys.push(fullKey); + } + } + return keys; +} + +function generateKeysFile() { + if (!fs.existsSync(enPath)) { + console.error("Error: en.json not found at", enPath); + process.exit(1); + } + + const enContent = JSON.parse(fs.readFileSync(enPath, "utf-8")); + const keys = extractKeys(enContent).sort(); + + const grouped = {}; + for (const key of keys) { + const category = key.split(".")[0]; + if (!grouped[category]) grouped[category] = []; + grouped[category].push(key); + } + + let output = `/** + * i18n Translation Keys Reference + * + * AUTO-GENERATED FILE + * Run: pnpm generate:i18n-keys + * + * This file provides autocomplete hints for translation keys. + * Import the \`t\` function from '$lib/i18n' to use translations. + * + * Usage: + * \`\`\`svelte + * + * + *

{\$t('app.name')}

+ *

{\$t('queue.items', { count: 5 })}

+ * \`\`\` + */ + +export type TranslationKeys =\n`; + + const categories = Object.keys(grouped).sort(); + for (let i = 0; i < categories.length; i++) { + const category = categories[i]; + const categoryKeys = grouped[category]; + + for (let j = 0; j < categoryKeys.length; j++) { + const key = categoryKeys[j]; + output += ` | '${key}'\n`; + } + + if (i < categories.length - 1) { + output += "\n"; + } + } + + output += " // Allows dynamic/computed keys while preserving autocomplete for known keys\n"; + output += " | (string & {});\n"; + + fs.writeFileSync(outputFile, output, "utf-8"); + console.log(`Generated ${keys.length} translation keys in keys.ts`); + + const otherLocales = fs + .readdirSync(i18nDir) + .filter((f) => f.endsWith(".json") && f !== "en.json"); + + let hadMismatch = false; + const enKeySet = new Set(keys); + for (const file of otherLocales) { + const p = path.join(i18nDir, file); + try { + const content = JSON.parse(fs.readFileSync(p, "utf-8")); + const localeKeys = new Set(extractKeys(content)); + const missing = [...enKeySet].filter((k) => !localeKeys.has(k)); + const extra = [...localeKeys].filter((k) => !enKeySet.has(k)); + if (missing.length > 0 || extra.length > 0) { + hadMismatch = true; + console.warn(`[${file}] missing: ${missing.length}, extra: ${extra.length}`); + } + } catch (err) { + hadMismatch = true; + console.warn(`[${file}] failed to parse: ${err.message}`); + } + } + if (!hadMismatch) { + console.log(`All ${otherLocales.length} other locales in sync with en.json`); + } +} + +generateKeysFile(); diff --git a/src/app.css b/src/app.css index 17871bbf..a7f07952 100644 --- a/src/app.css +++ b/src/app.css @@ -8,6 +8,12 @@ --padding: 10px; --border-radius: 11px; --sidebar-width: 80px; + --bottom-nav-height: 44px; + --mobile-nav-clearance: calc(var(--bottom-nav-height) + env(safe-area-inset-bottom, 0px) + var(--padding)); + --safe-area-top: env(safe-area-inset-top, 0px); + --safe-area-right: env(safe-area-inset-right, 0px); + --safe-area-bottom: env(safe-area-inset-bottom, 0px); + --safe-area-left: env(safe-area-inset-left, 0px); --font-system: 'DM Sans', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; --font-mono: 'IBM Plex Mono', 'DejaVu Sans Mono', 'Liberation Mono', 'Noto Sans Mono', 'Ubuntu Mono', 'Courier New', monospace; diff --git a/src/app.d.ts b/src/app.d.ts new file mode 100644 index 00000000..aec25a7d --- /dev/null +++ b/src/app.d.ts @@ -0,0 +1,6 @@ +declare const __COMMIT_HASH__: string; +declare const __GIT_BRANCH__: string; +declare const __APP_VERSION__: string; +declare const __BUILD_DATE__: string; + +declare namespace App {} diff --git a/src/app.html b/src/app.html index c5fbfa5d..ece14f6e 100644 --- a/src/app.html +++ b/src/app.html @@ -3,7 +3,7 @@ - + OmniGet %sveltekit.head% diff --git a/src/lib/plugin-invoke.test.ts b/src/lib/plugin-invoke.test.ts new file mode 100644 index 00000000..c9c1ec8a --- /dev/null +++ b/src/lib/plugin-invoke.test.ts @@ -0,0 +1,58 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; + +const invokeMock = vi.fn(); + +vi.mock("@tauri-apps/api/core", () => ({ + invoke: (cmd: string, args: unknown) => invokeMock(cmd, args), +})); + +import { pluginInvoke } from "./plugin-invoke"; + +describe("pluginInvoke", () => { + beforeEach(() => { + invokeMock.mockReset(); + }); + + it("routes through the plugin_command Tauri handler", async () => { + invokeMock.mockResolvedValueOnce({ ok: true }); + await pluginInvoke("courses", "hotmart_login", { email: "e", password: "p" }); + expect(invokeMock).toHaveBeenCalledTimes(1); + expect(invokeMock.mock.calls[0][0]).toBe("plugin_command"); + }); + + it("passes pluginId, command, and args in the payload", async () => { + invokeMock.mockResolvedValueOnce(undefined); + await pluginInvoke("telegram", "list_chats", { limit: 20 }); + expect(invokeMock.mock.calls[0][1]).toEqual({ + pluginId: "telegram", + command: "list_chats", + args: { limit: 20 }, + }); + }); + + it("defaults args to an empty object when not provided", async () => { + invokeMock.mockResolvedValueOnce("ok"); + await pluginInvoke("convert", "list_formats"); + expect(invokeMock.mock.calls[0][1]).toEqual({ + pluginId: "convert", + command: "list_formats", + args: {}, + }); + }); + + it("propagates the resolved value", async () => { + invokeMock.mockResolvedValueOnce({ success: true, count: 3 }); + const result = await pluginInvoke<{ success: boolean; count: number }>( + "courses", + "list", + ); + expect(result).toEqual({ success: true, count: 3 }); + }); + + it("propagates rejection from invoke", async () => { + invokeMock.mockRejectedValueOnce(new Error("plugin not loaded")); + await expect(pluginInvoke("missing", "cmd")).rejects.toThrow( + "plugin not loaded", + ); + }); +}); diff --git a/vite.config.js b/vite.config.js index 3ecfa0a7..3120b2ff 100644 --- a/vite.config.js +++ b/vite.config.js @@ -1,32 +1,63 @@ import { defineConfig } from "vite"; import { sveltekit } from "@sveltejs/kit/vite"; +import { execSync } from "child_process"; +import { readFileSync } from "fs"; -// @ts-expect-error process is a nodejs global const host = process.env.TAURI_DEV_HOST; -// https://vite.dev/config/ -export default defineConfig(async () => ({ - plugins: [sveltekit()], +function getGitInfo() { + try { + return { + hash: execSync("git rev-parse HEAD").toString().trim(), + branch: execSync("git rev-parse --abbrev-ref HEAD").toString().trim(), + }; + } catch { + return { hash: "unknown", branch: "unknown" }; + } +} - // Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build` - // - // 1. prevent Vite from obscuring rust errors - clearScreen: false, - // 2. tauri expects a fixed port, fail if that port is not available - server: { - port: 1420, - strictPort: true, - host: host || false, - hmr: host - ? { - protocol: "ws", - host, - port: 1421, - } - : undefined, - watch: { - // 3. tell Vite to ignore watching `src-tauri` - ignored: ["**/src-tauri/**"], +function getVersion() { + try { + return JSON.parse(readFileSync("package.json", "utf8")).version; + } catch { + return "0.0.0"; + } +} + +// https://vite.dev/config/ +export default defineConfig(async () => { + const gitInfo = getGitInfo(); + + return { + plugins: [sveltekit()], + + define: { + __COMMIT_HASH__: JSON.stringify(gitInfo.hash), + __GIT_BRANCH__: JSON.stringify(gitInfo.branch), + __APP_VERSION__: JSON.stringify(getVersion()), + __BUILD_DATE__: JSON.stringify(new Date().toISOString().split("T")[0]), }, - }, -})); + + // Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build` + // + // 1. prevent Vite from obscuring rust errors + clearScreen: false, + // 2. tauri expects a fixed port, fail if that port is not available + server: { + port: 1420, + strictPort: true, + host: host || false, + hmr: host + ? { + protocol: "ws", + host, + port: 1421, + } + : undefined, + watch: { + // 3. tell Vite to ignore watching `src-tauri` + ignored: ["**/src-tauri/**"], + }, + }, + }; +}); diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 00000000..4e322410 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,22 @@ +import { defineConfig } from "vitest/config"; +import path from "path"; + +export default defineConfig({ + test: { + include: ["src/**/*.test.ts"], + environment: "node", + globals: false, + }, + resolve: { + alias: { + $lib: path.resolve(__dirname, "src/lib"), + $components: path.resolve(__dirname, "src/components"), + }, + }, + define: { + __COMMIT_HASH__: JSON.stringify("test-commit"), + __GIT_BRANCH__: JSON.stringify("test-branch"), + __APP_VERSION__: JSON.stringify("0.0.0-test"), + __BUILD_DATE__: JSON.stringify("2026-04-13"), + }, +});