refactor: cleanup duplicates and dead code with fallow suggests
Some checks failed
Build / node (22.x) (push) Has been cancelled
Build / extension (22.x) (push) Has been cancelled
Build / bun (push) Has been cancelled

This commit is contained in:
Toil 2026-09-23 01:20:28 +03:00
parent a0502a5587
commit 2ec15a71cf
No known key found for this signature in database
GPG key ID: 57248E2819D32070
69 changed files with 356 additions and 452 deletions

19
.fallowrc.jsonc Normal file
View file

@ -0,0 +1,19 @@
{
"$schema": "https://raw.githubusercontent.com/fallow-rs/fallow/main/schema.json",
"ignorePatterns": [
".storybook/**",
// bundle
"dist/**",
"dist-ext/**",
".output/**",
// autogenerated file
"./src/types/localization.ts"
],
"ignoreDependencies": ["@storybook/addon-a11y", "@storybook/addon-docs"],
"dynamicallyLoaded": ["src/extension/**/*.ts", "src/shims/nodeCrypto.ts"],
"rules": {
"unused-export": "warn",
// many false-positive detections with this rule
"unused-class-member": "off"
}
}

1
.gitignore vendored
View file

@ -158,3 +158,4 @@ dist/*.proxy.user.js
*storybook.log
storybook-static
.fallow/

View file

@ -22,7 +22,6 @@ function isSolidPlugin(plugin: PluginOption): boolean {
const config: StorybookConfig = {
stories: ["../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"],
addons: [
"@chromatic-com/storybook",
"@storybook/addon-vitest",
"@storybook/addon-a11y",
"@storybook/addon-docs",

View file

@ -18,7 +18,6 @@
},
"devDependencies": {
"@biomejs/biome": "^2.5.14",
"@chromatic-com/storybook": "latest",
"@crxjs/vite-plugin": "^2.7.1",
"@storybook/addon-a11y": "^10.6.0",
"@storybook/addon-docs": "^10.6.0",
@ -106,8 +105,6 @@
"@bufbuild/protobuf": ["@bufbuild/protobuf@2.15.0", "", {}, "sha512-DAheWUkVr/SJTWCc+lg9dhY0eN4SaWlf4+bG1KzHeXbnqt0AfB/NX0Z+VunGlM1ki1B4zVvye27MpKh/svySUA=="],
"@chromatic-com/storybook": ["@chromatic-com/storybook@5.3.1", "", { "dependencies": { "@neoconfetti/react": "^1.0.0", "chromatic": "^18.0.1", "jsonfile": "^6.1.0", "strip-ansi": "^7.1.0" }, "peerDependencies": { "storybook": "^0.0.0-0 || ^10.1.0 || ^10.1.0-0 || ^10.2.0-0 || ^10.3.0-0 || ^10.4.0-0 || ^10.5.0-0 || ^10.6.0-0 || ^11.0.0-0" } }, "sha512-eUcbR9CddVmKAnjOiBCOz5rNdT096jiXIjR+qaWbUuTlgRdpc9yjTrk4/hiPAAykZrPYVNJN11p6rJgW9KNpKg=="],
"@crxjs/vite-plugin": ["@crxjs/vite-plugin@2.7.1", "", { "dependencies": { "@webcomponents/custom-elements": "^1.5.0", "acorn-walk": "^8.3.5", "convert-source-map": "^2.0.0", "debug": "^4.4.3", "es-module-lexer": "^0.10.0", "fs-extra": "^11.3.5", "jsesc": "^3.0.2", "magic-string": "^0.30.12", "node-html-parser": "^7.1.0", "pathe": "^2.0.1", "picocolors": "^1.1.1", "rollup": "2.80.0", "rxjs": "7.5.7", "tinyglobby": "^0.2.17" }, "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-mn77Lc1ilIvbO7ZqijjNy+AnRgc8srkfHOtV6HrGrYZgRe8YI3aIcSjHJCU5Kq75SejEZGrdAoHdn3gBGeKpEA=="],
"@devicefarmer/adbkit": ["@devicefarmer/adbkit@3.3.9", "", { "dependencies": { "@devicefarmer/adbkit-logcat": "^2.1.2", "@devicefarmer/adbkit-monkey": "~1.2.1", "bluebird": "~3.7", "commander": "^9.1.0", "debug": "~4.3.1", "node-forge": "^1.3.1", "split": "~1.0.1" }, "bin": { "adbkit": "bin/adbkit" } }, "sha512-AYxk5G/b2BPEfubcuxVEZEOpsLf0isqrPNJnnwAlUjZAiLdFg/uvLNRDuBhsC/Ar62X+rUhVlvlL+tKp9HCjfQ=="],
@ -258,8 +255,6 @@
"@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.2.4", "", { "dependencies": { "@tybys/wasm-util": "^0.10.3" }, "peerDependencies": { "@emnapi/core": "^1.7.1 || ^2.0.0-alpha.4", "@emnapi/runtime": "^1.7.1 || ^2.0.0-alpha.4" } }, "sha512-AJxoUD2/15ESHbvpcyjU274nsAPLuOtPHCk0vKJM5pj//Fg/B1FXNWjPnXTT9PymCYYiHo4zPj0ZomXBKhoy7g=="],
"@neoconfetti/react": ["@neoconfetti/react@1.0.0", "", {}, "sha512-klcSooChXXOzIm+SE5IISIAn3bYzYfPjbX7D7HoqZL84oAfgREeSg5vSIaSFH+DaGzzvImTyWe1OyrJ67vik4A=="],
"@oxc-parser/binding-android-arm-eabi": ["@oxc-parser/binding-android-arm-eabi@0.127.0", "", { "os": "android", "cpu": "arm" }, "sha512-0LC7ye4hvqbIKxAzThzvswgHLFu2AURKzYLeSVvLdu2TBOYWQDmHnTqPLeA597BcUCxiLqLsS4CJ5uoI5WYWCQ=="],
"@oxc-parser/binding-android-arm64": ["@oxc-parser/binding-android-arm64@0.127.0", "", { "os": "android", "cpu": "arm64" }, "sha512-b5jtVTH6AU5CJXHNdj7Jj9IEiR9yVjjnwHzPJhGyHGPdcsZSzBCkS9GBbV33niRMvKthDwQRFRJfI4a+k4PvYg=="],
@ -494,8 +489,6 @@
"@volar/typescript": ["@volar/typescript@2.4.28", "", { "dependencies": { "@volar/language-core": "2.4.28", "path-browserify": "^1.0.1", "vscode-uri": "^3.0.8" } }, "sha512-Ja6yvWrbis2QtN4ClAKreeUZPVYMARDYZl9LMEv1iQ1QdepB6wn0jTRxA9MftYmYa4DQ4k/DaSZpFPUfxl8giw=="],
"@vot.js/core": ["@vot.js/core@3.1.2", "", { "dependencies": { "@vot.js/shared": "^3.1.2" }, "peerDependencies": { "typescript": "^6.0.0" } }, "sha512-Hi+csDJA4+E268lsbOQBBLgWYwjOeMH7p0kg5Atwk6ZnmMYwPZIozvKtqGaRFcVj5zg1VzrJZ8cDC6QThQ0Gpg=="],
"@vot.js/ext": ["@vot.js/ext@3.1.2", "", { "dependencies": { "@vot.js/core": "^3.1.2", "@vot.js/shared": "^3.1.2" }, "peerDependencies": { "typescript": "^6.0.0" } }, "sha512-hfdBnvgT8JYkWnlZ9hli6wUXJZKWlyGz7cNWuCiBSD/Sfs7ghZ7uUungi2tRjlp7L/iMI3FAvR7z/8iVrRK7sg=="],
@ -526,7 +519,7 @@
"ansi-align": ["ansi-align@3.0.1", "", { "dependencies": { "string-width": "^4.1.0" } }, "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w=="],
"ansi-regex": ["ansi-regex@6.3.0", "", {}, "sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ=="],
"ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
"ansi-styles": ["ansi-styles@7.0.0", "", {}, "sha512-kKvt3m4uwzqL0wlkPd09CmljPJGOZZ4D0fP65sqFSvPkMRKhNi+74MgIJ5QxE6SxqB4t4KyUFGg8+n5zjo6hew=="],
@ -602,8 +595,6 @@
"chokidar": ["chokidar@5.0.0", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="],
"chromatic": ["chromatic@18.9.4", "", { "dependencies": { "semver": "^7.3.5" }, "peerDependencies": { "@chromatic-com/cypress": "^0.*.* || ^1.0.0", "@chromatic-com/playwright": "^0.*.* || ^1.0.0", "@chromatic-com/vitest": "^0.*.* || ^1.0.0" }, "optionalPeers": ["@chromatic-com/cypress", "@chromatic-com/playwright", "@chromatic-com/vitest"], "bin": { "chroma": "dist/bin.cjs", "chromatic": "dist/bin.cjs", "chromatic-cli": "dist/bin.cjs" } }, "sha512-+/socRBceUAk19XwvnnJTgkgw6YJ5Fh58HoWM5ipiLwTnZS82r/cCM164ROKYVajeuCeo3ROsH+6nVHAylin5g=="],
"chrome-launcher": ["chrome-launcher@1.2.0", "", { "dependencies": { "@types/node": "*", "escape-string-regexp": "^4.0.0", "is-wsl": "^2.2.0", "lighthouse-logger": "^2.0.1" }, "bin": { "print-chrome-path": "bin/print-chrome-path.cjs" } }, "sha512-JbuGuBNss258bvGil7FT4HKdC3SC2K7UAEUqiPy3ACS3Yxo3hAW6bvFpCu2HsIJLgTqxgEX6BkujvzZfLpUD0Q=="],
"cli-boxes": ["cli-boxes@3.0.0", "", {}, "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g=="],
@ -1174,7 +1165,7 @@
"string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="],
"strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="],
"strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
"strip-bom": ["strip-bom@5.0.0", "", {}, "sha512-p+byADHF7SzEcVnLvc/r3uognM1hUhObuHXxJcgLCfD194XAkaLbjq3Wzb0N5G2tgIjH0dgT708Z51QxMeu60A=="],
@ -1354,12 +1345,8 @@
"boxen/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="],
"cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
"cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
"columnify/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
"config-chain/ini": ["ini@1.3.8", "", {}, "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="],
"cross-spawn/which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
@ -1390,8 +1377,6 @@
"jws/safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
"pretty-format/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
"pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="],
"rc/ini": ["ini@1.3.8", "", {}, "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="],
@ -1400,8 +1385,6 @@
"rolldown/@oxc-project/types": ["@oxc-project/types@0.150.0", "", {}, "sha512-rDS5/31E9HfPl/CIzGrn0DOlvBbXFseQ5URJ9sYMfstbKLD/c6Gm9vmRzRGDdAXyOIL4zmO37lc9RIwYqVruZw=="],
"string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
"thread-stream/real-require": ["real-require@1.0.0", "", {}, "sha512-P4nbQYQfePJxRSmY+v/KINxVucm4NF3p3s7pJveMTtom52FR4YGltUQLB8idDXwDDWW+eYrWDFbuzUnjoWHF7g=="],
"vitest/es-module-lexer": ["es-module-lexer@2.3.2", "", {}, "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw=="],
@ -1416,6 +1399,8 @@
"wrap-ansi/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="],
"wrap-ansi/strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="],
"wsl-utils/is-wsl": ["is-wsl@3.1.1", "", { "dependencies": { "is-inside-container": "^1.0.0" } }, "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw=="],
"zip-dir/jszip": ["jszip@3.10.2", "", { "dependencies": { "lie": "~3.3.0", "pako": "~1.0.2", "readable-stream": "~2.3.6", "setimmediate": "^1.0.5" } }, "sha512-3l+rb15IOWtUhU0H5MFqES/T6Kh7abYwjosBey/vD6hDt8zoEffkSC5Ws5SGtgVw3gBx2NEbhTeSW1+kWkpyTQ=="],
@ -1432,12 +1417,10 @@
"boxen/string-width/emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="],
"cliui/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
"boxen/string-width/strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="],
"cliui/wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
"columnify/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
"cross-spawn/which/isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
"eslint/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
@ -1446,14 +1429,20 @@
"fx-runner/which/isexe": ["isexe@3.1.5", "", {}, "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w=="],
"string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
"web-ext/open/wsl-utils": ["wsl-utils@0.3.1", "", { "dependencies": { "is-wsl": "^3.1.0", "powershell-utils": "^0.1.0" } }, "sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg=="],
"widest-line/string-width/emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="],
"widest-line/string-width/strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="],
"wrap-ansi/string-width/emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="],
"wrap-ansi/strip-ansi/ansi-regex": ["ansi-regex@6.3.0", "", {}, "sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ=="],
"boxen/string-width/strip-ansi/ansi-regex": ["ansi-regex@6.3.0", "", {}, "sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ=="],
"web-ext/open/wsl-utils/is-wsl": ["is-wsl@3.1.1", "", { "dependencies": { "is-inside-container": "^1.0.0" } }, "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw=="],
"widest-line/string-width/strip-ansi/ansi-regex": ["ansi-regex@6.3.0", "", {}, "sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ=="],
}
}

View file

@ -16,7 +16,6 @@
],
"devDependencies": {
"@biomejs/biome": "^2.5.14",
"@chromatic-com/storybook": "latest",
"@crxjs/vite-plugin": "^2.7.1",
"@storybook/addon-a11y": "^10.6.0",
"@storybook/addon-docs": "^10.6.0",

View file

@ -7,6 +7,7 @@ import type { RequestLang, ResponseLang } from "@vot.js/shared/types/data";
import type { ClientSession, SessionModule } from "@vot.js/shared/types/secure";
import Chaimu from "chaimu/client";
import { initAudioContext } from "chaimu/player";
import {
minLongWaitingCount,
proxyWorkerHostMode1,
@ -23,10 +24,11 @@ import { createVideoLifecycleHost } from "./core/videoLifecycleHost";
import { VOTVideoManager } from "./core/videoManager";
import { localizationProvider, t } from "./localization/localizationProvider";
import { Notifier } from "./notify";
import type { ProcessedSubtitles } from "./subtitles/processor";
import { SubtitlesWidget } from "./subtitles/widget";
import type { ResponseLanguageSubtitles, StorageData } from "./types/storage";
import type { ProcessedSubtitles } from "./types/subtitles";
import type { OverlayMount } from "./types/uiManager";
import type { VideoData } from "./types/videoHandler";
import { UIManager } from "./ui/manager";
import { isSameOverlayMount } from "./ui/mount";
import { OverlayVisibilityController } from "./ui/overlayVisibilityController";
@ -88,7 +90,6 @@ import {
updateTranslation as updateTranslationImpl,
validateAudioUrl as validateAudioUrlImpl,
} from "./videoHandler/modules/translation";
import type { VideoData } from "./videoHandler/shared";
import {
type ApplyVolumeLinkDeltaResult,
applyVolumeLinkDelta,

View file

@ -313,7 +313,7 @@ async function getPlayer(
);
}
export function createAudioChunkStream(
function createAudioChunkStream(
targetWindow: MseWindow,
videoId: string,
signal: AbortSignal,
@ -842,7 +842,7 @@ async function handleTopRequest(
).appendChild(iframe);
}
export function initMseProxyHandler(): void {
function initMseProxyHandler(): void {
const pageWindow = globalThis as unknown as MseWindow;
if (
pageWindow[BOOT_KEY] ||

View file

@ -205,7 +205,7 @@ export async function mintPagePoToken(
}
}
export function selectGvsPoTokenBinding(
function selectGvsPoTokenBinding(
videoId: string,
options: {
loggedIn: boolean;
@ -234,6 +234,28 @@ function getConfigValue(config: YouTubeConfig, key: string): unknown {
return config.get?.(key) ?? config.data_?.[key];
}
function cloneInnertubeContext(
config: YouTubeConfig,
unavailableMessage: string,
): {
context: {
client?: Record<string, unknown>;
thirdParty?: Record<string, unknown>;
};
client: Record<string, unknown>;
} {
const rawContext = getConfigValue(config, "INNERTUBE_CONTEXT");
if (!rawContext || typeof rawContext !== "object") {
throw new Error(unavailableMessage);
}
const context = JSON.parse(JSON.stringify(rawContext)) as {
client?: Record<string, unknown>;
thirdParty?: Record<string, unknown>;
};
context.client ??= {};
return { context, client: context.client };
}
function buildContentPlaybackContext(
signatureTimestamp: unknown,
): Record<string, unknown> {
@ -274,7 +296,7 @@ function findJsonValueEnd(source: string, start: number): number {
// The page keeps its config in the ytcfg global, which a sandboxed userscript
// realm cannot read. Both calling forms carry plain JSON, so the same inline
// script that builds ytcfg can be replayed from its source text instead.
export function parseYtcfgData(source: string): Record<string, unknown> {
function parseYtcfgData(source: string): Record<string, unknown> {
const data: Record<string, unknown> = {};
const pattern = /ytcfg\s*\.\s*set\s*\(/g;
const skipSpaces = (index: number) => {
@ -334,7 +356,7 @@ function readYtcfgFromDocument(targetWindow: Window): Record<string, unknown> {
return data;
}
export async function resolveYtcfg(
async function resolveYtcfg(
targetWindow: WebAbrWindow,
signal: AbortSignal,
): Promise<YouTubeConfig> {
@ -382,17 +404,10 @@ export function buildWebEmbeddedPlayerRequest(
videoId: string,
extractedSignatureTimestamp?: number,
): Record<string, unknown> {
const rawContext = getConfigValue(config, "INNERTUBE_CONTEXT");
if (!rawContext || typeof rawContext !== "object") {
throw new Error("Audio downloader. web_embedded context is unavailable");
}
const context = JSON.parse(JSON.stringify(rawContext)) as {
client?: Record<string, unknown>;
thirdParty?: Record<string, unknown>;
};
context.client ??= {};
const client = context.client;
const { context, client } = cloneInnertubeContext(
config,
"Audio downloader. web_embedded context is unavailable",
);
client.clientName = "WEB_EMBEDDED_PLAYER";
client.clientVersion =
getConfigValue(config, "INNERTUBE_CLIENT_VERSION") ?? client.clientVersion;
@ -963,7 +978,7 @@ function buildSolvedUrl(
return url.toString();
}
export async function* resolveWebEmbeddedFormatUrl(
async function* resolveWebEmbeddedFormatUrl(
targetWindow: WebAbrWindow,
format: WebEmbeddedFormat,
playerCode: () => Promise<string | undefined>,
@ -1111,17 +1126,10 @@ export function buildWebPlayerRequest(
videoId: string,
extractedSignatureTimestamp?: number,
): Record<string, unknown> {
const rawContext = getConfigValue(config, "INNERTUBE_CONTEXT");
if (!rawContext || typeof rawContext !== "object") {
throw new Error("Audio downloader. web client context is unavailable");
}
const context = JSON.parse(JSON.stringify(rawContext)) as {
client?: Record<string, unknown>;
thirdParty?: Record<string, unknown>;
};
context.client ??= {};
const client = context.client;
const { context, client } = cloneInnertubeContext(
config,
"Audio downloader. web client context is unavailable",
);
client.clientName = "WEB";
client.clientVersion =
getConfigValue(config, "INNERTUBE_CLIENT_VERSION") ?? client.clientVersion;

View file

@ -9,7 +9,7 @@ const MESSAGE_TYPE = "get-audio-chunks-by-mse-in-main-world";
export const STREAM_TIMEOUT_MS = 30 * 60_000;
const MESSAGE_TIMEOUT_MS = 5 * 60_000;
export function parseAudioBridgeChunk(payload: unknown): AudioChunk {
function parseAudioBridgeChunk(payload: unknown): AudioChunk {
if (!payload || typeof payload !== "object" || !("buffer" in payload)) {
throw new Error("Audio downloader. Invalid audio bridge chunk");
}

View file

@ -14,17 +14,5 @@
$margin: 0 !important
);
border: solid 1px var(--vot-border-color) !important;
color: rgb(var(--vot-helper-theme));
background-color: transparent;
@include button.vot-button-layers(rgb(var(--vot-helper-theme)));
@include button.vot-button-layer-hover(0.04);
@include button.vot-button-layer-active(0.16);
&[disabled="true"],
&[aria-disabled="true"] {
background-color: transparent;
color: rgba(var(--vot-onsurface-rgb, 0, 0, 0), 0.38);
cursor: initial;
@include button.vot-button-layer-reset-disabled;
}
@include button.vot-button-theme-transparent;
}

View file

@ -1,6 +1,7 @@
import { createSignal, onCleanup } from "solid-js";
import type { Meta, StoryObj } from "storybook-solidjs-vite";
import { DownloadIcon } from "../Icons/DownloadIcon";
import { startProgressAnimation } from "../Icons/ProgressIcon.stories";
import { ProgressIconButton } from "./ProgressIconButton";
const meta = {
@ -40,16 +41,11 @@ export const ProgressIconButtonChanging: Story = {
onClick={() => {
setShowProgress(true);
const intervalId = window.setInterval(() => {
const nextProgress = Math.min(progress() + 0.1, 100);
setProgress(nextProgress);
if (nextProgress === 100) {
window.clearInterval(intervalId);
setShowProgress(false);
}
}, 10);
const intervalId = startProgressAnimation(
progress,
setProgress,
() => setShowProgress(false),
);
onCleanup(() => window.clearInterval(intervalId));
}}

View file

@ -14,17 +14,5 @@
$margin: 0 !important
);
border: none !important;
color: rgb(var(--vot-helper-theme));
background-color: transparent;
@include button.vot-button-layers(rgb(var(--vot-helper-theme)));
@include button.vot-button-layer-hover(0.04);
@include button.vot-button-layer-active(0.16);
&[disabled="true"],
&[aria-disabled="true"] {
background-color: transparent;
color: rgba(var(--vot-onsurface-rgb, 0, 0, 0), 0.38);
cursor: initial;
@include button.vot-button-layer-reset-disabled;
}
@include button.vot-button-theme-transparent;
}

View file

@ -82,3 +82,19 @@
opacity: 0;
}
}
@mixin vot-button-theme-transparent {
color: rgb(var(--vot-helper-theme));
background-color: transparent;
@include vot-button-layers(rgb(var(--vot-helper-theme)));
@include vot-button-layer-hover(0.04);
@include vot-button-layer-active(0.16);
&[disabled="true"],
&[aria-disabled="true"] {
background-color: transparent;
color: rgba(var(--vot-onsurface-rgb, 0, 0, 0), 0.38);
cursor: initial;
@include vot-button-layer-reset-disabled;
}
}

View file

@ -14,6 +14,7 @@ export type SliderLabelDescProps = {
ref?: (element: HTMLElement) => void;
};
// fallow-ignore-next-line unused-export
export function SliderLabelDesc(props: SliderLabelDescProps): JSX.Element {
return (
<vot-block ref={props.ref} class="vot-slider-label__text-desc">

View file

@ -1,7 +1,30 @@
import { createSignal, onCleanup } from "solid-js";
import { type Accessor, createSignal, onCleanup, type Setter } from "solid-js";
import type { Meta, StoryObj } from "storybook-solidjs-vite";
import { ProgressIcon } from "./ProgressIcon";
const PROGRESS_STEP = 0.1;
const PROGRESS_INTERVAL_MS = 10;
export function startProgressAnimation(
progress: Accessor<number>,
setProgress: Setter<number>,
onComplete?: () => void,
) {
const intervalId = window.setInterval(() => {
const nextProgress = Math.min(progress() + PROGRESS_STEP, 100);
setProgress(nextProgress);
if (nextProgress === 100) {
window.clearInterval(intervalId);
onComplete?.();
}
}, PROGRESS_INTERVAL_MS);
return intervalId;
}
const meta = {
component: ProgressIcon,
render: (args) => (
@ -40,15 +63,7 @@ export const ProgressIconChanging: Story = {
render: () => {
const [progress, setProgress] = createSignal(0);
const intervalId = window.setInterval(() => {
const nextProgress = Math.min(progress() + 0.1, 100);
setProgress(nextProgress);
if (nextProgress === 100) {
window.clearInterval(intervalId);
}
}, 10);
const intervalId = startProgressAnimation(progress, setProgress);
onCleanup(() => window.clearInterval(intervalId));

View file

@ -197,6 +197,26 @@ export const SegmentedButtonPrimaryAction: Story = {
},
};
const playColumnTouchVoiceSelection = (canvasElement: HTMLElement) => {
const translateButton = canvasElement.querySelector<HTMLElement>(
".vot-translate-button",
);
expect(translateButton).not.toBeNull();
if (!translateButton) {
return;
}
dispatchPrimaryPointerUp(translateButton, "touch");
const popover =
canvasElement.ownerDocument.querySelector<HTMLElement>(
".vot-voice-popover",
);
return {
translateButton,
popover,
};
};
export const SegmentedButtonColumnTouchVoiceSelection: Story = {
args: {
direction: "column",
@ -204,20 +224,7 @@ export const SegmentedButtonColumnTouchVoiceSelection: Story = {
onTranslateClick: fn(),
},
play: ({ args, canvasElement }) => {
const translateButton = canvasElement.querySelector<HTMLElement>(
".vot-translate-button",
);
expect(translateButton).not.toBeNull();
if (!translateButton) {
return;
}
dispatchPrimaryPointerUp(translateButton, "touch");
const popover =
canvasElement.ownerDocument.querySelector<HTMLElement>(
".vot-voice-popover",
);
const { popover } = playColumnTouchVoiceSelection(canvasElement);
expect(args.onTranslateClick).not.toHaveBeenCalled();
expect(popover?.hidden).toBe(false);
},
@ -231,20 +238,7 @@ export const SegmentedButtonColumnTouchError: Story = {
status: "error",
},
play: ({ args, canvasElement }) => {
const translateButton = canvasElement.querySelector<HTMLElement>(
".vot-translate-button",
);
expect(translateButton).not.toBeNull();
if (!translateButton) {
return;
}
dispatchPrimaryPointerUp(translateButton, "touch");
const popover =
canvasElement.ownerDocument.querySelector<HTMLElement>(
".vot-voice-popover",
);
const { popover } = playColumnTouchVoiceSelection(canvasElement);
expect(args.onTranslateClick).toHaveBeenCalledTimes(1);
expect(popover?.hidden).not.toBe(false);
},

View file

@ -157,9 +157,7 @@ export type LanguagePairSelectProps = {
onResponseLanguageSelect?: (lang: ResponseLang) => void | Promise<void>;
controlsRef?: (controls: LanguagePairSelectControls) => void;
};
export function LanguagePairSelect(
props: LanguagePairSelectProps,
): JSX.Element {
function LanguagePairSelect(props: LanguagePairSelectProps): JSX.Element {
const fromLangsOptions = genSelectOptionsByLangs(availableLangs);
const toLangsOptions = genSelectOptionsByLangs(availableTTS);

View file

@ -44,11 +44,6 @@ export const DEFAULT_AUTO_VOLUME = 15;
*/
export const DEFAULT_SMART_DUCKING_STRENGTH = 80;
/**
* Max audio volume percentage (if available)
*/
export const maxAudioVolume = 900;
/**
* The number of repeated responses after which the message turns into
* "translation is delayed, please wait"

View file

@ -5,7 +5,7 @@ import {
type AuthMessageData,
} from "../../types/core/auth/message";
export function createAuthDataMessage(data: AuthMessageData): AuthDataMessage {
function createAuthDataMessage(data: AuthMessageData): AuthDataMessage {
return {
source: AUTH_DATA_MESSAGE_SOURCE,
type: AUTH_DATA_MESSAGE_TYPE,

View file

@ -22,13 +22,11 @@ import { GM_fetch } from "../../utils/gm";
import { votStorage } from "../../utils/storage";
import { base64UrlEncode } from "../../utils/utils";
export function createCodeVerifier(): string {
function createCodeVerifier(): string {
return base64UrlEncode(crypto.getRandomValues(new Uint8Array(32)));
}
export async function createCodeChallenge(
codeVerifier: string,
): Promise<string> {
async function createCodeChallenge(codeVerifier: string): Promise<string> {
const digest = await crypto.subtle.digest(
"SHA-256",
new TextEncoder().encode(codeVerifier),
@ -37,7 +35,7 @@ export async function createCodeChallenge(
return base64UrlEncode(new Uint8Array(digest));
}
export const isTokenError = (data: unknown): data is TokenError => {
const isTokenError = (data: unknown): data is TokenError => {
return (
typeof data === "object" &&
data !== null &&
@ -46,7 +44,7 @@ export const isTokenError = (data: unknown): data is TokenError => {
);
};
export async function getOAuthTokenByCode(code: string): Promise<TokenInfo> {
async function getOAuthTokenByCode(code: string): Promise<TokenInfo> {
const res = await GM_fetch(YANDEX_AUTH_TOKEN_URL, {
method: "POST",
headers: {
@ -70,7 +68,7 @@ export async function getOAuthTokenByCode(code: string): Promise<TokenInfo> {
return data;
}
export const isAuthError = (data: unknown): data is AuthError => {
const isAuthError = (data: unknown): data is AuthError => {
return (
typeof data === "object" &&
data !== null &&
@ -79,7 +77,7 @@ export const isAuthError = (data: unknown): data is AuthError => {
);
};
export async function getUserInfo(token: string): Promise<UserInfo> {
async function getUserInfo(token: string): Promise<UserInfo> {
const res = await GM_fetch(`${YANDEX_USER_INFO_URL}?format=json`, {
method: "GET",
headers: {

View file

@ -7,9 +7,7 @@ export type BootstrapPolicyInput = {
authOrigin: string;
};
export function shouldSkipIframeBootstrap(
input: BootstrapPolicyInput,
): boolean {
function shouldSkipIframeBootstrap(input: BootstrapPolicyInput): boolean {
if (!input.isIframe) return false;
return (
input.href === "about:blank" ||

View file

@ -6,7 +6,7 @@ import type {
import { computeExpiresAt } from "../utils/responseCache";
import { votStorage } from "../utils/storage";
export const YANDEX_TTL_MS = 2 * 60 * 60 * 1000; // 2 hours
const YANDEX_TTL_MS = 2 * 60 * 60 * 1000; // 2 hours
const VOT_SESSION_STORAGE_KEY = "VOTSession";
type TimedCacheEntry<T> = {

View file

@ -1,6 +1,6 @@
import { containsCrossShadow } from "../utils/dom";
export interface DocumentWithFullscreen extends Document {
interface DocumentWithFullscreen extends Document {
webkitFullscreenElement?: Element | null;
webkitExitFullscreen?: () => Promise<void>;
}

View file

@ -1,13 +1,8 @@
export const EXTERNAL_VOLUME_HOSTS = new Set(["youtube", "googledrive"]);
const EXTERNAL_VOLUME_HOSTS = new Set(["youtube", "googledrive"]);
const YOUTUBE_LIKE_HOSTS = EXTERNAL_VOLUME_HOSTS;
export const YOUTUBE_LIKE_HOSTS = EXTERNAL_VOLUME_HOSTS;
export const MUTE_SYNC_DISABLED_HOSTS = new Set(["rutube", "ok"]);
export const TRANSLATION_DOWNLOAD_HOSTS = new Set([
"youtube",
"invidious",
"piped",
]);
const MUTE_SYNC_DISABLED_HOSTS = new Set(["rutube", "ok"]);
const TRANSLATION_DOWNLOAD_HOSTS = new Set(["youtube", "invidious", "piped"]);
export function isExternalVolumeHost(host: string): boolean {
return EXTERNAL_VOLUME_HOSTS.has(host);

View file

@ -6,7 +6,7 @@ export type LifecycleTranslationResetHost = {
resetSubtitlesWidget(): void;
};
export function resetLifecycleTranslation(
function resetLifecycleTranslation(
host: LifecycleTranslationResetHost,
options: {
requireVideoData?: boolean;

View file

@ -7,7 +7,7 @@ export type OverlayMountTargets = {
subtitlesMountContainer: HTMLElement | ShadowRoot;
};
export function resolveOverlayBaseContainer(
function resolveOverlayBaseContainer(
container: HTMLElement,
site: ServiceConf,
): HTMLElement {

View file

@ -9,17 +9,19 @@ import type { RequestLang, ResponseLang } from "@vot.js/shared/types/data";
import { AudioDownloader } from "../audioDownloader";
import { STREAM_TIMEOUT_MS } from "../audioDownloader/strategies/webAudioBridge";
import { t } from "../localization/localizationProvider";
import { deleteAccount } from "../stores/account";
import type {
DownloadedAudioData,
DownloadedPartialAudioData,
} from "../types/audioDownloader";
import type { VideoData } from "../types/videoHandler";
import {
createAbortableDelay,
createAbortableWaiter,
NEVER_ABORTED_SIGNAL,
throwIfAborted,
} from "../utils/abort";
import { deleteAccount, hasAccountToken } from "../utils/account";
import { hasAccountToken } from "../utils/account";
import debug from "../utils/debug";
import {
getErrorMessage,
@ -29,7 +31,6 @@ import {
} from "../utils/errors";
import type { VideoHandler } from "../VideoHandler";
import VOTLocalizedError from "../VOTLocalizedError";
import type { VideoData } from "../videoHandler/shared";
import { openAuthWindow } from "./auth/window";
import {
getTranslationAuthErrorKind,

View file

@ -1,11 +1,12 @@
import type { VideoDataSubtitle } from "@vot.js/core/types/client";
import type { ServiceConf, VideoService } from "@vot.js/ext/types/service";
import type { RequestLang, ResponseLang } from "@vot.js/shared/types/data";
import type { OverlayViewControls } from "../components/OverlayView/OverlayView";
import type { StorageData } from "../types/storage";
import type { VideoData } from "../types/videoHandler";
import debug from "../utils/debug";
import { containsCrossShadow } from "../utils/dom";
import type { VideoData } from "../videoHandler/shared";
import { findConnectedContainerBySelector } from "./containerResolution";
import { hideLifecycleOverlay, resetAndHideLifecycle } from "./lifecycleShared";

View file

@ -3,11 +3,13 @@ import { getVideoData } from "@vot.js/ext/utils/videoData";
import votConfig from "@vot.js/shared/config";
import { availableLangs } from "@vot.js/shared/consts";
import type { RequestLang, ResponseLang } from "@vot.js/shared/types/data";
import {
getYoutubeAudioFormatLanguage as getYoutubeAudioFormatLanguageTag,
selectSmallestAudioFormat,
} from "../audioDownloader/utils";
import { localizationProvider } from "../localization/localizationProvider";
import type { VideoData as RuntimeVideoData } from "../types/videoHandler";
import debug from "../utils/debug";
import { GM_fetch } from "../utils/gm";
import { cleanText } from "../utils/text";
@ -19,7 +21,6 @@ import {
} from "../utils/volume";
import type { VideoHandler } from "../VideoHandler";
import VOTLocalizedError from "../VOTLocalizedError";
import type { VideoData as RuntimeVideoData } from "../videoHandler/shared";
import { isExternalVolumeHost } from "./hostPolicies";
import { detect } from "./translateApis";
@ -272,7 +273,7 @@ function resolveYoutubeDetectedLanguageFromSubtitles(
);
}
export async function resolveDetectedLanguageForVideo(
async function resolveDetectedLanguageForVideo(
options: ResolveDetectedLanguageOptions,
): Promise<ResolveDetectedLanguageResult> {
if (options.isStream) {

View file

@ -190,14 +190,11 @@ async function ensureDnrHeaderStripRule(
return isNew;
}
export async function ensureDnrOriginStripRuleForYoutubei(
url: string,
forbiddenHeaders: Record<string, string> = {},
): Promise<boolean> {
if (!isYoutubeMobileUrl(url)) return false;
const requestHeaders: DnrRequestHeader[] = [...YOUTUBEI_BASE_HEADERS];
function buildRequestHeaders(
baseHeaders: DnrRequestHeader[],
forbiddenHeaders: Record<string, string>,
): DnrRequestHeader[] {
const requestHeaders: DnrRequestHeader[] = [...baseHeaders];
for (const [header, value] of Object.entries(forbiddenHeaders)) {
const name = normalizeHeaderName(header);
if (!name || name.toLowerCase() === "origin") continue;
@ -207,6 +204,18 @@ export async function ensureDnrOriginStripRuleForYoutubei(
value: String(value),
});
}
return requestHeaders;
}
export async function ensureDnrOriginStripRuleForYoutubei(
url: string,
forbiddenHeaders: Record<string, string> = {},
): Promise<boolean> {
if (!isYoutubeMobileUrl(url)) return false;
const requestHeaders = buildRequestHeaders(
YOUTUBEI_BASE_HEADERS,
forbiddenHeaders,
);
return ensureDnrHeaderStripRule(
url,
@ -222,18 +231,10 @@ export async function ensureDnrStripRuleForGooglevideo(
forbiddenHeaders: Record<string, string> = {},
): Promise<boolean> {
if (!isGooglevideoUrl(url)) return false;
const requestHeaders: DnrRequestHeader[] = [...GOOGLEVIDEO_BASE_HEADERS];
for (const [header, value] of Object.entries(forbiddenHeaders)) {
const name = normalizeHeaderName(header);
if (!name || name.toLowerCase() === "origin") continue;
requestHeaders.push({
header: name,
operation: "set",
value: String(value),
});
}
const requestHeaders = buildRequestHeaders(
GOOGLEVIDEO_BASE_HEADERS,
forbiddenHeaders,
);
return ensureDnrHeaderStripRule(
url,

View file

@ -1,6 +1,5 @@
import debug from "../../utils/debug";
import {
type AnyObject,
BG_MSG_NOTIFICATION,
type BridgeWireMessage,
isOurMessage,
@ -10,11 +9,7 @@ import {
TYPE_XHR_ABORT,
TYPE_XHR_START,
} from "../shared/constants";
import {
getSameWindowPostMessageTargetOrigin,
isSameWindowBridgeEvent,
toPageMessage,
} from "../shared/transport";
import { isSameWindowBridgeEvent, postToPage } from "../shared/transport";
import { asErrorMessage } from "../shared/utils";
import { ext, runtimeSendMessage } from "../shared/webext";
import { handleBridgeRequest } from "./request-handler";
@ -44,16 +39,6 @@ function injectPageModule(fileName: string): void {
parent.appendChild(script);
}
function postToPage(payload: AnyObject) {
const { message, transfer } = toPageMessage(payload);
const targetOrigin = getSameWindowPostMessageTargetOrigin();
if (transfer.length) {
globalThis.postMessage(message, targetOrigin, transfer);
return;
}
globalThis.postMessage(message, targetOrigin);
}
function sendResponse(
id: string,
ok: boolean,

View file

@ -8,10 +8,7 @@ import {
} from "../shared/bodySerialization";
import type { AnyObject } from "../shared/constants";
import { PORT_NAME, TYPE_XHR_ACK, TYPE_XHR_EVENT } from "../shared/constants";
import {
getSameWindowPostMessageTargetOrigin,
toPageMessage,
} from "../shared/transport";
import { postToPage } from "../shared/transport";
import { ext, runtimeMessagesUseStructuredClone } from "../shared/webext";
import {
isYandexApiHostname,
@ -178,16 +175,6 @@ function mergeHeadersIfMissing(
}
}
function postToPage(payload: AnyObject) {
const { message, transfer } = toPageMessage(payload);
const targetOrigin = getSameWindowPostMessageTargetOrigin();
if (transfer.length) {
globalThis.postMessage(message, targetOrigin, transfer);
return;
}
globalThis.postMessage(message, targetOrigin);
}
function settleXhrPort(requestId: string, state: XhrPortState): void {
state.settled = true;
try {

View file

@ -60,3 +60,13 @@ export function toPageMessage(payload: AnyObject): {
transfer: getTransferables(payload),
};
}
export function postToPage(payload: AnyObject): void {
const { message, transfer } = toPageMessage(payload);
const targetOrigin = getSameWindowPostMessageTargetOrigin();
if (transfer.length) {
globalThis.postMessage(message, targetOrigin, transfer);
return;
}
globalThis.postMessage(message, targetOrigin);
}

View file

@ -36,7 +36,7 @@ const availableLocales: readonly LangOverride[] = (() => {
: (["auto", ...locales] as LangOverride[]);
})();
export function resolveRuntimeLocaleVersion(
function resolveRuntimeLocaleVersion(
buildVersion: string,
scriptVersion: string,
) {

View file

@ -1,9 +1,11 @@
import type VOTClient from "@vot.js/ext";
import { createStore } from "solid-js/store";
import type { Account } from "../types/storage";
import { hasValidAccountToken } from "../utils/account";
import type { Account, StorageData } from "../types/storage";
import { hasValidAccountToken, isAccountExpired } from "../utils/account";
import { votStorage } from "../utils/storage";
export type AccountStore = {
type AccountStore = {
isRefreshing: boolean;
isLoggedIn: boolean;
username?: string;
@ -12,6 +14,11 @@ export type AccountStore = {
token?: string;
};
type AccountStateOwner = {
data?: Partial<StorageData>;
votClient?: VOTClient;
};
function createInitialState(): AccountStore {
return {
isRefreshing: false,
@ -46,3 +53,30 @@ export async function updateAccountFromStorage() {
const data = await votStorage.get<Partial<Account>>("account", {});
updateAccount(data);
}
function clearAccountState(owner?: AccountStateOwner): void {
if (owner?.data) {
owner.data.account = {};
}
if (owner?.votClient) {
owner.votClient.provider.apiToken = undefined;
}
}
export async function deleteAccount(owner?: AccountStateOwner): Promise<void> {
clearAccountState(owner);
await votStorage.delete("account");
resetAccount();
}
export async function deleteExpiredAccount(
owner?: AccountStateOwner,
now = Date.now(),
): Promise<boolean> {
if (!isAccountExpired(owner?.data?.account, now)) {
return false;
}
await deleteAccount(owner);
return true;
}

View file

@ -1,6 +1,6 @@
import { createStore } from "solid-js/store";
export type LocaleStore = {
type LocaleStore = {
updatedAt: number;
hash: string;
};
@ -15,7 +15,3 @@ function createInitialState(): LocaleStore {
export const [locale, setLocale] = createStore<LocaleStore>(
createInitialState(),
);
export function resetLocale() {
setLocale(createInitialState());
}

View file

@ -20,7 +20,7 @@ import type { DetectService, TranslateService } from "../types/translateApis";
import { isSupportGMXhr } from "../utils/gm";
import { calculatedResLang } from "../utils/localization";
export type SettingsStore = {
type SettingsStore = {
// menu
defaultVolume: number;
responseLanguage: ResponseLang;
@ -116,7 +116,3 @@ function createInitialState(): SettingsStore {
export const [settings, setSettings] = createStore<SettingsStore>(
createInitialState(),
);
export function resetSettings() {
setSettings(createInitialState());
}

View file

@ -499,6 +499,15 @@ const buildStyledSpans = (segments: StyledTextSegment[]) => {
return { text, styledSpans };
};
const prepareDisplayText = (
text: string,
): { text: string; leadingTrim: number } => {
const normalized = text.replaceAll("\u00A0", " ");
const leadingTrim = normalized.length - normalized.trimStart().length;
const trimmedEnd = Math.max(leadingTrim, normalized.trimEnd().length);
return { text: normalized.slice(leadingTrim, trimmedEnd), leadingTrim };
};
const trimStyledDisplayResult = (
text: string,
styledSpans: SubtitleStyledSpan[],
@ -506,14 +515,7 @@ const trimStyledDisplayResult = (
text: string;
styledSpans: SubtitleStyledSpan[];
} => {
const normalizedText = text.replaceAll("\u00A0", " ");
const leadingTrim = normalizedText.length - normalizedText.trimStart().length;
const trailingTrim = normalizedText.length - normalizedText.trimEnd().length;
const trimmedEnd = Math.max(
leadingTrim,
normalizedText.length - trailingTrim,
);
const finalText = normalizedText.slice(leadingTrim, trimmedEnd);
const { text: finalText, leadingTrim } = prepareDisplayText(text);
const finalSpans = styledSpans
.map((span) => ({
start: Math.max(0, span.start - leadingTrim),
@ -533,14 +535,8 @@ const trimStyledDisplayResult = (
};
const trimPlainDisplayText = (text: string): string => {
const normalizedText = text.replaceAll("\u00A0", " ");
const leadingTrim = normalizedText.length - normalizedText.trimStart().length;
const trailingTrim = normalizedText.length - normalizedText.trimEnd().length;
const trimmedEnd = Math.max(
leadingTrim,
normalizedText.length - trailingTrim,
);
return normalizedText.slice(leadingTrim, trimmedEnd);
const { text: trimmed } = prepareDisplayText(text);
return trimmed;
};
const buildPlainDisplayModel = (

View file

@ -13,10 +13,6 @@ export class FullscreenLayerController {
this.container = container;
}
getWidgetParentElement(): HTMLElement | ShadowRoot {
return this.container;
}
getLayoutRootElement(): HTMLElement {
return this.container instanceof ShadowRoot
? (this.container.host as HTMLElement)

View file

@ -27,7 +27,7 @@ export type HighlightState = {
applied: Uint8Array;
};
export const NO_HIGHLIGHT_INDEX = -1;
const NO_HIGHLIGHT_INDEX = -1;
const PASSED_CLASS = "passed";
export function createHighlightState(): HighlightState {

View file

@ -11,7 +11,7 @@
// - Eye-tracking research (Szarkowska et al.) showing that breaking inside a
// phrase increases regressions and dwell time.
export type LineBreakLexicon = {
type LineBreakLexicon = {
// Words that must not end line 1 (they bind forward to the next word).
bindsForward: Set<string>;
// Words that read better at the start of line 2.
@ -392,7 +392,7 @@ const SCRIPTIO_CONTINUA = new Set([
"bo",
]);
export const getBaseLanguage = (locale?: string): string =>
const getBaseLanguage = (locale?: string): string =>
(locale ?? "").toLowerCase().split(/[-_]/u)[0] ?? "";
export const isScriptioContinua = (locale?: string): boolean =>
@ -400,20 +400,20 @@ export const isScriptioContinua = (locale?: string): boolean =>
const WORD_CHARS_RE = /[^\p{L}\p{N}'\u2019]+/gu;
export const normalizeLexiconWord = (value: string): string =>
const normalizeLexiconWord = (value: string): string =>
value.replaceAll(WORD_CHARS_RE, " ").trim().toLowerCase();
export const lastWordOf = (text: string): string => {
const lastWordOf = (text: string): string => {
const parts = normalizeLexiconWord(text).split(" ").filter(Boolean);
return parts.at(-1) ?? "";
};
export const firstWordOf = (text: string): string => {
const firstWordOf = (text: string): string => {
const parts = normalizeLexiconWord(text).split(" ").filter(Boolean);
return parts[0] ?? "";
};
export const LINE_BREAK_PENALTY = {
const LINE_BREAK_PENALTY = {
bindsForward: 240,
prefersLineStart: -60,
} as const;

View file

@ -35,7 +35,7 @@ export type CapturedVerticalAnchorState = {
travelPx: number;
};
export function getVerticalAnchorBounds({
function getVerticalAnchorBounds({
elementHeight,
boxHeight,
bottomInset,

View file

@ -26,15 +26,6 @@ import {
import { shouldInsertSpaceBetweenTextFragments } from "./textSpacing";
import { parseSubtitleDescriptor } from "./types";
export type {
ProcessedSubtitles,
SubtitleDescriptor,
SubtitleLine,
SubtitlesClient,
SubtitleToken,
VideoDataForSubtitles,
} from "../types/subtitles";
type ProcessedSubtitlesInput = Partial<ProcessedSubtitles>;
type YoutubeSubtitleSegment = {

View file

@ -1060,9 +1060,7 @@ export const sortProcessedSubtitles = (
),
});
export const toSubtitlesData = (
processed: ProcessedSubtitles,
): SubtitlesData => {
const toSubtitlesData = (processed: ProcessedSubtitles): SubtitlesData => {
const subtitles = processed.subtitles.map((line) => ({
text: line.text,
startMs: line.startMs,

View file

@ -1,27 +1,16 @@
export type {
BuiltInSubtitleFontFamily,
SubtitleDescriptor,
SubtitleFontFamily,
SubtitleFormat,
SubtitleToken,
VideoDataForSubtitles,
} from "../types/subtitles";
export {
import type { SubtitleDescriptor } from "../types/subtitles";
import {
type BuiltInSubtitleFontFamily,
type SubtitleFormat,
subtitleFontFamilies,
subtitleFormats,
} from "../types/subtitles";
import type { SubtitleDescriptor } from "../types/subtitles";
import { subtitleFontFamilies, subtitleFormats } from "../types/subtitles";
const subtitleFormatsSet = new Set(subtitleFormats);
export function isSubtitleFormat(
value: unknown,
): value is import("../types/subtitles").SubtitleFormat {
function isSubtitleFormat(value: unknown): value is SubtitleFormat {
return (
typeof value === "string" &&
subtitleFormatsSet.has(value as import("../types/subtitles").SubtitleFormat)
typeof value === "string" && subtitleFormatsSet.has(value as SubtitleFormat)
);
}
@ -64,8 +53,6 @@ export function parseSubtitleDescriptor(
export function isBuiltInSubtitleFontFamily(
fontFamily: string,
): fontFamily is import("../types/subtitles").BuiltInSubtitleFontFamily {
return subtitleFontFamilies.includes(
fontFamily as import("../types/subtitles").BuiltInSubtitleFontFamily,
);
): fontFamily is BuiltInSubtitleFontFamily {
return subtitleFontFamilies.includes(fontFamily as BuiltInSubtitleFontFamily);
}

View file

@ -66,7 +66,7 @@ export function findNextCueBoundaryMs(
}
/** Earliest word-highlight threshold strictly after `timeMs`. */
export function findNextThresholdMs(
function findNextThresholdMs(
timeMs: number,
thresholds: readonly number[],
): number | null {

View file

@ -7,7 +7,7 @@ export const positions = [
] as const;
export type Position = (typeof positions)[number];
export const directions = ["default", "row", "column"] as const;
const directions = ["default", "row", "column"] as const;
export type Direction = (typeof directions)[number];
export type Status = "none" | "error" | "success";

View file

@ -41,7 +41,7 @@ export const subtitleFontFamilyCss = {
barlow: `"Barlow", ${subtitleBaseFontFamilyCSS}`,
} as const satisfies Record<BuiltInSubtitleFontFamily, string>;
export const subtitlePositionPresets = [
const subtitlePositionPresets = [
"bottom-center",
"center",
"top-center",

View file

@ -1,13 +1,7 @@
import type { IntervalIdleChecker } from "../utils/intervalIdleChecker";
import type { VideoHandler } from "../VideoHandler";
import type { Direction, Position } from "./components/votButton";
import type { StorageData } from "./storage";
export type ButtonLayout = {
direction: Direction;
position: Position;
};
export type OverlayMount = {
root: HTMLElement | ShadowRoot;
portalContainer: HTMLElement;

View file

@ -4,6 +4,7 @@ import { serializeProcessedSubtitles } from "../subtitles/standards";
import type { Status } from "../types/components/votButton";
import type { StorageData } from "../types/storage";
import type { OverlayMount, UIManagerProps } from "../types/uiManager";
import type { VideoData } from "../types/videoHandler";
import debug from "../utils/debug";
import { downloadTranslation } from "../utils/download";
import { GM_fetch } from "../utils/gm";
@ -15,7 +16,6 @@ import {
downloadBlob,
} from "../utils/utils";
import type { VideoHandler } from "../VideoHandler";
import type { VideoData } from "../videoHandler/shared";
import { safeSetPlayerVolume } from "../videoHandler/translationVolume";
import { applyOverlayMountUpdate } from "./mount";
import { OverlayController } from "./overlayController";

View file

@ -54,7 +54,6 @@ import {
import { account, resetAccount, updateAccount } from "../stores/account";
import { setLocale } from "../stores/locale";
import { setSettings } from "../stores/settings";
import type { SubtitleFormat } from "../subtitles/types";
import type { Position } from "../types/components/votButton";
import type {
Account,
@ -62,7 +61,7 @@ import type {
StorageData,
TranslateProxyStatus,
} from "../types/storage";
import type { SubtitleFontFamily } from "../types/subtitles";
import type { SubtitleFontFamily, SubtitleFormat } from "../types/subtitles";
import type { TranslateService } from "../types/translateApis";
import debug from "../utils/debug";
import { EventImpl } from "../utils/eventImpl";

View file

@ -1,7 +1,7 @@
import { openAuthWindow } from "../core/auth/window";
import { t } from "../localization/localizationProvider";
import { deleteExpiredAccount } from "../stores/account";
import type { Status } from "../types/components/votButton";
import { deleteExpiredAccount } from "../utils/account";
import debug from "../utils/debug";
import { isAbortError } from "../utils/errors";
import type { VideoHandler } from "../VideoHandler";

View file

@ -1,11 +1,4 @@
import type VOTClient from "@vot.js/ext";
import type { StorageData } from "../types/storage";
import { votStorage } from "./storage";
type AccountStateOwner = {
data?: Partial<StorageData>;
votClient?: VOTClient;
};
export function hasAccountToken(account?: Partial<StorageData["account"]>) {
return typeof account?.token === "string" && account.token.length > 0;
@ -29,29 +22,3 @@ export function hasValidAccountToken(
) {
return hasAccountToken(account) && !isAccountExpired(account, now);
}
export function clearAccountState(owner?: AccountStateOwner): void {
if (owner?.data) {
owner.data.account = {};
}
if (owner?.votClient) {
owner.votClient.provider.apiToken = undefined;
}
}
export async function deleteAccount(owner?: AccountStateOwner): Promise<void> {
clearAccountState(owner);
await votStorage.delete("account");
}
export async function deleteExpiredAccount(
owner?: AccountStateOwner,
now = Date.now(),
): Promise<boolean> {
if (!isAccountExpired(owner?.data?.account, now)) {
return false;
}
await deleteAccount(owner);
return true;
}

View file

@ -37,42 +37,6 @@ export function containsCrossShadow(container: Node, target: Node): boolean {
return false;
}
export function isDocumentViewportElement(
element: Element | null | undefined,
): boolean {
return element === document.body || element === document.documentElement;
}
export function resolveScopedFullscreenElement(
fullscreenEl: Element | null | undefined,
anchors: Array<Node | null | undefined>,
options: {
allowDocumentViewport?: boolean;
} = {},
): HTMLElement | null {
const { allowDocumentViewport = false } = options;
if (!(fullscreenEl instanceof HTMLElement)) {
return null;
}
if (isDocumentViewportElement(fullscreenEl) && !allowDocumentViewport) {
return null;
}
for (const anchor of anchors) {
if (
anchor &&
(containsCrossShadow(fullscreenEl, anchor) ||
containsCrossShadow(anchor, fullscreenEl))
) {
return fullscreenEl;
}
}
return null;
}
type CrossShadowTarget = Element | Document;
export function closestCrossShadow(

View file

@ -3,7 +3,7 @@ export type ComposedTreeNode<T> = {
getRootNode?: () => Node | null | undefined;
};
export function getComposedParentElement<T extends object>(
function getComposedParentElement<T extends object>(
node: ComposedTreeNode<T> | null | undefined,
): T | null {
if (!node) return null;

View file

@ -1,16 +1,15 @@
import { isDocumentHidden } from "./environment";
export type IntervalIdleMode = "active" | "idle" | "hidden";
type IntervalIdleMode = "active" | "idle" | "hidden";
type IntervalIdleTickSource = "start" | "interval" | "immediate";
export type IntervalIdleTickSource = "start" | "interval" | "immediate";
export type IntervalIdleTickContext = {
type IntervalIdleTickContext = {
nowMs: number;
mode: IntervalIdleMode;
source: IntervalIdleTickSource;
};
export type IntervalIdleProfile = {
type IntervalIdleProfile = {
/**
* Polling interval used by the checker loop.
* Mirrors the fixed `IDLE_CHECK_INTERVAL_MS` approach from `app.js`.
@ -35,7 +34,7 @@ export type IntervalIdleProfile = {
type IntervalIdleSubscriber = (ctx: IntervalIdleTickContext) => void;
export type IntervalIdleSubscribeOptions = {
type IntervalIdleSubscribeOptions = {
/**
* Reports whether this subscriber currently has work that a periodic tick
* could perform. When every subscriber reports `false`, the checker stops its

View file

@ -259,16 +259,25 @@ class VOTStorage {
return globalThis.localStorage.setItem(name, JSON.stringify(value));
}
private async getChangeContext(name: string): Promise<{
support: StorageSupport;
storageKey: StorageKey;
shouldNotify: boolean;
oldValue: unknown;
}> {
const support = this.resolveSupport();
const shouldNotify = this.shouldUseSyntheticListeners(support);
const oldValue = shouldNotify ? await this.getRaw(name) : undefined;
return { support, storageKey: name as StorageKey, shouldNotify, oldValue };
}
async setRaw<T extends KeysOrDefaultValue = undefined>(
name: string,
value: T,
): Promise<void> {
const support = this.resolveSupport();
const storageKey = name as StorageKey;
const shouldNotify = this.shouldUseSyntheticListeners(support);
const oldValue = shouldNotify
? await this.getRaw<T | undefined>(name)
: undefined;
const { support, storageKey, shouldNotify, oldValue } =
await this.getChangeContext(name);
if (support.promiseSet && GM.setValue) {
await GM.setValue(name, value);
@ -298,10 +307,8 @@ class VOTStorage {
}
async deleteRaw(name: string): Promise<void> {
const support = this.resolveSupport();
const storageKey = name as StorageKey;
const shouldNotify = this.shouldUseSyntheticListeners(support);
const oldValue = shouldNotify ? await this.getRaw(name) : undefined;
const { support, storageKey, shouldNotify, oldValue } =
await this.getChangeContext(name);
if (support.promiseDelete && GM.deleteValue) {
await GM.deleteValue(name);
@ -343,15 +350,14 @@ class VOTStorage {
| ((id: unknown) => unknown)
| undefined)
: undefined;
if (typeof addListener === "function") {
const gmListener = this.createTypedListener(listener);
const listenerId = addListener(name, gmListener);
return () => {
if (typeof removeListener === "function") {
removeListener(listenerId);
}
};
const unsubscribe = this.registerGMListener(
addListener,
removeListener,
name,
listener,
);
if (unsubscribe) {
return unsubscribe;
}
}
@ -371,15 +377,14 @@ class VOTStorage {
}
).GM_removeValueChangeListener
: undefined;
if (typeof addListener === "function") {
const gmListener = this.createTypedListener(listener);
const listenerId = addListener(name, gmListener);
return () => {
if (typeof removeListener === "function") {
removeListener(listenerId);
}
};
const unsubscribe = this.registerGMListener(
addListener,
removeListener,
name,
listener,
);
if (unsubscribe) {
return unsubscribe;
}
}
@ -423,6 +428,29 @@ class VOTStorage {
};
}
private registerGMListener(
addListener:
| ((
key: string,
callback: StorageValueChangeListener<unknown>,
) => unknown)
| undefined,
removeListener: ((id: unknown) => unknown) | undefined,
name: StorageKey,
listener: StorageValueChangeListener<unknown>,
): (() => void) | undefined {
if (typeof addListener !== "function") {
return undefined;
}
const listenerId = addListener(name, this.createTypedListener(listener));
return () => {
if (typeof removeListener === "function") {
removeListener(listenerId);
}
};
}
private getLocalStorageListeners(name: StorageKey) {
const existing = this.localStorageListeners.get(name);
if (existing) {

View file

@ -103,11 +103,6 @@ export function fnv1a32ToKeyPart(str: string): string {
return (hash >>> 0).toString(36);
}
export interface DocumentWithFullscreen extends Document {
webkitFullscreenElement?: Element | null;
webkitExitFullscreen?: () => Promise<void>;
}
export const isPiPAvailable = () => Boolean(document.pictureInPictureEnabled);
async function writeBlobToHandle(

View file

@ -427,7 +427,7 @@ function bindGlobalDismissAndHotkeys(ctx: ExtraEventsContext): void {
self.container.draggable = false;
}
}
export function bindPlaybackRefreshOnResume(ctx: ExtraEventsContext): void {
function bindPlaybackRefreshOnResume(ctx: ExtraEventsContext): void {
const { self, add } = ctx;
let wasPausedSinceLastPlay = false;

View file

@ -21,7 +21,7 @@ export function isProxyClientEnabled(config: ProxyConfig): boolean {
return Boolean(config.translateProxyEnabled);
}
export function isProxyRoutingEnabled(config: ProxyConfig): boolean {
function isProxyRoutingEnabled(config: ProxyConfig): boolean {
return config.translateProxyEnabled === 2;
}

View file

@ -7,7 +7,7 @@ import {
import type {
SubtitleDescriptor,
VideoDataForSubtitles,
} from "../../subtitles/types";
} from "../../types/subtitles";
import debug from "../../utils/debug";
import type { VideoHandler } from "../../VideoHandler";
import { proxifyYandexSubtitlesUrl } from "./proxyShared";

View file

@ -2,15 +2,13 @@
localizationProvider,
t,
} from "../../localization/localizationProvider";
import {
parseSubtitleDescriptor,
type SubtitleDescriptor,
} from "../../subtitles/types";
import { parseSubtitleDescriptor } from "../../subtitles/types";
import {
AUTO_SUBTITLE_LANGUAGE_VALUE,
ORIGINAL_SUBTITLE_LANGUAGE_VALUE,
type ResponseLanguageSubtitles,
} from "../../types/storage";
import type { SubtitleDescriptor } from "../../types/subtitles";
export type IndexedSubtitleDescriptor = {
descriptor: SubtitleDescriptor;

View file

@ -3,11 +3,11 @@ import type { RequestLang, ResponseLang } from "@vot.js/shared/types/data";
import { isTranslationDownloadHost } from "../../core/hostPolicies";
import { notifyTranslationFailureIfNeeded } from "../../core/translationErrors";
import { t } from "../../localization/localizationProvider";
import type { VideoData } from "../../types/videoHandler";
import debug from "../../utils/debug";
import { toErrorMessage } from "../../utils/errors";
import type { VideoHandler } from "../../VideoHandler";
import VOTLocalizedError from "../../VOTLocalizedError";
import type { VideoData } from "../shared";
import { applyTranslationPlaybackVolume } from "../translationVolume";
import {
isYandexAudioUrlOrProxy,

View file

@ -1,7 +1,7 @@
import type { RequestLang, ResponseLang } from "@vot.js/shared/types/data";
import type { CacheTranslationSuccess } from "../../types/core/cacheManager";
import type { VideoData } from "../shared";
import type { VideoData } from "../../types/videoHandler";
import type { ActionContext } from "./translationTypes";
export type TranslationAudioResult = {
@ -68,7 +68,7 @@ export async function requestTranslationAudio(
};
}
export function buildTranslationCacheValue(options: {
function buildTranslationCacheValue(options: {
videoId: string;
requestLang: string;
responseLang: string;

View file

@ -1,8 +1,3 @@
export type {
VideoData,
VideoData as RuntimeVideoData,
} from "../types/videoHandler";
/**
* Country code used for proxy settings. Populated lazily during init.
*/

View file

@ -6,7 +6,7 @@ export type VolumeLinkState = {
lastTranslationPercent: number;
};
export type VolumeLinkDirection = "translation" | "video";
type VolumeLinkDirection = "translation" | "video";
type ApplyVolumeLinkDeltaInput = {
state: VolumeLinkState;

View file

@ -8,7 +8,7 @@ import {
getReleaseDownloadBase,
} from "../paths";
export async function writeChromeUpdatesManifest({
async function writeChromeUpdatesManifest({
version,
extensionId,
}: {

View file

@ -45,7 +45,7 @@ function normalizeHostPermission(
return `*://*.${value}/*`;
}
export function normalizeHostPermissions(list: string[] = []): string[] {
function normalizeHostPermissions(list: string[] = []): string[] {
const normalized = list
.map((item) => normalizeHostPermission(item))
.filter((value): value is string => Boolean(value));
@ -87,7 +87,7 @@ function normalizeWebAccessibleResourceMatches(
];
}
export function createWebAccessibleResources(
function createWebAccessibleResources(
matches: string[],
): Array<Record<string, unknown>> {
const normalizedMatches = normalizeWebAccessibleResourceMatches(matches);
@ -107,7 +107,7 @@ export function createWebAccessibleResources(
// Match splitting (origin-fallback vs direct)
// ----------------------------------------------------------------
export function splitMatchesForOriginFallback(matches: string[] = []): {
function splitMatchesForOriginFallback(matches: string[] = []): {
originFallbackMatches: string[];
directMatches: string[];
} {
@ -129,19 +129,6 @@ export function splitMatchesForOriginFallback(matches: string[] = []): {
return { originFallbackMatches, directMatches };
}
// ----------------------------------------------------------------
// Icons
// ----------------------------------------------------------------
export function buildIconsMap(
sizes: readonly number[] = EXTENSION_ICON_SIZES,
pathPrefix: string = "icons",
): Record<number, string> {
return Object.fromEntries(
sizes.map((size) => [size, `${pathPrefix}/icon-${size}.png`]),
) as Record<number, string>;
}
// ----------------------------------------------------------------
// Content scripts
// ----------------------------------------------------------------
@ -152,7 +139,7 @@ export interface ManifestPathStrategy {
background?: Record<string, unknown>;
}
export function createContentScriptEntries({
function createContentScriptEntries({
matches,
excludeMatches,
includeWorld,

View file

@ -10,7 +10,7 @@ export const OUT_TEMP_DIR = path.resolve(DIST_EXT_DIR, "_tmp");
export const EXTENSION_NAME = "vot-extension";
export const FIREFOX_EXTENSION_NAME = `${EXTENSION_NAME}-firefox`;
export const CHROME_EXTENSION_NAME = `${EXTENSION_NAME}-chrome`;
const CHROME_EXTENSION_NAME = `${EXTENSION_NAME}-chrome`;
export const FIREFOX_UPDATES_MANIFEST_FILE = `${FIREFOX_EXTENSION_NAME}-updates.json`;
export const FIREFOX_UPDATES_MANIFEST_PATH = path.join(
@ -35,7 +35,7 @@ export function viteCacheDir(name: string): string {
return path.resolve(ROOT_DIR, "node_modules", ".vite", name);
}
export const nodeCryptoAlias = {
const nodeCryptoAlias = {
"node:crypto": path.resolve(SOURCE_DIR, "shims", "nodeCrypto.ts"),
} as const;