mirror of
https://github.com/ilyhalight/voice-over-translation.git
synced 2026-04-28 03:20:22 +00:00
Release v1.11.5: UI rewrite and bugfixes
Bump release to 1.11.5 with several fixes and refactors: rewrite of UI mounting, simplified audio loading, and multiple site compatibility fixes (VKVideo subtitles, mobile audio downloads, archive.org, Coursehunter, Yandex.Disk /d, Reddit, Odysee, YouTube Embed, proxy auto-enable logic). Update changelog, bump userscript/extension versions and publish new Chrome/Firefox build artifacts; update firefox updates manifest. Add shadow DOM mount support and other UI/module improvements across source files, plus general dependency and build updates.
This commit is contained in:
parent
1adeac38e6
commit
34e54405c8
36 changed files with 10520 additions and 696 deletions
82
bun.lock
82
bun.lock
|
|
@ -10,6 +10,7 @@
|
|||
"@vot.js/shared": "^2.4.12",
|
||||
"bowser": "^2.14.1",
|
||||
"chaimu": "^1.0.6",
|
||||
"lit": "^3.3.2",
|
||||
"lit-html": "^3.3.2",
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
@ -17,12 +18,11 @@
|
|||
"@types/bun": "^1.3.11",
|
||||
"@vot.js/core": "^2.4.12",
|
||||
"crx3": "^2.0.0",
|
||||
"lefthook": "^2.1.4",
|
||||
"lightningcss": "^1.32.0",
|
||||
"npm-run-all2": "^8.0.4",
|
||||
"sass": "^1.98.0",
|
||||
"typescript": "^5.9.3",
|
||||
"vite": "^8.0.1",
|
||||
"sass": "^1.99.0",
|
||||
"typescript": "^6.0.2",
|
||||
"vite": "^8.0.5",
|
||||
"zip-a-folder": "^6.1.0",
|
||||
},
|
||||
},
|
||||
|
|
@ -36,45 +36,49 @@
|
|||
|
||||
"@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg=="],
|
||||
|
||||
"@lit-labs/ssr-dom-shim": ["@lit-labs/ssr-dom-shim@1.5.1", "", {}, "sha512-Aou5UdlSpr5whQe8AA/bZG0jMj96CoJIWbGfZ91qieWu5AWUMKw8VR/pAkQkJYvBNhmCcWnZlyyk5oze8JIqYA=="],
|
||||
|
||||
"@lit/reactive-element": ["@lit/reactive-element@2.1.2", "", { "dependencies": { "@lit-labs/ssr-dom-shim": "^1.5.0" } }, "sha512-pbCDiVMnne1lYUIaYNN5wrwQXDtHaYtg7YEFPeW+hws6U47WeFvISGUWekPGKWOP1ygrs0ef0o1VJMk1exos5A=="],
|
||||
|
||||
"@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.1", "", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@tybys/wasm-util": "^0.10.1" } }, "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A=="],
|
||||
|
||||
"@oxc-project/types": ["@oxc-project/types@0.120.0", "", {}, "sha512-k1YNu55DuvAip/MGE1FTsIuU3FUCn6v/ujG9V7Nq5Df/kX2CWb13hhwD0lmJGMGqE+bE1MXvv9SZVnMzEXlWcg=="],
|
||||
"@oxc-project/types": ["@oxc-project/types@0.122.0", "", {}, "sha512-oLAl5kBpV4w69UtFZ9xqcmTi+GENWOcPF7FCrczTiBbmC0ibXxCwyvZGbO39rCVEuLGAZM84DH0pUIyyv/YJzA=="],
|
||||
|
||||
"@parcel/watcher": ["@parcel/watcher@2.5.0", "", { "dependencies": { "detect-libc": "^1.0.3", "is-glob": "^4.0.3", "micromatch": "^4.0.5", "node-addon-api": "^7.0.0" }, "optionalDependencies": { "@parcel/watcher-win32-x64": "2.5.0" } }, ""],
|
||||
|
||||
"@parcel/watcher-win32-x64": ["@parcel/watcher-win32-x64@2.5.0", "", { "os": "win32", "cpu": "x64" }, ""],
|
||||
|
||||
"@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.0-rc.10", "", { "os": "android", "cpu": "arm64" }, "sha512-jOHxwXhxmFKuXztiu1ORieJeTbx5vrTkcOkkkn2d35726+iwhrY1w/+nYY/AGgF12thg33qC3R1LMBF5tHTZHg=="],
|
||||
"@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.0-rc.12", "", { "os": "android", "cpu": "arm64" }, "sha512-pv1y2Fv0JybcykuiiD3qBOBdz6RteYojRFY1d+b95WVuzx211CRh+ytI/+9iVyWQ6koTh5dawe4S/yRfOFjgaA=="],
|
||||
|
||||
"@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.0-rc.10", "", { "os": "darwin", "cpu": "arm64" }, "sha512-gED05Teg/vtTZbIJBc4VNMAxAFDUPkuO/rAIyyxZjTj1a1/s6z5TII/5yMGZ0uLRCifEtwUQn8OlYzuYc0m70w=="],
|
||||
"@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.0-rc.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-cFYr6zTG/3PXXF3pUO+umXxt1wkRK/0AYT8lDwuqvRC+LuKYWSAQAQZjCWDQpAH172ZV6ieYrNnFzVVcnSflAg=="],
|
||||
|
||||
"@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.0.0-rc.10", "", { "os": "darwin", "cpu": "x64" }, "sha512-rI15NcM1mA48lqrIxVkHfAqcyFLcQwyXWThy+BQ5+mkKKPvSO26ir+ZDp36AgYoYVkqvMcdS8zOE6SeBsR9e8A=="],
|
||||
"@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.0.0-rc.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-ZCsYknnHzeXYps0lGBz8JrF37GpE9bFVefrlmDrAQhOEi4IOIlcoU1+FwHEtyXGx2VkYAvhu7dyBf75EJQffBw=="],
|
||||
|
||||
"@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.0.0-rc.10", "", { "os": "freebsd", "cpu": "x64" }, "sha512-XZRXHdTa+4ME1MuDVp021+doQ+z6Ei4CCFmNc5/sKbqb8YmkiJdj8QKlV3rCI0AJtAeSB5n0WGPuJWNL9p/L2w=="],
|
||||
"@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.0.0-rc.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-dMLeprcVsyJsKolRXyoTH3NL6qtsT0Y2xeuEA8WQJquWFXkEC4bcu1rLZZSnZRMtAqwtrF/Ib9Ddtpa/Gkge9Q=="],
|
||||
|
||||
"@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.10", "", { "os": "linux", "cpu": "arm" }, "sha512-R0SQMRluISSLzFE20sPWYHVmJdDQnRyc/FzSCN72BqQmh2SOZUFG+N3/vBZpR4C6WpEUVYJLrYUXaj43sJsNLA=="],
|
||||
"@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.12", "", { "os": "linux", "cpu": "arm" }, "sha512-YqWjAgGC/9M1lz3GR1r1rP79nMgo3mQiiA+Hfo+pvKFK1fAJ1bCi0ZQVh8noOqNacuY1qIcfyVfP6HoyBRZ85Q=="],
|
||||
|
||||
"@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.0.0-rc.10", "", { "os": "linux", "cpu": "arm64" }, "sha512-Y1reMrV/o+cwpduYhJuOE3OMKx32RMYCidf14y+HssARRmhDuWXJ4yVguDg2R/8SyyGNo+auzz64LnPK9Hq6jg=="],
|
||||
"@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.0.0-rc.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-/I5AS4cIroLpslsmzXfwbe5OmWvSsrFuEw3mwvbQ1kDxJ822hFHIx+vsN/TAzNVyepI/j/GSzrtCIwQPeKCLIg=="],
|
||||
|
||||
"@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.0.0-rc.10", "", { "os": "linux", "cpu": "arm64" }, "sha512-vELN+HNb2IzuzSBUOD4NHmP9yrGwl1DVM29wlQvx1OLSclL0NgVWnVDKl/8tEks79EFek/kebQKnNJkIAA4W2g=="],
|
||||
"@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.0.0-rc.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-V6/wZztnBqlx5hJQqNWwFdxIKN0m38p8Jas+VoSfgH54HSj9tKTt1dZvG6JRHcjh6D7TvrJPWFGaY9UBVOaWPw=="],
|
||||
|
||||
"@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.10", "", { "os": "linux", "cpu": "ppc64" }, "sha512-ZqrufYTgzxbHwpqOjzSsb0UV/aV2TFIY5rP8HdsiPTv/CuAgCRjM6s9cYFwQ4CNH+hf9Y4erHW1GjZuZ7WoI7w=="],
|
||||
"@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-AP3E9BpcUYliZCxa3w5Kwj9OtEVDYK6sVoUzy4vTOJsjPOgdaJZKFmN4oOlX0Wp0RPV2ETfmIra9x1xuayFB7g=="],
|
||||
|
||||
"@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.0.0-rc.10", "", { "os": "linux", "cpu": "s390x" }, "sha512-gSlmVS1FZJSRicA6IyjoRoKAFK7IIHBs7xJuHRSmjImqk3mPPWbR7RhbnfH2G6bcmMEllCt2vQ/7u9e6bBnByg=="],
|
||||
"@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.0.0-rc.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-nWwpvUSPkoFmZo0kQazZYOrT7J5DGOJ/+QHHzjvNlooDZED8oH82Yg67HvehPPLAg5fUff7TfWFHQS8IV1n3og=="],
|
||||
|
||||
"@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.0.0-rc.10", "", { "os": "linux", "cpu": "x64" }, "sha512-eOCKUpluKgfObT2pHjztnaWEIbUabWzk3qPZ5PuacuPmr4+JtQG4k2vGTY0H15edaTnicgU428XW/IH6AimcQw=="],
|
||||
"@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.0.0-rc.12", "", { "os": "linux", "cpu": "x64" }, "sha512-RNrafz5bcwRy+O9e6P8Z/OCAJW/A+qtBczIqVYwTs14pf4iV1/+eKEjdOUta93q2TsT/FI0XYDP3TCky38LMAg=="],
|
||||
|
||||
"@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.0.0-rc.10", "", { "os": "linux", "cpu": "x64" }, "sha512-Xdf2jQbfQowJnLcgYfD/m0Uu0Qj5OdxKallD78/IPPfzaiaI4KRAwZzHcKQ4ig1gtg1SuzC7jovNiM2TzQsBXA=="],
|
||||
"@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.0.0-rc.12", "", { "os": "linux", "cpu": "x64" }, "sha512-Jpw/0iwoKWx3LJ2rc1yjFrj+T7iHZn2JDg1Yny1ma0luviFS4mhAIcd1LFNxK3EYu3DHWCps0ydXQ5i/rrJ2ig=="],
|
||||
|
||||
"@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.0.0-rc.10", "", { "os": "none", "cpu": "arm64" }, "sha512-o1hYe8hLi1EY6jgPFyxQgQ1wcycX+qz8eEbVmot2hFkgUzPxy9+kF0u0NIQBeDq+Mko47AkaFFaChcvZa9UX9Q=="],
|
||||
"@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.0.0-rc.12", "", { "os": "none", "cpu": "arm64" }, "sha512-vRugONE4yMfVn0+7lUKdKvN4D5YusEiPilaoO2sgUWpCvrncvWgPMzK00ZFFJuiPgLwgFNP5eSiUlv2tfc+lpA=="],
|
||||
|
||||
"@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.0.0-rc.10", "", { "dependencies": { "@napi-rs/wasm-runtime": "^1.1.1" }, "cpu": "none" }, "sha512-Ugv9o7qYJudqQO5Y5y2N2SOo6S4WiqiNOpuQyoPInnhVzCY+wi/GHltcLHypG9DEUYMB0iTB/huJrpadiAcNcA=="],
|
||||
"@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.0.0-rc.12", "", { "dependencies": { "@napi-rs/wasm-runtime": "^1.1.1" }, "cpu": "none" }, "sha512-ykGiLr/6kkiHc0XnBfmFJuCjr5ZYKKofkx+chJWDjitX+KsJuAmrzWhwyOMSHzPhzOHOy7u9HlFoa5MoAOJ/Zg=="],
|
||||
|
||||
"@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.0.0-rc.10", "", { "os": "win32", "cpu": "arm64" }, "sha512-7UODQb4fQUNT/vmgDZBl3XOBAIOutP5R3O/rkxg0aLfEGQ4opbCgU5vOw/scPe4xOqBwL9fw7/RP1vAMZ6QlAQ=="],
|
||||
"@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.0.0-rc.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-5eOND4duWkwx1AzCxadcOrNeighiLwMInEADT0YM7xeEOOFcovWZCq8dadXgcRHSf3Ulh1kFo/qvzoFiCLOL1Q=="],
|
||||
|
||||
"@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.0.0-rc.10", "", { "os": "win32", "cpu": "x64" }, "sha512-PYxKHMVHOb5NJuDL53vBUl1VwUjymDcYI6rzpIni0C9+9mTiJedvUxSk7/RPp7OOAm3v+EjgMu9bIy3N6b408w=="],
|
||||
"@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.0.0-rc.12", "", { "os": "win32", "cpu": "x64" }, "sha512-PyqoipaswDLAZtot351MLhrlrh6lcZPo2LSYE+VDxbVk24LVKAGOuE4hb8xZQmrPAuEtTZW8E6D2zc5EUZX4Lw=="],
|
||||
|
||||
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.10", "", {}, "sha512-UkVDEFk1w3mveXeKgaTuYfKWtPbvgck1dT8TUG3bnccrH0XtLTuAyfCoks4Q/M5ZGToSVJTIQYCzy2g/atAOeg=="],
|
||||
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.12", "", {}, "sha512-HHMwmarRKvoFsJorqYlFeFRzXZqCt2ETQlEDOb9aqssrnVBB1/+xgTGtuTrIk5vzLNX1MjMtTf7W9z3tsSbrxw=="],
|
||||
|
||||
"@toil/gm-types": ["@toil/gm-types@1.0.4", "", { "peerDependencies": { "typescript": "^5.8.2" } }, "sha512-DbqZsYYrVXaBB4usDN3/HbkIhk2M+ECKv06lEScSoCMJUZ1qbtpbYLIqioEk2GE9adwWjNpH+y/Bf0ij3wQxhw=="],
|
||||
|
||||
|
|
@ -132,28 +136,6 @@
|
|||
|
||||
"json-parse-even-better-errors": ["json-parse-even-better-errors@4.0.0", "", {}, "sha512-lR4MXjGNgkJc7tkQ97kb2nuEMnNCyU//XYVH0MKTGcXEiSudQ5MKGKen3C5QubYy0vmq+JGitUg92uuywGEwIA=="],
|
||||
|
||||
"lefthook": ["lefthook@2.1.4", "", { "optionalDependencies": { "lefthook-darwin-arm64": "2.1.4", "lefthook-darwin-x64": "2.1.4", "lefthook-freebsd-arm64": "2.1.4", "lefthook-freebsd-x64": "2.1.4", "lefthook-linux-arm64": "2.1.4", "lefthook-linux-x64": "2.1.4", "lefthook-openbsd-arm64": "2.1.4", "lefthook-openbsd-x64": "2.1.4", "lefthook-windows-arm64": "2.1.4", "lefthook-windows-x64": "2.1.4" }, "bin": { "lefthook": "bin/index.js" } }, "sha512-JNfJ5gAn0KADvJ1I6/xMcx70+/6TL6U9gqGkKvPw5RNMfatC7jIg0Evl97HN846xmfz959BV70l8r3QsBJk30w=="],
|
||||
|
||||
"lefthook-darwin-arm64": ["lefthook-darwin-arm64@2.1.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-BUAAE9+rUrjr39a+wH/1zHmGrDdwUQ2Yq/z6BQbM/yUb9qtXBRcQ5eOXxApqWW177VhGBpX31aqIlfAZ5Q7wzw=="],
|
||||
|
||||
"lefthook-darwin-x64": ["lefthook-darwin-x64@2.1.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-K1ncIMEe84fe+ss1hQNO7rIvqiKy2TJvTFpkypvqFodT7mJXZn7GLKYTIXdIuyPAYthRa9DwFnx5uMoHwD2F1Q=="],
|
||||
|
||||
"lefthook-freebsd-arm64": ["lefthook-freebsd-arm64@2.1.4", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-PVUhjOhVN71YaYsVdQyNbFZ4a2jFB2Tg5hKrrn9kaWpx64aLz/XivLjwr8sEuTaP1GRlEWBpW6Bhrcsyo39qFw=="],
|
||||
|
||||
"lefthook-freebsd-x64": ["lefthook-freebsd-x64@2.1.4", "", { "os": "freebsd", "cpu": "x64" }, "sha512-ZWV9o/LeyWNEBoVO+BhLqxH3rGTba05nkm5NvMjEFSj7LbUNUDbQmupZwtHl1OMGJO66eZP0CalzRfUH6GhBxQ=="],
|
||||
|
||||
"lefthook-linux-arm64": ["lefthook-linux-arm64@2.1.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-iWN0pGnTjrIvNIcSI1vQBJXUbybTqJ5CLMniPA0olabMXQfPDrdMKVQe+mgdwHK+E3/Y0H0ZNL3lnOj6Sk6szA=="],
|
||||
|
||||
"lefthook-linux-x64": ["lefthook-linux-x64@2.1.4", "", { "os": "linux", "cpu": "x64" }, "sha512-96bTBE/JdYgqWYAJDh+/e/0MaxJ25XTOAk7iy/fKoZ1ugf6S0W9bEFbnCFNooXOcxNVTan5xWKfcjJmPIKtsJA=="],
|
||||
|
||||
"lefthook-openbsd-arm64": ["lefthook-openbsd-arm64@2.1.4", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-oYUoK6AIJNEr9lUSpIMj6g7sWzotvtc3ryw7yoOyQM6uqmEduw73URV/qGoUcm4nqqmR93ZalZwR2r3Gd61zvw=="],
|
||||
|
||||
"lefthook-openbsd-x64": ["lefthook-openbsd-x64@2.1.4", "", { "os": "openbsd", "cpu": "x64" }, "sha512-i/Dv9Jcm68y9cggr1PhyUhOabBGP9+hzQPoiyOhKks7y9qrJl79A8XfG6LHekSuYc2VpiSu5wdnnrE1cj2nfTg=="],
|
||||
|
||||
"lefthook-windows-arm64": ["lefthook-windows-arm64@2.1.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-hSww7z+QX4YMnw2lK7DMrs3+w7NtxksuMKOkCKGyxUAC/0m1LAICo0ZbtdDtZ7agxRQQQ/SEbzFRhU5ysNcbjA=="],
|
||||
|
||||
"lefthook-windows-x64": ["lefthook-windows-x64@2.1.4", "", { "os": "win32", "cpu": "x64" }, "sha512-eE68LwnogxwcPgGsbVGPGxmghyMGmU9SdGwcc+uhGnUxPz1jL89oECMWJNc36zjVK24umNeDAzB5KA3lw1MuWw=="],
|
||||
|
||||
"lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="],
|
||||
|
||||
"lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="],
|
||||
|
|
@ -178,6 +160,10 @@
|
|||
|
||||
"lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="],
|
||||
|
||||
"lit": ["lit@3.3.2", "", { "dependencies": { "@lit/reactive-element": "^2.1.0", "lit-element": "^4.2.0", "lit-html": "^3.3.0" } }, "sha512-NF9zbsP79l4ao2SNrH3NkfmFgN/hBYSQo90saIVI1o5GpjAdCPVstVzO1MrLOakHoEhYkrtRjPK6Ob521aoYWQ=="],
|
||||
|
||||
"lit-element": ["lit-element@4.2.2", "", { "dependencies": { "@lit-labs/ssr-dom-shim": "^1.5.0", "@lit/reactive-element": "^2.1.0", "lit-html": "^3.3.0" } }, "sha512-aFKhNToWxoyhkNDmWZwEva2SlQia+jfG0fjIWV//YeTaWrVnOxD89dPKfigCUspXFmjzOEUQpOkejH5Ly6sG0w=="],
|
||||
|
||||
"lit-html": ["lit-html@3.3.2", "", { "dependencies": { "@types/trusted-types": "^2.0.2" } }, "sha512-Qy9hU88zcmaxBXcc10ZpdK7cOLXvXpRoBxERdtqV9QOrfpMZZ6pSYP91LhpPtap3sFMUiL7Tw2RImbe0Al2/kw=="],
|
||||
|
||||
"lzma": ["lzma@2.3.2", "", { "bin": { "lzma.js": "bin/lzma.js" } }, "sha512-DcfiawQ1avYbW+hsILhF38IKAlnguc/fjHrychs9hdxe4qLykvhT5VTGNs5YRWgaNePh7NTxGD4uv4gKsRomCQ=="],
|
||||
|
|
@ -216,9 +202,9 @@
|
|||
|
||||
"resolve-protobuf-schema": ["resolve-protobuf-schema@2.1.0", "", { "dependencies": { "protocol-buffers-schema": "^3.3.1" } }, "sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ=="],
|
||||
|
||||
"rolldown": ["rolldown@1.0.0-rc.10", "", { "dependencies": { "@oxc-project/types": "=0.120.0", "@rolldown/pluginutils": "1.0.0-rc.10" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.0-rc.10", "@rolldown/binding-darwin-arm64": "1.0.0-rc.10", "@rolldown/binding-darwin-x64": "1.0.0-rc.10", "@rolldown/binding-freebsd-x64": "1.0.0-rc.10", "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.10", "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.10", "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.10", "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.10", "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.10", "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.10", "@rolldown/binding-linux-x64-musl": "1.0.0-rc.10", "@rolldown/binding-openharmony-arm64": "1.0.0-rc.10", "@rolldown/binding-wasm32-wasi": "1.0.0-rc.10", "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.10", "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.10" }, "bin": { "rolldown": "bin/cli.mjs" } }, "sha512-q7j6vvarRFmKpgJUT8HCAUljkgzEp4LAhPlJUvQhA5LA1SUL36s5QCysMutErzL3EbNOZOkoziSx9iZC4FddKA=="],
|
||||
"rolldown": ["rolldown@1.0.0-rc.12", "", { "dependencies": { "@oxc-project/types": "=0.122.0", "@rolldown/pluginutils": "1.0.0-rc.12" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.0-rc.12", "@rolldown/binding-darwin-arm64": "1.0.0-rc.12", "@rolldown/binding-darwin-x64": "1.0.0-rc.12", "@rolldown/binding-freebsd-x64": "1.0.0-rc.12", "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.12", "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.12", "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.12", "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.12", "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.12", "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.12", "@rolldown/binding-linux-x64-musl": "1.0.0-rc.12", "@rolldown/binding-openharmony-arm64": "1.0.0-rc.12", "@rolldown/binding-wasm32-wasi": "1.0.0-rc.12", "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.12", "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.12" }, "bin": { "rolldown": "bin/cli.mjs" } }, "sha512-yP4USLIMYrwpPHEFB5JGH1uxhcslv6/hL0OyvTuY+3qlOSJvZ7ntYnoWpehBxufkgN0cvXxppuTu5hHa/zPh+A=="],
|
||||
|
||||
"sass": ["sass@1.98.0", "", { "dependencies": { "chokidar": "^4.0.0", "immutable": "^5.1.5", "source-map-js": ">=0.6.2 <2.0.0" }, "optionalDependencies": { "@parcel/watcher": "^2.4.1" }, "bin": { "sass": "sass.js" } }, "sha512-+4N/u9dZ4PrgzGgPlKnaaRQx64RO0JBKs9sDhQ2pLgN6JQZ25uPQZKQYaBJU48Kd5BxgXoJ4e09Dq7nMcOUW3A=="],
|
||||
"sass": ["sass@1.99.0", "", { "dependencies": { "chokidar": "^4.0.0", "immutable": "^5.1.5", "source-map-js": ">=0.6.2 <2.0.0" }, "optionalDependencies": { "@parcel/watcher": "^2.4.1" }, "bin": { "sass": "sass.js" } }, "sha512-kgW13M54DUB7IsIRM5LvJkNlpH+WhMpooUcaWGFARkF1Tc82v9mIWkCbCYf+MBvpIUBSeSOTilpZjEPr2VYE6Q=="],
|
||||
|
||||
"shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, ""],
|
||||
|
||||
|
|
@ -234,11 +220,11 @@
|
|||
|
||||
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||
|
||||
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
||||
"typescript": ["typescript@6.0.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ=="],
|
||||
|
||||
"undici-types": ["undici-types@5.26.5", "", {}, ""],
|
||||
|
||||
"vite": ["vite@8.0.1", "", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.3", "postcss": "^8.5.8", "rolldown": "1.0.0-rc.10", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.1.0", "esbuild": "^0.27.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-wt+Z2qIhfFt85uiyRt5LPU4oVEJBXj8hZNWKeqFG4gRG/0RaRGJ7njQCwzFVjO+v4+Ipmf5CY7VdmZRAYYBPHw=="],
|
||||
"vite": ["vite@8.0.5", "", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", "postcss": "^8.5.8", "rolldown": "1.0.0-rc.12", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.1.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-nmu43Qvq9UopTRfMx2jOYW5l16pb3iDC1JH6yMuPkpVbzK0k+L7dfsEDH4jRgYFmsg0sTAqkojoZgzLMlwHsCQ=="],
|
||||
|
||||
"which": ["which@5.0.0", "", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ=="],
|
||||
|
||||
|
|
@ -252,6 +238,8 @@
|
|||
|
||||
"micromatch/picomatch": ["picomatch@2.3.1", "", {}, ""],
|
||||
|
||||
"vite/picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="],
|
||||
|
||||
"cross-spawn/which/isexe": ["isexe@2.0.0", "", {}, ""],
|
||||
}
|
||||
}
|
||||
|
|
|
|||
17
changelog.md
17
changelog.md
|
|
@ -1,4 +1,19 @@
|
|||
# 1.11.4
|
||||
# 1.11.5
|
||||
|
||||
- Переписан механизм монтирования UI.
|
||||
- Упрощена логика загрзуки аудио.
|
||||
- Исправлено получение субтитров на VKVideo (#1424)
|
||||
- Исправлена загрузка аудио на мобильных устройствах (#1646)
|
||||
- Улучшена/исправлена работа archive.org (#1636)
|
||||
- Исправлена работа Coursehunter (#1476)
|
||||
- Исправлена работа /d/ для Яндекс Диска (#1572)
|
||||
- Исправлена работа Reddit (#1613)
|
||||
- Исправлена работа Odysee (#1643)
|
||||
- Исправлена работа YouTube Embed (#1637)
|
||||
- Исправлена логика автоматического включения проксирования (#1638)
|
||||
- Обновлены зависимости и проведена оптимизация/очистка кода
|
||||
|
||||
# 1.11.4
|
||||
|
||||
- Добавлена поддержка Mediafile Cloud (#1603), Jove (#1593), Datacamp (#1606), PreserveTube (#1366)
|
||||
- Добавлена настройка "Язык субтитров по умолчанию": можно выбрать автоопределение, язык оригинального видео или конкретный язык
|
||||
|
|
|
|||
BIN
dist-ext/vot-extension-chrome-1.11.5.zip
Normal file
BIN
dist-ext/vot-extension-chrome-1.11.5.zip
Normal file
Binary file not shown.
BIN
dist-ext/vot-extension-firefox-1.11.5.xpi
Normal file
BIN
dist-ext/vot-extension-firefox-1.11.5.xpi
Normal file
Binary file not shown.
|
|
@ -3,8 +3,13 @@
|
|||
"vot-extension@firefox": {
|
||||
"updates": [
|
||||
{
|
||||
<<<<<<< Updated upstream
|
||||
"version": "1.11.4",
|
||||
"update_link": "https://raw.githubusercontent.com/ilyhalight/voice-over-translation/master/dist-ext/vot-extension-firefox-1.11.4.xpi"
|
||||
=======
|
||||
"version": "1.11.5",
|
||||
"update_link": "https://raw.githubusercontent.com/ilyhalight/voice-over-translation/master/dist-ext/vot-extension-firefox-1.11.5.xpi"
|
||||
>>>>>>> Stashed changes
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
123
dist/vot-min.user.js
vendored
123
dist/vot-min.user.js
vendored
File diff suppressed because one or more lines are too long
9650
dist/vot.user.js
vendored
9650
dist/vot.user.js
vendored
File diff suppressed because one or more lines are too long
|
|
@ -18,13 +18,13 @@
|
|||
"@toil/translate": "^1.0.8",
|
||||
"@types/bun": "^1.3.11",
|
||||
"@vot.js/core": "^2.4.12",
|
||||
"sass": "^1.98.0",
|
||||
"crx3": "^2.0.0",
|
||||
"lefthook": "^2.1.4",
|
||||
"lefthook": "^2.1.5",
|
||||
"lightningcss": "^1.32.0",
|
||||
"npm-run-all2": "^8.0.4",
|
||||
"sass": "^1.99.0",
|
||||
"typescript": "^5.9.3",
|
||||
"vite": "^8.0.1",
|
||||
"vite": "^8.0.5",
|
||||
"zip-a-folder": "^6.1.0"
|
||||
},
|
||||
"scripts": {
|
||||
|
|
@ -50,6 +50,7 @@
|
|||
"@vot.js/shared": "^2.4.12",
|
||||
"bowser": "^2.14.1",
|
||||
"chaimu": "^1.0.6",
|
||||
"lit": "^3.3.2",
|
||||
"lit-html": "^3.3.2"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -73,6 +73,14 @@ Limitations:
|
|||
- Doesn't work in the video preview
|
||||
- To ensure that the script works, you need to [enable the "Bypass Media CSP" setting](https://github.com/ilyhalight/voice-over-translation/wiki/%5BEN%5D-FAQ) in the extension or delete the CSP in another way
|
||||
|
||||
## Preservetube
|
||||
|
||||
Status: [✅] Working
|
||||
|
||||
Available (sub)domains:
|
||||
|
||||
- `preservetube.com`
|
||||
|
||||
## Zdf
|
||||
|
||||
Status: [✅] Working
|
||||
|
|
@ -510,6 +518,7 @@ Available (sub)domains:
|
|||
|
||||
- `dailymotion.com`
|
||||
- `www.dailymotion.com`
|
||||
- `player.dailymotion.com`
|
||||
- `geo*.dailymotion.com` (embedded player, on www.dailymotion.com it works)
|
||||
- `dai.ly`
|
||||
|
||||
|
|
@ -694,9 +703,7 @@ Status: [✅] Working
|
|||
|
||||
Available (sub)domains:
|
||||
|
||||
- `kodik.info`
|
||||
- `kodik.biz`
|
||||
- `kodik.cc`
|
||||
- `kodikplayer.com`
|
||||
|
||||
## Patreon
|
||||
|
||||
|
|
@ -829,6 +836,16 @@ Limitations:
|
|||
|
||||
- You must be logged in to the site
|
||||
|
||||
## Datacamp
|
||||
|
||||
Status: [✅] Working
|
||||
|
||||
Available (sub)domains:
|
||||
|
||||
- `datacamp.com`
|
||||
- `campus.datacamp.com`
|
||||
- `projector.datacamp.com`
|
||||
|
||||
## Coursera
|
||||
|
||||
Status: [✅] Working
|
||||
|
|
@ -859,6 +876,15 @@ Available paths:
|
|||
- /video/VIDEO_ID/VIDEO_NAME
|
||||
- /embed/VIDEO_ID
|
||||
|
||||
## Jove
|
||||
|
||||
Status: [✅] Working
|
||||
|
||||
Available (sub)domains:
|
||||
|
||||
- `app.jove.com`
|
||||
- `www.jove.com`
|
||||
|
||||
## Linkedin
|
||||
|
||||
Status: [✅] Working
|
||||
|
|
@ -905,6 +931,16 @@ Available paths:
|
|||
|
||||
- /video/watch/VIDEO_ID
|
||||
|
||||
## Bunnystream
|
||||
|
||||
Status: [✅] Working
|
||||
|
||||
Available (sub)domains:
|
||||
|
||||
- `video.bunnycdn.com`
|
||||
- `iframe.mediadelivery.net`
|
||||
- `*b-cdn.net`
|
||||
|
||||
## Cloudflarestream
|
||||
|
||||
Status: [✅] Working
|
||||
|
|
@ -1108,6 +1144,15 @@ Available paths:
|
|||
|
||||
- /content/i2cs/*
|
||||
|
||||
## Mediafile
|
||||
|
||||
Status: [✅] Working
|
||||
|
||||
Available (sub)domains:
|
||||
|
||||
- `mediafile.cc`
|
||||
- `www.mediafile.cc`
|
||||
|
||||
## Direct link to MP4/WEBM
|
||||
|
||||
Status: [✅] Working
|
||||
|
|
|
|||
|
|
@ -73,6 +73,14 @@
|
|||
- Не работает в предпросмотре видео
|
||||
- Для гарантированной работы скрипта необходимо [включить настройку "Обход Media CSP"](https://github.com/ilyhalight/voice-over-translation/wiki/%5BRU%5D-FAQ) в расширение или удалить CSP другим способом
|
||||
|
||||
## Preservetube
|
||||
|
||||
Статус: [✅] Работает
|
||||
|
||||
Доступные (под)домены:
|
||||
|
||||
- `preservetube.com`
|
||||
|
||||
## Zdf
|
||||
|
||||
Статус: [✅] Работает
|
||||
|
|
@ -510,6 +518,7 @@
|
|||
|
||||
- `dailymotion.com`
|
||||
- `www.dailymotion.com`
|
||||
- `player.dailymotion.com`
|
||||
- `geo*.dailymotion.com` (встраиваемый плеер, на www.dailymotion.com работает)
|
||||
- `dai.ly`
|
||||
|
||||
|
|
@ -694,9 +703,7 @@
|
|||
|
||||
Доступные (под)домены:
|
||||
|
||||
- `kodik.info`
|
||||
- `kodik.biz`
|
||||
- `kodik.cc`
|
||||
- `kodikplayer.com`
|
||||
|
||||
## Patreon
|
||||
|
||||
|
|
@ -829,6 +836,16 @@
|
|||
|
||||
- Необходимо быть авторизованным на сайте
|
||||
|
||||
## Datacamp
|
||||
|
||||
Статус: [✅] Работает
|
||||
|
||||
Доступные (под)домены:
|
||||
|
||||
- `datacamp.com`
|
||||
- `campus.datacamp.com`
|
||||
- `projector.datacamp.com`
|
||||
|
||||
## Coursera
|
||||
|
||||
Статус: [✅] Работает
|
||||
|
|
@ -859,6 +876,15 @@
|
|||
- /video/VIDEO_ID/VIDEO_NAME
|
||||
- /embed/VIDEO_ID
|
||||
|
||||
## Jove
|
||||
|
||||
Статус: [✅] Работает
|
||||
|
||||
Доступные (под)домены:
|
||||
|
||||
- `app.jove.com`
|
||||
- `www.jove.com`
|
||||
|
||||
## Linkedin
|
||||
|
||||
Статус: [✅] Работает
|
||||
|
|
@ -905,6 +931,16 @@
|
|||
|
||||
- /video/watch/VIDEO_ID
|
||||
|
||||
## Bunnystream
|
||||
|
||||
Статус: [✅] Работает
|
||||
|
||||
Доступные (под)домены:
|
||||
|
||||
- `video.bunnycdn.com`
|
||||
- `iframe.mediadelivery.net`
|
||||
- `*b-cdn.net`
|
||||
|
||||
## Cloudflarestream
|
||||
|
||||
Статус: [✅] Работает
|
||||
|
|
@ -1108,6 +1144,15 @@
|
|||
|
||||
- /content/i2cs/*
|
||||
|
||||
## Mediafile
|
||||
|
||||
Статус: [✅] Работает
|
||||
|
||||
Доступные (под)домены:
|
||||
|
||||
- `mediafile.cc`
|
||||
- `www.mediafile.cc`
|
||||
|
||||
## Direct link to MP4/WEBM
|
||||
|
||||
Статус: [✅] Работает
|
||||
|
|
|
|||
|
|
@ -635,7 +635,7 @@ function getSupportedSites() {
|
|||
async function main() {
|
||||
const supportedSites = getSupportedSites();
|
||||
const langs = ["ru", "en"];
|
||||
for await (const lang of langs) {
|
||||
for (const lang of langs) {
|
||||
const mdText = `${genMarkdown(supportedSites, lang).join("\n\n")}\n`;
|
||||
|
||||
await Bun.write(
|
||||
|
|
|
|||
|
|
@ -16,8 +16,8 @@ Use `AudioDownloader.downloadAudioToUint8Array(...)`:
|
|||
const downloader = new AudioDownloader({ fetchImplementation });
|
||||
const result = await downloader.downloadAudioToUint8Array({
|
||||
videoId: "memM8flkwrA",
|
||||
client: "ANDROID",
|
||||
videoQuality: "bestefficiency",
|
||||
client: "ANDROID_VR",
|
||||
audioQuality: "bestefficiency",
|
||||
signal,
|
||||
});
|
||||
|
||||
|
|
@ -27,6 +27,6 @@ const result = await downloader.downloadAudioToUint8Array({
|
|||
The module is intended to run in browser context and returns audio bytes for
|
||||
further chunking/upload by the parent audio downloader strategy.
|
||||
|
||||
Supported `videoQuality` values:
|
||||
Supported `audioQuality` values:
|
||||
- `best`
|
||||
- `bestefficiency`
|
||||
|
|
|
|||
|
|
@ -10,5 +10,4 @@ export {
|
|||
AudioDownloader,
|
||||
buildClientAttemptOrder,
|
||||
extractVideoId,
|
||||
YtWatchContextForbiddenError,
|
||||
} from "./src/AudioDownloader";
|
||||
|
|
|
|||
|
|
@ -6,27 +6,13 @@ import {
|
|||
} from "./internal/format-selection";
|
||||
|
||||
export type AudioDownloadQuality = ProgressiveQuality;
|
||||
export type AudioDownloadClient =
|
||||
| "IOS"
|
||||
| "WEB"
|
||||
| "MWEB"
|
||||
| "ANDROID"
|
||||
| "ANDROID_VR"
|
||||
| "YTMUSIC"
|
||||
| "YTMUSIC_ANDROID"
|
||||
| "YTSTUDIO_ANDROID"
|
||||
| "TV"
|
||||
| "TV_SIMPLY"
|
||||
| "TV_EMBEDDED"
|
||||
| "YTKIDS"
|
||||
| "WEB_EMBEDDED"
|
||||
| "WEB_CREATOR";
|
||||
export type AudioDownloadClient = "ANDROID_VR";
|
||||
|
||||
export interface AudioStreamRequest {
|
||||
videoId?: string;
|
||||
videoUrl?: string;
|
||||
client?: AudioDownloadClient;
|
||||
videoQuality?: AudioDownloadQuality;
|
||||
audioQuality?: AudioDownloadQuality;
|
||||
signal?: AbortSignal;
|
||||
}
|
||||
|
||||
|
|
@ -59,16 +45,6 @@ export interface AudioDownloaderOptions {
|
|||
fetchImplementation?: typeof fetch;
|
||||
}
|
||||
|
||||
export class YtWatchContextForbiddenError extends Error {
|
||||
readonly status: number;
|
||||
|
||||
constructor(status = 403) {
|
||||
super(`Failed to load watch page: ${status}`);
|
||||
this.name = "YtWatchContextForbiddenError";
|
||||
this.status = status;
|
||||
}
|
||||
}
|
||||
|
||||
interface WatchContext {
|
||||
apiKey: string;
|
||||
clientVersion: string;
|
||||
|
|
@ -108,16 +84,8 @@ interface ResolvedPlayableFormat {
|
|||
|
||||
const VIDEO_ID_PATTERN = /^[a-zA-Z0-9_-]{11}$/;
|
||||
const YT_BASE = "https://www.youtube.com";
|
||||
const ANDROID_CLIENT_VERSION = "19.44.38";
|
||||
const ANDROID_VR_CLIENT_VERSION = "1.60.19";
|
||||
const IOS_CLIENT_VERSION = "19.45.4";
|
||||
const CLIENT_FALLBACK_ORDER: readonly AudioDownloadClient[] = [
|
||||
"ANDROID_VR",
|
||||
"ANDROID",
|
||||
"IOS",
|
||||
"WEB",
|
||||
"MWEB",
|
||||
];
|
||||
const CLIENTS: readonly AudioDownloadClient[] = ["ANDROID_VR"];
|
||||
const DEFAULT_HEADERS = {
|
||||
accept: "*/*",
|
||||
origin: YT_BASE,
|
||||
|
|
@ -131,64 +99,21 @@ function withSignal(signal: AbortSignal | undefined): RequestInit {
|
|||
|
||||
function resolveInnertubeClient(
|
||||
requestedClient: AudioDownloadClient | undefined,
|
||||
watchContext: WatchContext,
|
||||
videoId: string,
|
||||
): Record<string, unknown> {
|
||||
switch (requestedClient) {
|
||||
case "ANDROID":
|
||||
case "YTMUSIC_ANDROID":
|
||||
case "YTSTUDIO_ANDROID":
|
||||
return {
|
||||
clientName: "ANDROID",
|
||||
clientVersion: ANDROID_CLIENT_VERSION,
|
||||
hl: "en",
|
||||
gl: "US",
|
||||
androidSdkVersion: 34,
|
||||
osName: "Android",
|
||||
osVersion: "14",
|
||||
platform: "MOBILE",
|
||||
};
|
||||
case "ANDROID_VR":
|
||||
return {
|
||||
clientName: "ANDROID_VR",
|
||||
clientVersion: ANDROID_VR_CLIENT_VERSION,
|
||||
hl: "en",
|
||||
gl: "US",
|
||||
androidSdkVersion: 31,
|
||||
osName: "Android",
|
||||
osVersion: "12",
|
||||
platform: "MOBILE",
|
||||
};
|
||||
case "IOS":
|
||||
return {
|
||||
clientName: "IOS",
|
||||
clientVersion: IOS_CLIENT_VERSION,
|
||||
hl: "en",
|
||||
gl: "US",
|
||||
platform: "MOBILE",
|
||||
osName: "iPhone",
|
||||
osVersion: "18.0.0.22A3354",
|
||||
deviceMake: "Apple",
|
||||
deviceModel: "iPhone16,2",
|
||||
};
|
||||
case "MWEB":
|
||||
return {
|
||||
clientName: "MWEB",
|
||||
clientVersion: watchContext.clientVersion,
|
||||
hl: "en",
|
||||
gl: "US",
|
||||
originalUrl: `${YT_BASE}/watch?v=${videoId}`,
|
||||
};
|
||||
default:
|
||||
return {
|
||||
clientName: "WEB",
|
||||
clientVersion: watchContext.clientVersion,
|
||||
hl: "en",
|
||||
gl: "US",
|
||||
utcOffsetMinutes: 0,
|
||||
originalUrl: `${YT_BASE}/watch?v=${videoId}`,
|
||||
};
|
||||
if (requestedClient !== undefined && requestedClient !== "ANDROID_VR") {
|
||||
throw new Error(`Unsupported Innertube client: ${requestedClient}`);
|
||||
}
|
||||
|
||||
return {
|
||||
clientName: "ANDROID_VR",
|
||||
clientVersion: ANDROID_VR_CLIENT_VERSION,
|
||||
hl: "en",
|
||||
gl: "US",
|
||||
androidSdkVersion: 31,
|
||||
osName: "Android",
|
||||
osVersion: "12",
|
||||
platform: "MOBILE",
|
||||
};
|
||||
}
|
||||
|
||||
export function extractVideoId(input: string): string {
|
||||
|
|
@ -416,8 +341,8 @@ export function buildClientAttemptOrder(
|
|||
requestedClient: AudioDownloadClient | undefined,
|
||||
): AudioDownloadClient[] {
|
||||
const ordered = requestedClient
|
||||
? [requestedClient, ...CLIENT_FALLBACK_ORDER]
|
||||
: [...CLIENT_FALLBACK_ORDER];
|
||||
? [requestedClient, ...CLIENTS]
|
||||
: [...CLIENTS];
|
||||
const seen = new Set<AudioDownloadClient>();
|
||||
|
||||
return ordered.filter((client) => {
|
||||
|
|
@ -507,7 +432,7 @@ export class AudioDownloader {
|
|||
|
||||
return this.withResolvedPlayableAudioFormat(
|
||||
request,
|
||||
request.videoQuality ?? "best",
|
||||
request.audioQuality ?? "best",
|
||||
"Chunk mode requires an adaptive audio stream format",
|
||||
"Unable to resolve streamable format for chunk mode",
|
||||
async ({ resolved, signal }) => {
|
||||
|
|
@ -577,7 +502,7 @@ export class AudioDownloader {
|
|||
): Promise<AudioStreamResult> {
|
||||
return this.withResolvedPlayableAudioFormat(
|
||||
request,
|
||||
request.videoQuality ?? "bestefficiency",
|
||||
request.audioQuality ?? "bestefficiency",
|
||||
"Selected stream is not audio-only",
|
||||
"Unable to download playable stream format",
|
||||
async ({ resolved, signal }) => {
|
||||
|
|
@ -673,10 +598,7 @@ export class AudioDownloader {
|
|||
quality,
|
||||
);
|
||||
|
||||
const streamUrl = this.resolveFormatUrl(
|
||||
chosenFormat,
|
||||
watchContext.clientVersion,
|
||||
);
|
||||
const streamUrl = this.resolveFormatUrl(chosenFormat);
|
||||
|
||||
return {
|
||||
videoId,
|
||||
|
|
@ -766,20 +688,12 @@ export class AudioDownloader {
|
|||
};
|
||||
}
|
||||
|
||||
private resolveFormatUrl(
|
||||
format: InnertubeFormat,
|
||||
clientVersion: string,
|
||||
): string {
|
||||
private resolveFormatUrl(format: InnertubeFormat): string {
|
||||
if (!format.url) {
|
||||
throw new Error("Selected format does not contain a direct stream URL");
|
||||
}
|
||||
const streamUrl = new URL(format.url);
|
||||
|
||||
const client = streamUrl.searchParams.get("c");
|
||||
if (client === "WEB") {
|
||||
streamUrl.searchParams.set("cver", clientVersion);
|
||||
}
|
||||
|
||||
streamUrl.searchParams.set("cpn", makeCPN());
|
||||
return streamUrl.toString();
|
||||
}
|
||||
|
|
@ -794,9 +708,6 @@ export class AudioDownloader {
|
|||
...withSignal(signal),
|
||||
});
|
||||
if (!response.ok) {
|
||||
if (response.status === 403) {
|
||||
throw new YtWatchContextForbiddenError(response.status);
|
||||
}
|
||||
throw new Error(`Failed to load watch page: ${response.status}`);
|
||||
}
|
||||
|
||||
|
|
@ -847,11 +758,7 @@ export class AudioDownloader {
|
|||
requestedClient: AudioDownloadClient | undefined,
|
||||
signal?: AbortSignal,
|
||||
): Promise<InnertubePlayerResponse> {
|
||||
const client = resolveInnertubeClient(
|
||||
requestedClient,
|
||||
watchContext,
|
||||
videoId,
|
||||
);
|
||||
const client = resolveInnertubeClient(requestedClient);
|
||||
if (watchContext.visitorData) {
|
||||
client.visitorData = watchContext.visitorData;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ import {
|
|||
type AudioChunkStreamResult,
|
||||
type AudioStreamRequest,
|
||||
AudioDownloader as YtAudioDownloader,
|
||||
YtWatchContextForbiddenError,
|
||||
} from "./index";
|
||||
|
||||
const DEFAULT_YT_AUDIO_QUALITY = "bestefficiency";
|
||||
|
|
@ -54,17 +53,6 @@ function createYtAudioFetch({
|
|||
});
|
||||
}
|
||||
|
||||
function isWatchContextForbiddenError(error: unknown): boolean {
|
||||
if (error instanceof YtWatchContextForbiddenError) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return (
|
||||
error instanceof Error &&
|
||||
/failed to load watch page:\s*403/i.test(error.message)
|
||||
);
|
||||
}
|
||||
|
||||
export async function getAudioFromYtAudio(
|
||||
{ videoId, signal }: GetAudioFromAPIOptions,
|
||||
deps: YtAudioStrategyDeps = {},
|
||||
|
|
@ -84,7 +72,7 @@ export async function getAudioFromYtAudio(
|
|||
const streamResult = await downloader.downloadAudioToChunkStream(
|
||||
{
|
||||
videoId,
|
||||
videoQuality: DEFAULT_YT_AUDIO_QUALITY,
|
||||
audioQuality: DEFAULT_YT_AUDIO_QUALITY,
|
||||
signal,
|
||||
},
|
||||
{ chunkSize },
|
||||
|
|
@ -101,13 +89,6 @@ export async function getAudioFromYtAudio(
|
|||
getMediaBuffers: streamResult.getMediaBuffers,
|
||||
};
|
||||
} catch (error) {
|
||||
if (isWatchContextForbiddenError(error)) {
|
||||
// 403 on watch-page key fetch is not recoverable in current context.
|
||||
// Skip buffered fallback so upper layer can immediately trigger
|
||||
// fail-audio-js instead of spending time on redundant retries.
|
||||
throw error;
|
||||
}
|
||||
|
||||
console.warn(
|
||||
"[VOT] ytAudio streaming mode failed, falling back to buffered mode",
|
||||
error,
|
||||
|
|
@ -116,7 +97,7 @@ export async function getAudioFromYtAudio(
|
|||
|
||||
const result = await downloader.downloadAudioToUint8Array({
|
||||
videoId,
|
||||
videoQuality: DEFAULT_YT_AUDIO_QUALITY,
|
||||
audioQuality: DEFAULT_YT_AUDIO_QUALITY,
|
||||
signal,
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -477,8 +477,29 @@ function handlePromiseResponse(data: AnyObject): boolean {
|
|||
|
||||
pending.delete(id);
|
||||
clearTimeout(item.timeoutId);
|
||||
<<<<<<< Updated upstream
|
||||
if (data.ok) item.resolve(data.result);
|
||||
else item.reject(new Error(toErrorMessage(data.error ?? "Bridge error")));
|
||||
=======
|
||||
if (data.ok) {
|
||||
debug.log("[VOT EXT][prelude] GM API response", {
|
||||
requestId: id,
|
||||
action: item.action,
|
||||
ok: true,
|
||||
resultType: Array.isArray(data.result) ? "array" : typeof data.result,
|
||||
});
|
||||
item.resolve(data.result);
|
||||
} else {
|
||||
const errorMessage = toErrorMessage(data.error ?? "Bridge error");
|
||||
debug.warn("[VOT EXT][prelude] GM API response", {
|
||||
requestId: id,
|
||||
action: item.action,
|
||||
ok: false,
|
||||
error: errorMessage,
|
||||
});
|
||||
item.reject(new Error(errorMessage));
|
||||
}
|
||||
>>>>>>> Stashed changes
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,11 @@
|
|||
{
|
||||
"name": "[VOT] - Voice Over Translation",
|
||||
"description": "A small extension that adds a Yandex Browser video translation to other browsers",
|
||||
<<<<<<< Updated upstream
|
||||
"version": "1.11.4",
|
||||
=======
|
||||
"version": "1.11.5",
|
||||
>>>>>>> Stashed changes
|
||||
"author": "Toil, SashaXser, MrSoczekXD, mynovelhost, sodapng",
|
||||
"namespace": "vot",
|
||||
"icon": "https://translate.yandex.ru/icons/favicon.ico",
|
||||
|
|
@ -74,9 +78,7 @@
|
|||
"*://*.archive.org/*",
|
||||
"*://*.patreon.com/*",
|
||||
"*://*.reddit.com/*",
|
||||
"*://*.kodik.info/*",
|
||||
"*://*.kodik.biz/*",
|
||||
"*://*.kodik.cc/*",
|
||||
"*://*.kodikplayer.com/*",
|
||||
"*://*.kick.com/*",
|
||||
"*://developer.apple.com/*",
|
||||
"*://dev.epicgames.com/*",
|
||||
|
|
|
|||
47
src/index.ts
47
src/index.ts
|
|
@ -35,7 +35,6 @@ import { UIManager } from "./ui/manager";
|
|||
import { isSameOverlayMount } from "./ui/mount";
|
||||
import { OverlayVisibilityController } from "./ui/overlayVisibilityController";
|
||||
import debug from "./utils/debug";
|
||||
import { resolveScopedFullscreenElement } from "./utils/dom";
|
||||
import { getEnvironmentInfo as getEnvironmentInfoImpl } from "./utils/environment";
|
||||
import { GM_fetch, isSupportGMXhr } from "./utils/gm";
|
||||
import { isIframe } from "./utils/iframeConnector";
|
||||
|
|
@ -83,6 +82,10 @@ import {
|
|||
enableSubtitlesForCurrentLangPair as enableSubtitlesForCurrentLangPairImpl,
|
||||
ensureSubtitlesForCurrentLangPair as ensureSubtitlesForCurrentLangPairImpl,
|
||||
loadSubtitles as loadSubtitlesImpl,
|
||||
<<<<<<< Updated upstream
|
||||
=======
|
||||
refreshAutoSubtitlesForCurrentLangPair as refreshAutoSubtitlesForCurrentLangPairImpl,
|
||||
>>>>>>> Stashed changes
|
||||
resolveSubtitlesLanguage,
|
||||
toggleSubtitlesForCurrentLangPair as toggleSubtitlesForCurrentLangPairImpl,
|
||||
updateSubtitlesLangSelect as updateSubtitlesLangSelectImpl,
|
||||
|
|
@ -275,7 +278,12 @@ export class VideoHandler {
|
|||
private getFullscreenOverlayRoot(): HTMLElement | null {
|
||||
const doc = document as DocumentWithFullscreen;
|
||||
const fullscreenEl = doc.fullscreenElement ?? doc.webkitFullscreenElement;
|
||||
return resolveScopedFullscreenElement(fullscreenEl, [this.container]);
|
||||
return fullscreenEl instanceof HTMLElement &&
|
||||
(fullscreenEl === this.container ||
|
||||
fullscreenEl.contains(this.container) ||
|
||||
this.container.contains(fullscreenEl))
|
||||
? fullscreenEl
|
||||
: null;
|
||||
}
|
||||
|
||||
private getOverlayMountPoints(container: HTMLElement = this.container): {
|
||||
|
|
@ -322,13 +330,12 @@ export class VideoHandler {
|
|||
private getOverlayMount(
|
||||
container: HTMLElement = this.container,
|
||||
): OverlayMount {
|
||||
const { root, portalContainer, subtitlesMountContainer, fullscreenRoot } =
|
||||
const { root, portalContainer, subtitlesMountContainer } =
|
||||
this.getOverlayMountPoints(container);
|
||||
return {
|
||||
root,
|
||||
portalContainer,
|
||||
subtitlesMountContainer,
|
||||
tooltipLayoutRoot: fullscreenRoot ?? this.tooltipLayoutRoot,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -520,9 +527,8 @@ export class VideoHandler {
|
|||
const { subtitlesMountContainer } = this.getOverlayMountPoints();
|
||||
this.subtitlesWidget = new SubtitlesWidget(
|
||||
this.video,
|
||||
subtitlesMountContainer,
|
||||
this.uiManager.votOverlayView?.root ?? subtitlesMountContainer,
|
||||
this.interactionChecker,
|
||||
this.tooltipLayoutRoot,
|
||||
);
|
||||
this.applySavedSubtitlesWidgetSettings(this.subtitlesWidget);
|
||||
}
|
||||
|
|
@ -590,24 +596,6 @@ export class VideoHandler {
|
|||
return this.getOverlayMountPoints().portalContainer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the root element used for tooltip layout calculations.
|
||||
* @returns {HTMLElement | undefined}
|
||||
*/
|
||||
get tooltipLayoutRoot() {
|
||||
switch (this.site.host) {
|
||||
case "kickstarter": {
|
||||
return document.getElementById("react-project-header") ?? undefined;
|
||||
}
|
||||
case "custom": {
|
||||
return undefined;
|
||||
}
|
||||
default: {
|
||||
return this.container;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the container element for event listeners.
|
||||
* @returns {HTMLElement} The event container.
|
||||
|
|
@ -897,6 +885,17 @@ export class VideoHandler {
|
|||
}
|
||||
|
||||
/**
|
||||
<<<<<<< Updated upstream
|
||||
=======
|
||||
* Re-evaluates the active subtitles track for the current language pair,
|
||||
* but only when auto-subtitles are enabled.
|
||||
*/
|
||||
refreshAutoSubtitlesForCurrentLangPair() {
|
||||
return this.callModuleAsync(refreshAutoSubtitlesForCurrentLangPairImpl);
|
||||
}
|
||||
|
||||
/**
|
||||
>>>>>>> Stashed changes
|
||||
* Toggles subtitles for the current video.
|
||||
*
|
||||
* - If subtitles are enabled, this disables them.
|
||||
|
|
|
|||
|
|
@ -176,3 +176,15 @@ html.vot-keyboard-nav vot-block *:focus-visible {
|
|||
z-index: 2147483647;
|
||||
}
|
||||
}
|
||||
|
||||
// The overlay mount itself stays click-through so host-page controls remain
|
||||
// usable around the injected UI. Explicitly re-enable hit-testing for the
|
||||
// actual interactive overlay surfaces inside the shadow root.
|
||||
.vot-overlay-root {
|
||||
pointer-events: none;
|
||||
|
||||
& > .vot-segmented-button,
|
||||
& > .vot-menu {
|
||||
pointer-events: auto;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,21 +1,11 @@
|
|||
import { resolveScopedFullscreenElement } from "../utils/dom";
|
||||
|
||||
type DocumentWithFullscreenElement = Document & {
|
||||
webkitFullscreenElement?: Element | null;
|
||||
};
|
||||
|
||||
type FullscreenLayerControllerOptions = {
|
||||
video?: HTMLVideoElement;
|
||||
container: HTMLElement;
|
||||
};
|
||||
|
||||
export class FullscreenLayerController {
|
||||
private readonly video?: HTMLVideoElement;
|
||||
private container: HTMLElement;
|
||||
private fullscreenLayer: HTMLElement | null = null;
|
||||
|
||||
constructor({ video, container }: FullscreenLayerControllerOptions) {
|
||||
this.video = video;
|
||||
constructor({ container }: FullscreenLayerControllerOptions) {
|
||||
this.container = container;
|
||||
}
|
||||
|
||||
|
|
@ -24,93 +14,21 @@ export class FullscreenLayerController {
|
|||
}
|
||||
|
||||
getWidgetParentElement(): HTMLElement {
|
||||
return this.shouldUseFullscreenViewportLayer()
|
||||
? this.ensureFullscreenLayer()
|
||||
: this.container;
|
||||
return this.container;
|
||||
}
|
||||
|
||||
getLayoutRootElement(): HTMLElement {
|
||||
return this.fullscreenLayer?.isConnected
|
||||
? this.fullscreenLayer
|
||||
: this.container;
|
||||
return this.container;
|
||||
}
|
||||
|
||||
syncWidgetContainer(widgetContainer: HTMLElement | null): void {
|
||||
const widgetParent = this.getWidgetParentElement();
|
||||
|
||||
if (
|
||||
widgetParent === this.container &&
|
||||
getComputedStyle(this.container).position === "static"
|
||||
) {
|
||||
if (getComputedStyle(this.container).position === "static") {
|
||||
this.container.style.position = "relative";
|
||||
}
|
||||
|
||||
if (widgetContainer && widgetContainer.parentElement !== widgetParent) {
|
||||
widgetParent.appendChild(widgetContainer);
|
||||
}
|
||||
|
||||
if (
|
||||
widgetParent === this.container &&
|
||||
this.fullscreenLayer?.parentElement
|
||||
) {
|
||||
this.fullscreenLayer.remove();
|
||||
this.fullscreenLayer = null;
|
||||
if (widgetContainer && widgetContainer.parentElement !== this.container) {
|
||||
this.container.appendChild(widgetContainer);
|
||||
}
|
||||
}
|
||||
|
||||
release(): void {
|
||||
if (!this.fullscreenLayer) return;
|
||||
this.fullscreenLayer.remove();
|
||||
this.fullscreenLayer = null;
|
||||
}
|
||||
|
||||
private getActiveFullscreenElement(): HTMLElement | null {
|
||||
const doc = document as DocumentWithFullscreenElement;
|
||||
const fullscreenEl = doc.fullscreenElement ?? doc.webkitFullscreenElement;
|
||||
return resolveScopedFullscreenElement(
|
||||
fullscreenEl,
|
||||
[this.container, this.video],
|
||||
{
|
||||
allowDocumentViewport: true,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
private isCurrentVideoInFullscreenSession(): boolean {
|
||||
const fullscreenEl = this.getActiveFullscreenElement();
|
||||
if (!fullscreenEl) return false;
|
||||
|
||||
if (
|
||||
fullscreenEl === this.container ||
|
||||
fullscreenEl.contains(this.container) ||
|
||||
this.container.contains(fullscreenEl)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return Boolean(
|
||||
this.video &&
|
||||
(fullscreenEl === this.video ||
|
||||
fullscreenEl.contains(this.video) ||
|
||||
this.video.contains(fullscreenEl)),
|
||||
);
|
||||
}
|
||||
|
||||
private shouldUseFullscreenViewportLayer(): boolean {
|
||||
return this.isCurrentVideoInFullscreenSession();
|
||||
}
|
||||
|
||||
private ensureFullscreenLayer(): HTMLElement {
|
||||
if (!this.fullscreenLayer) {
|
||||
const layer = document.createElement("vot-block");
|
||||
layer.classList.add("vot-subtitles-layer");
|
||||
this.fullscreenLayer = layer;
|
||||
}
|
||||
|
||||
if (this.fullscreenLayer.parentElement !== this.container) {
|
||||
this.container.appendChild(this.fullscreenLayer);
|
||||
}
|
||||
|
||||
return this.fullscreenLayer;
|
||||
}
|
||||
release(): void {}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,12 @@ import type {
|
|||
} from "../types/subtitles";
|
||||
import UI from "../ui";
|
||||
import Tooltip from "../ui/components/tooltip";
|
||||
import {
|
||||
createShadowMount,
|
||||
destroyShadowMount,
|
||||
reparentShadowMount,
|
||||
type ShadowMount,
|
||||
} from "../ui/shadowMount";
|
||||
import type { IntervalIdleChecker } from "../utils/intervalIdleChecker";
|
||||
import { votStorage } from "../utils/storage";
|
||||
import { translate } from "../utils/translateApis";
|
||||
|
|
@ -102,7 +108,7 @@ export class SubtitlesWidget {
|
|||
private readonly video?: HTMLVideoElement;
|
||||
private container: HTMLElement;
|
||||
private readonly fullscreenLayerController: FullscreenLayerController;
|
||||
private tooltipLayoutRoot?: HTMLElement;
|
||||
private tooltipMount?: ShadowMount;
|
||||
private subtitlesContainer: HTMLElement | null = null;
|
||||
private subtitlesBlock: HTMLElement | null = null;
|
||||
private renderedHighlightEls: HTMLSpanElement[] = [];
|
||||
|
|
@ -218,16 +224,13 @@ export class SubtitlesWidget {
|
|||
video: HTMLVideoElement | undefined,
|
||||
container: HTMLElement,
|
||||
intervalIdleChecker: IntervalIdleChecker,
|
||||
tooltipLayoutRoot: HTMLElement | undefined = undefined,
|
||||
) {
|
||||
this.video = video;
|
||||
this.container = container;
|
||||
this.fullscreenLayerController = new FullscreenLayerController({
|
||||
video,
|
||||
container,
|
||||
});
|
||||
this.intervalIdleChecker = intervalIdleChecker;
|
||||
this.tooltipLayoutRoot = tooltipLayoutRoot;
|
||||
this.useVideoFrameCallbacks =
|
||||
!!this.video &&
|
||||
typeof this.video.requestVideoFrameCallback === "function";
|
||||
|
|
@ -241,26 +244,19 @@ export class SubtitlesWidget {
|
|||
});
|
||||
this.bindEvents();
|
||||
}
|
||||
public updateMount({
|
||||
container,
|
||||
tooltipLayoutRoot,
|
||||
}: {
|
||||
container: HTMLElement;
|
||||
tooltipLayoutRoot?: HTMLElement;
|
||||
}): void {
|
||||
public updateMount({ container }: { container: HTMLElement }): void {
|
||||
const containerChanged = this.container !== container;
|
||||
const tooltipRootChanged = this.tooltipLayoutRoot !== tooltipLayoutRoot;
|
||||
|
||||
this.container = container;
|
||||
this.fullscreenLayerController.updateContainer(container);
|
||||
this.tooltipLayoutRoot = tooltipLayoutRoot;
|
||||
|
||||
this.syncWidgetMount();
|
||||
|
||||
if (containerChanged || tooltipRootChanged) {
|
||||
if (containerChanged) {
|
||||
const parentElement = this.getTokenTooltipParentElement();
|
||||
this.tokenTooltip?.updateMount({
|
||||
parentElement: this.getTokenTooltipParentElement(),
|
||||
layoutRoot: this.tooltipLayoutRoot ?? document.documentElement,
|
||||
parentElement,
|
||||
layoutRoot: this.tooltipMount?.host,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -491,23 +487,51 @@ export class SubtitlesWidget {
|
|||
}
|
||||
}
|
||||
private syncGuideLayerMount(): void {
|
||||
const widgetParent =
|
||||
this.fullscreenLayerController.getWidgetParentElement();
|
||||
const guidesLayer = this.ensureGuidesLayer();
|
||||
if (guidesLayer.parentElement !== widgetParent) {
|
||||
widgetParent.appendChild(guidesLayer);
|
||||
if (guidesLayer.parentElement !== this.container) {
|
||||
this.container.appendChild(guidesLayer);
|
||||
}
|
||||
}
|
||||
private syncWidgetMount(): void {
|
||||
this.fullscreenLayerController.syncWidgetContainer(this.subtitlesContainer);
|
||||
this.fullscreenLayerController.syncWidgetContainer(null);
|
||||
if (
|
||||
this.subtitlesContainer &&
|
||||
this.subtitlesContainer.parentElement !== this.container
|
||||
) {
|
||||
this.container.appendChild(this.subtitlesContainer);
|
||||
}
|
||||
if (this.tooltipMount) {
|
||||
reparentShadowMount(this.tooltipMount, this.container);
|
||||
}
|
||||
this.syncGuideLayerMount();
|
||||
}
|
||||
private ensureTooltipMount(): ShadowMount {
|
||||
if (!this.tooltipMount) {
|
||||
this.tooltipMount = createShadowMount({
|
||||
parent: this.container,
|
||||
rootClasses: ["vot-portal-local"],
|
||||
hostStyles: {
|
||||
position: "absolute",
|
||||
inset: "0",
|
||||
display: "block",
|
||||
"pointer-events": "none",
|
||||
},
|
||||
rootStyles: {
|
||||
position: "relative",
|
||||
display: "block",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
"pointer-events": "none",
|
||||
},
|
||||
});
|
||||
} else {
|
||||
reparentShadowMount(this.tooltipMount, this.container);
|
||||
}
|
||||
|
||||
return this.tooltipMount;
|
||||
}
|
||||
private getTokenTooltipParentElement(): HTMLElement {
|
||||
const widgetParent =
|
||||
this.fullscreenLayerController.getWidgetParentElement();
|
||||
return widgetParent === this.container
|
||||
? document.documentElement
|
||||
: widgetParent;
|
||||
return this.ensureTooltipMount().root;
|
||||
}
|
||||
private createSubtitlesContainer(): HTMLElement {
|
||||
if (this.subtitlesContainer) {
|
||||
|
|
@ -519,7 +543,8 @@ export class SubtitlesWidget {
|
|||
this.syncWidgetMount();
|
||||
container.addEventListener("pointerdown", this.onPointerDownBound, {
|
||||
signal: this.abortController.signal,
|
||||
passive: true,
|
||||
passive: false,
|
||||
capture: true,
|
||||
});
|
||||
this.syncVisualStyleVars();
|
||||
this.insetCacheReady = false;
|
||||
|
|
@ -677,16 +702,17 @@ export class SubtitlesWidget {
|
|||
this.dragDocListenersAttached = true;
|
||||
document.addEventListener("pointermove", this.onPointerMoveBound, {
|
||||
passive: false,
|
||||
capture: true,
|
||||
});
|
||||
document.addEventListener("pointerup", this.onPointerUpBound);
|
||||
document.addEventListener("pointercancel", this.onPointerUpBound);
|
||||
document.addEventListener("pointerup", this.onPointerUpBound, true);
|
||||
document.addEventListener("pointercancel", this.onPointerUpBound, true);
|
||||
}
|
||||
private detachDragDocumentListeners(): void {
|
||||
if (!this.dragDocListenersAttached) return;
|
||||
this.dragDocListenersAttached = false;
|
||||
document.removeEventListener("pointermove", this.onPointerMoveBound);
|
||||
document.removeEventListener("pointerup", this.onPointerUpBound);
|
||||
document.removeEventListener("pointercancel", this.onPointerUpBound);
|
||||
document.removeEventListener("pointermove", this.onPointerMoveBound, true);
|
||||
document.removeEventListener("pointerup", this.onPointerUpBound, true);
|
||||
document.removeEventListener("pointercancel", this.onPointerUpBound, true);
|
||||
}
|
||||
private onResize(): void {
|
||||
this.syncWidgetMount();
|
||||
|
|
@ -822,6 +848,7 @@ export class SubtitlesWidget {
|
|||
return;
|
||||
if (!event.isPrimary) return;
|
||||
if (event.pointerType === "mouse" && event.button !== 0) return;
|
||||
event.stopPropagation();
|
||||
const layout = this.getLayoutSize();
|
||||
const { rect: containerRect, w, h, scaleX, scaleY } = layout;
|
||||
if (!w || !h) return;
|
||||
|
|
@ -849,11 +876,6 @@ export class SubtitlesWidget {
|
|||
this.dragging.offset.y = anchorY - pointerY;
|
||||
this.hideSnapGuides();
|
||||
this.attachDragDocumentListeners();
|
||||
const captureEl: Element | null =
|
||||
this.subtitlesBlock ?? (target instanceof Element ? target : null);
|
||||
try {
|
||||
(captureEl as HTMLElement | null)?.setPointerCapture(event.pointerId);
|
||||
} catch {}
|
||||
}
|
||||
private onPointerUp(event: PointerEvent): void {
|
||||
if (this.dragging.pointerId === null) return;
|
||||
|
|
@ -886,6 +908,9 @@ export class SubtitlesWidget {
|
|||
this.dragging.moved = true;
|
||||
this.suppressTokenClicksUntil = performance.now() + 450;
|
||||
this.releaseTooltip();
|
||||
try {
|
||||
this.subtitlesContainer?.setPointerCapture(event.pointerId);
|
||||
} catch {}
|
||||
} else {
|
||||
this.dragging.moved = true;
|
||||
}
|
||||
|
|
@ -1488,6 +1513,8 @@ export class SubtitlesWidget {
|
|||
}
|
||||
this.tokenTooltip?.release();
|
||||
this.tokenTooltip = undefined;
|
||||
destroyShadowMount(this.tooltipMount);
|
||||
this.tooltipMount = undefined;
|
||||
return this;
|
||||
}
|
||||
private clearPendingSchedulerState(): void {
|
||||
|
|
@ -1588,13 +1615,14 @@ export class SubtitlesWidget {
|
|||
this.subtitlesBlock?.offsetWidth ?? 0,
|
||||
Math.min(globalThis.innerWidth * 0.6, 320),
|
||||
);
|
||||
const tooltipMount = this.ensureTooltipMount();
|
||||
|
||||
return new Tooltip({
|
||||
target,
|
||||
anchor: this.subtitlesBlock ?? target,
|
||||
layoutRoot: this.tooltipLayoutRoot,
|
||||
content,
|
||||
parentElement: this.getTokenTooltipParentElement(),
|
||||
parentElement: tooltipMount.root,
|
||||
layoutRoot: tooltipMount.host,
|
||||
offset: { x: 4, y: 12 },
|
||||
maxWidth: tooltipMaxWidth,
|
||||
borderRadius: 12,
|
||||
|
|
@ -2042,6 +2070,8 @@ export class SubtitlesWidget {
|
|||
this.subtitlesContainer.remove();
|
||||
this.subtitlesContainer = null;
|
||||
}
|
||||
destroyShadowMount(this.tooltipMount);
|
||||
this.tooltipMount = undefined;
|
||||
this.fullscreenLayerController.release();
|
||||
if (this.safeAreaProbeEl) {
|
||||
this.safeAreaProbeEl.remove();
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@ export type OverlayMount = {
|
|||
root: HTMLElement;
|
||||
portalContainer: HTMLElement;
|
||||
subtitlesMountContainer: HTMLElement;
|
||||
tooltipLayoutRoot?: HTMLElement;
|
||||
};
|
||||
|
||||
export type UIManagerProps = {
|
||||
|
|
|
|||
20
src/ui.ts
20
src/ui.ts
|
|
@ -1,28 +1,8 @@
|
|||
import type { TemplateResult } from "lit-html";
|
||||
import { render } from "lit-html";
|
||||
import { localizationProvider } from "./localization/localizationProvider";
|
||||
import mainScss from "./styles/main.scss?inline";
|
||||
import type { LitHtml } from "./types/components/shared";
|
||||
|
||||
function injectMainStyles(css: string): HTMLStyleElement | HTMLElement {
|
||||
const gmAddStyle = (
|
||||
globalThis as typeof globalThis & {
|
||||
GM_addStyle?: (styleText: string) => HTMLStyleElement | HTMLElement;
|
||||
}
|
||||
).GM_addStyle;
|
||||
|
||||
if (typeof gmAddStyle === "function") {
|
||||
return gmAddStyle(css);
|
||||
}
|
||||
|
||||
const style = document.createElement("style");
|
||||
style.textContent = css;
|
||||
(document.head || document.documentElement).appendChild(style);
|
||||
return style;
|
||||
}
|
||||
|
||||
injectMainStyles(mainScss);
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
__votKeyboardNavInitialized?: boolean;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { EventImpl } from "../../core/eventImpl";
|
||||
import type { DialogProps } from "../../types/components/dialog";
|
||||
import UI from "../../ui";
|
||||
import { getDeepActiveElement } from "../../utils/dom";
|
||||
import { CLOSE_ICON } from "../icons";
|
||||
import {
|
||||
addComponentEventListener,
|
||||
|
|
@ -156,7 +157,7 @@ export default class Dialog {
|
|||
|
||||
open() {
|
||||
// Temp dialogs are created visible; still run focus/keyboard setup.
|
||||
this.previouslyFocused ??= document.activeElement;
|
||||
this.previouslyFocused ??= getDeepActiveElement(document);
|
||||
|
||||
this.hidden = false;
|
||||
this.attachKeydownTrap();
|
||||
|
|
@ -340,7 +341,9 @@ export default class Dialog {
|
|||
|
||||
const first = focusables[0];
|
||||
const last = focusables.at(-1) ?? first;
|
||||
const active = document.activeElement;
|
||||
const active = getDeepActiveElement(
|
||||
this.container.getRootNode() as Document | ShadowRoot,
|
||||
);
|
||||
|
||||
if (e.shiftKey) {
|
||||
if (active === first || active === this.box) {
|
||||
|
|
|
|||
|
|
@ -30,6 +30,8 @@ export default class Tooltip {
|
|||
pageHeight!: number;
|
||||
globalOffsetX!: number;
|
||||
globalOffsetY!: number;
|
||||
renderOffsetX!: number;
|
||||
renderOffsetY!: number;
|
||||
maxWidth?: number;
|
||||
backgroundColor?: string;
|
||||
borderRadius?: number;
|
||||
|
|
@ -224,7 +226,7 @@ export default class Tooltip {
|
|||
// Layout bounds may be computed against a player container while the
|
||||
// tooltip itself is mounted into a portal attached elsewhere in the DOM.
|
||||
// We therefore calculate in layout-root coordinates and later convert
|
||||
// back to viewport coordinates for the rendered tooltip.
|
||||
// back into the tooltip parent's coordinate space before rendering.
|
||||
if (this.layoutRoot === document.documentElement) {
|
||||
this.globalOffsetX = 0;
|
||||
this.globalOffsetY = 0;
|
||||
|
|
@ -234,6 +236,11 @@ export default class Tooltip {
|
|||
this.globalOffsetY = top;
|
||||
}
|
||||
|
||||
const { left: parentLeft, top: parentTop } =
|
||||
this.parentElement.getBoundingClientRect();
|
||||
this.renderOffsetX = parentLeft;
|
||||
this.renderOffsetY = parentTop;
|
||||
|
||||
this.pageWidth =
|
||||
this.layoutRoot.clientWidth || document.documentElement.clientWidth;
|
||||
this.pageHeight =
|
||||
|
|
@ -423,8 +430,8 @@ export default class Tooltip {
|
|||
|
||||
this.position = resolvedPosition;
|
||||
return {
|
||||
top: coords.top + this.globalOffsetY,
|
||||
left: coords.left + this.globalOffsetX,
|
||||
top: coords.top + this.globalOffsetY - this.renderOffsetY,
|
||||
left: coords.left + this.globalOffsetX - this.renderOffsetX,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,9 +9,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 ui from "../ui";
|
||||
import debug from "../utils/debug";
|
||||
import { resolveScopedFullscreenElement } from "../utils/dom";
|
||||
import { downloadTranslation } from "../utils/download";
|
||||
import { GM_fetch } from "../utils/gm";
|
||||
import type { IntervalIdleChecker } from "../utils/intervalIdleChecker";
|
||||
|
|
@ -23,6 +21,12 @@ import {
|
|||
downloadBlob,
|
||||
} from "../utils/utils";
|
||||
import { applyOverlayMountUpdate } from "./mount";
|
||||
import {
|
||||
createShadowMount,
|
||||
destroyShadowMount,
|
||||
reparentShadowMount,
|
||||
type ShadowMount,
|
||||
} from "./shadowMount";
|
||||
import { handleTranslationButtonCommand } from "./translationCommands";
|
||||
import { OverlayView } from "./views/overlay";
|
||||
import { SettingsView } from "./views/settings";
|
||||
|
|
@ -36,6 +40,7 @@ export class UIManager {
|
|||
data: Partial<StorageData>;
|
||||
|
||||
votGlobalPortal?: HTMLElement;
|
||||
private globalPortalMount?: ShadowMount;
|
||||
/**
|
||||
* Contains all elements over video player e.g. button, menu and etc
|
||||
*/
|
||||
|
|
@ -65,8 +70,8 @@ export class UIManager {
|
|||
return this.mount.portalContainer;
|
||||
}
|
||||
|
||||
get tooltipLayoutRoot(): HTMLElement | undefined {
|
||||
return this.mount.tooltipLayoutRoot;
|
||||
getSubtitlesMountContainer(): HTMLElement {
|
||||
return this.votOverlayView?.root ?? this.mount.subtitlesMountContainer;
|
||||
}
|
||||
|
||||
isInitialized(): this is {
|
||||
|
|
@ -84,8 +89,11 @@ export class UIManager {
|
|||
|
||||
this.initialized = true;
|
||||
|
||||
this.votGlobalPortal = ui.createPortal();
|
||||
this.getGlobalPortalHost(this.mount).appendChild(this.votGlobalPortal);
|
||||
this.globalPortalMount = createShadowMount({
|
||||
parent: this.getGlobalPortalHost(this.mount),
|
||||
rootClasses: ["vot-portal"],
|
||||
});
|
||||
this.votGlobalPortal = this.globalPortalMount.root;
|
||||
|
||||
this.votOverlayView = new OverlayView({
|
||||
mount: this.mount,
|
||||
|
|
@ -105,22 +113,25 @@ export class UIManager {
|
|||
});
|
||||
this.votSettingsView.initUI();
|
||||
|
||||
this.videoHandler?.subtitlesWidget?.updateMount({
|
||||
container: this.getSubtitlesMountContainer(),
|
||||
});
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
updateMount(mount: OverlayMount) {
|
||||
const globalPortalHost = this.getGlobalPortalHost(mount);
|
||||
if (this.votGlobalPortal?.parentElement !== globalPortalHost) {
|
||||
globalPortalHost.appendChild(this.votGlobalPortal);
|
||||
}
|
||||
reparentShadowMount(
|
||||
this.globalPortalMount,
|
||||
this.getGlobalPortalHost(mount),
|
||||
);
|
||||
|
||||
this.mount = applyOverlayMountUpdate(this.mount, mount, (nextMount) => {
|
||||
this.votOverlayView?.updateMount(nextMount);
|
||||
});
|
||||
|
||||
this.videoHandler?.subtitlesWidget?.updateMount({
|
||||
container: mount.subtitlesMountContainer,
|
||||
tooltipLayoutRoot: mount.tooltipLayoutRoot,
|
||||
container: this.getSubtitlesMountContainer(),
|
||||
});
|
||||
|
||||
return this;
|
||||
|
|
@ -131,11 +142,11 @@ export class UIManager {
|
|||
webkitFullscreenElement?: Element | null;
|
||||
};
|
||||
const fullscreenEl = doc.fullscreenElement ?? doc.webkitFullscreenElement;
|
||||
const isCurrentVideoFullscreen = Boolean(
|
||||
resolveScopedFullscreenElement(fullscreenEl, [mount.root], {
|
||||
allowDocumentViewport: true,
|
||||
}),
|
||||
);
|
||||
const isCurrentVideoFullscreen =
|
||||
fullscreenEl instanceof HTMLElement &&
|
||||
(fullscreenEl === mount.root ||
|
||||
fullscreenEl.contains(mount.root) ||
|
||||
mount.root.contains(fullscreenEl));
|
||||
return isCurrentVideoFullscreen ? mount.root : document.documentElement;
|
||||
}
|
||||
|
||||
|
|
@ -167,11 +178,13 @@ export class UIManager {
|
|||
}
|
||||
|
||||
try {
|
||||
const isPiPActive =
|
||||
this.videoHandler.video === document.pictureInPictureElement;
|
||||
await (isPiPActive
|
||||
? document.exitPictureInPicture()
|
||||
: this.videoHandler.video.requestPictureInPicture());
|
||||
// this.videoHandler.video.disablePictureInPicture = false;
|
||||
const inPiP = document.pictureInPictureElement != null;
|
||||
if (inPiP) {
|
||||
await document.exitPictureInPicture();
|
||||
} else {
|
||||
await this.videoHandler.video.requestPictureInPicture();
|
||||
}
|
||||
} catch (err) {
|
||||
debug.warn("[VOT] Failed to toggle Picture-in-Picture", err);
|
||||
}
|
||||
|
|
@ -675,7 +688,9 @@ export class UIManager {
|
|||
// Each view is now idempotent and releases events before DOM.
|
||||
this.votOverlayView.release();
|
||||
this.votSettingsView.release();
|
||||
this.votGlobalPortal.remove();
|
||||
destroyShadowMount(this.globalPortalMount);
|
||||
this.globalPortalMount = undefined;
|
||||
this.votGlobalPortal = undefined;
|
||||
|
||||
this.initialized = false;
|
||||
return this;
|
||||
|
|
|
|||
|
|
@ -13,8 +13,7 @@ export function isSameOverlayMount(
|
|||
return (
|
||||
previous.root === next.root &&
|
||||
previous.portalContainer === next.portalContainer &&
|
||||
previous.subtitlesMountContainer === next.subtitlesMountContainer &&
|
||||
previous.tooltipLayoutRoot === next.tooltipLayoutRoot
|
||||
previous.subtitlesMountContainer === next.subtitlesMountContainer
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -34,17 +33,3 @@ export function applyOverlayMountUpdate(
|
|||
onChanged(next);
|
||||
return next;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tooltip geometry must be refreshed when either the button/root is reparented
|
||||
* or the layout root itself changes.
|
||||
*/
|
||||
export function didTooltipMountContextChange(
|
||||
previous: OverlayMount,
|
||||
next: OverlayMount,
|
||||
): boolean {
|
||||
return (
|
||||
previous.root !== next.root ||
|
||||
previous.tooltipLayoutRoot !== next.tooltipLayoutRoot
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import debug from "../utils/debug";
|
||||
import { containsCrossShadow, getDeepActiveElement } from "../utils/dom";
|
||||
import type { IntervalIdleChecker } from "../utils/intervalIdleChecker";
|
||||
import type { OverlayView } from "./views/overlay";
|
||||
|
||||
|
|
@ -140,7 +141,7 @@ export class OverlayVisibilityController {
|
|||
|
||||
if (
|
||||
relatedNode &&
|
||||
(currentNode?.contains(relatedNode) ||
|
||||
((currentNode && containsCrossShadow(currentNode, relatedNode)) ||
|
||||
this.deps.isInteractiveNode(relatedNode))
|
||||
) {
|
||||
return;
|
||||
|
|
@ -164,7 +165,7 @@ export class OverlayVisibilityController {
|
|||
typeof document !== "undefined" &&
|
||||
typeof document.hasFocus === "function";
|
||||
if (canCheckFocus && document.hasFocus()) {
|
||||
active = document.activeElement;
|
||||
active = getDeepActiveElement(document);
|
||||
}
|
||||
if (active && this.deps.isInteractiveNode(active)) {
|
||||
debug.log("[OverlayVisibility] skip hide (focus inside overlay)");
|
||||
|
|
|
|||
142
src/ui/shadowMount.ts
Normal file
142
src/ui/shadowMount.ts
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
import mainScss from "../styles/main.scss?inline";
|
||||
|
||||
type InlineStyleMap = Partial<Record<string, string>>;
|
||||
|
||||
export type ShadowMount = {
|
||||
host: HTMLElement;
|
||||
root: HTMLElement;
|
||||
shadowRoot: ShadowRoot;
|
||||
};
|
||||
|
||||
type CreateShadowMountOptions = {
|
||||
parent: HTMLElement;
|
||||
hostTag?: string;
|
||||
rootTag?: string;
|
||||
hostClasses?: string[];
|
||||
rootClasses?: string[];
|
||||
hostStyles?: InlineStyleMap;
|
||||
rootStyles?: InlineStyleMap;
|
||||
delegatesFocus?: boolean;
|
||||
};
|
||||
|
||||
const shadowScopedCssText = scopeCssForShadowRoots(mainScss);
|
||||
let sharedShadowStyleSheet: CSSStyleSheet | null | undefined;
|
||||
|
||||
function scopeCssForShadowRoots(cssText: string): string {
|
||||
return cssText
|
||||
.replace(/:root\b/g, ":host")
|
||||
.replace(/html\.vot-keyboard-nav/g, ":host-context(.vot-keyboard-nav)")
|
||||
.replace(/:fullscreen(?=\s|,)/g, ":host-context(:fullscreen)")
|
||||
.replace(
|
||||
/:-webkit-full-screen(?=\s|,)/g,
|
||||
":host-context(:-webkit-full-screen)",
|
||||
);
|
||||
}
|
||||
|
||||
function applyInlineStyles(
|
||||
element: HTMLElement,
|
||||
styles: InlineStyleMap | undefined,
|
||||
): void {
|
||||
if (!styles) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const [name, value] of Object.entries(styles)) {
|
||||
if (typeof value !== "string") {
|
||||
continue;
|
||||
}
|
||||
element.style.setProperty(name, value);
|
||||
}
|
||||
}
|
||||
|
||||
function getSharedShadowStyleSheet(): CSSStyleSheet | null {
|
||||
if (sharedShadowStyleSheet !== undefined) {
|
||||
return sharedShadowStyleSheet;
|
||||
}
|
||||
|
||||
const canUseConstructableSheets =
|
||||
typeof CSSStyleSheet !== "undefined" &&
|
||||
typeof CSSStyleSheet.prototype.replaceSync === "function";
|
||||
|
||||
if (!canUseConstructableSheets) {
|
||||
sharedShadowStyleSheet = null;
|
||||
return sharedShadowStyleSheet;
|
||||
}
|
||||
|
||||
const sheet = new CSSStyleSheet();
|
||||
sheet.replaceSync(shadowScopedCssText);
|
||||
sharedShadowStyleSheet = sheet;
|
||||
return sharedShadowStyleSheet;
|
||||
}
|
||||
|
||||
function adoptScopedStyles(shadowRoot: ShadowRoot): void {
|
||||
const sharedSheet = getSharedShadowStyleSheet();
|
||||
if (sharedSheet) {
|
||||
if (!shadowRoot.adoptedStyleSheets.includes(sharedSheet)) {
|
||||
shadowRoot.adoptedStyleSheets = [
|
||||
...shadowRoot.adoptedStyleSheets,
|
||||
sharedSheet,
|
||||
];
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const style = document.createElement("style");
|
||||
style.textContent = shadowScopedCssText;
|
||||
shadowRoot.append(style);
|
||||
}
|
||||
|
||||
export function createShadowMount({
|
||||
parent,
|
||||
hostTag = "vot-shadow-host",
|
||||
rootTag = "vot-block",
|
||||
hostClasses = [],
|
||||
rootClasses = [],
|
||||
hostStyles,
|
||||
rootStyles,
|
||||
delegatesFocus = false,
|
||||
}: CreateShadowMountOptions): ShadowMount {
|
||||
const host = document.createElement(hostTag);
|
||||
if (hostClasses.length > 0) {
|
||||
host.classList.add(...hostClasses);
|
||||
}
|
||||
applyInlineStyles(host, hostStyles);
|
||||
|
||||
const shadowRoot = host.attachShadow({
|
||||
mode: "open",
|
||||
delegatesFocus,
|
||||
});
|
||||
adoptScopedStyles(shadowRoot);
|
||||
|
||||
const root = document.createElement(rootTag);
|
||||
if (rootClasses.length > 0) {
|
||||
root.classList.add(...rootClasses);
|
||||
}
|
||||
applyInlineStyles(root, rootStyles);
|
||||
shadowRoot.append(root);
|
||||
|
||||
parent.append(host);
|
||||
|
||||
return {
|
||||
host,
|
||||
root,
|
||||
shadowRoot,
|
||||
};
|
||||
}
|
||||
|
||||
export function reparentShadowMount(
|
||||
mount: ShadowMount | undefined,
|
||||
parent: HTMLElement,
|
||||
): void {
|
||||
if (!mount) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (mount.host.parentElement !== parent) {
|
||||
parent.append(mount.host);
|
||||
}
|
||||
}
|
||||
|
||||
export function destroyShadowMount(mount: ShadowMount | undefined): void {
|
||||
mount?.host.remove();
|
||||
}
|
||||
|
|
@ -25,7 +25,12 @@ import Tooltip from "../components/tooltip";
|
|||
import VOTButton from "../components/votButton";
|
||||
import VOTMenu from "../components/votMenu";
|
||||
import { SETTINGS_ICON, SUBTITLES_ICON } from "./../icons";
|
||||
import { didTooltipMountContextChange } from "../mount";
|
||||
import {
|
||||
createShadowMount,
|
||||
destroyShadowMount,
|
||||
reparentShadowMount,
|
||||
type ShadowMount,
|
||||
} from "../shadowMount";
|
||||
|
||||
export class OverlayView {
|
||||
private static readonly BIG_CONTAINER_WIDTH_PX = 550;
|
||||
|
|
@ -52,6 +57,7 @@ export class OverlayView {
|
|||
private readonly data: Partial<StorageData>;
|
||||
private readonly videoHandler?: VideoHandler;
|
||||
private readonly intervalIdleChecker: IntervalIdleChecker;
|
||||
private overlayMount?: ShadowMount;
|
||||
|
||||
private readonly events: {
|
||||
[K in keyof OverlayViewEventMap]: EventImpl<OverlayViewEventMap[K]>;
|
||||
|
|
@ -113,26 +119,20 @@ export class OverlayView {
|
|||
}
|
||||
|
||||
get root(): HTMLElement {
|
||||
return this.mount.root;
|
||||
return this.overlayMount?.root ?? this.mount.root;
|
||||
}
|
||||
|
||||
get portalContainer(): HTMLElement {
|
||||
return this.mount.portalContainer;
|
||||
}
|
||||
|
||||
get tooltipLayoutRoot(): HTMLElement | undefined {
|
||||
return this.mount.tooltipLayoutRoot;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update mount points (root/tooltipLayoutRoot) when the player container changes.
|
||||
* Update mount points when the player container changes.
|
||||
* Moves already-mounted UI nodes and rebinds root-bound listeners (dragging).
|
||||
*/
|
||||
updateMount(nextMount: OverlayMount): this {
|
||||
const prevRoot = this.mount.root;
|
||||
const nextRoot = nextMount.root;
|
||||
const prevTooltipRoot = this.mount.tooltipLayoutRoot;
|
||||
const nextTooltipRoot = nextMount.tooltipLayoutRoot;
|
||||
|
||||
this.mount = nextMount;
|
||||
|
||||
|
|
@ -140,34 +140,23 @@ export class OverlayView {
|
|||
return this;
|
||||
}
|
||||
|
||||
// Move mounted nodes to new containers.
|
||||
if (prevRoot !== nextRoot) {
|
||||
if (this.votButton) {
|
||||
nextRoot.appendChild(this.votButton.container);
|
||||
}
|
||||
if (this.votMenu) {
|
||||
nextRoot.appendChild(this.votMenu.container);
|
||||
if (this.overlayMount) {
|
||||
reparentShadowMount(this.overlayMount, nextRoot);
|
||||
} else {
|
||||
if (this.votButton) {
|
||||
nextRoot.appendChild(this.votButton.container);
|
||||
}
|
||||
if (this.votMenu) {
|
||||
nextRoot.appendChild(this.votMenu.container);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Tooltip geometry depends on both the layout root and the overlay root:
|
||||
// some fullscreen transitions only reparent the button/root while keeping
|
||||
// the same tooltipLayoutRoot, so force a refresh for either change.
|
||||
if (
|
||||
this.votButtonTooltip &&
|
||||
didTooltipMountContextChange(
|
||||
{
|
||||
root: prevRoot,
|
||||
portalContainer: this.mount.portalContainer,
|
||||
subtitlesMountContainer: this.mount.subtitlesMountContainer,
|
||||
tooltipLayoutRoot: prevTooltipRoot,
|
||||
},
|
||||
nextMount,
|
||||
)
|
||||
) {
|
||||
// If tooltipLayoutRoot becomes undefined, fall back to documentElement.
|
||||
if (this.votButtonTooltip && prevRoot !== nextRoot) {
|
||||
this.votButtonTooltip.updateMount({
|
||||
layoutRoot: nextTooltipRoot ?? document.documentElement,
|
||||
parentElement: this.root,
|
||||
layoutRoot: this.overlayMount?.host,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -256,6 +245,23 @@ export class OverlayView {
|
|||
}
|
||||
|
||||
this.initialized = true;
|
||||
this.overlayMount = createShadowMount({
|
||||
parent: this.mount.root,
|
||||
rootClasses: ["vot-overlay-root"],
|
||||
hostStyles: {
|
||||
position: "absolute",
|
||||
inset: "0",
|
||||
display: "block",
|
||||
"pointer-events": "none",
|
||||
},
|
||||
rootStyles: {
|
||||
position: "relative",
|
||||
display: "block",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
"pointer-events": "none",
|
||||
},
|
||||
});
|
||||
|
||||
// #region Shared logic
|
||||
const { position, direction } = this.calcButtonLayout(buttonPosition);
|
||||
|
|
@ -282,8 +288,8 @@ export class OverlayView {
|
|||
autoLayout: false,
|
||||
hidden: direction === "row",
|
||||
bordered: false,
|
||||
parentElement: this.globalPortal,
|
||||
layoutRoot: this.tooltipLayoutRoot,
|
||||
parentElement: this.root,
|
||||
layoutRoot: this.overlayMount.host,
|
||||
});
|
||||
|
||||
// #endregion VOT Button
|
||||
|
|
@ -598,9 +604,11 @@ export class OverlayView {
|
|||
|
||||
// Keep menu open while interacting with dialogs spawned from it
|
||||
// (language picker, etc.).
|
||||
const isInsideDialog =
|
||||
target instanceof HTMLElement &&
|
||||
!!target.closest(".vot-dialog-container");
|
||||
const isInsideDialog = path.some(
|
||||
(node) =>
|
||||
node instanceof HTMLElement &&
|
||||
node.classList.contains("vot-dialog-container"),
|
||||
);
|
||||
|
||||
if (
|
||||
isInsideMenu ||
|
||||
|
|
@ -997,6 +1005,8 @@ export class OverlayView {
|
|||
this.votButton?.remove();
|
||||
this.votMenu?.remove();
|
||||
this.votButtonTooltip?.release();
|
||||
destroyShadowMount(this.overlayMount);
|
||||
this.overlayMount = undefined;
|
||||
}
|
||||
|
||||
private doReleaseUIEvents(): void {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ type DebugMethod = (...text: unknown[]) => void;
|
|||
|
||||
const noop: DebugMethod = () => {};
|
||||
|
||||
const log: DebugMethod = DEBUG_MODE
|
||||
const log: DebugMethod = !DEBUG_MODE
|
||||
? (...text: unknown[]) => {
|
||||
console.log(
|
||||
"%c[VOT DEBUG]",
|
||||
|
|
@ -12,7 +12,7 @@ const log: DebugMethod = DEBUG_MODE
|
|||
}
|
||||
: noop;
|
||||
|
||||
const warn: DebugMethod = DEBUG_MODE
|
||||
const warn: DebugMethod = !DEBUG_MODE
|
||||
? (...text: unknown[]) => {
|
||||
console.warn(
|
||||
"%c[VOT DEBUG]",
|
||||
|
|
@ -22,7 +22,7 @@ const warn: DebugMethod = DEBUG_MODE
|
|||
}
|
||||
: noop;
|
||||
|
||||
const error: DebugMethod = DEBUG_MODE
|
||||
const error: DebugMethod = !DEBUG_MODE
|
||||
? (...text: unknown[]) => {
|
||||
console.error(
|
||||
"%c[VOT DEBUG]",
|
||||
|
|
|
|||
|
|
@ -8,6 +8,22 @@ function getComposableParent(node: Node | null): Node | null {
|
|||
return node.parentNode ?? null;
|
||||
}
|
||||
|
||||
export function getDeepActiveElement(
|
||||
root: Document | ShadowRoot = document,
|
||||
): Element | null {
|
||||
let activeElement: Element | null = root.activeElement;
|
||||
|
||||
while (activeElement instanceof HTMLElement && activeElement.shadowRoot) {
|
||||
const nestedActiveElement = activeElement.shadowRoot.activeElement;
|
||||
if (!nestedActiveElement) {
|
||||
break;
|
||||
}
|
||||
activeElement = nestedActiveElement;
|
||||
}
|
||||
|
||||
return activeElement;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether `target` is a descendant of `container` in the composed tree
|
||||
* (crossing ShadowRoot boundaries via hosts).
|
||||
|
|
|
|||
325
src/utils/gm.ts
325
src/utils/gm.ts
|
|
@ -1,5 +1,3 @@
|
|||
// Minimal HTTP method type for GM_xmlhttpRequest compatibility.
|
||||
// (Avoid pulling external typings just for this union.)
|
||||
type HttpMethod =
|
||||
| "GET"
|
||||
| "POST"
|
||||
|
|
@ -13,6 +11,7 @@ type HttpMethod =
|
|||
import { executeWithResponseCache } from "../core/cacheManager";
|
||||
import type { FetchOpts } from "../types/utils/gm";
|
||||
import { createTimeoutSignal } from "./abort";
|
||||
import { browserInfo } from "./browserInfo";
|
||||
import debug from "./debug";
|
||||
import { getErrorMessage, isAbortError, makeAbortError } from "./errors";
|
||||
import { getHeaders } from "./utils";
|
||||
|
|
@ -20,11 +19,13 @@ import { getHeaders } from "./utils";
|
|||
const YANDEX_API_HOST = "api.browser.yandex.ru";
|
||||
const GOOGLEVIDEO_HOST_SUFFIX = "googlevideo.com";
|
||||
const HEADER_LINE_RE = /^([\w-]+):\s*(.+)$/;
|
||||
// Matches statusText reason-phrase: printable ASCII except control chars
|
||||
const URL_SCHEME_RE = /^[a-zA-Z][a-zA-Z\d+.-]*:/;
|
||||
|
||||
type RequestUrlLike = string | URL | Request;
|
||||
type GmXhrResponse = {
|
||||
finalUrl?: string;
|
||||
response?: Blob;
|
||||
response?: Blob | null;
|
||||
responseHeaders?: string;
|
||||
status?: number;
|
||||
statusText?: string;
|
||||
|
|
@ -61,8 +62,8 @@ function hasSupportedGmXhr(): boolean {
|
|||
|
||||
export const isProxyOnlyExtension =
|
||||
!(typeof IS_EXTENSION !== "undefined" && IS_EXTENSION) &&
|
||||
!!scriptHandler &&
|
||||
!hasSupportedGmXhr();
|
||||
(browserInfo.browser?.name === "Safari" ||
|
||||
!["Tampermonkey", "Violentmonkey"].includes(scriptHandler));
|
||||
export const isSupportGM4 =
|
||||
typeof GM !== "undefined" || (globalThis as any).GM !== undefined;
|
||||
export const isSupportGMXhr = hasSupportedGmXhr();
|
||||
|
|
@ -75,9 +76,7 @@ function getRequestHost(url: string): string | undefined {
|
|||
if (!URL_SCHEME_RE.test(normalizedUrl)) {
|
||||
try {
|
||||
return new URL(`https://${normalizedUrl}`).hostname.toLowerCase();
|
||||
} catch {
|
||||
// fall through
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
|
@ -104,8 +103,6 @@ function shouldUseGmXhr(
|
|||
);
|
||||
}
|
||||
|
||||
// These endpoints are routinely blocked by page-world CORS. Going through
|
||||
// native fetch first only adds noisy console errors and an extra failed hop.
|
||||
return (
|
||||
isHostOrSubdomain(host, YANDEX_API_HOST) ||
|
||||
isHostOrSubdomain(host, GOOGLEVIDEO_HOST_SUFFIX)
|
||||
|
|
@ -155,65 +152,87 @@ function getGmXhrErrorMessage(error: unknown): string {
|
|||
error?: unknown;
|
||||
statusText?: unknown;
|
||||
};
|
||||
if (typeof maybeError?.error === "string") {
|
||||
if (
|
||||
typeof maybeError?.error === "string" &&
|
||||
maybeError.error.trim().length > 0
|
||||
) {
|
||||
return maybeError.error;
|
||||
}
|
||||
if (typeof maybeError?.statusText === "string") {
|
||||
if (
|
||||
typeof maybeError?.statusText === "string" &&
|
||||
maybeError.statusText.trim().length > 0 &&
|
||||
maybeError.statusText !== '""' &&
|
||||
maybeError.statusText !== "''"
|
||||
) {
|
||||
return maybeError.statusText;
|
||||
}
|
||||
|
||||
return getErrorMessage(error) || "Unknown error";
|
||||
return getErrorMessage(error) || "Unknown GM XHR error";
|
||||
}
|
||||
|
||||
async function gmXhrFetch(
|
||||
function buildResponse(resp: GmXhrResponse, urlStr: string): Response {
|
||||
const status = resp.status;
|
||||
const statusText = typeof resp.statusText === "string" ? resp.statusText : "";
|
||||
const body = resp.response instanceof Blob ? resp.response : null;
|
||||
const responseHeaders = parseResponseHeaders(resp.responseHeaders);
|
||||
|
||||
const response = new Response(body, {
|
||||
status,
|
||||
statusText,
|
||||
headers: responseHeaders,
|
||||
});
|
||||
|
||||
Object.defineProperty(response, "url", {
|
||||
value: resp.finalUrl ?? urlStr,
|
||||
});
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
async function executeCallbackGmXhr(
|
||||
gmXhr: GmXhrCallbackApi,
|
||||
urlStr: string,
|
||||
timeout: number,
|
||||
fetchOptions: Omit<FetchOpts, "timeout">,
|
||||
method: string,
|
||||
headers: Record<string, string>,
|
||||
): Promise<Response> {
|
||||
<<<<<<< Updated upstream
|
||||
const headers = getHeaders(fetchOptions.headers);
|
||||
const callbackGmXhr = getCallbackGmXhr();
|
||||
const promiseGmXhr = getPromiseGmXhr();
|
||||
=======
|
||||
return new Promise((resolve, reject) => {
|
||||
let settled = false;
|
||||
let onAbort: (() => void) | undefined;
|
||||
>>>>>>> Stashed changes
|
||||
|
||||
if (callbackGmXhr) {
|
||||
return await new Promise((resolve, reject) => {
|
||||
let settled = false;
|
||||
let onAbort: (() => void) | undefined;
|
||||
const cleanupAbort = () => {
|
||||
if (onAbort) {
|
||||
fetchOptions.signal?.removeEventListener("abort", onAbort);
|
||||
}
|
||||
};
|
||||
const failOnce = (error: Error) => {
|
||||
const cleanupAbort = () => {
|
||||
if (onAbort) {
|
||||
fetchOptions.signal?.removeEventListener("abort", onAbort);
|
||||
}
|
||||
};
|
||||
|
||||
const failOnce = (error: Error) => {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
cleanupAbort();
|
||||
reject(error);
|
||||
};
|
||||
|
||||
const request = gmXhr({
|
||||
method: method as HttpMethod,
|
||||
url: urlStr,
|
||||
responseType: "blob" as any,
|
||||
data: fetchOptions.body as any,
|
||||
timeout,
|
||||
headers,
|
||||
onload: (resp: GmXhrResponse) => {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
cleanupAbort();
|
||||
reject(error);
|
||||
};
|
||||
|
||||
const request = callbackGmXhr({
|
||||
method: (fetchOptions.method || "GET") as HttpMethod,
|
||||
url: urlStr,
|
||||
responseType: "blob" as any,
|
||||
data: fetchOptions.body as any,
|
||||
timeout,
|
||||
headers,
|
||||
onload: (resp: GmXhrResponse) => {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
cleanupAbort();
|
||||
const responseHeaders = parseResponseHeaders(resp.responseHeaders);
|
||||
|
||||
const response = new Response(resp.response as Blob, {
|
||||
status: resp.status,
|
||||
statusText:
|
||||
typeof resp.statusText === "string" ? resp.statusText : "",
|
||||
headers: responseHeaders,
|
||||
});
|
||||
|
||||
Object.defineProperty(response, "url", {
|
||||
value: resp.finalUrl ?? urlStr,
|
||||
});
|
||||
|
||||
<<<<<<< Updated upstream
|
||||
resolve(response);
|
||||
},
|
||||
ontimeout: () => failOnce(new Error("Timeout")),
|
||||
|
|
@ -227,26 +246,92 @@ async function gmXhrFetch(
|
|||
request?.abort?.();
|
||||
} catch {
|
||||
// ignore abort races
|
||||
=======
|
||||
try {
|
||||
const response = buildResponse(resp, urlStr);
|
||||
debug.log("[GM_fetch] GM_xmlhttpRequest completed", {
|
||||
url: response.url,
|
||||
method,
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
});
|
||||
resolve(response);
|
||||
} catch (buildErr) {
|
||||
// Constructing Response failed even after sanitization — surface error
|
||||
debug.warn("[GM_fetch] GM_xmlhttpRequest response build failed", {
|
||||
url: urlStr,
|
||||
method,
|
||||
error: getErrorMessage(buildErr),
|
||||
rawStatus: resp.status,
|
||||
rawStatusText: resp.statusText,
|
||||
});
|
||||
reject(
|
||||
buildErr instanceof Error
|
||||
? buildErr
|
||||
: new Error(getErrorMessage(buildErr)),
|
||||
);
|
||||
>>>>>>> Stashed changes
|
||||
}
|
||||
},
|
||||
ontimeout: () => {
|
||||
debug.warn("[GM_fetch] GM_xmlhttpRequest timed out", {
|
||||
url: urlStr,
|
||||
method,
|
||||
timeout,
|
||||
});
|
||||
failOnce(new Error("Timeout"));
|
||||
},
|
||||
onerror: (error: unknown) => {
|
||||
// Safari can deliver an empty-string error — fall back to generic message
|
||||
const message = getGmXhrErrorMessage(error);
|
||||
debug.warn("[GM_fetch] GM_xmlhttpRequest failed", {
|
||||
url: urlStr,
|
||||
method,
|
||||
error: message,
|
||||
});
|
||||
failOnce(new Error(message));
|
||||
},
|
||||
onabort: () => {
|
||||
debug.warn("[GM_fetch] GM_xmlhttpRequest aborted", {
|
||||
url: urlStr,
|
||||
method,
|
||||
});
|
||||
failOnce(makeAbortError());
|
||||
};
|
||||
|
||||
if (fetchOptions.signal) {
|
||||
fetchOptions.signal.addEventListener("abort", onAbort, { once: true });
|
||||
if (fetchOptions.signal.aborted) {
|
||||
onAbort();
|
||||
return;
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (!promiseGmXhr) {
|
||||
throw new TypeError("GM_xmlhttpRequest is not available");
|
||||
}
|
||||
onAbort = () => {
|
||||
try {
|
||||
request?.abort?.();
|
||||
} catch {}
|
||||
failOnce(makeAbortError());
|
||||
};
|
||||
|
||||
<<<<<<< Updated upstream
|
||||
const request = promiseGmXhr({
|
||||
method: (fetchOptions.method || "GET") as HttpMethod,
|
||||
=======
|
||||
if (fetchOptions.signal) {
|
||||
fetchOptions.signal.addEventListener("abort", onAbort, { once: true });
|
||||
if (fetchOptions.signal.aborted) {
|
||||
onAbort();
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function executePromiseGmXhr(
|
||||
gmXhr: GmXhrPromiseApi,
|
||||
urlStr: string,
|
||||
timeout: number,
|
||||
fetchOptions: Omit<FetchOpts, "timeout">,
|
||||
method: string,
|
||||
headers: Record<string, string>,
|
||||
): Promise<Response> {
|
||||
const request = gmXhr({
|
||||
method: method as HttpMethod,
|
||||
>>>>>>> Stashed changes
|
||||
url: urlStr,
|
||||
responseType: "blob" as any,
|
||||
data: fetchOptions.body as any,
|
||||
|
|
@ -264,9 +349,7 @@ async function gmXhrFetch(
|
|||
abortHandler = () => {
|
||||
try {
|
||||
request.abort?.();
|
||||
} catch {
|
||||
// ignore abort races
|
||||
}
|
||||
} catch {}
|
||||
reject(makeAbortError());
|
||||
};
|
||||
|
||||
|
|
@ -279,17 +362,8 @@ async function gmXhrFetch(
|
|||
});
|
||||
|
||||
const resp = (await Promise.race([request, abortPromise])) as GmXhrResponse;
|
||||
const responseHeaders = parseResponseHeaders(resp.responseHeaders);
|
||||
|
||||
const response = new Response(resp.response as Blob, {
|
||||
status: resp.status,
|
||||
statusText: typeof resp.statusText === "string" ? resp.statusText : "",
|
||||
headers: responseHeaders,
|
||||
});
|
||||
|
||||
Object.defineProperty(response, "url", {
|
||||
value: resp.finalUrl ?? urlStr,
|
||||
});
|
||||
const response = buildResponse(resp, urlStr);
|
||||
|
||||
return response;
|
||||
} finally {
|
||||
|
|
@ -299,6 +373,68 @@ async function gmXhrFetch(
|
|||
}
|
||||
}
|
||||
|
||||
async function gmXhrFetch(
|
||||
urlStr: string,
|
||||
timeout: number,
|
||||
fetchOptions: Omit<FetchOpts, "timeout">,
|
||||
): Promise<Response> {
|
||||
const headers = getHeaders(fetchOptions.headers);
|
||||
const method = (fetchOptions.method || "GET").toUpperCase();
|
||||
debug.log("[GM_fetch] GM_xmlhttpRequest start", {
|
||||
url: urlStr,
|
||||
method,
|
||||
timeout,
|
||||
headerCount: Object.keys(headers).length,
|
||||
});
|
||||
|
||||
const callbackGmXhr = getCallbackGmXhr();
|
||||
if (callbackGmXhr) {
|
||||
debug.log("[GM_fetch] attempting callback-style GM_xmlhttpRequest");
|
||||
try {
|
||||
return await executeCallbackGmXhr(
|
||||
callbackGmXhr,
|
||||
urlStr,
|
||||
timeout,
|
||||
fetchOptions,
|
||||
method,
|
||||
headers,
|
||||
);
|
||||
} catch (error) {
|
||||
if (isAbortError(error)) throw error;
|
||||
debug.warn("[GM_fetch] callback-style GM_xmlhttpRequest failed", {
|
||||
url: urlStr,
|
||||
method,
|
||||
error: getGmXhrErrorMessage(error),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const promiseGmXhr = getPromiseGmXhr();
|
||||
if (promiseGmXhr) {
|
||||
debug.log("[GM_fetch] attempting promise-style GM.xmlHttpRequest");
|
||||
try {
|
||||
return await executePromiseGmXhr(
|
||||
promiseGmXhr,
|
||||
urlStr,
|
||||
timeout,
|
||||
fetchOptions,
|
||||
method,
|
||||
headers,
|
||||
);
|
||||
} catch (error) {
|
||||
if (isAbortError(error)) throw error;
|
||||
debug.warn("[GM_fetch] promise-style GM.xmlHttpRequest failed", {
|
||||
url: urlStr,
|
||||
method,
|
||||
error: getGmXhrErrorMessage(error),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
debug.warn("[GM_fetch] none of the GM approaches worked");
|
||||
throw new Error("All GM approaches failed");
|
||||
}
|
||||
|
||||
export async function GM_fetch(
|
||||
url: RequestUrlLike,
|
||||
opts: FetchOpts = {},
|
||||
|
|
@ -320,7 +456,33 @@ export async function GM_fetch(
|
|||
reason: forceGmXhr ? "forced" : "host-policy",
|
||||
url: urlStr,
|
||||
});
|
||||
return await gmXhrFetch(urlStr, timeout, fetchOptions);
|
||||
try {
|
||||
return await gmXhrFetch(urlStr, timeout, fetchOptions);
|
||||
} catch (err) {
|
||||
if (isAbortError(err)) throw err;
|
||||
if (forceGmXhr || shouldUseGmXhr(host, urlStr)) throw err;
|
||||
debug.warn(
|
||||
"[GM_fetch] all GM approaches failed, falling back to native fetch",
|
||||
{
|
||||
url: urlStr,
|
||||
method,
|
||||
host: host ?? "unknown",
|
||||
error: getErrorMessage(err) || "Unknown error",
|
||||
},
|
||||
);
|
||||
const { signal, cleanup } = createTimeoutSignal(
|
||||
timeout,
|
||||
fetchOptions.signal,
|
||||
);
|
||||
try {
|
||||
return await fetch(url, {
|
||||
...fetchOptions,
|
||||
signal,
|
||||
});
|
||||
} finally {
|
||||
cleanup();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const { signal, cleanup } = createTimeoutSignal(
|
||||
|
|
@ -336,11 +498,20 @@ export async function GM_fetch(
|
|||
if (signal.aborted || isAbortError(err)) {
|
||||
throw err;
|
||||
}
|
||||
<<<<<<< Updated upstream
|
||||
// If fetch fails, retry via GM_xmlhttpRequest.
|
||||
debug.log(
|
||||
"GM_fetch preventing CORS by GM_xmlhttpRequest",
|
||||
getErrorMessage(err) || "Unknown error",
|
||||
);
|
||||
=======
|
||||
debug.warn("[GM_fetch] fetch failed, retrying via GM_xmlhttpRequest", {
|
||||
url: urlStr,
|
||||
method,
|
||||
host: host ?? "unknown",
|
||||
error: getErrorMessage(err) || "Unknown error",
|
||||
});
|
||||
>>>>>>> Stashed changes
|
||||
return await gmXhrFetch(urlStr, timeout, fetchOptions);
|
||||
} finally {
|
||||
cleanup();
|
||||
|
|
|
|||
|
|
@ -179,9 +179,10 @@ export async function downloadBlob(
|
|||
|
||||
if (options.preferShare) {
|
||||
const shareResult = await shareBlob(blob, filename);
|
||||
return shareResult === "shared";
|
||||
if (shareResult === "shared") {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return triggerBlobDownload(blob, filename);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,12 @@ import {
|
|||
import { resetAndHideLifecycle } from "../../core/lifecycleShared";
|
||||
import type { VideoHandler } from "../../index";
|
||||
import debug from "../../utils/debug";
|
||||
<<<<<<< Updated upstream
|
||||
=======
|
||||
import { containsCrossShadow, getDeepActiveElement } from "../../utils/dom";
|
||||
>>>>>>> Stashed changes
|
||||
import { GM_fetch } from "../../utils/gm";
|
||||
import { isIframe } from "../../utils/iframeConnector";
|
||||
import { getPlatformEventConfig } from "../../utils/platformEvents";
|
||||
import { clampPercentInt } from "../../utils/volume";
|
||||
import { handlePlaybackResumedTranslationRefresh } from "./translation";
|
||||
|
|
@ -253,6 +258,14 @@ function bindYouTubeVolumeSync(ctx: ExtraEventsContext): void {
|
|||
videoPercent = toPercentInt(fallbackVolume * 100);
|
||||
}
|
||||
self.syncVideoVolumeSlider();
|
||||
<<<<<<< Updated upstream
|
||||
=======
|
||||
const activeOverlayView = self.uiManager.votOverlayView;
|
||||
if (!activeOverlayView?.isInitialized()) return;
|
||||
const videoPercent = toPercentInt(
|
||||
activeOverlayView.videoVolumeSlider.value,
|
||||
);
|
||||
>>>>>>> Stashed changes
|
||||
syncAudioTranslationVolumeFromVideo(self, videoPercent);
|
||||
});
|
||||
const ytpVolumePanel = document.querySelector(".ytp-volume-panel");
|
||||
|
|
@ -378,7 +391,7 @@ function bindGlobalDismissAndHotkeys(ctx: ExtraEventsContext): void {
|
|||
const keyboardEvent = event as KeyboardEvent;
|
||||
if (keyboardEvent.repeat) return;
|
||||
userPressedKeys.add(keyboardEvent.code);
|
||||
const activeElement = document.activeElement as HTMLElement | null;
|
||||
const activeElement = getDeepActiveElement(document) as HTMLElement | null;
|
||||
const activeTag = activeElement?.tagName?.toLowerCase?.() ?? "";
|
||||
const isInputElement =
|
||||
["input", "textarea"].includes(activeTag) ||
|
||||
|
|
@ -421,6 +434,7 @@ function bindGlobalDismissAndHotkeys(ctx: ExtraEventsContext): void {
|
|||
add(globalThis, "blur", clearUserPressedKeys);
|
||||
const eventContainer = self.getEventContainer();
|
||||
if (eventContainer) {
|
||||
<<<<<<< Updated upstream
|
||||
addMany(eventContainer, ["pointerenter", "pointerdown"], (event) =>
|
||||
self.overlayVisibility.handleHostInteraction(event),
|
||||
);
|
||||
|
|
@ -433,6 +447,38 @@ function bindGlobalDismissAndHotkeys(ctx: ExtraEventsContext): void {
|
|||
add(eventContainer, "pointerleave", (event) =>
|
||||
self.overlayVisibility.scheduleHide(event),
|
||||
);
|
||||
=======
|
||||
const useWindowEvents =
|
||||
isIframe() && typeof globalThis.window !== "undefined";
|
||||
const interactionTarget = useWindowEvents
|
||||
? globalThis.window
|
||||
: eventContainer;
|
||||
|
||||
if (useWindowEvents) {
|
||||
addMany(
|
||||
interactionTarget,
|
||||
["pointermove", "pointerdown"],
|
||||
(event) => self.overlayVisibility.handleHostInteraction(event),
|
||||
{ passive: true },
|
||||
);
|
||||
add(interactionTarget, "blur", () =>
|
||||
self.overlayVisibility.scheduleHide(),
|
||||
);
|
||||
} else {
|
||||
addMany(interactionTarget, ["pointerenter", "pointerdown"], (event) =>
|
||||
self.overlayVisibility.handleHostInteraction(event),
|
||||
);
|
||||
add(
|
||||
interactionTarget,
|
||||
"pointermove",
|
||||
(event) => self.overlayVisibility.handleHostInteraction(event),
|
||||
{ passive: true },
|
||||
);
|
||||
add(interactionTarget, "pointerleave", (event) =>
|
||||
self.overlayVisibility.scheduleHide(event),
|
||||
);
|
||||
}
|
||||
>>>>>>> Stashed changes
|
||||
}
|
||||
self.rebindOverlayVisibilityTargets();
|
||||
if (platformConfig.allowTouchMoveHandler) {
|
||||
|
|
@ -462,7 +508,7 @@ export function bindPlaybackRefreshOnResume(ctx: ExtraEventsContext): void {
|
|||
add(self.video, "playing", () => {
|
||||
if (!wasPausedSinceLastPlay) return;
|
||||
wasPausedSinceLastPlay = false;
|
||||
void handlePlaybackResumedTranslationRefresh.call(self).catch((error) => {
|
||||
handlePlaybackResumedTranslationRefresh.call(self).catch((error) => {
|
||||
debug.log(
|
||||
"[VOT] Failed to refresh translation after playback resumed",
|
||||
error,
|
||||
|
|
@ -581,8 +627,9 @@ export function isOverlayInteractiveNode(
|
|||
const buttonContainer = overlayView?.votButton?.container;
|
||||
const menuContainer = overlayView?.votMenu?.container;
|
||||
return (
|
||||
(buttonContainer instanceof Node && buttonContainer.contains(node)) ||
|
||||
(menuContainer instanceof Node && menuContainer.contains(node))
|
||||
(buttonContainer instanceof Node &&
|
||||
containsCrossShadow(buttonContainer, node)) ||
|
||||
(menuContainer instanceof Node && containsCrossShadow(menuContainer, node))
|
||||
);
|
||||
}
|
||||
export function getAutoHideDelay(this: VideoHandler): number {
|
||||
|
|
|
|||
|
|
@ -26,9 +26,19 @@ function getPreferredSubtitlesLanguage(
|
|||
handler: VideoHandler,
|
||||
): string | undefined {
|
||||
const videoData = handler.videoData;
|
||||
<<<<<<< Updated upstream
|
||||
return handler.getPreferredSubtitlesLanguage(
|
||||
videoData?.detectedLanguage,
|
||||
videoData?.responseLanguage,
|
||||
=======
|
||||
return (
|
||||
handler.getPreferredSubtitlesLanguage(
|
||||
videoData?.detectedLanguage,
|
||||
videoData?.responseLanguage,
|
||||
) ??
|
||||
videoData?.responseLanguage ??
|
||||
handler.translateToLang
|
||||
>>>>>>> Stashed changes
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue