mirror of
https://github.com/th-ch/youtube-music.git
synced 2026-01-12 02:51:46 +00:00
Compare commits
116 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 34cb79eeaf | |||
| 1c30a07031 | |||
| 5dd5f41ef5 | |||
| 45a3c11d51 | |||
| 8bd3b4d3f0 | |||
| 4e3cb5806d | |||
| 6563eb4ddd | |||
| 895386f6f8 | |||
| 3810955e56 | |||
| 59c521e53f | |||
| 25d266f8f9 | |||
| 0c3c380591 | |||
| a20cfa30a1 | |||
| fefe899393 | |||
| 55759e8d7a | |||
| ddb561937f | |||
| 198cb71a4c | |||
| c34b880752 | |||
| 76944e3e41 | |||
| 68cd76f2af | |||
| 81145b52b7 | |||
| 2a19dab061 | |||
| 6958d59d4f | |||
| 8a51dfad87 | |||
| 5bb4d9efbe | |||
| 927aa5f24b | |||
| d695bc93a1 | |||
| b05fb4ccbe | |||
| 299eb7e7d6 | |||
| ae26333224 | |||
| 35176469b0 | |||
| 4e74f9cbc5 | |||
| 4091b36f36 | |||
| b3f805fce6 | |||
| b129a3e8d8 | |||
| 64ea1fdb58 | |||
| 8fcf59ed0a | |||
| 9811ca63de | |||
| 9028f88299 | |||
| fd47766d93 | |||
| 26b12c7208 | |||
| 8da9b3454d | |||
| 205cbefc83 | |||
| 0e94c72eef | |||
| c055641351 | |||
| c0a3aa99de | |||
| 8a8976acef | |||
| e409165e1b | |||
| b278140796 | |||
| 397056a54d | |||
| edecd65419 | |||
| 4d2d0b7bd6 | |||
| 0ca4e34efd | |||
| 43f3226c3a | |||
| 0a6dbecc05 | |||
| f5aa179cd6 | |||
| 3140e91dda | |||
| 022f8ff65c | |||
| 5e63cc2e89 | |||
| 880ed99846 | |||
| 222e78c85b | |||
| 050d55c736 | |||
| 13ef8560ff | |||
| 78d990c079 | |||
| 4d3e2c09da | |||
| aa899d247a | |||
| ee0c512529 | |||
| 5f9b522307 | |||
| c207e29980 | |||
| df4d2d6b72 | |||
| c3dd20cabd | |||
| 7a6db95d1a | |||
| bc6825d63b | |||
| 5e79e9e0f2 | |||
| 5e303c2ba8 | |||
| 0bd9c16356 | |||
| f0f5d9da2f | |||
| f46c431f4c | |||
| 62410e9ee2 | |||
| 46f76f1408 | |||
| 5e071e16d8 | |||
| c0238588bd | |||
| 30002d660a | |||
| 48eeb6bca3 | |||
| e67699fed5 | |||
| 8aeae45965 | |||
| ce7491941b | |||
| 1dce03c4f2 | |||
| 62eae6d5d0 | |||
| 15b2b26b84 | |||
| 9664c17c47 | |||
| 8067dad2fa | |||
| 4dcaa510d9 | |||
| b6e918089d | |||
| 1c9e6b1bb8 | |||
| ebd304c252 | |||
| 36083c4173 | |||
| a084b060d8 | |||
| 432c79b606 | |||
| 0f1f0ee933 | |||
| 9b1a4b8d88 | |||
| 1a7a665915 | |||
| 623ecf7fb8 | |||
| 0dc9c6a1a9 | |||
| 72c5eaa5ff | |||
| 0f47b94b7d | |||
| 9abe15f1ad | |||
| 96afda92c8 | |||
| 5c6fd4a739 | |||
| 23b87a876d | |||
| 737fd05369 | |||
| c5bcd89f16 | |||
| 377e1be0b2 | |||
| a92049c0c9 | |||
| 27a2955bba | |||
| cc940e2020 |
@ -49,9 +49,10 @@ this [wiki page](https://wiki.archlinux.org/index.php/Arch_User_Repository#Insta
|
||||
|
||||
### MacOS
|
||||
|
||||
You can install the app using Homebrew:
|
||||
You can install the app using Homebrew (see the [cask definition](https://github.com/th-ch/homebrew-youtube-music)):
|
||||
|
||||
```bash
|
||||
brew install --cask https://raw.githubusercontent.com/th-ch/youtube-music/master/youtube-music.rb
|
||||
brew install th-ch/youtube-music/youtube-music
|
||||
```
|
||||
|
||||
If you install the app manually and get an error "is damaged and can’t be opened." when launching the app, run the following in the Terminal:
|
||||
|
||||
BIN
assets/youtube-music-tray-paused.png
Normal file
BIN
assets/youtube-music-tray-paused.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.9 KiB |
44
changelog.md
44
changelog.md
@ -2,8 +2,52 @@
|
||||
|
||||
All notable changes to this project will be documented in this file. Dates are displayed in UTC.
|
||||
|
||||
#### [v3.2.1](https://github.com/th-ch/youtube-music/compare/v3.2.0...v3.2.1)
|
||||
|
||||
- fix: fix #1574 [`#1574`](https://github.com/th-ch/youtube-music/issues/1574)
|
||||
- fix: fix #1575 [`#1575`](https://github.com/th-ch/youtube-music/issues/1575)
|
||||
- chore(i18n): Translated using Weblate [`f5aa179`](https://github.com/th-ch/youtube-music/commit/f5aa179cd639eb4b8f70f1264b5b459ebcc16695)
|
||||
- chore(i18n): Translated using Weblate (English) [`e409165`](https://github.com/th-ch/youtube-music/commit/e409165e1bed85f3d1aea3a565e7b9e462b1e05b)
|
||||
- chore(i18n): Translated using Weblate (Czech) [`0ca4e34`](https://github.com/th-ch/youtube-music/commit/0ca4e34efd86e877314e5a245f266065b4cf0013)
|
||||
|
||||
#### [v3.2.0](https://github.com/th-ch/youtube-music/compare/v3.1.1...v3.2.0)
|
||||
|
||||
> 1 January 2024
|
||||
|
||||
- feat(album-color-theme): improve `Album Color Theme` style [`#1571`](https://github.com/th-ch/youtube-music/pull/1571)
|
||||
- feat(menu): add more detail in Menu [`#1570`](https://github.com/th-ch/youtube-music/pull/1570)
|
||||
- feat(music-together): Add new plugin `Music Together` [`#1562`](https://github.com/th-ch/youtube-music/pull/1562)
|
||||
- chore(deps): update dependency rollup to v4.9.2 [`#1567`](https://github.com/th-ch/youtube-music/pull/1567)
|
||||
- fix(deps): update dependency i18next to v23.7.13 [`#1569`](https://github.com/th-ch/youtube-music/pull/1569)
|
||||
- feat: Add new plugin `Album actions` [`#1515`](https://github.com/th-ch/youtube-music/pull/1515)
|
||||
- fix(deps): update dependency i18next to v23.7.12 [`#1564`](https://github.com/th-ch/youtube-music/pull/1564)
|
||||
- fix: Only apply scale factor on Windows [`#1565`](https://github.com/th-ch/youtube-music/pull/1565)
|
||||
- chore(deps): update dependency @typescript-eslint/eslint-plugin to v6.16.0 [`#1556`](https://github.com/th-ch/youtube-music/pull/1556)
|
||||
- chore(deps): update pnpm to v8.13.1 [`#1557`](https://github.com/th-ch/youtube-music/pull/1557)
|
||||
- chore(deps): update dependency ws to v8.16.0 [`#1559`](https://github.com/th-ch/youtube-music/pull/1559)
|
||||
- fix(deps): update dependency youtubei.js to v8.1.0 [`#1560`](https://github.com/th-ch/youtube-music/pull/1560)
|
||||
- fix(deps): update dependency node-html-parser to v6.1.12 [`#1554`](https://github.com/th-ch/youtube-music/pull/1554)
|
||||
- Revert "fix(deps): update dependency @xhayper/discord-rpc to v1.1.2" [`#1552`](https://github.com/th-ch/youtube-music/pull/1552)
|
||||
- feat(ambient-mode): support ambient mode on `Song section` [`#1555`](https://github.com/th-ch/youtube-music/issues/1555)
|
||||
- fix: fixed an issue with the download button disappearing [`#1551`](https://github.com/th-ch/youtube-music/issues/1551)
|
||||
- fix: fix `homebrew cask` [`#1514`](https://github.com/th-ch/youtube-music/issues/1514)
|
||||
- fix: pnpm build error [`13ef856`](https://github.com/th-ch/youtube-music/commit/13ef8560ff43353030537403be7da82542ba535e)
|
||||
- chore(i18n): Translated using Weblate (Czech) [`0dc9c6a`](https://github.com/th-ch/youtube-music/commit/0dc9c6a1a90bce6505614617b827e816cbaaf875)
|
||||
- chore(deps): update dependency @typescript-eslint/eslint-plugin to v6.15.0 [`c5bcd89`](https://github.com/th-ch/youtube-music/commit/c5bcd89f164b51d7380486a8ae35edd0caeea842)
|
||||
|
||||
#### [v3.1.1](https://github.com/th-ch/youtube-music/compare/v3.1.0...v3.1.1)
|
||||
|
||||
> 18 December 2023
|
||||
|
||||
- fix: fix renderer plugin load timing [`#1522`](https://github.com/th-ch/youtube-music/issues/1522)
|
||||
- chore(i18n): Translated using Weblate (Lithuanian) [`fc1a7cd`](https://github.com/th-ch/youtube-music/commit/fc1a7cda62b6e33e5f5d57a5a6e0adef6a32bf9a)
|
||||
- chore(i18n): Translated using Weblate (Chinese (Simplified)) [`eba7026`](https://github.com/th-ch/youtube-music/commit/eba7026b89bbfdd3ac07cf728a66ba9bdd274ec0)
|
||||
- chore(deps): update dependency rollup to v4.8.0 [`a601d0b`](https://github.com/th-ch/youtube-music/commit/a601d0b3d2dee0fabad79a18e1a7dd0ca84ccf01)
|
||||
|
||||
#### [v3.1.0](https://github.com/th-ch/youtube-music/compare/v3.0.2...v3.1.0)
|
||||
|
||||
> 11 December 2023
|
||||
|
||||
- chore(deps): update dependency electron to v28 [`#1498`](https://github.com/th-ch/youtube-music/pull/1498)
|
||||
- Enable/Disable Navigation without restart [`#1507`](https://github.com/th-ch/youtube-music/pull/1507)
|
||||
- Turkish(tr)_lang_file [`#1513`](https://github.com/th-ch/youtube-music/pull/1513)
|
||||
|
||||
@ -258,7 +258,7 @@ import style from './style.css?inline'; // 스타일을 인라인으로 가져
|
||||
|
||||
import { createPlugin } from '@/utils';
|
||||
|
||||
const builder = createPlugin({
|
||||
export default createPlugin({
|
||||
name: 'Plugin Label',
|
||||
restartNeeded: true, // 값이 true면, YTM은 재시작 다이얼로그를 표시합니다
|
||||
config: {
|
||||
@ -274,7 +274,7 @@ const builder = createPlugin({
|
||||
```typescript
|
||||
import { createPlugin } from '@/utils';
|
||||
|
||||
const builder = createPlugin({
|
||||
export default createPlugin({
|
||||
name: 'Plugin Label',
|
||||
restartNeeded: true, // 값이 true면, YTM은 재시작 다이얼로그를 표시합니다
|
||||
config: {
|
||||
|
||||
66
package.json
66
package.json
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "youtube-music",
|
||||
"productName": "YouTube Music",
|
||||
"version": "3.1.1",
|
||||
"version": "3.2.2",
|
||||
"description": "YouTube Music Desktop App - including custom plugins",
|
||||
"main": "./dist/main/index.js",
|
||||
"license": "MIT",
|
||||
@ -105,17 +105,17 @@
|
||||
"dev": "electron-vite dev --watch",
|
||||
"dev:debug": "cross-env ELECTRON_ENABLE_LOGGING=1 pnpm dev",
|
||||
"clean": "del-cli dist && del-cli pack && del-cli .vite-inspect",
|
||||
"dist": "pnpm clean && pnpm build && electron-builder --win --mac --linux -p never",
|
||||
"dist:linux": "pnpm clean && pnpm build && electron-builder --linux -p never",
|
||||
"dist:mac": "pnpm clean && pnpm build && electron-builder --mac dmg:x64 -p never",
|
||||
"dist:mac:arm64": "pnpm clean && pnpm build && electron-builder --mac dmg:arm64 -p never",
|
||||
"dist:win": "pnpm clean && pnpm build && electron-builder --win -p never",
|
||||
"dist:win:x64": "pnpm clean && pnpm build && electron-builder --win nsis-web:x64 -p never",
|
||||
"dist": "pnpm clean && pnpm build && pnpm electron-builder --win --mac --linux -p never",
|
||||
"dist:linux": "pnpm clean && pnpm build && pnpm electron-builder --linux -p never",
|
||||
"dist:mac": "pnpm clean && pnpm build && pnpm electron-builder --mac dmg:x64 -p never",
|
||||
"dist:mac:arm64": "pnpm clean && pnpm build && pnpm electron-builder --mac dmg:arm64 -p never",
|
||||
"dist:win": "pnpm clean && pnpm build && pnpm electron-builder --win -p never",
|
||||
"dist:win:x64": "pnpm clean && pnpm build && pnpm electron-builder --win nsis-web:x64 -p never",
|
||||
"lint": "eslint .",
|
||||
"changelog": "npx --yes auto-changelog",
|
||||
"release:linux": "pnpm clean && pnpm build && electron-builder --linux -p always -c.snap.publish=github",
|
||||
"release:mac": "pnpm clean && pnpm build && electron-builder --mac -p always",
|
||||
"release:win": "pnpm clean && pnpm build && electron-builder --win -p always",
|
||||
"release:linux": "pnpm clean && pnpm build && pnpm electron-builder --linux -p always -c.snap.publish=github",
|
||||
"release:mac": "pnpm clean && pnpm build && pnpm electron-builder --mac -p always",
|
||||
"release:win": "pnpm clean && pnpm build && pnpm electron-builder --win -p always",
|
||||
"typecheck": "tsc -p tsconfig.json --noEmit"
|
||||
},
|
||||
"engines": {
|
||||
@ -123,14 +123,17 @@
|
||||
},
|
||||
"pnpm": {
|
||||
"overrides": {
|
||||
"esbuild": "0.18.20",
|
||||
"esbuild": "0.19.11",
|
||||
"usocket": "1.0.1",
|
||||
"rollup": "4.9.1",
|
||||
"rollup": "4.9.3",
|
||||
"node-gyp": "10.0.1",
|
||||
"xml2js": "0.6.2",
|
||||
"node-fetch": "3.3.2",
|
||||
"@electron/universal": "2.0.0",
|
||||
"@babel/runtime": "7.23.2"
|
||||
"@electron/universal": "2.0.1",
|
||||
"@babel/runtime": "7.23.7"
|
||||
},
|
||||
"patchedDependencies": {
|
||||
"vudio@2.1.1": "patches/vudio@2.1.1.patch"
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
@ -140,13 +143,14 @@
|
||||
"@electron/remote": "2.1.1",
|
||||
"@ffmpeg.wasm/core-mt": "0.12.0",
|
||||
"@ffmpeg.wasm/main": "0.12.0",
|
||||
"@foobar404/wave": "2.0.4",
|
||||
"@foobar404/wave": "2.0.5",
|
||||
"@jellybrick/electron-better-web-request": "1.0.4",
|
||||
"@jellybrick/mpris-service": "2.1.4",
|
||||
"@xhayper/discord-rpc": "1.1.1",
|
||||
"async-mutex": "0.4.0",
|
||||
"butterchurn": "3.0.0-beta.4",
|
||||
"butterchurn-presets": "3.0.0-beta.4",
|
||||
"color": "4.2.3",
|
||||
"conf": "10.2.0",
|
||||
"custom-electron-prompt": "1.5.7",
|
||||
"dbus-next": "0.10.2",
|
||||
@ -162,49 +166,53 @@
|
||||
"filenamify": "6.0.0",
|
||||
"howler": "2.2.4",
|
||||
"html-to-text": "9.0.5",
|
||||
"i18next": "23.7.11",
|
||||
"i18next": "23.7.16",
|
||||
"keyboardevent-from-electron-accelerator": "2.0.0",
|
||||
"keyboardevents-areequal": "0.2.2",
|
||||
"node-html-parser": "6.1.11",
|
||||
"node-html-parser": "6.1.12",
|
||||
"node-id3": "0.2.6",
|
||||
"peerjs": "1.5.2",
|
||||
"semver": "7.5.4",
|
||||
"serve": "14.2.1",
|
||||
"simple-youtube-age-restriction-bypass": "github:organization/Simple-YouTube-Age-Restriction-Bypass#v2.5.9",
|
||||
"ts-morph": "21.0.1",
|
||||
"vudio": "2.1.1",
|
||||
"x11": "2.3.0",
|
||||
"youtubei.js": "8.0.0"
|
||||
"youtubei.js": "8.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "1.41.0-alpha-dec-18-2023",
|
||||
"@playwright/test": "1.41.0-alpha-jan-5-2024",
|
||||
"@total-typescript/ts-reset": "0.5.1",
|
||||
"@types/color": "3.0.6",
|
||||
"@types/electron-localshortcut": "3.1.3",
|
||||
"@types/howler": "2.2.11",
|
||||
"@types/html-to-text": "9.0.4",
|
||||
"@typescript-eslint/eslint-plugin": "6.14.0",
|
||||
"@types/semver": "7.5.6",
|
||||
"@typescript-eslint/eslint-plugin": "6.17.0",
|
||||
"bufferutil": "4.0.8",
|
||||
"builtin-modules": "3.3.0",
|
||||
"cross-env": "7.0.3",
|
||||
"del-cli": "5.1.0",
|
||||
"electron": "28.0.0",
|
||||
"electron": "29.0.0-alpha.7",
|
||||
"electron-builder": "24.9.1",
|
||||
"electron-devtools-installer": "3.2.0",
|
||||
"electron-vite": "2.0.0-beta.1",
|
||||
"esbuild": "0.18.20",
|
||||
"electron-vite": "2.0.0-beta.3",
|
||||
"esbuild": "0.19.11",
|
||||
"eslint": "8.56.0",
|
||||
"eslint-import-resolver-exports": "1.0.0-beta.5",
|
||||
"eslint-import-resolver-typescript": "3.6.1",
|
||||
"eslint-plugin-import": "2.29.1",
|
||||
"eslint-plugin-prettier": "5.0.1",
|
||||
"eslint-plugin-prettier": "5.1.2",
|
||||
"glob": "10.3.10",
|
||||
"node-gyp": "10.0.1",
|
||||
"playwright": "1.41.0-alpha-dec-18-2023",
|
||||
"rollup": "4.9.1",
|
||||
"playwright": "1.41.0-alpha-jan-5-2024",
|
||||
"rollup": "4.9.3",
|
||||
"typescript": "5.3.3",
|
||||
"utf-8-validate": "6.0.3",
|
||||
"vite": "5.0.10",
|
||||
"vite": "5.0.11",
|
||||
"vite-plugin-inspect": "0.8.1",
|
||||
"vite-plugin-resolve": "2.5.1",
|
||||
"ws": "8.15.1"
|
||||
"ws": "8.16.0"
|
||||
},
|
||||
"auto-changelog": {
|
||||
"hideCredit": true,
|
||||
@ -212,5 +220,5 @@
|
||||
"unreleased": true,
|
||||
"output": "changelog.md"
|
||||
},
|
||||
"packageManager": "pnpm@8.12.1"
|
||||
"packageManager": "pnpm@8.14.0"
|
||||
}
|
||||
|
||||
20
patches/vudio@2.1.1.patch
Normal file
20
patches/vudio@2.1.1.patch
Normal file
@ -0,0 +1,20 @@
|
||||
diff --git a/umd/vudio.js b/umd/vudio.js
|
||||
index d0d1127e57125ad4e77442af2db4a26998c7b385..c0b66bd4327c65c31dc6e588bfa4ae6ec70bd3b8 100644
|
||||
--- a/umd/vudio.js
|
||||
+++ b/umd/vudio.js
|
||||
@@ -147,7 +147,6 @@
|
||||
|
||||
source.connect(this.analyser);
|
||||
this.analyser.fftSize = this.option.accuracy * 2;
|
||||
- this.analyser.connect(audioContext.destination);
|
||||
|
||||
this.freqByteData = new Uint8Array(this.analyser.frequencyBinCount);
|
||||
|
||||
@@ -207,7 +206,6 @@
|
||||
|
||||
source.connect(this.analyser);
|
||||
this.analyser.fftSize = this.option.accuracy * 2;
|
||||
- this.analyser.connect(audioContext.destination);
|
||||
},
|
||||
|
||||
__rebuildData : function (freqByteData, horizontalAlign) {
|
||||
888
pnpm-lock.yaml
generated
888
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -2,9 +2,9 @@
|
||||
"common": {
|
||||
"console": {
|
||||
"plugins": {
|
||||
"execute-failed": "Selhalo execute pluginu {{pluginName}}::{{contextName}}",
|
||||
"executed-at-ms": "Plugin {{pluginName}}::{{contextName}} executed at {{ms}}ms",
|
||||
"initialize-failed": "Selhala initialize \"{{pluginName}}\" pluginu",
|
||||
"execute-failed": "Selhalo spuštění pluginu {{pluginName}}::{{contextName}}",
|
||||
"executed-at-ms": "Plugin {{pluginName}}::{{contextName}} spuštěn za {{ms}}ms",
|
||||
"initialize-failed": "Selhalo zapnutí \"{{pluginName}}\" pluginu",
|
||||
"load-all": "Načítání všech pluginů",
|
||||
"load-failed": "Selhalo načtení \"{{pluginName}}\" pluginu",
|
||||
"loaded": "Plugin \"{{pluginName}}\" načten",
|
||||
@ -32,20 +32,20 @@
|
||||
"css-file-not-found": "CSS soubor \"{{cssFile}}\" neexistuje, ignorováno"
|
||||
},
|
||||
"unresponsive": {
|
||||
"details": "Unresponsive chyba!\n{{error}}"
|
||||
"details": "Chyba - Aplikace nereaguje!\n{{error}}"
|
||||
},
|
||||
"when-ready": {
|
||||
"clearing-cache-after-20s": "Čištění mezipaměti aplikace"
|
||||
},
|
||||
"window": {
|
||||
"tried-to-render-offscreen": "Okno se pokusilo render na pozadí, Velikost okna ={{windowSize}}, displaySize={{displaySize}}, position={{position}}"
|
||||
"tried-to-render-offscreen": "Okno se pokusilo vykreslit na pozadí, velikost okna = {{windowSize}}, display velikost = {{displaySize}}, pozice = {{position}}"
|
||||
}
|
||||
},
|
||||
"dialog": {
|
||||
"hide-menu-enabled": {
|
||||
"detail": "Menu je skryté, stiskněte 'Alt' k zobrazení (nebo 'Escape', pokud používáte in-app-menu)",
|
||||
"message": "Skrýt Menu je povoleno",
|
||||
"title": "Skrýt Menu Povolené"
|
||||
"detail": "Menu je skryté, stiskněte 'Alt' k jeho zobrazení (nebo 'Escape', pokud používáte in-app-menu)",
|
||||
"message": "Skrýt menu je povoleno",
|
||||
"title": "Skrýt menu Povolené"
|
||||
},
|
||||
"need-to-restart": {
|
||||
"buttons": {
|
||||
@ -72,9 +72,9 @@
|
||||
"download": "Stáhnout",
|
||||
"ok": "OK"
|
||||
},
|
||||
"detail": "Nová verze je k dispozici a lze stáhnout na {{downloadLink}}",
|
||||
"detail": "Nová verze je k dispozici a lze ji stáhnout na {{downloadLink}}",
|
||||
"message": "Nová verze je dostupná",
|
||||
"title": "Aktualizace k dispozici"
|
||||
"title": "Aktualizace je k dispozici"
|
||||
}
|
||||
},
|
||||
"menu": {
|
||||
@ -82,7 +82,7 @@
|
||||
"navigation": {
|
||||
"label": "Navigace",
|
||||
"submenu": {
|
||||
"copy-current-url": "Kopírovat aktuální URL adresu",
|
||||
"copy-current-url": "Zkopírovat aktuální URL adresu",
|
||||
"go-back": "Jít zpátky",
|
||||
"go-forward": "Jít dopředu",
|
||||
"quit": "Ukončit",
|
||||
@ -95,28 +95,28 @@
|
||||
"advanced-options": {
|
||||
"label": "Pokročilé možnosti",
|
||||
"submenu": {
|
||||
"auto-reset-app-cache": "Při spuštění aplikace, se resetuje její mezipaměť",
|
||||
"auto-reset-app-cache": "Při spuštění aplikace se resetuje její mezipaměť",
|
||||
"disable-hardware-acceleration": "Vypnout hardware zrychlení",
|
||||
"edit-config-json": "Upravit config.json",
|
||||
"override-user-agent": "Přepsat User-Agent",
|
||||
"restart-on-config-changes": "Restartovat na změny v konfiguraci",
|
||||
"override-user-agent": "Přepsat uživatelského agenta",
|
||||
"restart-on-config-changes": "Restartovat aplikaci na změny v konfiguraci",
|
||||
"set-proxy": {
|
||||
"label": "Nastavit proxy",
|
||||
"prompt": {
|
||||
"label": "Zadejte adresu proxy: (nechejte prázdné to disable)",
|
||||
"placeholder": "Příklad: socks5://127.0.0.1:9999",
|
||||
"label": "Zadejte adresu proxy: (k vypnutí nechte pole prázdné)",
|
||||
"placeholder": "Příklad: SOCKS5://127.0.0.1:9999",
|
||||
"title": "Nastavit proxy"
|
||||
}
|
||||
},
|
||||
"toggle-dev-tools": "Toggle Vývojářské nástroje"
|
||||
"toggle-dev-tools": "Přepínat vývojářské nástroje"
|
||||
}
|
||||
},
|
||||
"always-on-top": "Vždy na vrchu",
|
||||
"auto-update": "Automatické aktualizace",
|
||||
"hide-menu": {
|
||||
"dialog": {
|
||||
"message": "Menu bude skryto na dalším launch, use [Alt] to show it (nebo backtick [`] pokud používáte in-app-menu)",
|
||||
"title": "Skrýt Menu Povoleno"
|
||||
"message": "Menu bude skryto na dalším spuštěním, použijte [Alt] k jeho zobrazení (nebo backtick [`] pokud používáte in-app-menu)",
|
||||
"title": "Skrýt menu Povoleno"
|
||||
},
|
||||
"label": "Skrýt menu"
|
||||
},
|
||||
@ -130,15 +130,17 @@
|
||||
"to-help-translate": "Chcete pomoc s překladem? Klikněte zde"
|
||||
}
|
||||
},
|
||||
"resume-on-start": "Resume poslední písničku při spuštění aplikace",
|
||||
"resume-on-start": "Při spuštění aplikace, pokračovat na poslední písničce",
|
||||
"single-instance-lock": "Zámek pro jednu instanci",
|
||||
"start-at-login": "Zapnutí aplikace po přihlášení",
|
||||
"starting-page": {
|
||||
"label": "Úvodní stránka",
|
||||
"unset": "Nenastaveno"
|
||||
},
|
||||
"tray": {
|
||||
"label": "Tray",
|
||||
"submenu": {
|
||||
"disabled": "Vypnuté",
|
||||
"disabled": "Vypnuto",
|
||||
"enabled-and-hide-app": "Povolit a skrýt aplikaci",
|
||||
"enabled-and-show-app": "Enabled a show aplikaci",
|
||||
"play-pause-on-click": "Přehrát/Pozastavit na kliknutí"
|
||||
@ -149,15 +151,15 @@
|
||||
"submenu": {
|
||||
"like-buttons": {
|
||||
"default": "Výchozí",
|
||||
"force-show": "Vynutit show",
|
||||
"hide": "Schovat",
|
||||
"force-show": "Vynutit zobrazení",
|
||||
"hide": "Skrýt",
|
||||
"label": "Like tlačítka"
|
||||
},
|
||||
"remove-upgrade-button": "Odebrat upgrade tlačítko",
|
||||
"theme": {
|
||||
"label": "Motiv",
|
||||
"submenu": {
|
||||
"import-css-file": "Import vlastní CSS soubor",
|
||||
"import-css-file": "Vložit vlastní CSS soubor",
|
||||
"no-theme": "Žádný motiv"
|
||||
}
|
||||
}
|
||||
@ -167,14 +169,15 @@
|
||||
},
|
||||
"plugins": {
|
||||
"enabled": "Povoleno",
|
||||
"label": "Pluginy"
|
||||
"label": "Pluginy",
|
||||
"new": "NOVÉ"
|
||||
},
|
||||
"view": {
|
||||
"label": "Zobrazení",
|
||||
"submenu": {
|
||||
"force-reload": "Vynutit znovu načtení",
|
||||
"reload": "Obnovit",
|
||||
"reset-zoom": "Actual velikost",
|
||||
"reset-zoom": "Skutečná velikost",
|
||||
"toggle-fullscreen": "Přepnout režim celé obrazovky",
|
||||
"zoom-in": "Přiblížit",
|
||||
"zoom-out": "Oddálit"
|
||||
@ -187,7 +190,7 @@
|
||||
"previous": "Minulý",
|
||||
"quit": "Ukončit",
|
||||
"restart": "Restartovat aplikaci",
|
||||
"show": "Ukázat okno"
|
||||
"show": "Zobrazit okno"
|
||||
}
|
||||
},
|
||||
"plugins": {
|
||||
@ -198,14 +201,19 @@
|
||||
},
|
||||
"name": "Blokovač reklam"
|
||||
},
|
||||
"album-actions": {
|
||||
"description": "Přidává Undislike, Dislike, Like, a Unlike tlačítka k apply this ke všem písničkám v seznamu písniček nebo albumu.",
|
||||
"name": "Album akce"
|
||||
},
|
||||
"album-color-theme": {
|
||||
"description": "Použije dynamický motiv a vizuální efekty na základě palety barev alba",
|
||||
"description": "Používá dynamický motiv a vizuální efekty na základě palety barev alba",
|
||||
"name": "Motiv podle barvy Alba"
|
||||
},
|
||||
"ambient-mode": {
|
||||
"description": "Applies a lighting efekty pomocí casting gentle barvy z videa, do vašeho screen’s pozadí.",
|
||||
"description": "Applies bleskové efekty pomocí casting jemných barev z videa, do vašeho pozadí obrazovky.",
|
||||
"menu": {
|
||||
"blur-amount": {
|
||||
"label": "Množství rozmazání",
|
||||
"submenu": {
|
||||
"pixels": "{{blurAmount}} pixelů"
|
||||
}
|
||||
@ -237,18 +245,22 @@
|
||||
"smoothness-transition": {
|
||||
"label": "Plynulý přechod",
|
||||
"submenu": {
|
||||
"during": "Během {{interpolationTime}}s"
|
||||
"during": "Během {{interpolationTime}} s"
|
||||
}
|
||||
},
|
||||
"use-fullscreen": {
|
||||
"label": "Používání režimu celé obrazovky"
|
||||
}
|
||||
},
|
||||
"name": "Ambientní režim"
|
||||
},
|
||||
"audio-compressor": {
|
||||
"description": "Apply compression k audiu (snižuje hlasitost nejhlasitěších částí signálu and zvyšuje hlasitost nejjemnějších částí)",
|
||||
"name": "Audio kompresor"
|
||||
},
|
||||
"blur-nav-bar": {
|
||||
"description": "Udělá navigační panel průhledným a rozmazaným",
|
||||
"name": "Rozmazaný navigační Bar"
|
||||
"description": "Udělá navigační panel průhledný a rozmazaný",
|
||||
"name": "Rozmazaný navigační panel"
|
||||
},
|
||||
"bypass-age-restrictions": {
|
||||
"description": "Obejít ověření věku na YouTube",
|
||||
@ -273,15 +285,15 @@
|
||||
}
|
||||
},
|
||||
"compact-sidebar": {
|
||||
"description": "Vždy set the sidebar v kompaktním režimu",
|
||||
"name": "Kompaktní Sidebar"
|
||||
"description": "Vždy nastavit postranní panel do kompaktního režimu",
|
||||
"name": "Kompaktní postranní panel"
|
||||
},
|
||||
"crossfade": {
|
||||
"description": "Crossfade mezi písničkami",
|
||||
"description": "Prolínání mezi písničkami",
|
||||
"menu": {
|
||||
"advanced": "Pokročilý"
|
||||
},
|
||||
"name": "Prolínání [beta]",
|
||||
"name": "Prolínání [Beta]",
|
||||
"prompt": {
|
||||
"options": {
|
||||
"multi-input": {
|
||||
@ -296,26 +308,35 @@
|
||||
}
|
||||
},
|
||||
"disable-autoplay": {
|
||||
"description": "Spustí písničku v režimu \"pozastaveno\"",
|
||||
"menu": {
|
||||
"apply-once": "Applies jenom na spuštění aplikace"
|
||||
},
|
||||
"name": "Zrušit automatické přehrávání"
|
||||
"name": "Vypnout automatické přehrávání"
|
||||
},
|
||||
"discord": {
|
||||
"backend": {
|
||||
"already-connected": "Pokusilo se spojit s aktivním spojením",
|
||||
"connected": "Připojeno k Discordu",
|
||||
"disconnected": "Odpojeno od Discordu"
|
||||
},
|
||||
"description": "Ukažte svým přátelům, co posloucháte s Rich Presence",
|
||||
"description": "Ukažte svým přátelům, co posloucháte s Bohatou přítomností",
|
||||
"menu": {
|
||||
"auto-reconnect": "Automaticky znovu připojit",
|
||||
"clear-activity": "Vymazat aktivitu",
|
||||
"clear-activity-after-timeout": "Vymazat aktivitu po timeout",
|
||||
"connected": "Připojeno",
|
||||
"disconnected": "Odpojeno",
|
||||
"hide-duration-left": "Skrýt zbývající duration",
|
||||
"hide-github-button": "Skrýt tlačítko s odkazem na GitHub",
|
||||
"play-on-youtube-music": "Hrát na YouTube Music"
|
||||
"play-on-youtube-music": "Hrát na YouTube Music",
|
||||
"set-inactivity-timeout": "Nastavit timeout pro neaktivitu"
|
||||
},
|
||||
"name": "Discord Bohatá přítomnost",
|
||||
"prompt": {
|
||||
"set-inactivity-timeout": {
|
||||
"label": "Zadejte inactivity timeout v sekundách:"
|
||||
"label": "Zadejte timeout neaktivity v sekundách:",
|
||||
"title": "Nastavit timeout pro neaktivitu"
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -334,70 +355,126 @@
|
||||
"ok": "OK"
|
||||
},
|
||||
"detail": "({{playlistSize}} písničky)",
|
||||
"message": "Stahování seznamu skladeb {{playlistTitle}}",
|
||||
"message": "Stahování seznamu písniček {{playlistTitle}}",
|
||||
"title": "Stahování začalo"
|
||||
}
|
||||
},
|
||||
"feedback": {
|
||||
"conversion-progress": "Konverze: {{percent}}%",
|
||||
"done": "Hotovo: {{filePath}}",
|
||||
"download-info": "Stahování {{artist}} - {{title}} [{{videoId}}",
|
||||
"download-progress": "Stahování: {{percent}}%",
|
||||
"downloading": "Stahování…",
|
||||
"downloading-counter": "Stahování {{current}}/{{total}}…",
|
||||
"downloading-playlist": "Stahování seznamu skladeb \"{{playlistTitle}}\" - {{playlistSize}} písničky ({{playlistId}})",
|
||||
"downloading-playlist": "Stahování seznamu písniček \"{{playlistTitle}}\" - {{playlistSize}} písničky ({{playlistId}})",
|
||||
"error-while-downloading": "Chyba při stahování \"{{author}} - {{title}}\": {{error}}",
|
||||
"folder-already-exists": "Složka {{playlistFolder}} již existuje",
|
||||
"getting-playlist-info": "Getting informace o seznamu skladeb…",
|
||||
"getting-playlist-info": "Získávání informací o seznamu písniček…",
|
||||
"loading": "Načítání…",
|
||||
"playlist-has-only-one-song": "Seznam skladeb má pouze jednu položku, downloading it directly",
|
||||
"playlist-id-not-found": "Žádné ID seznamu skladeb nenalezeno",
|
||||
"playlist-is-empty": "Seznam skladeb je prázdný",
|
||||
"playlist-has-only-one-song": "Seznam písniček má pouze jednu položku, stahuje se přímo",
|
||||
"playlist-id-not-found": "Žádné ID seznamu písnček nenalezeno",
|
||||
"playlist-is-empty": "Seznam písniček je prázdný",
|
||||
"playlist-is-mix-or-private": "Chyba při získávání informací o seznamu písniček: ujistite se, že se nejedná o soukromý nebo \"Namíchaný pro vás\" seznam písniček\n\n{{error}}",
|
||||
"preparing-file": "Připravování souboru…",
|
||||
"saving": "Ukládání…",
|
||||
"trying-to-get-playlist-id": "Trying to get ID seznamu skladeb: {{playlistId}}",
|
||||
"video-id-not-found": "Video nebylo nalezeno"
|
||||
"trying-to-get-playlist-id": "Trying se získat ID seznamu písniček: {{playlistId}}",
|
||||
"video-id-not-found": "Video nebylo nalezeno",
|
||||
"writing-id3": "Psaní ID3 značek…"
|
||||
}
|
||||
},
|
||||
"description": "Stahuje MP3 / source audio přímo z rozhraní",
|
||||
"menu": {
|
||||
"choose-download-folder": "Vybrat download složku",
|
||||
"download-playlist": "Stáhnout seznam skladeb",
|
||||
"choose-download-folder": "Vybrat složku pro stahování",
|
||||
"download-playlist": "Stáhnout seznam písniček",
|
||||
"presets": "Předvolby",
|
||||
"skip-existing": "Přeskočit existující soubory"
|
||||
},
|
||||
"name": "Stahovač",
|
||||
"renderer": {
|
||||
"can-not-update-progress": "Progress nemůže být aktualizován"
|
||||
},
|
||||
"templates": {
|
||||
"button": "Stáhnout"
|
||||
}
|
||||
},
|
||||
"exponential-volume": {
|
||||
"description": "Dělá posuvník hlasitosti exponenciální, takže je snazší vybrat nižší hlasitost.",
|
||||
"name": "Exponenciální hlasitost"
|
||||
},
|
||||
"in-app-menu": {
|
||||
"description": "Dává menu-bars a fancy, tmavý nebo album-color vzhled"
|
||||
"description": "Dává menu panelům fancy, tmavý nebo album-color vzhled",
|
||||
"menu": {
|
||||
"hide-dom-window-controls": "Skrýt DOM window controls"
|
||||
}
|
||||
},
|
||||
"last-fm": {
|
||||
"description": "Přidat scrobbling podporu pro Last.fm",
|
||||
"name": "Last.fm"
|
||||
},
|
||||
"lumiastream": {
|
||||
"description": "Přidává Lumia Stream podporu"
|
||||
"description": "Přidává Lumia Stream podporu",
|
||||
"name": "Lumia Stream [Beta]"
|
||||
},
|
||||
"lyrics-genius": {
|
||||
"description": "Přidat lyrics podporu pro většinu písniček",
|
||||
"description": "Přidává lyrics podporu pro většinu písniček",
|
||||
"renderer": {
|
||||
"fetched-lyrics": "Fetched lyrics pro Genius"
|
||||
}
|
||||
},
|
||||
"music-together": {
|
||||
"description": "Sdílejte seznam písniček s ostatními. Když the host hraje písničku, uslyší jí i všichni ostatní.",
|
||||
"dialog": {
|
||||
"enter-host": "Zadejte Host ID"
|
||||
},
|
||||
"internal": {
|
||||
"save": "Uložit",
|
||||
"unknown-user": "Neznámý uživatel"
|
||||
},
|
||||
"menu": {
|
||||
"click-to-copy-id": "Zkopírovat Host ID",
|
||||
"close": "Zavřít Hudba Spolu",
|
||||
"connected-users": "Připojení uživatelé",
|
||||
"disconnect": "Odpojit od Hudby Spolu",
|
||||
"empty-user": "Žadní připojení uživatelé",
|
||||
"host": "Hudba Spolu Host",
|
||||
"join": "Připojit se k Hudbě Spolu",
|
||||
"permission": {
|
||||
"all": "Povolit hostům ovládat seznam písniček a přehrávač",
|
||||
"host-only": "Jenom host může ovládat seznam písniček a přehrávač",
|
||||
"playlist": "Povolit hostům ovládat seznam písniček"
|
||||
},
|
||||
"set-permission": "Změnit ovládací oprávnění",
|
||||
"status": {
|
||||
"disconnected": "Odpojen",
|
||||
"guest": "Připojený/á jako Guest",
|
||||
"host": "Připojený/á jako Host"
|
||||
}
|
||||
},
|
||||
"name": "Hudba Spolu [Beta]",
|
||||
"toast": {
|
||||
"add-song-failed": "Selhalo přidání písničky",
|
||||
"closed": "Hudba Spolu zavřena",
|
||||
"disconnected": "Hudba Spolu odpojena",
|
||||
"host-failed": "Selhalo hostování Hudby Spolu",
|
||||
"id-copied": "Host ID zkopírováno do schránky",
|
||||
"join-failed": "Selhalo připojení k Hudba Spolu",
|
||||
"joined": "Připojil/a jste se k Hudbě Spolu",
|
||||
"permission-changed": "Oprávnění Hudby Spolu se změnilo na \"{{permission}}\"",
|
||||
"remove-song-failed": "Selhalo odstranění písničky",
|
||||
"user-connected": "{{name}} se připojil/a k Hudbě Spolu",
|
||||
"user-disconnected": "{{name}} odpustil/a Hudba Spolu"
|
||||
}
|
||||
},
|
||||
"navigation": {
|
||||
"description": "Další/Zpátky navigační šipky přímo integrovány do rozhraní, jako ve vašem oblíbeném prohlížeči",
|
||||
"name": "Navigace"
|
||||
},
|
||||
"no-google-login": {
|
||||
"description": "Odstranit Google login tlačítka a odkazy z rozhraní",
|
||||
"description": "Odstranit tlačítka Google přihlášení a odkazy z rozhraní",
|
||||
"name": "Žádné Google přihlášení"
|
||||
},
|
||||
"notifications": {
|
||||
"description": "Display oznámení when a písnička starts hraje (interactive notifications are available on Windows)",
|
||||
"description": "Zobrazit oznámení, když písnička začne hrát (interaktivní notifikace jsou dostupné na Windows)",
|
||||
"menu": {
|
||||
"interactive": "Interaktivní oznámení",
|
||||
"interactive-settings": {
|
||||
@ -405,7 +482,7 @@
|
||||
"submenu": {
|
||||
"hide-button-text": "Skrýt text tlačítka",
|
||||
"refresh-on-play-pause": "Refresh na Přehrát/Pozastavit",
|
||||
"tray-controls": "Otevřít/Zavřít on tray click"
|
||||
"tray-controls": "Otevřít/Zavřít aplikaci na kliknutí na tray ikonu"
|
||||
}
|
||||
},
|
||||
"priority": "Priorita Oznámení",
|
||||
@ -414,6 +491,7 @@
|
||||
"name": "Oznámení"
|
||||
},
|
||||
"picture-in-picture": {
|
||||
"description": "Povoluje switch aplikaci do režimu obrázek v obrázku",
|
||||
"menu": {
|
||||
"always-on-top": "Vždy na vrchu",
|
||||
"hotkey": {
|
||||
@ -422,8 +500,8 @@
|
||||
"keybind-options": {
|
||||
"hotkey": "Klávesová zkratka"
|
||||
},
|
||||
"label": "Vybrat klávesovou zkratku pro toggle obrázek v obrázku",
|
||||
"title": "Obrázek v obrázku klávesová zkratka"
|
||||
"label": "Vybrat klávesovou zkratku pro přepínání obrázek v obrázku",
|
||||
"title": "klávesová zkratka pro obrázek v obrázku"
|
||||
}
|
||||
},
|
||||
"save-window-position": "Uložit pozici okna",
|
||||
@ -436,14 +514,16 @@
|
||||
}
|
||||
},
|
||||
"playback-speed": {
|
||||
"description": "Posloiuchej rychle, poslouchej pomalu! Adds a slider, který kontroluje rychlost písníčky",
|
||||
"description": "Poslouchej rychle, poslouchej pomalu! Přidává slider, který kontroluje rychlost písníčky",
|
||||
"name": "Rychlost přehrávání",
|
||||
"templates": {
|
||||
"button": "Rychlost"
|
||||
}
|
||||
},
|
||||
"precise-volume": {
|
||||
"description": "Přesná kontrola hlasitosti pomocí kolečka myši/klávesnicových zkratek, s vlastní HUD a customizable hlasitostních steps",
|
||||
"menu": {
|
||||
"custom-volume-steps": "Nastavit vlastní hlasitostní steps",
|
||||
"global-shortcuts": "Globální klávesové zkratky"
|
||||
},
|
||||
"name": "Přesná hlasitost",
|
||||
@ -452,7 +532,13 @@
|
||||
"keybind-options": {
|
||||
"decrease": "Snížit hlasitost",
|
||||
"increase": "Zvýšit hlasitost"
|
||||
}
|
||||
},
|
||||
"label": "Vybrat globální klávesnicové zkratky:",
|
||||
"title": "Globální klávesnicové zkratky hlasitosti"
|
||||
},
|
||||
"volume-steps": {
|
||||
"label": "Vybrat Zvýšení/Snížení hlasitost Steps",
|
||||
"title": "Hlasitostní steps"
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -465,11 +551,15 @@
|
||||
"title": "Vybrat kvalitu videa"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Umožňuje měnit kvalitu videa pomocí tlačítka na video overlay",
|
||||
"name": "Měnič kvality videa"
|
||||
},
|
||||
"shortcuts": {
|
||||
"description": "Dovoluje nastavit globální klávesové zkratky pro playback (přehrát/pozastavit/další/předchozí) a vypínání media OSD pomocí přepisování media klíčů, zapínání Ctrl/CMD + F k vyhledávání, zapínání Linux MPRIS podporu pro media klíče, a vlastní klávesové zkratky pro pokročilé uživatele.",
|
||||
"menu": {
|
||||
"override-media-keys": "Přepsat Media Keys"
|
||||
"override-media-keys": "Přepsat media klíče",
|
||||
"set-keybinds": "Nastavit globální Controls písniček"
|
||||
},
|
||||
"name": "Zkratky (& MPRIS)",
|
||||
"prompt": {
|
||||
@ -478,19 +568,26 @@
|
||||
"next": "Další",
|
||||
"play-pause": "Přehrát / Pozastavit",
|
||||
"previous": "Předchozí"
|
||||
}
|
||||
},
|
||||
"label": "Vybrat globální klávesnicové zkratky pro ovládání písniček:",
|
||||
"title": "Globální klávesnicové zkratky"
|
||||
}
|
||||
}
|
||||
},
|
||||
"skip-disliked-songs": {
|
||||
"description": "Přeskakovat disliked písničky",
|
||||
"name": "Přeskočit Disliked písničky"
|
||||
},
|
||||
"skip-silences": {
|
||||
"description": "Automaticky přeskakovat tichá místa v písničkách",
|
||||
"name": "Přeskočit Tichá místa"
|
||||
"name": "Přeskakovat Tichá místa"
|
||||
},
|
||||
"sponsorblock": {
|
||||
"description": "Automaticky přeskakuje non-music části jako intro/outro nebo části of music videos, kde nehraje písnčka"
|
||||
"description": "Automaticky přeskakuje nehudební části jako intro/outro nebo části hudebních videí, kde nehraje písnčka",
|
||||
"name": "SponsorBlock"
|
||||
},
|
||||
"taskbar-mediacontrol": {
|
||||
"description": "Ovládejte přehrávání z vašeho hlavního panelu Windows",
|
||||
"description": "Ovládejte přehrávání z vašeho Windows hlavního panelu",
|
||||
"name": "Hlavní panel Media Control"
|
||||
},
|
||||
"touchbar": {
|
||||
@ -498,10 +595,11 @@
|
||||
"name": "Touch Bar"
|
||||
},
|
||||
"tuna-obs": {
|
||||
"description": "Integrace s OBS's plugin Tuna"
|
||||
"description": "Integrace s OBS's plugin Tuna",
|
||||
"name": "Tuna OBS"
|
||||
},
|
||||
"video-toggle": {
|
||||
"description": "Přidává tlačítko switch mezi videem/písničkou mode. Může také optionally remove celou video kartu",
|
||||
"description": "Přidává tlačítko k switch mezi video/písničko režimem. Může také odstranit celou video kartu",
|
||||
"menu": {
|
||||
"align": {
|
||||
"label": "Zarovnání",
|
||||
@ -513,9 +611,15 @@
|
||||
},
|
||||
"force-hide": "Vynutit odstranění karty videa",
|
||||
"mode": {
|
||||
"label": "Režim"
|
||||
"label": "Režim",
|
||||
"submenu": {
|
||||
"custom": "Vlastní přepínač",
|
||||
"disabled": "Vypnuto",
|
||||
"native": "Původní přepínač"
|
||||
}
|
||||
}
|
||||
},
|
||||
"name": "Přepínač videa",
|
||||
"templates": {
|
||||
"button": "Písnička"
|
||||
}
|
||||
|
||||
@ -170,7 +170,8 @@
|
||||
},
|
||||
"plugins": {
|
||||
"enabled": "Enabled",
|
||||
"label": "Plugins"
|
||||
"label": "Plugins",
|
||||
"new": "NEW"
|
||||
},
|
||||
"view": {
|
||||
"label": "View",
|
||||
@ -185,6 +186,10 @@
|
||||
}
|
||||
},
|
||||
"tray": {
|
||||
"tooltip": {
|
||||
"default": "YouTube Music",
|
||||
"with-song-info": "YouTube Music: {{artist}} - {{title}}"
|
||||
},
|
||||
"next": "Next",
|
||||
"play-pause": "Play/Pause",
|
||||
"previous": "Previous",
|
||||
@ -201,6 +206,10 @@
|
||||
},
|
||||
"name": "Adblocker"
|
||||
},
|
||||
"album-actions": {
|
||||
"description": "Adds Undislike, Dislike, Like, and Unlike buttons to apply this to all songs in a playlist or album.",
|
||||
"name": "Album actions"
|
||||
},
|
||||
"album-color-theme": {
|
||||
"description": "Applies a dynamic theme and visual effects based on the album color palette",
|
||||
"name": "Album Color Theme"
|
||||
@ -289,7 +298,7 @@
|
||||
"menu": {
|
||||
"advanced": "Advanced"
|
||||
},
|
||||
"name": "Crossfade [beta]",
|
||||
"name": "Crossfade [Beta]",
|
||||
"prompt": {
|
||||
"options": {
|
||||
"multi-input": {
|
||||
@ -414,7 +423,7 @@
|
||||
},
|
||||
"lumiastream": {
|
||||
"description": "Adds Lumia Stream support",
|
||||
"name": "Lumia Stream [beta]"
|
||||
"name": "Lumia Stream [Beta]"
|
||||
},
|
||||
"lyrics-genius": {
|
||||
"description": "Adds lyrics support for most songs",
|
||||
@ -426,6 +435,52 @@
|
||||
"fetched-lyrics": "Fetched lyrics for Genius"
|
||||
}
|
||||
},
|
||||
"music-together": {
|
||||
"description": "Share a playlist with others. When the host plays a song, everyone else will hear the same song",
|
||||
"dialog": {
|
||||
"enter-host": "Enter Host ID"
|
||||
},
|
||||
"internal": {
|
||||
"save": "Save",
|
||||
"track-source": "Track Source",
|
||||
"unknown-user": "Unknown User"
|
||||
},
|
||||
"menu": {
|
||||
"click-to-copy-id": "Copy Host ID",
|
||||
"close": "Close Music Together",
|
||||
"connected-users": "Connected Users",
|
||||
"disconnect": "Disconnect Music Together",
|
||||
"empty-user": "No connected users",
|
||||
"host": "Music Together Host",
|
||||
"join": "Join Music Together",
|
||||
"permission": {
|
||||
"all": "Allow guests to control playlist and player",
|
||||
"host-only": "Only the host can control playlist and player",
|
||||
"playlist": "Allow guests to control playlist"
|
||||
},
|
||||
"set-permission": "Change Control Permission",
|
||||
"status": {
|
||||
"disconnected": "Disconnected",
|
||||
"guest": "Connected as Guest",
|
||||
"host": "Connected as Host"
|
||||
}
|
||||
},
|
||||
"name": "Music Together [Beta]",
|
||||
"toast": {
|
||||
"add-song-failed": "Failed to add song",
|
||||
"closed": "Music Together closed",
|
||||
"disconnected": "Music Together disconnected",
|
||||
"host-failed": "Failed to host Music Together",
|
||||
"id-copied": "Host ID copied to clipboard",
|
||||
"id-copy-failed": "Failed to copy Host ID to clipboard",
|
||||
"join-failed": "Failed to join Music Together",
|
||||
"joined": "Joined Music Together",
|
||||
"permission-changed": "Music Together permission changed to \"{{permission}}\"",
|
||||
"remove-song-failed": "Failed to remove song",
|
||||
"user-connected": "{{name}} joined Music Together",
|
||||
"user-disconnected": "{{name}} left Music Together"
|
||||
}
|
||||
},
|
||||
"navigation": {
|
||||
"description": "Next/Back navigation arrows directly integrated in the interface, like in your favorite browser",
|
||||
"name": "Navigation"
|
||||
|
||||
@ -170,7 +170,8 @@
|
||||
},
|
||||
"plugins": {
|
||||
"enabled": "Habilitado",
|
||||
"label": "Plugins"
|
||||
"label": "Plugins",
|
||||
"new": "NUEVO"
|
||||
},
|
||||
"view": {
|
||||
"label": "Ver",
|
||||
@ -190,7 +191,11 @@
|
||||
"previous": "Anterior",
|
||||
"quit": "Salir",
|
||||
"restart": "Reiniciar la aplicación",
|
||||
"show": "Mostrar ventana"
|
||||
"show": "Mostrar ventana",
|
||||
"tooltip": {
|
||||
"default": "YouTube Music",
|
||||
"with-song-info": "YouTube Music: {{artist}} - {{title}}"
|
||||
}
|
||||
}
|
||||
},
|
||||
"plugins": {
|
||||
@ -201,6 +206,10 @@
|
||||
},
|
||||
"name": "Adblocker"
|
||||
},
|
||||
"album-actions": {
|
||||
"description": "Añade los botones \"No me gusta\", \"No me gusta\", \"Me gusta\" y \"No me gusta\" para aplicarlos a todas las canciones de una lista de reproducción o un álbum.",
|
||||
"name": "Acciones del álbum"
|
||||
},
|
||||
"album-color-theme": {
|
||||
"description": "Aplica un tema dinámico y efectos visuales basados en la paleta de colores del álbum",
|
||||
"name": "Color del álbum"
|
||||
@ -289,7 +298,7 @@
|
||||
"menu": {
|
||||
"advanced": "Avanzado"
|
||||
},
|
||||
"name": "Crossfade [beta]",
|
||||
"name": "Crossfade [Beta]",
|
||||
"prompt": {
|
||||
"options": {
|
||||
"multi-input": {
|
||||
@ -414,7 +423,7 @@
|
||||
},
|
||||
"lumiastream": {
|
||||
"description": "Agrega soporte para Lumia Stream",
|
||||
"name": "Lumia Stream [beta]"
|
||||
"name": "Lumia Stream [Beta]"
|
||||
},
|
||||
"lyrics-genius": {
|
||||
"description": "Añade el soporte para las letras para la mayoría de las canciones",
|
||||
@ -426,6 +435,51 @@
|
||||
"fetched-lyrics": "Letras recuperadas de Genius"
|
||||
}
|
||||
},
|
||||
"music-together": {
|
||||
"description": "Comparte una lista de reproducción con los demás. Cuando el anfitrión reproduzca una canción, todos los demás escucharán la misma",
|
||||
"dialog": {
|
||||
"enter-host": "Introduzca el ID del host"
|
||||
},
|
||||
"internal": {
|
||||
"save": "Guardar",
|
||||
"track-source": "Fuente de la pista",
|
||||
"unknown-user": "Usuario desconocido"
|
||||
},
|
||||
"menu": {
|
||||
"click-to-copy-id": "Copiar el ID del host",
|
||||
"close": "Cerrar Music Together",
|
||||
"connected-users": "Usuarios conectados",
|
||||
"disconnect": "Desactivar Music Together",
|
||||
"empty-user": "No hay usuarios conectados",
|
||||
"host": "Host de Music Together",
|
||||
"join": "Únase a Music Together",
|
||||
"permission": {
|
||||
"all": "Permite a los invitados controlar la lista de reproducción y el reproductor",
|
||||
"host-only": "Sólo el anfitrión puede controlar la lista de reproducción y el reproductor",
|
||||
"playlist": "Permita que los invitados controlen la lista de reproducción"
|
||||
},
|
||||
"set-permission": "Permiso de control de cambios",
|
||||
"status": {
|
||||
"disconnected": "Desconectado",
|
||||
"guest": "Conectado como invitado",
|
||||
"host": "Conectado como anfitrión"
|
||||
}
|
||||
},
|
||||
"name": "Music Together [Beta]",
|
||||
"toast": {
|
||||
"add-song-failed": "No se puede añadir la canción",
|
||||
"closed": "Music Together cerrado",
|
||||
"disconnected": "Music Together desconectados",
|
||||
"host-failed": "Fallo el host de Music Together",
|
||||
"id-copied": "ID del host copiado en el portapapeles",
|
||||
"join-failed": "Fallo en la unión a Music Together",
|
||||
"joined": "Unido a Music Together",
|
||||
"permission-changed": "Permiso de Music Together cambiado a \"{{permission}}\"",
|
||||
"remove-song-failed": "Error al eliminar la canción",
|
||||
"user-connected": "{{name}} se unió a Music Together",
|
||||
"user-disconnected": "{{name}} dejó Music Together"
|
||||
}
|
||||
},
|
||||
"navigation": {
|
||||
"description": "Flechas de navegación Siguiente/Atrás directamente integradas en la interfaz, como en tu navegador favorito",
|
||||
"name": "Navegación"
|
||||
|
||||
@ -289,7 +289,7 @@
|
||||
"menu": {
|
||||
"advanced": "Avancé"
|
||||
},
|
||||
"name": "Fondu enchaîné [bêta]",
|
||||
"name": "Fondu enchaîné [Bêta]",
|
||||
"prompt": {
|
||||
"options": {
|
||||
"multi-input": {
|
||||
@ -414,7 +414,7 @@
|
||||
},
|
||||
"lumiastream": {
|
||||
"description": "Ajoute la prise en charge de Lumia Stream",
|
||||
"name": "Lumia Stream [bêta]"
|
||||
"name": "Lumia Stream [Bêta]"
|
||||
},
|
||||
"lyrics-genius": {
|
||||
"description": "Ajoute la prise en charge des paroles pour la plupart des chansons",
|
||||
|
||||
@ -7,7 +7,9 @@
|
||||
"initialize-failed": "Gagal dalam menginisialisasi plugin \"{{pluginName}}\"",
|
||||
"load-all": "Memuat semua plugin",
|
||||
"load-failed": "Gagal memuat plugin \"{{pluginName}}\"",
|
||||
"loaded": "Plugin \"{{pluginName}}\" dimuat"
|
||||
"loaded": "Plugin \"{{pluginName}}\" dimuat",
|
||||
"unload-failed": "Gagal untuk memuat plugin \"{{pluginName}}\"",
|
||||
"unloaded": "Plugin \"{{pluginName}}\" telah dikeluarkan"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@ -170,7 +170,8 @@
|
||||
},
|
||||
"plugins": {
|
||||
"enabled": "Attivato",
|
||||
"label": "Plugin"
|
||||
"label": "Plugin",
|
||||
"new": "NUOVO"
|
||||
},
|
||||
"view": {
|
||||
"label": "Visualizzazione",
|
||||
@ -201,6 +202,10 @@
|
||||
},
|
||||
"name": "Adblocker"
|
||||
},
|
||||
"album-actions": {
|
||||
"description": "Aggiunge i pulsanti Undislike, Dislike, Like e Unlike a tutti i brani di una playlist o di un album.",
|
||||
"name": "Azioni album"
|
||||
},
|
||||
"album-color-theme": {
|
||||
"description": "Applica un tema dinamico e degli effetti visivi basandosi sul colore dell'album",
|
||||
"name": "Tema abbinato a colore album"
|
||||
@ -414,7 +419,7 @@
|
||||
},
|
||||
"lumiastream": {
|
||||
"description": "Aggiungi supporto per Lumia Stream",
|
||||
"name": "Lumia Stream [beta]"
|
||||
"name": "Lumia Stream [Beta]"
|
||||
},
|
||||
"lyrics-genius": {
|
||||
"description": "Aggiunge il supporto dei testi per la maggior parte delle canzoni",
|
||||
@ -426,6 +431,51 @@
|
||||
"fetched-lyrics": "Testi recuperati per Genius"
|
||||
}
|
||||
},
|
||||
"music-together": {
|
||||
"description": "Condividi una playlist con altri. Quando l'Host riproduce un brano, tutti gli altri ascolteranno lo stesso brano",
|
||||
"dialog": {
|
||||
"enter-host": "Inserisci l'ID dell'Host"
|
||||
},
|
||||
"internal": {
|
||||
"save": "Salva",
|
||||
"track-source": "Traccia sorgente",
|
||||
"unknown-user": "Utente sconosciuto"
|
||||
},
|
||||
"menu": {
|
||||
"click-to-copy-id": "Copia l'ID dell'Host",
|
||||
"close": "Chiudi Music Together",
|
||||
"connected-users": "Utenti connessi",
|
||||
"disconnect": "Disconetti Music Together",
|
||||
"empty-user": "Utenti non connessi",
|
||||
"host": "Music Together Host",
|
||||
"join": "Unisciti a Music Together",
|
||||
"permission": {
|
||||
"all": "Consenti ai Guest di controllare la playlist e il player",
|
||||
"host-only": "Solo l'Host può controllare la playlist e il player",
|
||||
"playlist": "Consenti ai Guest di controllare la playlist"
|
||||
},
|
||||
"set-permission": "Cambia autorizzazione di controllo",
|
||||
"status": {
|
||||
"disconnected": "Disconnesso",
|
||||
"guest": "Connesso come Guest",
|
||||
"host": "Connesso come Host"
|
||||
}
|
||||
},
|
||||
"name": "Music Together [Beta]",
|
||||
"toast": {
|
||||
"add-song-failed": "Impossibile aggiungere il brano",
|
||||
"closed": "Music Together chiuso",
|
||||
"disconnected": "Music Together disconnesso",
|
||||
"host-failed": "Impossibile ospitare Music Together",
|
||||
"id-copied": "L'ID dell Host è stato copiato negli appunti",
|
||||
"join-failed": "Impossibile unirsi a Music Together",
|
||||
"joined": "Unito a Music Together",
|
||||
"permission-changed": "L'autorizzazione di Music Together è cambiata in {{permission}}",
|
||||
"remove-song-failed": "Impossibile rimuovere il brano",
|
||||
"user-connected": "{{name}} si è unito a Music Together",
|
||||
"user-disconnected": "{{name}} ha lasciato Music Together"
|
||||
}
|
||||
},
|
||||
"navigation": {
|
||||
"description": "Frecce di navigazione Avanti/Indietro integrate direttamente nell'interfaccia, come nel tuo browser preferito",
|
||||
"name": "Navigazione"
|
||||
|
||||
@ -170,7 +170,8 @@
|
||||
},
|
||||
"plugins": {
|
||||
"enabled": "활성화",
|
||||
"label": "확장"
|
||||
"label": "확장",
|
||||
"new": "NEW"
|
||||
},
|
||||
"view": {
|
||||
"label": "보기",
|
||||
@ -190,7 +191,11 @@
|
||||
"previous": "이전",
|
||||
"quit": "종료",
|
||||
"restart": "앱 재시작",
|
||||
"show": "창 표시"
|
||||
"show": "창 표시",
|
||||
"tooltip": {
|
||||
"default": "유튜브 뮤직",
|
||||
"with-song-info": "유튜브 뮤직: {{artist}} - {{title}}"
|
||||
}
|
||||
}
|
||||
},
|
||||
"plugins": {
|
||||
@ -201,6 +206,10 @@
|
||||
},
|
||||
"name": "광고 차단기"
|
||||
},
|
||||
"album-actions": {
|
||||
"description": "좋아요, 싫어요 버튼을 추가하고, 결과를 재생 목록 또는 앨범의 모든 노래에 적용합니다.",
|
||||
"name": "앨범 액션"
|
||||
},
|
||||
"album-color-theme": {
|
||||
"description": "앨범 색상 팔레트를 기반으로 동적 테마 및 시각 효과를 적용합니다",
|
||||
"name": "앨범 컬러 기반 테마"
|
||||
@ -426,6 +435,52 @@
|
||||
"fetched-lyrics": "Genius에서 가사 불러옴"
|
||||
}
|
||||
},
|
||||
"music-together": {
|
||||
"description": "여러명과 함께 플레이리스트를 공유합니다. 호스트가 음악을 재생하면, 다른 사용자들도 같은 노래를 들을 수 있습니다",
|
||||
"dialog": {
|
||||
"enter-host": "호스트 아이디를 입력하세요"
|
||||
},
|
||||
"internal": {
|
||||
"save": "저장",
|
||||
"track-source": "재생 중인 트랙 출처",
|
||||
"unknown-user": "알 수 없는 사용자"
|
||||
},
|
||||
"menu": {
|
||||
"click-to-copy-id": "호스트 아이디 복사",
|
||||
"close": "Music Together 닫기",
|
||||
"connected-users": "연결된 사용자",
|
||||
"disconnect": "Music Together 연결 끊기",
|
||||
"empty-user": "연결된 사용자 없음",
|
||||
"host": "Music Together 호스트",
|
||||
"join": "Music Together 참여",
|
||||
"permission": {
|
||||
"all": "게스트가 모두 제어 가능",
|
||||
"host-only": "호스트만 제어 가능",
|
||||
"playlist": "게스트가 재생목록 제어 가능"
|
||||
},
|
||||
"set-permission": "제어 권한 변경",
|
||||
"status": {
|
||||
"disconnected": "연결 끊김",
|
||||
"guest": "게스트로 연결됨",
|
||||
"host": "호스트로 연결됨"
|
||||
}
|
||||
},
|
||||
"name": "Music Together [베타]",
|
||||
"toast": {
|
||||
"add-song-failed": "노래 추가 실패",
|
||||
"closed": "Music Together가 닫혔습니다",
|
||||
"disconnected": "Music Together 연결이 끊어졌습니다",
|
||||
"host-failed": "Music Together를 열 수 없습니다",
|
||||
"id-copied": "호스트 아이디가 클립보드에 복사되었습니다",
|
||||
"id-copy-failed": "호스트 ID를 클립보드에 복사하지 못했습니다",
|
||||
"join-failed": "Music Together에 참여할 수 없습니다",
|
||||
"joined": "Music Together에 참여했습니다",
|
||||
"permission-changed": "Music Together 제어 권한이 \"{{permission}}\"(으)로 변경되었습니다",
|
||||
"remove-song-failed": "노래 제거 실패",
|
||||
"user-connected": "{{name}}님이 Music Together에 참여했습니다",
|
||||
"user-disconnected": "{{name}}님이 Music Together에서 나갔습니다"
|
||||
}
|
||||
},
|
||||
"navigation": {
|
||||
"description": "브라우저에서처럼, UI에 직접 통합된 앞으로/뒤로 탐색하는 화살표",
|
||||
"name": "탐색"
|
||||
|
||||
@ -99,7 +99,7 @@
|
||||
"auto-reset-app-cache": "Perkrauti programos talpyklą, kai programa paleidžiama",
|
||||
"disable-hardware-acceleration": "Išjungti aparatūros pagreitį",
|
||||
"edit-config-json": "Redaguoti config.json",
|
||||
"override-user-agent": "Perrašyti User-Agent",
|
||||
"override-user-agent": "Perrašyti \"User-Agent\"",
|
||||
"restart-on-config-changes": "Perkrauti po config pasikeitimo",
|
||||
"set-proxy": {
|
||||
"label": "Nustatyti įgaliotajį serverį",
|
||||
@ -116,7 +116,7 @@
|
||||
"auto-update": "Automatinis Atnaujinimas",
|
||||
"hide-menu": {
|
||||
"dialog": {
|
||||
"message": "Meniu bus paslėpta per kitą paleidimą, naudokite [Alt], kad ją parodyti (arba [`] jei naudojama programos meniu)",
|
||||
"message": "Meniu bus paslėpta per kitą paleidimą, naudokite [Alt], kad ją parodyti (arba kairinio kirčio ženklą [`] jei naudojama programos meniu)",
|
||||
"title": "\"Paslėpti Meniu\" įjungtas"
|
||||
},
|
||||
"label": "Paslėpti Meniu"
|
||||
@ -144,7 +144,7 @@
|
||||
"disabled": "Išjungta",
|
||||
"enabled-and-hide-app": "Įjungta ir slėpti programos langą",
|
||||
"enabled-and-show-app": "Įjungta ir rodyti programos langą",
|
||||
"play-pause-on-click": "Leisti/Sustabdyti ant paspaudimo"
|
||||
"play-pause-on-click": "Paleisti/Pristabdyti ant paspaudimo"
|
||||
}
|
||||
},
|
||||
"visual-tweaks": {
|
||||
@ -175,7 +175,7 @@
|
||||
"view": {
|
||||
"label": "Vaizdas",
|
||||
"submenu": {
|
||||
"force-reload": "Priverstinis perkrovimas",
|
||||
"force-reload": "Priverstinai perkrauti",
|
||||
"reload": "Perkrauti",
|
||||
"reset-zoom": "Tikras dydis",
|
||||
"toggle-fullscreen": "Įjungti/Išjungti Pilną Ekraną",
|
||||
@ -186,7 +186,7 @@
|
||||
},
|
||||
"tray": {
|
||||
"next": "Kitas",
|
||||
"play-pause": "Leisti/Sustabdyti",
|
||||
"play-pause": "Paleisti/Pristabdyti",
|
||||
"previous": "Ankstesnis",
|
||||
"quit": "Išeiti",
|
||||
"restart": "Perkrauti programą",
|
||||
@ -239,7 +239,7 @@
|
||||
}
|
||||
},
|
||||
"smoothness-transition": {
|
||||
"label": "Perėjimo švelnumas",
|
||||
"label": "Perliejimo švelnumas",
|
||||
"submenu": {
|
||||
"during": "Per {{interpolationTime}}s"
|
||||
}
|
||||
@ -289,7 +289,7 @@
|
||||
"menu": {
|
||||
"advanced": "Išplėstinė"
|
||||
},
|
||||
"name": "Perliejimas [beta]",
|
||||
"name": "Perliejimas [Beta]",
|
||||
"prompt": {
|
||||
"options": {
|
||||
"multi-input": {
|
||||
@ -307,7 +307,7 @@
|
||||
}
|
||||
},
|
||||
"disable-autoplay": {
|
||||
"description": "Pradeda dainą sustabdytame rėžime",
|
||||
"description": "Pradeda dainą pristabdytame rėžime",
|
||||
"menu": {
|
||||
"apply-once": "Pritaiko tik per programos paleidimą"
|
||||
},
|
||||
@ -319,11 +319,11 @@
|
||||
"connected": "Prisijungta prie \"Discord\"",
|
||||
"disconnected": "Atsijungta nuo \"Discord\""
|
||||
},
|
||||
"description": "Parodyk savo draugams ko tu klausaisi su \"Turtingas Buvimas\"",
|
||||
"description": "Parodyk savo draugams ko tu klausaisi su \"Turtingas Buvimas\" (Rich Presence)",
|
||||
"menu": {
|
||||
"auto-reconnect": "Automatiškai prisijungti",
|
||||
"clear-activity": "Išvalyti veiksmus",
|
||||
"clear-activity-after-timeout": "Išvalyti veiksmus po skirtojo laiko",
|
||||
"clear-activity": "Išvalyti veiklą",
|
||||
"clear-activity-after-timeout": "Išvalyti veiklą po skirtojo laiko",
|
||||
"connected": "Prisijungta",
|
||||
"disconnected": "Atsijungta",
|
||||
"hide-duration-left": "Slėpti kiek liko laiko",
|
||||
@ -331,7 +331,7 @@
|
||||
"play-on-youtube-music": "Leisti ant \"Youtube Music\"",
|
||||
"set-inactivity-timeout": "Nustatyti neveiklumo laiką"
|
||||
},
|
||||
"name": "\"Discord\" Turtingas Buvimas",
|
||||
"name": "\"Discord\" Turtingas Buvimas (Rich Presence)",
|
||||
"prompt": {
|
||||
"set-inactivity-timeout": {
|
||||
"label": "Įveskite neveiklumo skirtąjį laiką sekundėmis:",
|
||||
@ -371,10 +371,10 @@
|
||||
"folder-already-exists": "Aplankas {{playlistFolder}} jau egzistuoja",
|
||||
"getting-playlist-info": "Gaunama grojaraščio informacija…",
|
||||
"loading": "Kraunama…",
|
||||
"playlist-has-only-one-song": "Grojaraštis turi tik vieną daiktą, jis atsisiunčiamas tiesiogiai",
|
||||
"playlist-has-only-one-song": "Grojaraštis turi tik vieną elementą, jis atsisiunčiamas tiesiogiai",
|
||||
"playlist-id-not-found": "Grojaraščio ID nerastas",
|
||||
"playlist-is-empty": "Grojaraštis yra tuščias",
|
||||
"playlist-is-mix-or-private": "Paklaida gaunant grojaraščio informaciją: Pasitikrink, kad nėra privatus ar \"Surinkta specialiai jums\" grojaraštis\n\n{{error}}",
|
||||
"playlist-is-mix-or-private": "Paklaida gaunant grojaraščio informaciją: Pasitikrink, kad jis nėra privatus ar \"Surinkta specialiai jums\" grojaraštis\n\n{{error}}",
|
||||
"preparing-file": "Failas paruošiamas…",
|
||||
"saving": "Išsaugojama…",
|
||||
"trying-to-get-playlist-id": "Bandoma gauti grojaraščio ID: {{playlistId}}",
|
||||
@ -414,7 +414,7 @@
|
||||
},
|
||||
"lumiastream": {
|
||||
"description": "Prideda \"Lumia Stream\" palaikymą",
|
||||
"name": "Lumia Stream [beta]"
|
||||
"name": "Lumia Stream [Beta]"
|
||||
},
|
||||
"lyrics-genius": {
|
||||
"description": "Prideda daugumai dainių žodžių tekstus",
|
||||
@ -423,7 +423,7 @@
|
||||
},
|
||||
"name": "\"Genius\" Žodžių tekstai",
|
||||
"renderer": {
|
||||
"fetched-lyrics": "Gauti žodžiai iš „Genius“."
|
||||
"fetched-lyrics": "Gauti žodžiai iš „Genius“"
|
||||
}
|
||||
},
|
||||
"navigation": {
|
||||
@ -442,7 +442,7 @@
|
||||
"label": "Interaktyvūs nustatymai",
|
||||
"submenu": {
|
||||
"hide-button-text": "Paslėpti mygtuko tekstą",
|
||||
"refresh-on-play-pause": "Atnaujinti ant Leisti/Sustabdyti",
|
||||
"refresh-on-play-pause": "Atnaujinti ant Paleidimo/Pristabdymo",
|
||||
"tray-controls": "Atidaryti/Uždaryti ant padėklo paspaudimo"
|
||||
}
|
||||
},
|
||||
@ -455,7 +455,7 @@
|
||||
"picture-in-picture": {
|
||||
"description": "Leidžia pakeisti programą į \"picture-in-picture\" rėžimą",
|
||||
"menu": {
|
||||
"always-on-top": "Visada viršuje",
|
||||
"always-on-top": "Visada ant viršaus",
|
||||
"hotkey": {
|
||||
"label": "Spartusis klavišas",
|
||||
"prompt": {
|
||||
@ -543,7 +543,7 @@
|
||||
},
|
||||
"skip-silences": {
|
||||
"description": "Automatiškai praleisti tylos dalis dainose",
|
||||
"name": "Praleisti Tylas"
|
||||
"name": "Praleisti Tylumas"
|
||||
},
|
||||
"sponsorblock": {
|
||||
"description": "Automatiškai praleidžia ne muzikines dalis, pvz., įžangą/užvedimą arba muzikinių vaizdo įrašų dalis, kuriose daina negrojama",
|
||||
|
||||
@ -287,7 +287,7 @@
|
||||
"menu": {
|
||||
"advanced": "Avansert"
|
||||
},
|
||||
"name": "Overgang [beta]",
|
||||
"name": "Overgang [Beta]",
|
||||
"prompt": {
|
||||
"options": {
|
||||
"multi-input": {
|
||||
@ -412,7 +412,7 @@
|
||||
},
|
||||
"lumiastream": {
|
||||
"description": "Legger til Lumia Stream-støtte",
|
||||
"name": "Lumia Stream [beta]"
|
||||
"name": "Lumia Stream [Beta]"
|
||||
},
|
||||
"lyrics-genius": {
|
||||
"description": "Gir sangtekststøtte for de fleste spor",
|
||||
|
||||
@ -105,7 +105,7 @@
|
||||
"label": "Ustaw proxy",
|
||||
"prompt": {
|
||||
"label": "Podaj adres Proxy: (zostaw pusty aby wyłączyć)",
|
||||
"placeholder": "Przykład: socks5://127.0.0.1:9999",
|
||||
"placeholder": "Przykład: SOCKS5://127.0.0.1:9999",
|
||||
"title": "Ustaw proxy"
|
||||
}
|
||||
},
|
||||
@ -241,7 +241,7 @@
|
||||
"smoothness-transition": {
|
||||
"label": "Płynność przejścia",
|
||||
"submenu": {
|
||||
"during": "W czasie {{interpolationTime}}s"
|
||||
"during": "W czasie {{interpolationTime}} s"
|
||||
}
|
||||
},
|
||||
"use-fullscreen": {
|
||||
@ -293,8 +293,8 @@
|
||||
"prompt": {
|
||||
"options": {
|
||||
"multi-input": {
|
||||
"fade-in-duration": "Czas wnikania (milisekundy)",
|
||||
"fade-out-duration": "Czas zanikania (milisekundy)",
|
||||
"fade-in-duration": "Czas wnikania (ms)",
|
||||
"fade-out-duration": "Czas zanikania (ms)",
|
||||
"fade-scaling": {
|
||||
"label": "Skalowanie zanikania",
|
||||
"linear": "Liniowe",
|
||||
@ -414,7 +414,7 @@
|
||||
},
|
||||
"lumiastream": {
|
||||
"description": "Dodaje obsługę Lumia Stream",
|
||||
"name": "Lumia Stream [beta]"
|
||||
"name": "Lumia Stream [Beta]"
|
||||
},
|
||||
"lyrics-genius": {
|
||||
"description": "Dodaje obsługę tekstów dla większości piosenek",
|
||||
|
||||
@ -289,7 +289,7 @@
|
||||
"menu": {
|
||||
"advanced": "Avançado"
|
||||
},
|
||||
"name": "Transição entre músicas [beta]",
|
||||
"name": "Transição entre músicas [Beta]",
|
||||
"prompt": {
|
||||
"options": {
|
||||
"multi-input": {
|
||||
@ -414,7 +414,7 @@
|
||||
},
|
||||
"lumiastream": {
|
||||
"description": "Adiciona suporte Lumia Stream",
|
||||
"name": "Lumia Stream [beta]"
|
||||
"name": "Lumia Stream [Beta]"
|
||||
},
|
||||
"lyrics-genius": {
|
||||
"description": "Adiciona suporte a letras para a maioria das músicas",
|
||||
|
||||
@ -99,6 +99,7 @@
|
||||
"auto-reset-app-cache": "Очищать кеш при запуске приложения",
|
||||
"disable-hardware-acceleration": "Отключить аппаратное ускорение",
|
||||
"edit-config-json": "Редактировать config.json",
|
||||
"override-user-agent": "Переопределить User-Agent",
|
||||
"restart-on-config-changes": "Перезапускать при изменениях конфига",
|
||||
"set-proxy": {
|
||||
"label": "Задать прокси",
|
||||
@ -131,6 +132,7 @@
|
||||
}
|
||||
},
|
||||
"resume-on-start": "Продолжать последнюю песню при запуске приложения",
|
||||
"single-instance-lock": "Запрет запуска нескольких экземпляров",
|
||||
"start-at-login": "Запуск при включении компьютера",
|
||||
"starting-page": {
|
||||
"label": "Стартовая страница",
|
||||
@ -399,17 +401,37 @@
|
||||
"description": "Делает слайдер громкости расширенным чтобы было легче выбирать низкие уровни.",
|
||||
"name": "Расширенная громкость"
|
||||
},
|
||||
"in-app-menu": {
|
||||
"description": "Придает меню модный вид"
|
||||
},
|
||||
"last-fm": {
|
||||
"name": "Last.fm"
|
||||
},
|
||||
"navigation": {
|
||||
"name": "Navigation"
|
||||
"name": "Навигация"
|
||||
},
|
||||
"no-google-login": {
|
||||
"name": "No Google Login"
|
||||
},
|
||||
"notifications": {
|
||||
"name": "Notifications"
|
||||
"name": "Уведомления"
|
||||
},
|
||||
"picture-in-picture": {
|
||||
"description": "Позволяет переключить приложение в режим «картинка в картинке»",
|
||||
"menu": {
|
||||
"always-on-top": "Всегда наверху",
|
||||
"hotkey": {
|
||||
"label": "Горячая клавиша",
|
||||
"prompt": {
|
||||
"keybind-options": {
|
||||
"hotkey": "Горячая клавиша"
|
||||
},
|
||||
"label": "Выберите горячую клавишу для переключения режима изображения в изображении"
|
||||
}
|
||||
},
|
||||
"save-window-position": "Сохранить положение окна",
|
||||
"save-window-size": "Сохранить размер окна"
|
||||
}
|
||||
},
|
||||
"shortcuts": {
|
||||
"prompt": {
|
||||
@ -437,15 +459,26 @@
|
||||
"right": "Right"
|
||||
}
|
||||
},
|
||||
"force-hide": "Скрыть обложку",
|
||||
"mode": {
|
||||
"label": "Mode"
|
||||
"label": "Mode",
|
||||
"submenu": {
|
||||
"custom": "Кастомный переключатель",
|
||||
"disabled": "Отключен",
|
||||
"native": "Стандартный переключатель"
|
||||
}
|
||||
}
|
||||
},
|
||||
"name": "Переключатель видео",
|
||||
"templates": {
|
||||
"button": "Song"
|
||||
}
|
||||
},
|
||||
"visualizer": {
|
||||
"description": "Заменяет обложку визуализатором музыки",
|
||||
"menu": {
|
||||
"visualizer-type": "Вид визуализации"
|
||||
},
|
||||
"name": "Визуалайзер"
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,7 +7,9 @@
|
||||
"initialize-failed": "\"{{pluginName}}\" eklentisi başlatılamadı",
|
||||
"load-all": "Tüm eklentiler yükleniyor",
|
||||
"load-failed": "\"{{pluginName}}\" eklentisi yüklenemedi",
|
||||
"loaded": "\"{{pluginName}}\" eklentisi yüklendi"
|
||||
"loaded": "\"{{pluginName}}\" eklentisi yüklendi",
|
||||
"unload-failed": "\"{{pluginName}}\" eklentisi çıkartılamadı",
|
||||
"unloaded": "\"{{pluginName}}\" eklentisi kaldırıldı"
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -23,56 +25,142 @@
|
||||
},
|
||||
"i18n": {
|
||||
"loaded": "i18n yüklendi"
|
||||
},
|
||||
"second-instance": {
|
||||
"receive-command": "Protokol üzerinden alınan komut: \"{{command}}\""
|
||||
},
|
||||
"theme": {
|
||||
"css-file-not-found": "\"{{cssFile}}\" adlı CSS dosyası bulunamadı, yok sayılıyor"
|
||||
},
|
||||
"unresponsive": {
|
||||
"details": "Yanıt verilmedi!\n{{error}}"
|
||||
},
|
||||
"when-ready": {
|
||||
"clearing-cache-after-20s": "Uygulama ön belleği temizleme"
|
||||
},
|
||||
"window": {
|
||||
"tried-to-render-offscreen": "Pencere ekranın dışında oluşturulmaya çalışıldı, windowSize={{windowSize}}, displaySize={{displaySize}}, position={{position}}"
|
||||
}
|
||||
},
|
||||
"dialog": {
|
||||
"hide-menu-enabled": {
|
||||
"message": "Gizli menüyü etkinleştir"
|
||||
"detail": "Menü gizli, göstermek için 'Alt' tuşunu kullanın (veya Uygulama İçi Menüyü kullanıyorsanız 'Escape' tuşunu kullanın)",
|
||||
"message": "Menüyü gizle etkinleştirildi",
|
||||
"title": "Menüyü gizle etkinleştirildi"
|
||||
},
|
||||
"need-to-restart": {
|
||||
"buttons": {
|
||||
"later": "Daha Sonra",
|
||||
"restart-now": "Şimdi yeniden başlat"
|
||||
},
|
||||
"detail": "\"{{pluginName}}\" eklentisinin çalışabilmesi için yeniden başlatman gerekiyor",
|
||||
"message": "\"{{pluginName}}\" için yeniden başlatman gerekiyor",
|
||||
"title": "Uygulamayı yeniden başlatman gerekiyor"
|
||||
},
|
||||
"unresponsive": {
|
||||
"buttons": {
|
||||
"quit": "Çıkış",
|
||||
"relaunch": "Yeniden Başlat",
|
||||
"wait": "Bekle"
|
||||
},
|
||||
"detail": "Rahatsızlık için özür dileriz! Lütfen ne yapacağınızı seçin:",
|
||||
"message": "Uygulama yanıt vermiyor",
|
||||
"title": "Pencere yanıt vermiyor"
|
||||
},
|
||||
"update-available": {
|
||||
"buttons": {
|
||||
"disable": "Güncellemeleri devre dışı bırak",
|
||||
"download": "İndir",
|
||||
"ok": "Tamam"
|
||||
}
|
||||
},
|
||||
"detail": "Yeni bir sürüm mevcut. {{downloadLink}} adresi üzerinden indirebilirsin",
|
||||
"message": "Yeni bir sürüm mevcut",
|
||||
"title": "Güncelleme Mevcut"
|
||||
}
|
||||
},
|
||||
"menu": {
|
||||
"about": "Hakkında",
|
||||
"navigation": {
|
||||
"label": "Navigasyon"
|
||||
"label": "Navigasyon",
|
||||
"submenu": {
|
||||
"copy-current-url": "Geçerli Url'yi kopyala",
|
||||
"go-back": "Geri dön",
|
||||
"go-forward": "İlerle",
|
||||
"quit": "Çıkış",
|
||||
"restart": "Uygulamayı Yeniden Başlat"
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"label": "Seçenekler",
|
||||
"submenu": {
|
||||
"advanced-options": {
|
||||
"label": "Gelişmiş Seçenekler",
|
||||
"submenu": {
|
||||
"auto-reset-app-cache": "Uygulama başlatıldığında uygulama önbelleğini sıfırla",
|
||||
"disable-hardware-acceleration": "Donanım hızlandırmayı devre dışı bırak",
|
||||
"edit-config-json": "Düzenle config.json",
|
||||
"override-user-agent": "\"User-Agent \"ı geçersiz kıl",
|
||||
"restart-on-config-changes": "Yapılandırma değişikliğinde yeniden başlat",
|
||||
"set-proxy": {
|
||||
"label": "Proxy ayarla",
|
||||
"prompt": {
|
||||
"label": "Proxy Adresini Gir: (devre dışı bırakmak için boş bırakın)",
|
||||
"placeholder": "Örnek: SOCKS5://127.0.0.1:9999",
|
||||
"title": "Proxy ayarla"
|
||||
}
|
||||
}
|
||||
},
|
||||
"toggle-dev-tools": "DevTools'u Aç / Kapat"
|
||||
}
|
||||
},
|
||||
"always-on-top": "Her zaman üstte",
|
||||
"auto-update": "Otomatik Güncelleme",
|
||||
"language": {
|
||||
"label": "Dil"
|
||||
"hide-menu": {
|
||||
"dialog": {
|
||||
"message": "Menü bir sonraki açılışta gizlenecektir, göstermek için [Alt] tuşunu kullanın (veya uygulama-içi-menü kullanıyorsanız [`] tuşuna geri basın)",
|
||||
"title": "Gizli Menü Aktif"
|
||||
},
|
||||
"label": "Gizli Menü"
|
||||
},
|
||||
"language": {
|
||||
"dialog": {
|
||||
"message": "Dil değişikliği yeniden başlattıktan sonra etkinleşecektir",
|
||||
"title": "Dil değiştirildi"
|
||||
},
|
||||
"label": "Dil",
|
||||
"submenu": {
|
||||
"to-help-translate": "Çeviriye yardım etmek ister misiniz? Buraya tıklayın"
|
||||
}
|
||||
},
|
||||
"resume-on-start": "Uygulama başlatıldığında son şarkıyı devam ettir",
|
||||
"single-instance-lock": "Tek Örnek Kilidi",
|
||||
"start-at-login": "Başlangıçta çalıştır",
|
||||
"starting-page": {
|
||||
"label": "Başlangıç sayfası",
|
||||
"unset": "Ayarlanmadı"
|
||||
},
|
||||
"tray": {
|
||||
"label": "Tepsi",
|
||||
"submenu": {
|
||||
"enabled-and-hide-app": "Uygulamayı etkinleştirin gizleyin.",
|
||||
"play-pause-on-click": "Tıklaynınca Oynat-Duraklat"
|
||||
"disabled": "Devre Dışı",
|
||||
"enabled-and-hide-app": "Uygulamayı etkinleştirin gizleyin",
|
||||
"enabled-and-show-app": "Etkinleştir ve uygulamayı göster",
|
||||
"play-pause-on-click": "Tıklandığında Oynat/Duraklat"
|
||||
}
|
||||
},
|
||||
"visual-tweaks": {
|
||||
"label": "Görsel İnce Ayarlar",
|
||||
"submenu": {
|
||||
"like-buttons": {
|
||||
"default": "Varsayılan"
|
||||
"default": "Varsayılan",
|
||||
"force-show": "Zorla göster",
|
||||
"hide": "Gizle",
|
||||
"label": "Beğenme düğmeleri"
|
||||
},
|
||||
"remove-upgrade-button": "Yükseltme düğmesini kaldır",
|
||||
"theme": {
|
||||
"label": "Tema",
|
||||
"submenu": {
|
||||
"import-css-file": "Özel CSS dosyanı içeri aktar",
|
||||
"no-theme": "Tema Yok"
|
||||
}
|
||||
}
|
||||
@ -81,31 +169,54 @@
|
||||
}
|
||||
},
|
||||
"plugins": {
|
||||
"label": "Eklentiler"
|
||||
"enabled": "Aktif",
|
||||
"label": "Eklentiler",
|
||||
"new": "YENİ"
|
||||
},
|
||||
"view": {
|
||||
"label": "Görüntü"
|
||||
"label": "Görüntü",
|
||||
"submenu": {
|
||||
"force-reload": "Zorla yeniden başlat",
|
||||
"reload": "Yeniden Başlat",
|
||||
"reset-zoom": "Asıl Boyut",
|
||||
"toggle-fullscreen": "Tam Ekran'a Geçiş",
|
||||
"zoom-in": "Yakınlaştır",
|
||||
"zoom-out": "Uzaklaştır"
|
||||
}
|
||||
}
|
||||
},
|
||||
"tray": {
|
||||
"next": "Sonraki",
|
||||
"play-pause": "Oynat/Durdur",
|
||||
"previous": "Önceki",
|
||||
"quit": "Çıkış",
|
||||
"restart": "Yeniden başlat",
|
||||
"show": "Pencereyi görüntüle"
|
||||
}
|
||||
},
|
||||
"plugins": {
|
||||
"adblocker": {
|
||||
"description": "Tüm reklamları ve izleyicileri engelle",
|
||||
"menu": {
|
||||
"blocker": "Engelleme Yöntemi"
|
||||
"blocker": "Engelleyici"
|
||||
},
|
||||
"name": "Adblocker"
|
||||
"name": "Reklam Engelleyici"
|
||||
},
|
||||
"album-actions": {
|
||||
"description": "Çalma listesindeki veya albümdeki tüm şarkılara Beğendim ve Beğenmedim düğmeleri ekler.",
|
||||
"name": "Albüm Eylemleri"
|
||||
},
|
||||
"album-color-theme": {
|
||||
"description": "Albümün renk paletine dayalı dinamik bir tema ve efektler uygular.",
|
||||
"description": "Albümün renk paletine dayalı dinamik bir tema ve efektler uygular",
|
||||
"name": "Albüm Renk Teması"
|
||||
},
|
||||
"ambient-mode": {
|
||||
"description": "Videodaki yumuşak renkleri ekranınızın arka planına yansıtarak bir ışık efekti uygular..",
|
||||
"menu": {
|
||||
"blur-amount": {
|
||||
"label": "Bulanıklık miktarı",
|
||||
"submenu": {
|
||||
"pixels": "{{blurAmount}} pixels"
|
||||
"pixels": "{{blurAmount}} piksel"
|
||||
}
|
||||
},
|
||||
"buffer": {
|
||||
@ -121,53 +232,82 @@
|
||||
}
|
||||
},
|
||||
"quality": {
|
||||
"label": "Kalite",
|
||||
"submenu": {
|
||||
"pixels": "{{quality}} pixels"
|
||||
"pixels": "{{quality}} piksel"
|
||||
}
|
||||
},
|
||||
"size": {
|
||||
"label": "Boyut",
|
||||
"submenu": {
|
||||
"percent": "{{size}}%"
|
||||
}
|
||||
},
|
||||
"smoothness-transition": {
|
||||
"label": "Yumuşak Geçiş",
|
||||
"submenu": {
|
||||
"during": "{{interpolationTime}} saniye içinde"
|
||||
"during": "{{interpolationTime}} saniye boyunca"
|
||||
}
|
||||
},
|
||||
"use-fullscreen": {
|
||||
"label": "Tam ekran kullanılıyor"
|
||||
}
|
||||
}
|
||||
},
|
||||
"name": "Ambiyans Modu"
|
||||
},
|
||||
"audio-compressor": {
|
||||
"description": "Ses sıkıştırma (dalganın en gürültülü bölümlerinin ses düzeyini azaltır ve daha yumuşak bölümlerin ses düzeyini artırır)"
|
||||
"description": "Ses sıkıştırma (dalganın en gürültülü bölümlerinin ses düzeyini azaltır ve daha yumuşak bölümlerin ses düzeyini artırır)",
|
||||
"name": "Ses Sıkıştırma"
|
||||
},
|
||||
"blur-nav-bar": {
|
||||
"description": "Gezinme çubuğunu şeffaf ve bulanık yapar"
|
||||
"description": "Gezinme çubuğunu şeffaf ve bulanık yapar",
|
||||
"name": "Navigasyon barını bulanıklaştır"
|
||||
},
|
||||
"bypass-age-restrictions": {
|
||||
"description": "YouTube yaş doğrulamasını atla"
|
||||
"description": "YouTube yaş doğrulamasını atla",
|
||||
"name": "Yaş doğrulamasını atla"
|
||||
},
|
||||
"captions-selector": {
|
||||
"description": "YouTube Music için altyazı seçici",
|
||||
"menu": {
|
||||
"autoload": "Son kullanılan altyazıyı otomatik olarak seç",
|
||||
"disable-captions": "Varsayılan olarak altyazı yok"
|
||||
},
|
||||
"name": "Altyazı Seçici",
|
||||
"prompt": {
|
||||
"selector": {
|
||||
"none": "Hiçbiri"
|
||||
"label": "Geçerli altyazı dili: {{language}}",
|
||||
"none": "Hiçbiri",
|
||||
"title": "Altyazı dilini seç"
|
||||
}
|
||||
},
|
||||
"templates": {
|
||||
"title": "Altyazı seçiciyi aç"
|
||||
}
|
||||
},
|
||||
"compact-sidebar": {
|
||||
"description": "Her zaman kompakt kenar çubugu"
|
||||
"description": "Her zaman kompakt kenar çubugu",
|
||||
"name": "Kompakt Kenar Çubuğu"
|
||||
},
|
||||
"crossfade": {
|
||||
"description": "Şarkılar arasında Çapraz Geçiş",
|
||||
"menu": {
|
||||
"advanced": "İleri düzey için"
|
||||
"advanced": "Gelişmiş"
|
||||
},
|
||||
"name": "Çapraz Geçiş [Beta]",
|
||||
"prompt": {
|
||||
"options": {
|
||||
"multi-input": {
|
||||
"fade-in-duration": "Güçlenme süresi (ms)",
|
||||
"fade-out-duration": "Zayıflama süresi (ms)",
|
||||
"fade-scaling": {
|
||||
"label": "Zayıflama Ölçeği",
|
||||
"linear": "Doğrusal",
|
||||
"logarithmic": "Logaritmik"
|
||||
}
|
||||
}
|
||||
},
|
||||
"seconds-before-end": "Bitişten N saniye önce çapraz geçiş"
|
||||
},
|
||||
"title": "Çapraz Geçiş ayarları"
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -175,14 +315,33 @@
|
||||
"description": "Şarkıların otomatik olarak duraklatılmasını sağlar",
|
||||
"menu": {
|
||||
"apply-once": "Yalnızca ilk şarkı için geçerlidir"
|
||||
}
|
||||
},
|
||||
"name": "Otomatik oynatmayı devre dışı bırak"
|
||||
},
|
||||
"discord": {
|
||||
"description": "Rich Presence ile Discord'da ne dinlediğinizi gösterin.",
|
||||
"backend": {
|
||||
"already-connected": "Aktif bağlantı olduğu halde bağlantı kurulmaya çalışıldı",
|
||||
"connected": "Discord'a bağlandı",
|
||||
"disconnected": "Discord ile bağlantı kesildi"
|
||||
},
|
||||
"description": "Rich Presence ile Discord'da ne dinlediğinizi gösterin",
|
||||
"menu": {
|
||||
"auto-reconnect": "Otomatik yeniden bağlan",
|
||||
"clear-activity": "Etkinliği temizle",
|
||||
"clear-activity-after-timeout": "Zaman aşımından sonra etkinliği temizle",
|
||||
"connected": "Bağlı",
|
||||
"disconnected": "Bağlantı kesildi",
|
||||
"hide-duration-left": "Kalan süreyi gizle",
|
||||
"hide-github-button": "GitHub bağlantısını gizle",
|
||||
"play-on-youtube-music": "YouTube Music de oynat",
|
||||
"set-inactivity-timeout": "Hareketsizlik zaman aşımını ayarla"
|
||||
},
|
||||
"name": "Discord Rich Presence",
|
||||
"prompt": {
|
||||
"set-inactivity-timeout": {
|
||||
"label": "Hareketsizlik zaman aşımını saniye cinsinden girin:",
|
||||
"title": "Hareketsizlik zaman aşımını ayarla"
|
||||
}
|
||||
}
|
||||
},
|
||||
"downloader": {
|
||||
@ -192,71 +351,298 @@
|
||||
"buttons": {
|
||||
"ok": "Tamam"
|
||||
},
|
||||
"title": "İndirmede hata meydana geldi!"
|
||||
"message": "Argh! Özür dilerim, indirme başarısız oldu…",
|
||||
"title": "İndirme sırasında bir hata meydana geldi!"
|
||||
},
|
||||
"start-download-playlist": {
|
||||
"buttons": {
|
||||
"ok": "Tamam"
|
||||
},
|
||||
"message": "Çalma listesini indir : {{playlistTitle}}",
|
||||
"detail": "({{playlistSize}} şarkı)",
|
||||
"message": "Oynatma listesini indir : {{playlistTitle}}",
|
||||
"title": "İndirme Başladı"
|
||||
}
|
||||
},
|
||||
"feedback": {
|
||||
"conversion-progress": "Dönüştürme : {{percent}}%",
|
||||
"converting": "Dönüştürülüyor…",
|
||||
"done": "Tamamlandı: {{filePath}}",
|
||||
"download-info": "{{artist}} - {{title}} [{{videoId}} indiriliyor",
|
||||
"download-progress": "İndirme : {{percent}}%",
|
||||
"preparing-file": "Dosya Hazırlanıyor…"
|
||||
"downloading": "İndiriliyor…",
|
||||
"downloading-counter": "İndiriliyor {{current}}/{{total}}…",
|
||||
"downloading-playlist": "\"{{playlistTitle}}\" şarkı listesi indiriliyor - {{playlistSize}} şarkı ({{playlistId}})",
|
||||
"error-while-downloading": "\"{{author}} - {{title}}\" indirilirken hata oluştu: {{error}}",
|
||||
"folder-already-exists": "{{playlistFolder}} klasörü zaten mevcut",
|
||||
"getting-playlist-info": "Oynatma listesi bilgisi alınıyor…",
|
||||
"loading": "Yükleniyor…",
|
||||
"playlist-has-only-one-song": "Oynatma listesinde yalnızca bir şarkı var, doğrudan indiriliyor",
|
||||
"playlist-id-not-found": "Oynatma listesi ID'si bulunamadı",
|
||||
"playlist-is-empty": "Oynatma listesi boş",
|
||||
"playlist-is-mix-or-private": "Çalma listesi bilgisi alınırken hata oluştu: özel veya \"Size özel karışık\" bir çalma listesi olmadığından emin olun\n\n{{error}}",
|
||||
"preparing-file": "Dosya Hazırlanıyor…",
|
||||
"saving": "Kaydediliyor…",
|
||||
"trying-to-get-playlist-id": "Çalma listesi ID'si alınmaya çalışılıyor: {{playlistId}}",
|
||||
"video-id-not-found": "Video bulunamadı",
|
||||
"writing-id3": "ID3 etiketleri yazılıyor…"
|
||||
}
|
||||
},
|
||||
"description": "MP3 / kaynak sesini doğrudan arayüzden indir",
|
||||
"menu": {
|
||||
"choose-download-folder": "İndirme klasörünü seç",
|
||||
"download-playlist": "Oynatma listesini indir",
|
||||
"presets": "Hazır Ayarlar",
|
||||
"skip-existing": "Mevcut dosyaları atla"
|
||||
},
|
||||
"name": "İndirici",
|
||||
"renderer": {
|
||||
"can-not-update-progress": "İlerleme güncellenemiyor"
|
||||
},
|
||||
"templates": {
|
||||
"button": "İndir"
|
||||
}
|
||||
},
|
||||
"exponential-volume": {
|
||||
"description": "Ses seviyesi kaydırıcısını üstel hale getirir, böylece daha düşük ses seviyelerini seçmek daha kolay olur.",
|
||||
"name": "Üstel Ses Seviyesi"
|
||||
},
|
||||
"in-app-menu": {
|
||||
"description": "Menü çubuklarına süslü, koyu veya albüm renginde bir görünüm verir",
|
||||
"menu": {
|
||||
"hide-dom-window-controls": "DOM penceresi kontrollerini gizle"
|
||||
},
|
||||
"name": "Uygulama İçi Menü"
|
||||
},
|
||||
"last-fm": {
|
||||
"description": "Last.fm için scrobbling desteği ekler",
|
||||
"name": "Last.fm"
|
||||
},
|
||||
"lumiastream": {
|
||||
"description": "Lumia Stream desteği ekler",
|
||||
"name": "Lumia Stream [Beta]"
|
||||
},
|
||||
"lyrics-genius": {
|
||||
"description": "Çoğu şarkı için şarkı sözü desteği ekler",
|
||||
"menu": {
|
||||
"romanized-lyrics": "Romanlaştırılmış Şarkı Sözleri"
|
||||
},
|
||||
"name": "Genius Şarkı Sözleri",
|
||||
"renderer": {
|
||||
"fetched-lyrics": "Şarkı sözleri genius tarafından alındı"
|
||||
}
|
||||
},
|
||||
"music-together": {
|
||||
"description": "Oynatma listesini başkalarıyla paylaşın. Sunucu sahibi bir şarkı çaldığında, diğer herkes aynı şarkıyı duyacak",
|
||||
"dialog": {
|
||||
"enter-host": "Sunucu ID'si Girin"
|
||||
},
|
||||
"internal": {
|
||||
"save": "Kaydet",
|
||||
"track-source": "Parça Kaynağı",
|
||||
"unknown-user": "Bilinmeyen Kullanıcı"
|
||||
},
|
||||
"menu": {
|
||||
"click-to-copy-id": "Sunucu ID'sini kopyala",
|
||||
"close": "Birlikte Müziği Kapat",
|
||||
"connected-users": "Bağlanan Kullanıcılar",
|
||||
"disconnect": "Birlikte Müzik Bağlantısını Kesin",
|
||||
"empty-user": "Bağlı kullanıcı bulunmuyor",
|
||||
"host": "Birlikte Müzik Sunucusu",
|
||||
"join": "Birlikte Müziğe Katıl",
|
||||
"permission": {
|
||||
"all": "Konukların oynatma listesini ve oynatıcıyı kontrol etmesine izin verin",
|
||||
"host-only": "Çalma listesini ve oynatıcıyı yalnızca yönetici kontrol edebilir",
|
||||
"playlist": "Konukların oynatma listesini kontrol etmesine izin ver"
|
||||
},
|
||||
"set-permission": "Kontrol İznini Değiştir",
|
||||
"status": {
|
||||
"disconnected": "Bağlantı kesildi",
|
||||
"guest": "Misafir olarak bağlandı",
|
||||
"host": "Ev Sahibi olarak bağlandı"
|
||||
}
|
||||
},
|
||||
"name": "Birlikte Müzik [Beta]",
|
||||
"toast": {
|
||||
"add-song-failed": "Şarkı eklenirken bir hata meydana geldi",
|
||||
"closed": "Birlikte Müzik kapatıldı",
|
||||
"disconnected": "Birlikte Müzik bağlantı kesildi",
|
||||
"host-failed": "Birlikte Müzik sunucusu kurulamadı",
|
||||
"id-copied": "Sunucu ID'si kopyalandı",
|
||||
"join-failed": "Birlikte Müziğe katılırken bir hata meydana geldi",
|
||||
"joined": "Birlikte Müziğe Katıldı",
|
||||
"permission-changed": "Birlikte Müzik yetkisi \"{{permission}}\" olarak değiştirildi",
|
||||
"remove-song-failed": "Şarkı kaldırılırken bir hata meydana geldi",
|
||||
"user-connected": "{{name}} Birlikte Müziğe Katıldı",
|
||||
"user-disconnected": "{{name}} Birlikte Müzik'ten ayrıldı"
|
||||
}
|
||||
},
|
||||
"navigation": {
|
||||
"description": "Favori tarayıcınızdaki gibi doğrudan arayüze entegre edilmiş İleri/Geri gezinme okları",
|
||||
"name": "Navigasyon"
|
||||
},
|
||||
"no-google-login": {
|
||||
"description": "Google giriş düğmelerini ve bağlantılarını arayüzden kaldır",
|
||||
"name": "Google Girişini Kaldır"
|
||||
},
|
||||
"notifications": {
|
||||
"description": "Bir şarkı çalmaya başladığında bir bildirim görüntüler (etkileşimli bildirimler Windows'ta mevcuttur)",
|
||||
"menu": {
|
||||
"interactive": "İnteraktif Bildirimler",
|
||||
"interactive-settings": {
|
||||
"label": "İnteraktif Ayarlar",
|
||||
"submenu": {
|
||||
"hide-button-text": "Buton metnini gizle",
|
||||
"refresh-on-play-pause": "Oynat/Duraklat'ta Yenile",
|
||||
"tray-controls": "Tepsi tıklamasıyla Aç/Kapat"
|
||||
}
|
||||
},
|
||||
"priority": "Bildirim Önceliği",
|
||||
"toast-style": "Bildirim Tarzı",
|
||||
"unpause-notification": "Şarkı tekrar oynatılınca bildirim göster"
|
||||
},
|
||||
"name": "Bildirimler"
|
||||
},
|
||||
"shortcuts": {
|
||||
"prompt": {
|
||||
"keybind": {
|
||||
"keybind-options": {
|
||||
"next": "İler"
|
||||
"picture-in-picture": {
|
||||
"description": "Uygulamayı resim-içinde-resim moduna geçirmeye izin verir",
|
||||
"menu": {
|
||||
"always-on-top": "Her zaman üstte",
|
||||
"hotkey": {
|
||||
"label": "Kısayol",
|
||||
"prompt": {
|
||||
"keybind-options": {
|
||||
"hotkey": "Kısayol"
|
||||
},
|
||||
"label": "Resim-içinde-resim arasında geçiş yapmak için bir kısayol tuşu seçin",
|
||||
"title": "Resim-içinde-resim Kısayol Tuşu"
|
||||
}
|
||||
},
|
||||
"save-window-position": "Pencere konumunu kaydet",
|
||||
"save-window-size": "Pencere boyutunu kaydet",
|
||||
"use-native-pip": "Tarayıcı yerel PiP'sini kullan"
|
||||
},
|
||||
"name": "Resim-içinde-resim",
|
||||
"templates": {
|
||||
"button": "Resim-içinde-resim"
|
||||
}
|
||||
},
|
||||
"playback-speed": {
|
||||
"description": "Hızlı dinle, yavaş dinle! Şarkı hızını kontrol eden bir kaydırıcı ekler",
|
||||
"name": "Oynatma Hızı",
|
||||
"templates": {
|
||||
"button": "Hız"
|
||||
}
|
||||
},
|
||||
"precise-volume": {
|
||||
"description": "Özel bir HUD ve özelleştirilebilir ses seviyesi adımları ile fare tekerleği / kısayol tuşlarını kullanarak ses seviyesini hassas bir şekilde kontrol edin",
|
||||
"menu": {
|
||||
"arrows-shortcuts": "Yerel Ok Tuşu Kontrolleri",
|
||||
"custom-volume-steps": "Özel Ses Seviyesi Adımlarını Ayarlama",
|
||||
"global-shortcuts": "Genel Kısayol Tuşları"
|
||||
},
|
||||
"name": "Hassas Ses Seviyesi",
|
||||
"prompt": {
|
||||
"global-shortcuts": {
|
||||
"keybind-options": {
|
||||
"decrease": "Ses Seviyesi Azaltma",
|
||||
"increase": "Ses Seviyesi Yükseltme"
|
||||
},
|
||||
"label": "Genel Ses Tuş Atamalarını seçin:",
|
||||
"title": "Genel Ses Tuş Atamaları"
|
||||
},
|
||||
"volume-steps": {
|
||||
"label": "Ses Artırma/Azaltma Kademelerini Seçin",
|
||||
"title": "Ses Kademeleri"
|
||||
}
|
||||
}
|
||||
},
|
||||
"quality-changer": {
|
||||
"backend": {
|
||||
"dialog": {
|
||||
"quality-changer": {
|
||||
"detail": "Mevcut Kalite: {{quality}}",
|
||||
"message": "Video Kalitesini Seçin:",
|
||||
"title": "Video Kalitesini Seçin"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Video katmanı üzerindeki bir düğme ile video kalitesinin değiştirilmesine izin verir",
|
||||
"name": "Video Kalitesi Değiştirici"
|
||||
},
|
||||
"shortcuts": {
|
||||
"description": "Oynatma için global kısayol tuşları (oynat/duraklat/sonraki/önceki) ayarlamaya ve medya tuşlarını geçersiz kılarak medya OSD'sini kapatmaya, arama yapmak için Ctrl/CMD + F tuşlarını açmaya, medya tuşları için Linux MPRIS desteğini açmaya ve ileri düzey kullanıcılar için özel kısayol tuşlarına izin verir.",
|
||||
"menu": {
|
||||
"override-media-keys": "Medya Tuşlarını Geçersiz Kıl",
|
||||
"set-keybinds": "Global Şarkı Kontrollerini Ayarla"
|
||||
},
|
||||
"name": "Kısayollar (& MPRIS)",
|
||||
"prompt": {
|
||||
"keybind": {
|
||||
"keybind-options": {
|
||||
"next": "İler",
|
||||
"play-pause": "Oynat / Durdur",
|
||||
"previous": "Önceki"
|
||||
},
|
||||
"label": "Şarkı Kontrolü için Genel Tuş Atamaları'nı seçin:",
|
||||
"title": "Genel Tuş Atamaları"
|
||||
}
|
||||
}
|
||||
},
|
||||
"skip-disliked-songs": {
|
||||
"description": "Beğenmediğin şarkıları atlar",
|
||||
"name": "Beğenmediklerini Atla"
|
||||
},
|
||||
"skip-silences": {
|
||||
"description": "Şarkılardaki sessiz bölümleri otomatik olarak atlar",
|
||||
"name": "Sessizlikleri Atla"
|
||||
},
|
||||
"sponsorblock": {
|
||||
"description": "Giriş/Çıkış gibi müzik olmayan kısımları veya müzik videolarında şarkının çalmadığı kısımları otomatik olarak atlar",
|
||||
"name": "SponsorBlock"
|
||||
},
|
||||
"taskbar-mediacontrol": {
|
||||
"description": "Windows görev çubuğu üzerinden oynatmayı kontrol edebilmenize olanak sağlar",
|
||||
"name": "Görev Çubuğu Medya Kontrolü"
|
||||
},
|
||||
"touchbar": {
|
||||
"description": "macOS kullanıcıları için bir TouchBar widget'ı ekler",
|
||||
"name": "TouchBar"
|
||||
},
|
||||
"tuna-obs": {
|
||||
"description": "OBS eklentisi Tuna ile entegrasyon sağlar",
|
||||
"name": "Tuna OBS"
|
||||
},
|
||||
"video-toggle": {
|
||||
"description": "Video/Şarkı modu arasında geçiş yapmak için bir düğme ekler. ayrıca isteğe bağlı olarak tüm video sekmesini kaldırabilir",
|
||||
"menu": {
|
||||
"align": {
|
||||
"label": "Hizalama",
|
||||
"submenu": {
|
||||
"left": "Sol",
|
||||
"middle": "Orta",
|
||||
"right": "Sağ"
|
||||
}
|
||||
},
|
||||
"force-hide": "Video sekmesini kaldırmaya zorla",
|
||||
"mode": {
|
||||
"label": "Mod"
|
||||
"label": "Mod",
|
||||
"submenu": {
|
||||
"custom": "Özel Ayar",
|
||||
"disabled": "Devre dışı",
|
||||
"native": "Yerel geçiş"
|
||||
}
|
||||
}
|
||||
},
|
||||
"name": "Video Geçiş",
|
||||
"templates": {
|
||||
"button": "Şarkı"
|
||||
}
|
||||
},
|
||||
"visualizer": {
|
||||
"description": "Oynatıcıya bir görselleştirici ekler",
|
||||
"menu": {
|
||||
"visualizer-type": "Görselleştirici Tipi"
|
||||
},
|
||||
"name": "Görselleştirici"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -402,7 +402,63 @@
|
||||
"name": "Експоненціальний обсяг"
|
||||
},
|
||||
"in-app-menu": {
|
||||
"description": "Надає меню-барам вишуканого, темного або кольору альбому вигляду"
|
||||
"description": "Надає меню-барам вишуканого, темного або кольору альбому вигляду",
|
||||
"menu": {
|
||||
"hide-dom-window-controls": "Сховати елементи керування вікном DOM"
|
||||
},
|
||||
"name": "Меню в програмі"
|
||||
},
|
||||
"last-fm": {
|
||||
"description": "Додати підтримку прокрутки для Last.fm",
|
||||
"name": "Last.fm"
|
||||
},
|
||||
"lumiastream": {
|
||||
"description": "Додано підтримку для Lumia Stream",
|
||||
"name": "Lumia Stream [бета-версія]"
|
||||
},
|
||||
"lyrics-genius": {
|
||||
"description": "Додає підтримку текстів для більшості пісень",
|
||||
"menu": {
|
||||
"romanized-lyrics": "Романізована лірика"
|
||||
}
|
||||
},
|
||||
"navigation": {
|
||||
"name": "Навігація"
|
||||
},
|
||||
"no-google-login": {
|
||||
"name": "Без входу в Google"
|
||||
},
|
||||
"notifications": {
|
||||
"description": "Відображати сповіщення, коли пісня починає грати (інтерактивні сповіщення доступні в Windows)",
|
||||
"menu": {
|
||||
"interactive": "Інтерактивні сповіщення",
|
||||
"interactive-settings": {
|
||||
"label": "Інтерактивні налаштування",
|
||||
"submenu": {
|
||||
"hide-button-text": "Сховати текст кнопки"
|
||||
}
|
||||
}
|
||||
},
|
||||
"name": "Сповіщення"
|
||||
},
|
||||
"picture-in-picture": {
|
||||
"description": "Дозволяє перемикати програму в режим «картинка в картинці»",
|
||||
"menu": {
|
||||
"always-on-top": "Завжди наверху",
|
||||
"hotkey": {
|
||||
"label": "Гаряча клавіша",
|
||||
"prompt": {
|
||||
"keybind-options": {
|
||||
"hotkey": "Гаряча клавіша"
|
||||
},
|
||||
"label": "Оберіть гарячу клавішу для перемикання режиму зображення в зображенні",
|
||||
"title": "Гаряча клавіша для режиму зображення в зображенні"
|
||||
}
|
||||
},
|
||||
"save-window-position": "Зберегти положення вікна",
|
||||
"save-window-size": "Зберегти розмір вікна"
|
||||
},
|
||||
"name": "Зображення в зображенні"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
21
src/i18n/resources/vi.json
Normal file
21
src/i18n/resources/vi.json
Normal file
@ -0,0 +1,21 @@
|
||||
{
|
||||
"common": {
|
||||
"console": {
|
||||
"plugins": {
|
||||
"execute-failed": "Lỗi khi bắt đầu phần mở rộng {{pluginName}}::{{contextName}}",
|
||||
"executed-at-ms": "Phần mở rộng {{pluginName}}::{{contextName}} đã bắt đầu trong {{ms}}ms",
|
||||
"initialize-failed": "Lỗi khi khởi động phần mở rộng \"{{pluginName}}\"",
|
||||
"load-all": "Đang tải tất cả phần mở rộng",
|
||||
"load-failed": "Lỗi khi tải phần mở rộng\"{{pluginName}}\"",
|
||||
"loaded": "Đã tải phần mở rộng \"{{pluginName}}\"",
|
||||
"unload-failed": "Lỗi khi hủy tải phần mở rộng \"{{pluginName}}\"",
|
||||
"unloaded": "Đã hủy tải phần mở rộng \"{{pluginName}}\""
|
||||
}
|
||||
}
|
||||
},
|
||||
"language": {
|
||||
"code": "vi",
|
||||
"local-name": "Tiếng Việt",
|
||||
"name": "Vietnamese"
|
||||
}
|
||||
}
|
||||
@ -170,7 +170,8 @@
|
||||
},
|
||||
"plugins": {
|
||||
"enabled": "已启用",
|
||||
"label": "插件"
|
||||
"label": "插件",
|
||||
"new": "新建"
|
||||
},
|
||||
"view": {
|
||||
"label": "视图",
|
||||
@ -201,6 +202,10 @@
|
||||
},
|
||||
"name": "广告屏蔽器"
|
||||
},
|
||||
"album-actions": {
|
||||
"description": "添加作用于播放列表或专辑中所有歌曲的全局“点赞/取消点赞”与“喜欢/取消喜欢”按钮。",
|
||||
"name": "专辑操作"
|
||||
},
|
||||
"album-color-theme": {
|
||||
"description": "根据专辑封面配色动态改变主题与视觉效果",
|
||||
"name": "专辑配色主题"
|
||||
@ -289,7 +294,7 @@
|
||||
"menu": {
|
||||
"advanced": "高级"
|
||||
},
|
||||
"name": "交叉淡化 [beta]",
|
||||
"name": "交叉淡化 [Beta]",
|
||||
"prompt": {
|
||||
"options": {
|
||||
"multi-input": {
|
||||
@ -426,6 +431,51 @@
|
||||
"fetched-lyrics": "已从 Genius 获取字幕"
|
||||
}
|
||||
},
|
||||
"music-together": {
|
||||
"description": "与他人共享播放列表。当发起人播放歌曲时,其他人也会听到相同歌曲",
|
||||
"dialog": {
|
||||
"enter-host": "输入发起人 ID"
|
||||
},
|
||||
"internal": {
|
||||
"save": "保存",
|
||||
"track-source": "追踪来源",
|
||||
"unknown-user": "未知用户"
|
||||
},
|
||||
"menu": {
|
||||
"click-to-copy-id": "复制发起者 ID",
|
||||
"close": "关闭 Music Together",
|
||||
"connected-users": "已连接用户",
|
||||
"disconnect": "断开 Music Together 连接",
|
||||
"empty-user": "没有已连接的用户",
|
||||
"host": "Music Together 发起者",
|
||||
"join": "加入 Music Together",
|
||||
"permission": {
|
||||
"all": "允许来宾控制播放列表与播放器",
|
||||
"host-only": "仅发起人可以控制播放列表与播放器",
|
||||
"playlist": "允许来宾控制播放列表"
|
||||
},
|
||||
"set-permission": "更改控制权限",
|
||||
"status": {
|
||||
"disconnected": "已断开连接",
|
||||
"guest": "已作为来宾连接",
|
||||
"host": "已作为发起人连接"
|
||||
}
|
||||
},
|
||||
"name": "Music Together [测试]",
|
||||
"toast": {
|
||||
"add-song-failed": "添加歌曲失败",
|
||||
"closed": "Music Together 已关闭",
|
||||
"disconnected": "Music Together 已断开连接",
|
||||
"host-failed": "发起 Music Together 失败",
|
||||
"id-copied": "已将发起者 ID 复制到剪切板",
|
||||
"join-failed": "加入 Music Together 失败",
|
||||
"joined": "已加入 Music Together",
|
||||
"permission-changed": "Music Together 权限已改为 \"{{permission}}\"",
|
||||
"remove-song-failed": "移除歌曲失败",
|
||||
"user-connected": "{{name}} 加入了 Music Together",
|
||||
"user-disconnected": "{{name}} 离开了 Music Together"
|
||||
}
|
||||
},
|
||||
"navigation": {
|
||||
"description": "如同浏览器般,在应用界面内直接显示前进/后退导航按钮",
|
||||
"name": "导航"
|
||||
|
||||
@ -53,9 +53,9 @@
|
||||
"later": "稍後",
|
||||
"restart-now": "立即重啟"
|
||||
},
|
||||
"detail": "外掛「{{pluginName}}」需要程式重新啟動之後才會生效",
|
||||
"message": "「{{pluginName}}」需要重新啟動",
|
||||
"title": "需要重新啟動"
|
||||
"detail": "\"{{pluginName}}\" 外掛需要重啟應用之後才會生效",
|
||||
"message": "\"{{pluginName}}\" 需要重啟應用",
|
||||
"title": "需要重啟應用"
|
||||
},
|
||||
"unresponsive": {
|
||||
"buttons": {
|
||||
@ -87,7 +87,7 @@
|
||||
"go-back": "回到上一頁",
|
||||
"go-forward": "回到下一頁",
|
||||
"quit": "退出",
|
||||
"restart": "重新啟動應用程式"
|
||||
"restart": "重啟應用"
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
@ -131,12 +131,12 @@
|
||||
"to-help-translate": "想要協助翻譯?按一下這裡"
|
||||
}
|
||||
},
|
||||
"resume-on-start": "繼續上次關閉應用程式前的音樂",
|
||||
"single-instance-lock": "禁止多開應用程式",
|
||||
"resume-on-start": "應用啟動時繼續上次播放的歌曲",
|
||||
"single-instance-lock": "單視窗鎖定",
|
||||
"start-at-login": "開機時啟動",
|
||||
"starting-page": {
|
||||
"label": "啟動頁面",
|
||||
"unset": "未設定"
|
||||
"unset": "不指定"
|
||||
},
|
||||
"tray": {
|
||||
"label": "系統閘圖式",
|
||||
@ -148,7 +148,7 @@
|
||||
}
|
||||
},
|
||||
"visual-tweaks": {
|
||||
"label": "視覺設定",
|
||||
"label": "介面設定",
|
||||
"submenu": {
|
||||
"like-buttons": {
|
||||
"default": "預設",
|
||||
@ -413,7 +413,7 @@
|
||||
"name": "Last.fm"
|
||||
},
|
||||
"lumiastream": {
|
||||
"description": "新增對Lumia Stream的支援",
|
||||
"description": "新增對 Lumia Stream 的支援",
|
||||
"name": "Lumia Stream [Beta]"
|
||||
},
|
||||
"lyrics-genius": {
|
||||
@ -558,7 +558,7 @@
|
||||
"name": "觸控列 (Touchbar) 支援"
|
||||
},
|
||||
"tuna-obs": {
|
||||
"description": "與OBS外掛Tuna連接",
|
||||
"description": "與 OBS 的 Tuna 外掛連接",
|
||||
"name": "Tuna OBS"
|
||||
},
|
||||
"video-toggle": {
|
||||
|
||||
@ -299,7 +299,7 @@ async function createMainWindow() {
|
||||
const { x: windowX, y: windowY } = windowPosition;
|
||||
const winSize = win.getSize();
|
||||
const display = screen.getDisplayNearestPoint(windowPosition);
|
||||
const scaleFactor = display.scaleFactor;
|
||||
const scaleFactor = is.windows() ? display.scaleFactor: 1;
|
||||
|
||||
const scaledWidth = Math.floor(windowSize.width / scaleFactor);
|
||||
const scaledHeight = Math.floor(windowSize.height / scaleFactor);
|
||||
@ -687,13 +687,15 @@ app.whenReady().then(async () => {
|
||||
const dialogOptions: Electron.MessageBoxOptions = {
|
||||
type: 'info',
|
||||
buttons: [
|
||||
t('main.dialog.update-available.buttons.download'),
|
||||
t('main.dialog.update-available.buttons.ok'),
|
||||
t('main.dialog.update-available.buttons.download'),
|
||||
t('main.dialog.update-available.buttons.disable'),
|
||||
],
|
||||
title: t('main.dialog.update-available.title'),
|
||||
message: t('main.dialog.update-available.message'),
|
||||
detail: t('main.dialog.update-available.detail', { downloadLink }),
|
||||
defaultId: 1,
|
||||
cancelId: 0,
|
||||
};
|
||||
|
||||
let dialogPromise: Promise<Electron.MessageBoxReturnValue>;
|
||||
@ -717,7 +719,7 @@ app.whenReady().then(async () => {
|
||||
break;
|
||||
}
|
||||
|
||||
default: {
|
||||
case 0: {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
25
src/menu.ts
25
src/menu.ts
@ -9,6 +9,7 @@ import {
|
||||
shell,
|
||||
} from 'electron';
|
||||
import prompt from 'custom-electron-prompt';
|
||||
import { satisfies } from 'semver';
|
||||
|
||||
import { allPlugins } from 'virtual:plugins';
|
||||
|
||||
@ -23,6 +24,8 @@ import promptOptions from './providers/prompt-options';
|
||||
import { getAllMenuTemplate, loadAllMenuPlugins } from './loader/menu';
|
||||
import { setLanguage, t } from '@/i18n';
|
||||
|
||||
import packageJson from '../package.json';
|
||||
|
||||
export type MenuTemplate = Electron.MenuItemConstructorOptions[];
|
||||
|
||||
// True only if in-app-menu was loaded on launch
|
||||
@ -31,10 +34,14 @@ const inAppMenuActive = config.plugins.isEnabled('in-app-menu');
|
||||
const pluginEnabledMenu = (
|
||||
plugin: string,
|
||||
label = '',
|
||||
description: string | undefined = undefined,
|
||||
isNew = false,
|
||||
hasSubmenu = false,
|
||||
refreshMenu: (() => void) | undefined = undefined,
|
||||
): Electron.MenuItemConstructorOptions => ({
|
||||
label: label || plugin,
|
||||
sublabel: isNew ? t('main.menu.plugins.new') : undefined,
|
||||
toolTip: description,
|
||||
type: 'checkbox',
|
||||
checked: config.plugins.isEnabled(plugin),
|
||||
click(item: Electron.MenuItem) {
|
||||
@ -66,12 +73,15 @@ export const mainMenuTemplate = async (
|
||||
|
||||
const menuResult = Object.entries(getAllMenuTemplate()).map(
|
||||
([id, template]) => {
|
||||
const pluginLabel = allPlugins[id]?.name?.() ?? id;
|
||||
const plugin = allPlugins[id];
|
||||
const pluginLabel = plugin?.name?.() ?? id;
|
||||
const pluginDescription = plugin?.description?.() ?? undefined;
|
||||
const isNew = plugin?.addedVersion ? satisfies(packageJson.version, plugin.addedVersion) : false;
|
||||
|
||||
if (!config.plugins.isEnabled(id)) {
|
||||
return [
|
||||
id,
|
||||
pluginEnabledMenu(id, pluginLabel, true, innerRefreshMenu),
|
||||
pluginEnabledMenu(id, pluginLabel, pluginDescription, isNew, true, innerRefreshMenu),
|
||||
] as const;
|
||||
}
|
||||
|
||||
@ -79,10 +89,14 @@ export const mainMenuTemplate = async (
|
||||
id,
|
||||
{
|
||||
label: pluginLabel,
|
||||
sublabel: isNew ? t('main.menu.plugins.new') : undefined,
|
||||
toolTip: pluginDescription,
|
||||
submenu: [
|
||||
pluginEnabledMenu(
|
||||
id,
|
||||
t('main.menu.plugins.enabled'),
|
||||
undefined,
|
||||
false,
|
||||
true,
|
||||
innerRefreshMenu,
|
||||
),
|
||||
@ -106,9 +120,12 @@ export const mainMenuTemplate = async (
|
||||
const predefinedTemplate = menuResult.find((it) => it[0] === id);
|
||||
if (predefinedTemplate) return predefinedTemplate[1];
|
||||
|
||||
const pluginLabel = allPlugins[id]?.name?.() ?? id;
|
||||
const plugin = allPlugins[id];
|
||||
const pluginLabel = plugin?.name?.() ?? id;
|
||||
const pluginDescription = plugin?.description?.() ?? undefined;
|
||||
const isNew = plugin?.addedVersion ? satisfies(packageJson.version, plugin.addedVersion) : false;
|
||||
|
||||
return pluginEnabledMenu(id, pluginLabel, true, innerRefreshMenu);
|
||||
return pluginEnabledMenu(id, pluginLabel, pluginDescription, isNew, true, innerRefreshMenu);
|
||||
});
|
||||
|
||||
const availableLanguages = Object.keys(languageResources);
|
||||
|
||||
@ -109,7 +109,7 @@ export default createPlugin({
|
||||
},
|
||||
},
|
||||
preload: {
|
||||
script: 'window.JSON = window._proxyJson; window._proxyJson = undefined; window.Response = window._proxyResponse; window._proxyResponse = undefined; 0',
|
||||
script: 'window.JSON.parse = window._proxyJsonParse; window._proxyJsonParse = undefined; window.Response.prototype.json = window._proxyResponseJson; window._proxyResponseJson = undefined; 0',
|
||||
async start({ getConfig }) {
|
||||
const config = await getConfig();
|
||||
|
||||
|
||||
@ -32,37 +32,17 @@ export const inject = (contextBridge) => {
|
||||
return o;
|
||||
};
|
||||
|
||||
contextBridge.exposeInMainWorld('_proxyJson', {
|
||||
parse: new Proxy(JSON.parse, {
|
||||
apply() {
|
||||
return pruner(Reflect.apply(...arguments));
|
||||
},
|
||||
}),
|
||||
stringify: JSON.stringify,
|
||||
[Symbol.toStringTag]: JSON[Symbol.toStringTag],
|
||||
});
|
||||
contextBridge.exposeInMainWorld('_proxyJsonParse', new Proxy(JSON.parse, {
|
||||
apply() {
|
||||
return pruner(Reflect.apply(...arguments));
|
||||
},
|
||||
}));
|
||||
|
||||
const withPrototype = (obj) => {
|
||||
const protos = Object.getPrototypeOf(obj);
|
||||
for (const [key, value] of Object.entries(protos)) {
|
||||
if (Object.prototype.hasOwnProperty.call(obj, key)) continue;
|
||||
if (typeof value === 'function') {
|
||||
obj[key] = function (...args) {
|
||||
return value.call(obj, ...args);
|
||||
}
|
||||
} else {
|
||||
obj[key] = value;
|
||||
}
|
||||
}
|
||||
return obj;
|
||||
};
|
||||
|
||||
Response.prototype.json = new Proxy(Response.prototype.json, {
|
||||
contextBridge.exposeInMainWorld('_proxyResponseJson', new Proxy(Response.prototype.json, {
|
||||
apply() {
|
||||
return Reflect.apply(...arguments).then((o) => pruner(o));
|
||||
},
|
||||
});
|
||||
contextBridge.exposeInMainWorld('_proxyResponse', withPrototype(Response));
|
||||
}));
|
||||
}
|
||||
|
||||
(function () {
|
||||
|
||||
179
src/plugins/album-actions/index.ts
Normal file
179
src/plugins/album-actions/index.ts
Normal file
@ -0,0 +1,179 @@
|
||||
import { t } from '@/i18n';
|
||||
import { createPlugin } from '@/utils';
|
||||
import { ElementFromHtml } from '@/plugins/utils/renderer';
|
||||
|
||||
import undislikeHTML from './templates/undislike.html?raw';
|
||||
import dislikeHTML from './templates/dislike.html?raw';
|
||||
import likeHTML from './templates/like.html?raw';
|
||||
import unlikeHTML from './templates/unlike.html?raw';
|
||||
|
||||
export default createPlugin({
|
||||
name: () => t('plugins.album-actions.name'),
|
||||
description: () => t('plugins.album-actions.description'),
|
||||
restartNeeded: false,
|
||||
addedVersion: '3.2.X',
|
||||
config: {
|
||||
enabled: false,
|
||||
},
|
||||
renderer: {
|
||||
observer: null as MutationObserver | null,
|
||||
loadObserver: null as MutationObserver | null,
|
||||
changeObserver: null as MutationObserver | null,
|
||||
waiting: false as boolean,
|
||||
start() {
|
||||
//Waits for pagechange
|
||||
this.onPageChange();
|
||||
this.observer = new MutationObserver(() => {
|
||||
this.onPageChange();
|
||||
});
|
||||
this.observer.observe(document.querySelector('#browse-page'), {
|
||||
attributes: false,
|
||||
childList: true,
|
||||
subtree: true,
|
||||
});
|
||||
},
|
||||
onPageChange() {
|
||||
if (this.waiting) {
|
||||
return;
|
||||
} else {
|
||||
this.waiting = true;
|
||||
}
|
||||
this.waitForElem('#continuations').then((continuations: HTMLElement) => {
|
||||
this.waiting = false;
|
||||
//Gets the for buttons
|
||||
let buttons: Array<HTMLElement> = [
|
||||
ElementFromHtml(undislikeHTML),
|
||||
ElementFromHtml(dislikeHTML),
|
||||
ElementFromHtml(likeHTML),
|
||||
ElementFromHtml(unlikeHTML),
|
||||
];
|
||||
//Finds the playlist
|
||||
const playlist =
|
||||
document.querySelector('ytmusic-shelf-renderer') ??
|
||||
document.querySelector('ytmusic-playlist-shelf-renderer');
|
||||
//Adds an observer for every button so it gets updated when one is clicked
|
||||
this.changeObserver?.disconnect();
|
||||
this.changeObserver = new MutationObserver(() => {
|
||||
this.stop();
|
||||
this.start();
|
||||
});
|
||||
const allButtons = playlist.querySelectorAll(
|
||||
'yt-button-shape.ytmusic-like-button-renderer',
|
||||
);
|
||||
for (const btn of allButtons)
|
||||
this.changeObserver.observe(btn, {
|
||||
attributes: true,
|
||||
childList: false,
|
||||
subtree: false,
|
||||
});
|
||||
//Determine if button is needed and colors the percentage
|
||||
const listsLength = playlist.querySelectorAll(
|
||||
'#button-shape-dislike > button',
|
||||
).length;
|
||||
if (continuations.children.length == 0 && listsLength > 0) {
|
||||
const counts = [
|
||||
playlist?.querySelectorAll(
|
||||
'#button-shape-dislike[aria-pressed=true] > button',
|
||||
).length,
|
||||
playlist?.querySelectorAll(
|
||||
'#button-shape-dislike[aria-pressed=false] > button',
|
||||
).length,
|
||||
playlist?.querySelectorAll(
|
||||
'#button-shape-like[aria-pressed=false] > button',
|
||||
).length,
|
||||
playlist?.querySelectorAll(
|
||||
'#button-shape-like[aria-pressed=true] > button',
|
||||
).length,
|
||||
];
|
||||
let i = 0;
|
||||
for (const count of counts) {
|
||||
if (count == 0) {
|
||||
buttons.splice(i, 1);
|
||||
i--;
|
||||
} else {
|
||||
buttons[i].children[0].children[0].style.setProperty(
|
||||
'-webkit-mask-size',
|
||||
`100% ${100 - (count / listsLength) * 100}%`,
|
||||
);
|
||||
}
|
||||
i++;
|
||||
}
|
||||
}
|
||||
const menu = document.querySelector('.detail-page-menu');
|
||||
if (menu && !document.querySelector('.like-menu')) {
|
||||
for (const button of buttons) {
|
||||
menu.appendChild(button);
|
||||
button.addEventListener('click', this.loadFullList);
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
loadFullList(event) {
|
||||
event.stopPropagation();
|
||||
const id: string = event.currentTarget.id,
|
||||
loader = document.getElementById('continuations');
|
||||
this.loadObserver = new MutationObserver(() => {
|
||||
this.applyToList(id, loader);
|
||||
});
|
||||
this.applyToList(id, loader);
|
||||
this.loadObserver.observe(loader, {
|
||||
attributes: true,
|
||||
childList: true,
|
||||
subtree: true,
|
||||
});
|
||||
loader?.style.setProperty('top', '0');
|
||||
loader?.style.setProperty('left', '50%');
|
||||
loader?.style.setProperty('position', 'absolute');
|
||||
},
|
||||
applyToList(id: string, loader: HTMLElement) {
|
||||
if (loader.children.length != 0) return;
|
||||
this.loadObserver?.disconnect();
|
||||
let playlistbuttons: NodeListOf<Element> | undefined;
|
||||
const playlist = document.querySelector('ytmusic-shelf-renderer')
|
||||
? document.querySelector('ytmusic-shelf-renderer')
|
||||
: document.querySelector('ytmusic-playlist-shelf-renderer');
|
||||
switch (id) {
|
||||
case 'allundislike':
|
||||
playlistbuttons = playlist?.querySelectorAll(
|
||||
'#button-shape-dislike[aria-pressed=true] > button',
|
||||
);
|
||||
break;
|
||||
case 'alldislike':
|
||||
playlistbuttons = playlist?.querySelectorAll(
|
||||
'#button-shape-dislike[aria-pressed=false] > button',
|
||||
);
|
||||
break;
|
||||
case 'alllike':
|
||||
playlistbuttons = playlist?.querySelectorAll(
|
||||
'#button-shape-like[aria-pressed=false] > button',
|
||||
);
|
||||
break;
|
||||
case 'allunlike':
|
||||
playlistbuttons = playlist?.querySelectorAll(
|
||||
'#button-shape-like[aria-pressed=true] > button',
|
||||
);
|
||||
break;
|
||||
default:
|
||||
}
|
||||
playlistButtons?.forEach((elem) => elem.click());
|
||||
},
|
||||
stop() {
|
||||
this.observer?.disconnect();
|
||||
this.changeObserver?.disconnect();
|
||||
for (const button of document.querySelectorAll('.like-menu')) {
|
||||
button.remove();
|
||||
}
|
||||
},
|
||||
waitForElem(selector: string) {
|
||||
return new Promise((resolve) => {
|
||||
const interval = setInterval(() => {
|
||||
const elem = document.querySelector(selector);
|
||||
if (!elem) return;
|
||||
|
||||
clearInterval(interval);
|
||||
resolve(elem);
|
||||
});
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
74
src/plugins/album-actions/templates/dislike.html
Normal file
74
src/plugins/album-actions/templates/dislike.html
Normal file
@ -0,0 +1,74 @@
|
||||
<button
|
||||
id="alldislike"
|
||||
data-type="dislike"
|
||||
data-filled="false"
|
||||
class="like-menu yt-spec-button-shape-next yt-spec-button-shape-next--text yt-spec-button-shape-next--mono yt-spec-button-shape-next--size-m yt-spec-button-shape-next--icon-button"
|
||||
aria-pressed="false"
|
||||
aria-label="Dislike all"
|
||||
>
|
||||
<div
|
||||
class="yt-spec-button-shape-next__icon"
|
||||
style="color: var(--ytmusic-setting-item-toggle-active)"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<div
|
||||
class="yt-spec-button-shape-next__icon"
|
||||
style="
|
||||
color: white;
|
||||
-webkit-mask: linear-gradient(grey, grey);
|
||||
-webkit-mask-size: 100% 50%;
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
z-index: 1;
|
||||
position: absolute;
|
||||
"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<div style="width: 24px; height: 24px">
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
preserveAspectRatio="xMidYMid meet"
|
||||
focusable="false"
|
||||
class="style-scope yt-icon"
|
||||
style="
|
||||
pointer-events: none;
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
"
|
||||
>
|
||||
<g class="style-scope yt-icon">
|
||||
<path
|
||||
d="M18,4h3v10h-3V4z M5.23,14h4.23l-1.52,4.94C7.62,19.97,8.46,21,9.62,21c0.58,0,1.14-0.24,1.52-0.65L17,14V4H6.57 C5.5,4,4.59,4.67,4.38,5.61l-1.34,6C2.77,12.85,3.82,14,5.23,14z"
|
||||
class="style-scope yt-icon"
|
||||
></path>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div style="width: 24px; height: 24px">
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
preserveAspectRatio="xMidYMid meet"
|
||||
focusable="false"
|
||||
class="style-scope yt-icon"
|
||||
style="pointer-events: none; display: block; width: 100%; height: 100%"
|
||||
>
|
||||
<g class="style-scope yt-icon">
|
||||
<path
|
||||
d="M18,4h3v10h-3V4z M5.23,14h4.23l-1.52,4.94C7.62,19.97,8.46,21,9.62,21c0.58,0,1.14-0.24,1.52-0.65L17,14V4H6.57 C5.5,4,4.59,4.67,4.38,5.61l-1.34,6C2.77,12.85,3.82,14,5.23,14z"
|
||||
class="style-scope yt-icon"
|
||||
></path>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<yt-touch-feedback-shape style="border-radius: inherit">
|
||||
<div
|
||||
class="yt-spec-touch-feedback-shape yt-spec-touch-feedback-shape--touch-response"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<div class="yt-spec-touch-feedback-shape__stroke"></div>
|
||||
<div class="yt-spec-touch-feedback-shape__fill"></div>
|
||||
</div>
|
||||
</yt-touch-feedback-shape>
|
||||
</button>
|
||||
74
src/plugins/album-actions/templates/like.html
Normal file
74
src/plugins/album-actions/templates/like.html
Normal file
@ -0,0 +1,74 @@
|
||||
<button
|
||||
id="alllike"
|
||||
data-type="like"
|
||||
data-filled="false"
|
||||
class="like-menu yt-spec-button-shape-next yt-spec-button-shape-next--text yt-spec-button-shape-next--mono yt-spec-button-shape-next--size-m yt-spec-button-shape-next--icon-button"
|
||||
aria-pressed="false"
|
||||
aria-label="Like all"
|
||||
>
|
||||
<div
|
||||
class="yt-spec-button-shape-next__icon"
|
||||
style="color: var(--ytmusic-setting-item-toggle-active)"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<div
|
||||
class="yt-spec-button-shape-next__icon"
|
||||
style="
|
||||
color: white;
|
||||
-webkit-mask: linear-gradient(grey, grey);
|
||||
-webkit-mask-size: 100% 50%;
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
z-index: 1;
|
||||
position: absolute;
|
||||
"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<div style="width: 24px; height: 24px">
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
preserveAspectRatio="xMidYMid meet"
|
||||
focusable="false"
|
||||
class="style-scope yt-icon"
|
||||
style="
|
||||
pointer-events: none;
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
"
|
||||
>
|
||||
<g class="style-scope yt-icon">
|
||||
<path
|
||||
d="M3,11h3v10H3V11z M18.77,11h-4.23l1.52-4.94C16.38,5.03,15.54,4,14.38,4c-0.58,0-1.14,0.24-1.52,0.65L7,11v10h10.43 c1.06,0,1.98-0.67,2.19-1.61l1.34-6C21.23,12.15,20.18,11,18.77,11z"
|
||||
class="style-scope yt-icon"
|
||||
></path>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div style="width: 24px; height: 24px">
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
preserveAspectRatio="xMidYMid meet"
|
||||
focusable="false"
|
||||
class="style-scope yt-icon"
|
||||
style="pointer-events: none; display: block; width: 100%; height: 100%"
|
||||
>
|
||||
<g class="style-scope yt-icon">
|
||||
<path
|
||||
d="M3,11h3v10H3V11z M18.77,11h-4.23l1.52-4.94C16.38,5.03,15.54,4,14.38,4c-0.58,0-1.14,0.24-1.52,0.65L7,11v10h10.43 c1.06,0,1.98-0.67,2.19-1.61l1.34-6C21.23,12.15,20.18,11,18.77,11z"
|
||||
class="style-scope yt-icon"
|
||||
></path>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<yt-touch-feedback-shape style="border-radius: inherit">
|
||||
<div
|
||||
class="yt-spec-touch-feedback-shape yt-spec-touch-feedback-shape--touch-response"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<div class="yt-spec-touch-feedback-shape__stroke"></div>
|
||||
<div class="yt-spec-touch-feedback-shape__fill"></div>
|
||||
</div>
|
||||
</yt-touch-feedback-shape>
|
||||
</button>
|
||||
74
src/plugins/album-actions/templates/undislike.html
Normal file
74
src/plugins/album-actions/templates/undislike.html
Normal file
@ -0,0 +1,74 @@
|
||||
<button
|
||||
id="allundislike"
|
||||
data-type="dislike"
|
||||
data-filled="true"
|
||||
class="like-menu yt-spec-button-shape-next yt-spec-button-shape-next--text yt-spec-button-shape-next--mono yt-spec-button-shape-next--size-m yt-spec-button-shape-next--icon-button"
|
||||
aria-pressed="false"
|
||||
aria-label="Undislike all"
|
||||
>
|
||||
<div
|
||||
class="yt-spec-button-shape-next__icon"
|
||||
style="color: var(--ytmusic-setting-item-toggle-active)"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<div
|
||||
class="yt-spec-button-shape-next__icon"
|
||||
style="
|
||||
color: white;
|
||||
-webkit-mask: linear-gradient(grey, grey);
|
||||
-webkit-mask-size: 100% 50%;
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
z-index: 1;
|
||||
position: absolute;
|
||||
"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<div style="width: 24px; height: 24px">
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
preserveAspectRatio="xMidYMid meet"
|
||||
focusable="false"
|
||||
class="style-scope yt-icon"
|
||||
style="
|
||||
pointer-events: none;
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
"
|
||||
>
|
||||
<g class="style-scope yt-icon">
|
||||
<path
|
||||
d="M17,4h-1H6.57C5.5,4,4.59,4.67,4.38,5.61l-1.34,6C2.77,12.85,3.82,14,5.23,14h4.23l-1.52,4.94C7.62,19.97,8.46,21,9.62,21 c0.58,0,1.14-0.24,1.52-0.65L17,14h4V4H17z M10.4,19.67C10.21,19.88,9.92,20,9.62,20c-0.26,0-0.5-0.11-0.63-0.3 c-0.07-0.1-0.15-0.26-0.09-0.47l1.52-4.94l0.4-1.29H9.46H5.23c-0.41,0-0.8-0.17-1.03-0.46c-0.12-0.15-0.25-0.4-0.18-0.72l1.34-6 C5.46,5.35,5.97,5,6.57,5H16v8.61L10.4,19.67z M20,13h-3V5h3V13z"
|
||||
class="style-scope yt-icon"
|
||||
></path>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div style="width: 24px; height: 24px">
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
preserveAspectRatio="xMidYMid meet"
|
||||
focusable="false"
|
||||
class="style-scope yt-icon"
|
||||
style="pointer-events: none; display: block; width: 100%; height: 100%"
|
||||
>
|
||||
<g class="style-scope yt-icon">
|
||||
<path
|
||||
d="M17,4h-1H6.57C5.5,4,4.59,4.67,4.38,5.61l-1.34,6C2.77,12.85,3.82,14,5.23,14h4.23l-1.52,4.94C7.62,19.97,8.46,21,9.62,21 c0.58,0,1.14-0.24,1.52-0.65L17,14h4V4H17z M10.4,19.67C10.21,19.88,9.92,20,9.62,20c-0.26,0-0.5-0.11-0.63-0.3 c-0.07-0.1-0.15-0.26-0.09-0.47l1.52-4.94l0.4-1.29H9.46H5.23c-0.41,0-0.8-0.17-1.03-0.46c-0.12-0.15-0.25-0.4-0.18-0.72l1.34-6 C5.46,5.35,5.97,5,6.57,5H16v8.61L10.4,19.67z M20,13h-3V5h3V13z"
|
||||
class="style-scope yt-icon"
|
||||
></path>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<yt-touch-feedback-shape style="border-radius: inherit">
|
||||
<div
|
||||
class="yt-spec-touch-feedback-shape yt-spec-touch-feedback-shape--touch-response"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<div class="yt-spec-touch-feedback-shape__stroke"></div>
|
||||
<div class="yt-spec-touch-feedback-shape__fill"></div>
|
||||
</div>
|
||||
</yt-touch-feedback-shape>
|
||||
</button>
|
||||
74
src/plugins/album-actions/templates/unlike.html
Normal file
74
src/plugins/album-actions/templates/unlike.html
Normal file
@ -0,0 +1,74 @@
|
||||
<button
|
||||
id="allunlike"
|
||||
data-type="like"
|
||||
data-filled="true"
|
||||
class="like-menu yt-spec-button-shape-next yt-spec-button-shape-next--text yt-spec-button-shape-next--mono yt-spec-button-shape-next--size-m yt-spec-button-shape-next--icon-button"
|
||||
aria-pressed="false"
|
||||
aria-label="Unlike all"
|
||||
>
|
||||
<div
|
||||
class="yt-spec-button-shape-next__icon"
|
||||
style="color: var(--ytmusic-setting-item-toggle-active)"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<div
|
||||
class="yt-spec-button-shape-next__icon"
|
||||
style="
|
||||
color: white;
|
||||
-webkit-mask: linear-gradient(grey, grey);
|
||||
-webkit-mask-size: 100% 50%;
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
z-index: 1;
|
||||
position: absolute;
|
||||
"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<div style="width: 24px; height: 24px">
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
preserveAspectRatio="xMidYMid meet"
|
||||
focusable="false"
|
||||
class="style-scope yt-icon"
|
||||
style="
|
||||
pointer-events: none;
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
"
|
||||
>
|
||||
<g class="style-scope yt-icon">
|
||||
<path
|
||||
d="M18.77,11h-4.23l1.52-4.94C16.38,5.03,15.54,4,14.38,4c-0.58,0-1.14,0.24-1.52,0.65L7,11H3v10h4h1h9.43 c1.06,0,1.98-0.67,2.19-1.61l1.34-6C21.23,12.15,20.18,11,18.77,11z M7,20H4v-8h3V20z M19.98,13.17l-1.34,6 C18.54,19.65,18.03,20,17.43,20H8v-8.61l5.6-6.06C13.79,5.12,14.08,5,14.38,5c0.26,0,0.5,0.11,0.63,0.3 c0.07,0.1,0.15,0.26,0.09,0.47l-1.52,4.94L13.18,12h1.35h4.23c0.41,0,0.8,0.17,1.03,0.46C19.92,12.61,20.05,12.86,19.98,13.17z"
|
||||
class="style-scope yt-icon"
|
||||
></path>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div style="width: 24px; height: 24px">
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
preserveAspectRatio="xMidYMid meet"
|
||||
focusable="false"
|
||||
class="style-scope yt-icon"
|
||||
style="pointer-events: none; display: block; width: 100%; height: 100%"
|
||||
>
|
||||
<g class="style-scope yt-icon">
|
||||
<path
|
||||
d="M18.77,11h-4.23l1.52-4.94C16.38,5.03,15.54,4,14.38,4c-0.58,0-1.14,0.24-1.52,0.65L7,11H3v10h4h1h9.43 c1.06,0,1.98-0.67,2.19-1.61l1.34-6C21.23,12.15,20.18,11,18.77,11z M7,20H4v-8h3V20z M19.98,13.17l-1.34,6 C18.54,19.65,18.03,20,17.43,20H8v-8.61l5.6-6.06C13.79,5.12,14.08,5,14.38,5c0.26,0,0.5,0.11,0.63,0.3 c0.07,0.1,0.15,0.26,0.09,0.47l-1.52,4.94L13.18,12h1.35h4.23c0.41,0,0.8,0.17,1.03,0.46C19.92,12.61,20.05,12.86,19.98,13.17z"
|
||||
class="style-scope yt-icon"
|
||||
></path>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<yt-touch-feedback-shape style="border-radius: inherit">
|
||||
<div
|
||||
class="yt-spec-touch-feedback-shape yt-spec-touch-feedback-shape--touch-response"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<div class="yt-spec-touch-feedback-shape__stroke"></div>
|
||||
<div class="yt-spec-touch-feedback-shape__fill"></div>
|
||||
</div>
|
||||
</yt-touch-feedback-shape>
|
||||
</button>
|
||||
@ -1,11 +1,13 @@
|
||||
import { FastAverageColor } from 'fast-average-color';
|
||||
import Color from 'color';
|
||||
|
||||
import style from './style.css?inline';
|
||||
|
||||
import { createPlugin } from '@/utils';
|
||||
import { t } from '@/i18n';
|
||||
|
||||
import type { VideoDataChanged } from '@/types/video-data-changed';
|
||||
const COLOR_KEY = '--ytmusic-album-color';
|
||||
const DARK_COLOR_KEY = '--ytmusic-album-color-dark';
|
||||
|
||||
export default createPlugin({
|
||||
name: () => t('plugins.album-color-theme.name'),
|
||||
@ -16,69 +18,8 @@ export default createPlugin({
|
||||
},
|
||||
stylesheets: [style],
|
||||
renderer: {
|
||||
hexToHSL: (H: string) => {
|
||||
// Convert hex to RGB first
|
||||
let r = 0;
|
||||
let g = 0;
|
||||
let b = 0;
|
||||
if (H.length == 4) {
|
||||
r = Number('0x' + H[1] + H[1]);
|
||||
g = Number('0x' + H[2] + H[2]);
|
||||
b = Number('0x' + H[3] + H[3]);
|
||||
} else if (H.length == 7) {
|
||||
r = Number('0x' + H[1] + H[2]);
|
||||
g = Number('0x' + H[3] + H[4]);
|
||||
b = Number('0x' + H[5] + H[6]);
|
||||
}
|
||||
// Then to HSL
|
||||
r /= 255;
|
||||
g /= 255;
|
||||
b /= 255;
|
||||
const cmin = Math.min(r, g, b);
|
||||
const cmax = Math.max(r, g, b);
|
||||
const delta = cmax - cmin;
|
||||
let h: number;
|
||||
let s: number;
|
||||
let l: number;
|
||||
|
||||
if (delta == 0) {
|
||||
h = 0;
|
||||
} else if (cmax == r) {
|
||||
h = ((g - b) / delta) % 6;
|
||||
} else if (cmax == g) {
|
||||
h = ((b - r) / delta) + 2;
|
||||
} else {
|
||||
h = ((r - g) / delta) + 4;
|
||||
}
|
||||
|
||||
h = Math.round(h * 60);
|
||||
|
||||
if (h < 0) {
|
||||
h += 360;
|
||||
}
|
||||
|
||||
l = (cmax + cmin) / 2;
|
||||
s = delta == 0 ? 0 : delta / (1 - Math.abs((2 * l) - 1));
|
||||
s = +(s * 100).toFixed(1);
|
||||
l = +(l * 100).toFixed(1);
|
||||
|
||||
//return "hsl(" + h + "," + s + "%," + l + "%)";
|
||||
return [h, s, l];
|
||||
},
|
||||
hue: 0,
|
||||
saturation: 0,
|
||||
lightness: 0,
|
||||
|
||||
changeElementColor: (
|
||||
element: HTMLElement | null,
|
||||
hue: number,
|
||||
saturation: number,
|
||||
lightness: number,
|
||||
) => {
|
||||
if (element) {
|
||||
element.style.backgroundColor = `hsl(${hue}, ${saturation}%, ${lightness}%)`;
|
||||
}
|
||||
},
|
||||
color: null as Color | null,
|
||||
darkColor: null as Color | null,
|
||||
|
||||
playerPage: null as HTMLElement | null,
|
||||
navBarBackground: null as HTMLElement | null,
|
||||
@ -103,113 +44,66 @@ export default createPlugin({
|
||||
'#mini-guide-background',
|
||||
);
|
||||
this.ytmusicAppLayout = document.querySelector<HTMLElement>('#layout');
|
||||
|
||||
const observer = new MutationObserver((mutationsList) => {
|
||||
for (const mutation of mutationsList) {
|
||||
if (mutation.type === 'attributes') {
|
||||
const isPageOpen =
|
||||
this.ytmusicAppLayout?.hasAttribute('player-page-open');
|
||||
if (isPageOpen) {
|
||||
this.changeElementColor(
|
||||
this.sidebarSmall,
|
||||
this.hue,
|
||||
this.saturation,
|
||||
this.lightness - 30,
|
||||
);
|
||||
} else {
|
||||
if (this.sidebarSmall) {
|
||||
this.sidebarSmall.style.backgroundColor = 'black';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (this.playerPage) {
|
||||
observer.observe(this.playerPage, { attributes: true });
|
||||
}
|
||||
},
|
||||
onPlayerApiReady(playerApi) {
|
||||
const fastAverageColor = new FastAverageColor();
|
||||
|
||||
document.addEventListener(
|
||||
'videodatachange',
|
||||
(event: CustomEvent<VideoDataChanged>) => {
|
||||
if (event.detail.name === 'dataloaded') {
|
||||
const playerResponse = playerApi.getPlayerResponse();
|
||||
const thumbnail =
|
||||
playerResponse?.videoDetails?.thumbnail?.thumbnails?.at(0);
|
||||
if (thumbnail) {
|
||||
fastAverageColor
|
||||
.getColorAsync(thumbnail.url)
|
||||
.then((albumColor) => {
|
||||
if (albumColor) {
|
||||
const [hue, saturation, lightness] = ([
|
||||
this.hue,
|
||||
this.saturation,
|
||||
this.lightness,
|
||||
] = this.hexToHSL(albumColor.hex));
|
||||
this.changeElementColor(
|
||||
this.playerPage,
|
||||
hue,
|
||||
saturation,
|
||||
lightness - 30,
|
||||
);
|
||||
this.changeElementColor(
|
||||
this.navBarBackground,
|
||||
hue,
|
||||
saturation,
|
||||
lightness - 15,
|
||||
);
|
||||
this.changeElementColor(
|
||||
this.ytmusicPlayerBar,
|
||||
hue,
|
||||
saturation,
|
||||
lightness - 15,
|
||||
);
|
||||
this.changeElementColor(
|
||||
this.playerBarBackground,
|
||||
hue,
|
||||
saturation,
|
||||
lightness - 15,
|
||||
);
|
||||
this.changeElementColor(
|
||||
this.sidebarBig,
|
||||
hue,
|
||||
saturation,
|
||||
lightness - 15,
|
||||
);
|
||||
if (
|
||||
this.ytmusicAppLayout?.hasAttribute('player-page-open')
|
||||
) {
|
||||
this.changeElementColor(
|
||||
this.sidebarSmall,
|
||||
hue,
|
||||
saturation,
|
||||
lightness - 30,
|
||||
);
|
||||
}
|
||||
const ytRightClickList =
|
||||
document.querySelector<HTMLElement>(
|
||||
'tp-yt-paper-listbox',
|
||||
);
|
||||
this.changeElementColor(
|
||||
ytRightClickList,
|
||||
hue,
|
||||
saturation,
|
||||
lightness - 15,
|
||||
);
|
||||
} else {
|
||||
if (this.playerPage) {
|
||||
this.playerPage.style.backgroundColor = '#000000';
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch((e) => console.error(e));
|
||||
}
|
||||
document.addEventListener('videodatachange', async (event) => {
|
||||
if (event.detail.name !== 'dataloaded') return;
|
||||
|
||||
const playerResponse = playerApi.getPlayerResponse();
|
||||
const thumbnail = playerResponse?.videoDetails?.thumbnail?.thumbnails?.at(0);
|
||||
if (!thumbnail) return;
|
||||
|
||||
const albumColor = await fastAverageColor.getColorAsync(thumbnail.url)
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
return null;
|
||||
});
|
||||
|
||||
if (albumColor) {
|
||||
const target = Color(albumColor.hex);
|
||||
|
||||
this.darkColor = target.darken(0.3).rgb();
|
||||
this.color = target.darken(0.15).rgb();
|
||||
|
||||
while (this.color.luminosity() > 0.5) {
|
||||
this.color = this.color?.darken(0.05);
|
||||
this.darkColor = this.darkColor?.darken(0.05);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
document.documentElement.style.setProperty(COLOR_KEY, `${~~this.color.red()}, ${~~this.color.green()}, ${~~this.color.blue()}`);
|
||||
document.documentElement.style.setProperty(DARK_COLOR_KEY, `${~~this.darkColor.red()}, ${~~this.darkColor.green()}, ${~~this.darkColor.blue()}`);
|
||||
} else {
|
||||
document.documentElement.style.setProperty(COLOR_KEY, '0, 0, 0');
|
||||
document.documentElement.style.setProperty(DARK_COLOR_KEY, '0, 0, 0');
|
||||
}
|
||||
|
||||
this.updateColor();
|
||||
});
|
||||
},
|
||||
getColor(key: string, alpha = 1) {
|
||||
return `rgba(var(${key}), ${alpha})`;
|
||||
},
|
||||
updateColor() {
|
||||
const change = (element: HTMLElement | null, color: string) => {
|
||||
if (element) {
|
||||
element.style.backgroundColor = color;
|
||||
}
|
||||
};
|
||||
|
||||
change(this.playerPage, this.getColor(DARK_COLOR_KEY));
|
||||
change(this.navBarBackground, this.getColor(COLOR_KEY));
|
||||
change(this.ytmusicPlayerBar, this.getColor(COLOR_KEY));
|
||||
change(this.playerBarBackground, this.getColor(COLOR_KEY));
|
||||
change(this.sidebarBig, this.getColor(COLOR_KEY));
|
||||
|
||||
if (this.ytmusicAppLayout?.hasAttribute('player-page-open')) {
|
||||
change(this.sidebarSmall, this.getColor(DARK_COLOR_KEY));
|
||||
}
|
||||
|
||||
const ytRightClickList = document.querySelector<HTMLElement>('tp-yt-paper-listbox');
|
||||
change(ytRightClickList, this.getColor(COLOR_KEY));
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@ -4,28 +4,24 @@ yt-page-navigation-progress {
|
||||
}
|
||||
|
||||
#player-page {
|
||||
transition:
|
||||
transform 300ms,
|
||||
background-color 300ms cubic-bezier(0.2, 0, 0.6, 1) !important;
|
||||
transition: transform 300ms,
|
||||
background-color 300ms cubic-bezier(0.2, 0, 0.6, 1) !important;
|
||||
}
|
||||
|
||||
#nav-bar-background {
|
||||
transition:
|
||||
opacity 200ms,
|
||||
background-color 300ms cubic-bezier(0.2, 0, 0.6, 1) !important;
|
||||
transition: opacity 200ms,
|
||||
background-color 300ms cubic-bezier(0.2, 0, 0.6, 1) !important;
|
||||
}
|
||||
|
||||
#mini-guide-background {
|
||||
transition:
|
||||
opacity 200ms,
|
||||
background-color 300ms cubic-bezier(0.2, 0, 0.6, 1) !important;
|
||||
transition: opacity 200ms,
|
||||
background-color 300ms cubic-bezier(0.2, 0, 0.6, 1) !important;
|
||||
border-right: 0px !important;
|
||||
}
|
||||
|
||||
#guide-wrapper {
|
||||
transition:
|
||||
opacity 200ms,
|
||||
background-color 300ms cubic-bezier(0.2, 0, 0.6, 1) !important;
|
||||
transition: opacity 200ms,
|
||||
background-color 300ms cubic-bezier(0.2, 0, 0.6, 1) !important;
|
||||
}
|
||||
|
||||
#img,
|
||||
@ -37,3 +33,35 @@ yt-page-navigation-progress {
|
||||
#items {
|
||||
border-radius: 10px !important;
|
||||
}
|
||||
|
||||
/* fix blur navigation bar */
|
||||
|
||||
ytmusic-app-layout > [slot='player-page'] {
|
||||
padding-top: 90px;
|
||||
margin-top: calc(-90px + var(--menu-bar-height, 0px)) !important;
|
||||
}
|
||||
|
||||
/* fix icon color */
|
||||
|
||||
.duration.ytmusic-player-queue-item, .byline.ytmusic-player-queue-item {
|
||||
color: rgba(255, 255, 255, 0.5) !important;
|
||||
--yt-endpoint-color: rgba(255, 255, 255, 0.5) !important;
|
||||
--yt-endpoint-hover-color: rgba(255, 255, 255, 0.5) !important;
|
||||
--yt-endpoint-visited-color: rgba(255, 255, 255, 0.5) !important;
|
||||
}
|
||||
|
||||
.icon.ytmusic-menu-navigation-item-renderer {
|
||||
color: rgba(255, 255, 255, 0.5) !important;
|
||||
}
|
||||
.menu.ytmusic-player-bar {
|
||||
--iron-icon-fill-color: rgba(255, 255, 255, 0.5) !important;
|
||||
}
|
||||
ytmusic-player-bar {
|
||||
color: rgba(255, 255, 255, 0.5) !important;
|
||||
}
|
||||
.time-info.ytmusic-player-bar {
|
||||
color: rgba(255, 255, 255, 0.5) !important;
|
||||
}
|
||||
.volume-slider.ytmusic-player-bar, .expand-volume-slider.ytmusic-player-bar {
|
||||
--paper-slider-container-color: rgba(255, 255, 255, 0.5) !important;
|
||||
}
|
||||
|
||||
@ -145,6 +145,73 @@ export default createPlugin({
|
||||
observer: null as MutationObserver | null,
|
||||
|
||||
start() {
|
||||
const injectBlurImage = () => {
|
||||
const songImage = document.querySelector<HTMLImageElement>(
|
||||
'#song-image',
|
||||
);
|
||||
const image = document.querySelector<HTMLImageElement>(
|
||||
'#song-image yt-img-shadow > img',
|
||||
);
|
||||
|
||||
if (!songImage) return null;
|
||||
if (!image) return null;
|
||||
|
||||
const blurImage = document.createElement('img');
|
||||
blurImage.classList.add('html5-blur-image');
|
||||
blurImage.src = image.src;
|
||||
|
||||
const applyImageAttribute = () => {
|
||||
const rect = image.getBoundingClientRect();
|
||||
|
||||
const newWidth = Math.floor(image.width || rect.width);
|
||||
const newHeight = Math.floor(image.height || rect.height);
|
||||
|
||||
if (newWidth === 0 || newHeight === 0) return;
|
||||
|
||||
if (this.isFullscreen) blurImage.classList.add('fullscreen');
|
||||
else blurImage.classList.remove('fullscreen');
|
||||
|
||||
const leftOffset = (newWidth * (this.sizeRatio - 1)) / 2;
|
||||
const topOffset = (newHeight * (this.sizeRatio - 1)) / 2;
|
||||
blurImage.style.setProperty('--left', `${-1 * leftOffset}px`);
|
||||
blurImage.style.setProperty('--top', `${-1 * topOffset}px`);
|
||||
blurImage.style.setProperty('--width', `${newWidth * this.sizeRatio}px`);
|
||||
blurImage.style.setProperty('--height', `${newHeight * this.sizeRatio}px`);
|
||||
blurImage.style.setProperty('--blur', `${this.blur}px`);
|
||||
blurImage.style.setProperty('--opacity', `${this.opacity}`);
|
||||
};
|
||||
|
||||
this.update = applyImageAttribute;
|
||||
|
||||
const observer = new MutationObserver((mutations) => {
|
||||
mutations.forEach((mutation) => {
|
||||
if (mutation.type === 'attributes') {
|
||||
applyImageAttribute();
|
||||
}
|
||||
});
|
||||
});
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
applyImageAttribute();
|
||||
});
|
||||
|
||||
applyImageAttribute();
|
||||
observer.observe(songImage, { attributes: true });
|
||||
resizeObserver.observe(songImage);
|
||||
window.addEventListener('resize', applyImageAttribute);
|
||||
|
||||
/* injecting */
|
||||
songImage.prepend(blurImage);
|
||||
|
||||
/* cleanup */
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
resizeObserver.disconnect();
|
||||
window.removeEventListener('resize', applyImageAttribute);
|
||||
|
||||
if (blurImage.isConnected) blurImage.remove();
|
||||
};
|
||||
};
|
||||
|
||||
const injectBlurVideo = (): (() => void) | null => {
|
||||
const songVideo = document.querySelector<HTMLDivElement>('#song-video');
|
||||
const video = document.querySelector<HTMLVideoElement>(
|
||||
@ -172,7 +239,6 @@ export default createPlugin({
|
||||
cancelAnimationFrame(lastEffectWorkId);
|
||||
|
||||
lastEffectWorkId = requestAnimationFrame(() => {
|
||||
// console.log('context', context);
|
||||
if (!context) return;
|
||||
|
||||
const width = this.qualityRatio;
|
||||
@ -280,13 +346,20 @@ export default createPlugin({
|
||||
};
|
||||
};
|
||||
|
||||
const isVideoMode = () => {
|
||||
const songVideo = document.querySelector<HTMLDivElement>('#song-video');
|
||||
if (!songVideo) return false;
|
||||
|
||||
return getComputedStyle(songVideo).display !== 'none';
|
||||
};
|
||||
|
||||
const playerPage = document.querySelector<HTMLElement>('#player-page');
|
||||
const ytmusicAppLayout = document.querySelector<HTMLElement>('#layout');
|
||||
|
||||
const isPageOpen = ytmusicAppLayout?.hasAttribute('player-page-open');
|
||||
if (isPageOpen) {
|
||||
this.unregister?.();
|
||||
this.unregister = injectBlurVideo() ?? null;
|
||||
this.unregister = (isVideoMode() ? injectBlurVideo() : injectBlurImage()) ?? null;
|
||||
}
|
||||
|
||||
const observer = new MutationObserver((mutationsList) => {
|
||||
@ -296,7 +369,7 @@ export default createPlugin({
|
||||
ytmusicAppLayout?.hasAttribute('player-page-open');
|
||||
if (isPageOpen) {
|
||||
this.unregister?.();
|
||||
this.unregister = injectBlurVideo() ?? null;
|
||||
this.unregister = (isVideoMode() ? injectBlurVideo() : injectBlurImage()) ?? null;
|
||||
} else {
|
||||
this.unregister?.();
|
||||
this.unregister = null;
|
||||
|
||||
@ -24,3 +24,17 @@
|
||||
#song-video .html5-video-container > video {
|
||||
top: 0 !important;
|
||||
}
|
||||
|
||||
#song-image .html5-blur-image {
|
||||
position: absolute;
|
||||
|
||||
left: var(--left, 0px);
|
||||
top: var(--top, 0px);
|
||||
width: var(--width, 100%) !important;
|
||||
height: var(--height, 100%) !important;
|
||||
|
||||
filter: blur(var(--blur, 100px));
|
||||
opacity: var(--opacity, 1);
|
||||
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@ -33,7 +33,7 @@ const menuObserver = new MutationObserver(() => {
|
||||
}
|
||||
|
||||
const menuUrl = document.querySelector<HTMLAnchorElement>(
|
||||
'tp-yt-paper-listbox [tabindex="-1"] #navigation-endpoint',
|
||||
'tp-yt-paper-listbox [tabindex="0"] #navigation-endpoint',
|
||||
)?.href;
|
||||
if (!menuUrl?.includes('watch?') && doneFirstLoad) {
|
||||
return;
|
||||
@ -42,11 +42,9 @@ const menuObserver = new MutationObserver(() => {
|
||||
menu.prepend(downloadButton);
|
||||
progress = document.querySelector('#ytmcustom-download');
|
||||
|
||||
if (doneFirstLoad) {
|
||||
return;
|
||||
if (!doneFirstLoad) {
|
||||
setTimeout(() => (doneFirstLoad ||= true), 500);
|
||||
}
|
||||
|
||||
setTimeout(() => (doneFirstLoad ||= true), 500);
|
||||
});
|
||||
|
||||
export const onRendererLoad = ({
|
||||
@ -56,7 +54,7 @@ export const onRendererLoad = ({
|
||||
let videoUrl = getSongMenu()
|
||||
// Selector of first button which is always "Start Radio"
|
||||
?.querySelector(
|
||||
'ytmusic-menu-navigation-item-renderer[tabindex="-1"] #navigation-endpoint',
|
||||
'ytmusic-menu-navigation-item-renderer[tabindex="0"] #navigation-endpoint',
|
||||
)
|
||||
?.getAttribute('href');
|
||||
if (videoUrl) {
|
||||
|
||||
@ -7,13 +7,14 @@ import type { MenuItem } from 'electron';
|
||||
interface PanelOptions {
|
||||
placement?: 'bottom' | 'right';
|
||||
order?: number;
|
||||
openOnHover?: boolean;
|
||||
}
|
||||
|
||||
export const createPanel = (
|
||||
parent: HTMLElement,
|
||||
anchor: HTMLElement,
|
||||
items: MenuItem[],
|
||||
options: PanelOptions = { placement: 'bottom', order: 0 },
|
||||
options: PanelOptions = { placement: 'bottom', order: 0, openOnHover: false },
|
||||
) => {
|
||||
const childPanels: HTMLElement[] = [];
|
||||
const panel = document.createElement('menu-panel');
|
||||
@ -51,6 +52,29 @@ export const createPanel = (
|
||||
menu.appendChild(iconWrapper);
|
||||
menu.append(item.label);
|
||||
|
||||
if (item.sublabel) {
|
||||
menu.classList.add('badge');
|
||||
const menuBadge = document.createElement('menu-item-badge');
|
||||
menuBadge.append(item.sublabel);
|
||||
menu.append(menuBadge);
|
||||
}
|
||||
if (item.toolTip) {
|
||||
const menuTooltip = document.createElement('menu-item-tooltip');
|
||||
menuTooltip.append(item.toolTip);
|
||||
|
||||
menu.addEventListener('mouseenter', () => {
|
||||
const rect = menu.getBoundingClientRect();
|
||||
menuTooltip.style.setProperty('max-width', `${rect.width - 8}px`);
|
||||
menuTooltip.style.setProperty('--x', `${rect.left}px`);
|
||||
menuTooltip.style.setProperty('--y', `${rect.top + rect.height}px`);
|
||||
menuTooltip.classList.add('show');
|
||||
});
|
||||
menu.addEventListener('mouseleave', () => {
|
||||
menuTooltip.classList.remove('show');
|
||||
});
|
||||
parent.append(menuTooltip);
|
||||
}
|
||||
|
||||
menu.addEventListener('click', async () => {
|
||||
await window.ipcRenderer.invoke('menu-event', item.commandId);
|
||||
const menuItem = (await window.ipcRenderer.invoke(
|
||||
@ -93,11 +117,12 @@ export const createPanel = (
|
||||
{
|
||||
placement: 'right',
|
||||
order: (options?.order ?? 0) + 1,
|
||||
openOnHover: true,
|
||||
},
|
||||
);
|
||||
|
||||
childPanels.push(child);
|
||||
children.push(...children);
|
||||
childPanels.push(...children);
|
||||
}
|
||||
|
||||
return panel.appendChild(menu);
|
||||
@ -132,6 +157,46 @@ export const createPanel = (
|
||||
}
|
||||
};
|
||||
|
||||
if (options.openOnHover) {
|
||||
let timeout: number | null = null;
|
||||
anchor.addEventListener('mouseenter', () => {
|
||||
if (timeout) window.clearTimeout(timeout);
|
||||
timeout = window.setTimeout(() => {
|
||||
if (!isOpened()) open();
|
||||
}, 225);
|
||||
});
|
||||
anchor.addEventListener('mouseleave', () => {
|
||||
if (timeout) window.clearTimeout(timeout);
|
||||
let mouseX = 0, mouseY = 0;
|
||||
const onMouseMove = (event: MouseEvent) => {
|
||||
mouseX = event.clientX;
|
||||
mouseY = event.clientY;
|
||||
};
|
||||
document.addEventListener('mousemove', onMouseMove);
|
||||
timeout = window.setTimeout(() => {
|
||||
document.removeEventListener('mousemove', onMouseMove);
|
||||
const now = document.elementFromPoint(mouseX, mouseY);
|
||||
if (now === panel || panel.contains(now)) {
|
||||
const onLeave = () => {
|
||||
document.addEventListener('mousemove', onMouseMove);
|
||||
timeout = window.setTimeout(() => {
|
||||
document.removeEventListener('mousemove', onMouseMove);
|
||||
const now = document.elementFromPoint(mouseX, mouseY);
|
||||
if (now === panel || panel.contains(now) || childPanels.some((it) => it.contains(now))) return;
|
||||
|
||||
if (isOpened()) close();
|
||||
panel.removeEventListener('mouseleave', onLeave);
|
||||
}, 225);
|
||||
};
|
||||
panel.addEventListener('mouseleave', onLeave);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isOpened()) close();
|
||||
}, 225);
|
||||
});
|
||||
}
|
||||
|
||||
anchor.addEventListener('click', () => {
|
||||
if (isOpened()) close();
|
||||
else open();
|
||||
|
||||
@ -22,8 +22,7 @@ title-bar {
|
||||
|
||||
color: #f1f1f1;
|
||||
font-size: 12px;
|
||||
padding: 4px 12px;
|
||||
padding-left: var(--offset-left, 12px);
|
||||
padding: 4px 12px 4px var(--offset-left, 12px);
|
||||
background-color: var(--titlebar-background-color, #030303);
|
||||
user-select: none;
|
||||
|
||||
@ -97,6 +96,8 @@ menu-panel.position-by-bottom {
|
||||
}
|
||||
|
||||
menu-item {
|
||||
position: relative;
|
||||
|
||||
-webkit-app-region: none;
|
||||
min-height: 32px;
|
||||
height: 32px;
|
||||
@ -109,6 +110,9 @@ menu-item {
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
menu-item.badge {
|
||||
grid-template-columns: 32px 1fr auto minmax(32px, auto);
|
||||
}
|
||||
menu-item:hover {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
@ -128,6 +132,56 @@ menu-separator {
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
menu-item-badge {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
min-width: 16px;
|
||||
height: 16px;
|
||||
padding: 0 4px;
|
||||
margin-left: 8px;
|
||||
|
||||
border-radius: 4px;
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
color: #f1f1f1;
|
||||
font-size: 10px;
|
||||
font-weight: 500;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
menu-item-tooltip {
|
||||
position: fixed;
|
||||
|
||||
left: var(--x, 0);
|
||||
top: var(--y, 0);
|
||||
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
min-width: 32px;
|
||||
padding: 4px;
|
||||
|
||||
border-radius: 4px;
|
||||
background-color: rgba(25, 25, 25, 0.8);
|
||||
color: #f1f1f1;
|
||||
font-size: 10px;
|
||||
|
||||
pointer-events: none;
|
||||
z-index: 1000;
|
||||
|
||||
opacity: 0;
|
||||
scale: 0.9;
|
||||
|
||||
transform-origin: 50% 0;
|
||||
transition: opacity 0.225s ease-out, scale 0.225s ease-out;
|
||||
}
|
||||
menu-item-tooltip.show {
|
||||
opacity: 1;
|
||||
scale: 1.0;
|
||||
}
|
||||
|
||||
/* classes */
|
||||
|
||||
.title-bar-icon {
|
||||
|
||||
149
src/plugins/music-together/connection.ts
Normal file
149
src/plugins/music-together/connection.ts
Normal file
@ -0,0 +1,149 @@
|
||||
import { DataConnection, Peer } from 'peerjs';
|
||||
|
||||
import type { Permission, Profile, VideoData } from './types';
|
||||
|
||||
export type ConnectionEventMap = {
|
||||
ADD_SONGS: { videoList: VideoData[], index?: number };
|
||||
REMOVE_SONG: { index: number };
|
||||
MOVE_SONG: { fromIndex: number; toIndex: number };
|
||||
IDENTIFY: { profile: Profile } | undefined;
|
||||
SYNC_PROFILE: { profiles: Record<string, Profile> } | undefined;
|
||||
SYNC_QUEUE: { videoList: VideoData[] } | undefined;
|
||||
SYNC_PROGRESS: { progress?: number; state?: number; index?: number; } | undefined;
|
||||
PERMISSION: Permission | undefined;
|
||||
};
|
||||
export type ConnectionEventUnion = {
|
||||
[Event in keyof ConnectionEventMap]: {
|
||||
type: Event;
|
||||
payload: ConnectionEventMap[Event];
|
||||
after?: ConnectionEventUnion[];
|
||||
};
|
||||
}[keyof ConnectionEventMap];
|
||||
|
||||
type PromiseUtil<T> = {
|
||||
promise: Promise<T>;
|
||||
resolve: (id: T) => void;
|
||||
reject: (err: unknown) => void;
|
||||
}
|
||||
|
||||
export type ConnectionListener = (event: ConnectionEventUnion, conn: DataConnection) => void;
|
||||
export type ConnectionMode = 'host' | 'guest' | 'disconnected';
|
||||
export class Connection {
|
||||
private peer: Peer;
|
||||
private _mode: ConnectionMode = 'disconnected';
|
||||
private connections: Record<string, DataConnection> = {};
|
||||
|
||||
private waitOpen: PromiseUtil<string> = {} as PromiseUtil<string>;
|
||||
private listeners: ConnectionListener[] = [];
|
||||
private connectionListeners: ((connection?: DataConnection) => void)[] = [];
|
||||
|
||||
constructor() {
|
||||
this.peer = new Peer({ debug: 0 });
|
||||
|
||||
this.waitOpen.promise = new Promise<string>((resolve, reject) => {
|
||||
this.waitOpen.resolve = resolve;
|
||||
this.waitOpen.reject = reject;
|
||||
});
|
||||
|
||||
this.peer.on('open', (id) => {
|
||||
this._mode = 'host';
|
||||
this.waitOpen.resolve(id);
|
||||
});
|
||||
this.peer.on('connection', (conn) => {
|
||||
this._mode = 'host';
|
||||
this.registerConnection(conn);
|
||||
});
|
||||
this.peer.on('error', (err) => {
|
||||
this._mode = 'disconnected';
|
||||
|
||||
this.waitOpen.reject(err);
|
||||
this.connectionListeners.forEach((listener) => listener());
|
||||
console.log(err);
|
||||
});
|
||||
}
|
||||
|
||||
/* public */
|
||||
async waitForReady() {
|
||||
return this.waitOpen.promise;
|
||||
}
|
||||
|
||||
async connect(id: string) {
|
||||
this._mode = 'guest';
|
||||
const conn = this.peer.connect(id);
|
||||
await this.registerConnection(conn);
|
||||
return conn;
|
||||
}
|
||||
|
||||
async disconnect() {
|
||||
if (this._mode === 'disconnected') throw new Error('Already disconnected');
|
||||
|
||||
this._mode = 'disconnected';
|
||||
this.connections = {};
|
||||
this.peer.destroy();
|
||||
}
|
||||
|
||||
/* utils */
|
||||
public get id() {
|
||||
return this.peer.id;
|
||||
}
|
||||
|
||||
public get mode() {
|
||||
return this._mode;
|
||||
}
|
||||
|
||||
public getConnections() {
|
||||
return Object.values(this.connections);
|
||||
}
|
||||
|
||||
public async broadcast<Event extends keyof ConnectionEventMap>(type: Event, payload: ConnectionEventMap[Event]) {
|
||||
await Promise.all(
|
||||
this.getConnections().map((conn) => conn.send({ type, payload }))
|
||||
);
|
||||
}
|
||||
|
||||
public on(listener: ConnectionListener) {
|
||||
this.listeners.push(listener);
|
||||
}
|
||||
|
||||
public onConnections(listener: (connections?: DataConnection) => void) {
|
||||
this.connectionListeners.push(listener);
|
||||
}
|
||||
|
||||
/* privates */
|
||||
private async registerConnection(conn: DataConnection) {
|
||||
return new Promise<DataConnection>((resolve, reject) => {
|
||||
this.peer.once('error', (err) => {
|
||||
this._mode = 'disconnected';
|
||||
|
||||
reject(err);
|
||||
this.connectionListeners.forEach((listener) => listener());
|
||||
});
|
||||
|
||||
conn.on('open', () => {
|
||||
this.connections[conn.connectionId] = conn;
|
||||
resolve(conn);
|
||||
this.connectionListeners.forEach((listener) => listener(conn));
|
||||
|
||||
conn.on('data', (data) => {
|
||||
if (!data || typeof data !== 'object' || !('type' in data) || !('payload' in data) || !data.type) {
|
||||
console.warn('Music Together: Invalid data', data);
|
||||
return;
|
||||
}
|
||||
|
||||
for (const listener of this.listeners) {
|
||||
listener(data as ConnectionEventUnion, conn);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const onClose = (err?: Error) => {
|
||||
if (err) reject(err);
|
||||
|
||||
delete this.connections[conn.connectionId];
|
||||
this.connectionListeners.forEach((listener) => listener(conn));
|
||||
};
|
||||
conn.on('error', onClose);
|
||||
conn.on('close', onClose);
|
||||
});
|
||||
}
|
||||
}
|
||||
138
src/plugins/music-together/element.ts
Normal file
138
src/plugins/music-together/element.ts
Normal file
@ -0,0 +1,138 @@
|
||||
import { ElementFromHtml } from '@/plugins/utils/renderer';
|
||||
|
||||
import itemHTML from './templates/item.html?raw';
|
||||
import popupHTML from './templates/popup.html?raw';
|
||||
|
||||
type Placement =
|
||||
'top'
|
||||
| 'bottom'
|
||||
| 'right'
|
||||
| 'left'
|
||||
| 'center'
|
||||
| 'middle'
|
||||
| 'center-middle'
|
||||
| 'top-left'
|
||||
| 'top-right'
|
||||
| 'bottom-left'
|
||||
| 'bottom-right';
|
||||
type PopupItem = (ItemRendererProps & { type: 'item'; })
|
||||
| { type: 'divider'; }
|
||||
| { type: 'custom'; element: HTMLElement; };
|
||||
|
||||
type PopupProps = {
|
||||
data: PopupItem[];
|
||||
anchorAt?: Placement;
|
||||
popupAt?: Placement;
|
||||
}
|
||||
export const Popup = (props: PopupProps) => {
|
||||
const popup = ElementFromHtml(popupHTML);
|
||||
const container = popup.querySelector<HTMLElement>('.music-together-popup-container')!;
|
||||
const items = props.data
|
||||
.map((props) => {
|
||||
if (props.type === 'item') return {
|
||||
type: 'item' as const,
|
||||
...ItemRenderer(props),
|
||||
};
|
||||
if (props.type === 'divider') return {
|
||||
type: 'divider' as const,
|
||||
element: ElementFromHtml('<div class="music-together-divider horizontal"></div>'),
|
||||
};
|
||||
if (props.type === 'custom') return {
|
||||
type: 'custom' as const,
|
||||
element: props.element,
|
||||
};
|
||||
|
||||
return null;
|
||||
})
|
||||
.filter(Boolean);
|
||||
|
||||
container.append(...items.map(({ element }) => element));
|
||||
popup.style.setProperty('opacity', '0');
|
||||
popup.style.setProperty('pointer-events', 'none');
|
||||
|
||||
document.body.append(popup);
|
||||
|
||||
return {
|
||||
element: popup,
|
||||
container,
|
||||
items,
|
||||
|
||||
show(x: number, y: number, anchor?: HTMLElement) {
|
||||
let left = x;
|
||||
let top = y;
|
||||
|
||||
if (anchor) {
|
||||
if (props.anchorAt?.includes('right')) left += anchor.clientWidth;
|
||||
if (props.anchorAt?.includes('bottom')) top += anchor.clientHeight;
|
||||
if (props.anchorAt?.includes('center')) left += anchor.clientWidth / 2;
|
||||
if (props.anchorAt?.includes('middle')) top += anchor.clientHeight / 2;
|
||||
}
|
||||
|
||||
if (props.popupAt?.includes('right')) left -= popup.clientWidth;
|
||||
if (props.popupAt?.includes('bottom')) top -= popup.clientHeight;
|
||||
if (props.popupAt?.includes('center')) left -= popup.clientWidth / 2;
|
||||
if (props.popupAt?.includes('middle')) top -= popup.clientHeight / 2;
|
||||
|
||||
popup.style.setProperty('left', `${left}px`);
|
||||
popup.style.setProperty('top', `${top}px`);
|
||||
popup.style.setProperty('opacity', '1');
|
||||
popup.style.setProperty('pointer-events', 'unset');
|
||||
|
||||
setTimeout(() => {
|
||||
const onClose = (event: MouseEvent) => {
|
||||
const isPopupClick = event.composedPath().some((element) => element === popup);
|
||||
if (!isPopupClick) {
|
||||
this.dismiss();
|
||||
document.removeEventListener('click', onClose);
|
||||
}
|
||||
};
|
||||
document.addEventListener('click', onClose);
|
||||
}, 16);
|
||||
},
|
||||
showAtAnchor(anchor: HTMLElement) {
|
||||
const { x, y } = anchor.getBoundingClientRect();
|
||||
this.show(x, y, anchor);
|
||||
},
|
||||
|
||||
isShowing() {
|
||||
return popup.style.getPropertyValue('opacity') === '1';
|
||||
},
|
||||
|
||||
dismiss() {
|
||||
popup.style.setProperty('opacity', '0');
|
||||
popup.style.setProperty('pointer-events', 'none');
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
type ItemRendererProps = {
|
||||
id?: string;
|
||||
icon?: Element;
|
||||
text: string;
|
||||
onClick?: () => void;
|
||||
};
|
||||
export const ItemRenderer = (props: ItemRendererProps) => {
|
||||
const element = ElementFromHtml(itemHTML);
|
||||
const iconContainer = element.querySelector<HTMLElement>('div.icon')!;
|
||||
const textContainer = element.querySelector<HTMLElement>('div.text')!;
|
||||
if (props.icon) iconContainer.appendChild(props.icon);
|
||||
textContainer.append(props.text);
|
||||
|
||||
if (props.onClick) {
|
||||
element.addEventListener('click', () => {
|
||||
props.onClick?.();
|
||||
});
|
||||
}
|
||||
if (props.id) element.id = props.id;
|
||||
|
||||
return {
|
||||
element,
|
||||
setIcon(icon: Element) {
|
||||
iconContainer.replaceChildren(icon);
|
||||
},
|
||||
setText(text: string) {
|
||||
textContainer.replaceChildren(text);
|
||||
},
|
||||
id: props.id
|
||||
};
|
||||
};
|
||||
3
src/plugins/music-together/icons/connect.svg
Normal file
3
src/plugins/music-together/icons/connect.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24">
|
||||
<path d="M480-640 280-440l56 56 104-103v407h80v-407l104 103 56-56-200-200ZM146-260q-32-49-49-105T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 59-17 115t-49 105l-58-58q22-37 33-78t11-84q0-134-93-227t-227-93q-134 0-227 93t-93 227q0 43 11 84t33 78l-58 58Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 408 B |
4
src/plugins/music-together/icons/key.svg
Normal file
4
src/plugins/music-together/icons/key.svg
Normal file
@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24">
|
||||
<path
|
||||
d="M280-400q-33 0-56.5-23.5T200-480q0-33 23.5-56.5T280-560q33 0 56.5 23.5T360-480q0 33-23.5 56.5T280-400Zm0 160q-100 0-170-70T40-480q0-100 70-170t170-70q67 0 121.5 33t86.5 87h352l120 120-180 180-80-60-80 60-85-60h-47q-32 54-86.5 87T280-240Zm0-80q56 0 98.5-34t56.5-86h125l58 41 82-61 71 55 75-75-40-40H435q-14-52-56.5-86T280-640q-66 0-113 47t-47 113q0 66 47 113t113 47Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 480 B |
3
src/plugins/music-together/icons/music-cast.svg
Normal file
3
src/plugins/music-together/icons/music-cast.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24">
|
||||
<path d="M560-160q-66 0-113-47t-47-113q0-66 47-113t113-47q23 0 42.5 5.5T640-458v-342h240v120H720v360q0 66-47 113t-113 47ZM80-320q0-99 38-186.5T221-659q65-65 152.5-103T560-800v80q-82 0-155 31.5t-127.5 86q-54.5 54.5-86 127T160-320H80Zm160 0q0-66 25.5-124.5t69-102Q378-590 436-615t124-25v80q-100 0-170 70t-70 170h-80Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 416 B |
4
src/plugins/music-together/icons/off.svg
Normal file
4
src/plugins/music-together/icons/off.svg
Normal file
@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24">
|
||||
<path
|
||||
d="M792-56 686-160H260q-92 0-156-64T40-380q0-77 47.5-137T210-594q3-8 6-15.5t6-16.5L56-792l56-56 736 736-56 56ZM260-240h346L284-562q-2 11-3 21t-1 21h-20q-58 0-99 41t-41 99q0 58 41 99t99 41Zm185-161Zm419 191-58-56q17-14 25.5-32.5T840-340q0-42-29-71t-71-29h-60v-80q0-83-58.5-141.5T480-720q-27 0-52 6.5T380-693l-58-58q35-24 74.5-36.5T480-800q117 0 198.5 81.5T760-520q69 8 114.5 59.5T920-340q0 39-15 72.5T864-210ZM593-479Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 529 B |
3
src/plugins/music-together/icons/tune.svg
Normal file
3
src/plugins/music-together/icons/tune.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24">
|
||||
<path d="M440-120v-240h80v80h320v80H520v80h-80Zm-320-80v-80h240v80H120Zm160-160v-80H120v-80h160v-80h80v240h-80Zm160-80v-80h400v80H440Zm160-160v-240h80v80h160v80H680v80h-80Zm-480-80v-80h400v80H120Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 298 B |
717
src/plugins/music-together/index.ts
Normal file
717
src/plugins/music-together/index.ts
Normal file
@ -0,0 +1,717 @@
|
||||
import prompt from 'custom-electron-prompt';
|
||||
|
||||
import { DataConnection } from 'peerjs';
|
||||
|
||||
import { t } from '@/i18n';
|
||||
import { createPlugin } from '@/utils';
|
||||
import promptOptions from '@/providers/prompt-options';
|
||||
|
||||
import { AppAPI, getDefaultProfile, Permission, Profile, VideoData } from './types';
|
||||
import { Queue } from './queue';
|
||||
import { Connection, ConnectionEventUnion } from './connection';
|
||||
import { createHostPopup } from './ui/host';
|
||||
import { createGuestPopup } from './ui/guest';
|
||||
import { createSettingPopup } from './ui/setting';
|
||||
|
||||
import settingHTML from './templates/setting.html?raw';
|
||||
import style from './style.css?inline';
|
||||
|
||||
import type { YoutubePlayer } from '@/types/youtube-player';
|
||||
import type { RendererContext } from '@/types/contexts';
|
||||
import type { VideoDataChanged } from '@/types/video-data-changed';
|
||||
|
||||
type RawAccountData = {
|
||||
accountName: {
|
||||
runs: { text: string }[];
|
||||
};
|
||||
accountPhoto: {
|
||||
thumbnails: { url: string; width: number; height: number; }[];
|
||||
};
|
||||
settingsEndpoint: unknown;
|
||||
manageAccountTitle: unknown;
|
||||
trackingParams: string;
|
||||
channelHandle: {
|
||||
runs: { text: string }[];
|
||||
};
|
||||
};
|
||||
|
||||
export default createPlugin<
|
||||
unknown,
|
||||
unknown,
|
||||
{
|
||||
connection?: Connection;
|
||||
ipc?: RendererContext<never>['ipc'];
|
||||
api: HTMLElement & AppAPI | null;
|
||||
queue?: Queue;
|
||||
playerApi?: YoutubePlayer;
|
||||
showPrompt: (title: string, label: string) => Promise<string>;
|
||||
popups: {
|
||||
host: ReturnType<typeof createHostPopup>;
|
||||
guest: ReturnType<typeof createGuestPopup>;
|
||||
setting: ReturnType<typeof createSettingPopup>;
|
||||
};
|
||||
elements: {
|
||||
setting: HTMLElement;
|
||||
icon: SVGElement;
|
||||
spinner: HTMLElement;
|
||||
};
|
||||
stateInterval?: number;
|
||||
updateNext: boolean;
|
||||
ignoreChange: boolean;
|
||||
rollbackInjector?: (() => void);
|
||||
me?: Omit<Profile, 'id'>;
|
||||
profiles: Record<string, Profile>;
|
||||
permission: Permission;
|
||||
videoChangeListener: (event: CustomEvent<VideoDataChanged>) => void;
|
||||
videoStateChangeListener: () => void;
|
||||
onHost: () => Promise<boolean>;
|
||||
onJoin: () => Promise<boolean>;
|
||||
onStop: () => void;
|
||||
putProfile: (id: string, profile?: Profile) => void;
|
||||
showSpinner: () => void;
|
||||
hideSpinner: () => void;
|
||||
initMyProfile: () => void;
|
||||
}
|
||||
>({
|
||||
name: () => t('plugins.music-together.name'),
|
||||
description: () => t('plugins.music-together.description'),
|
||||
restartNeeded: false,
|
||||
addedVersion: '3.2.X',
|
||||
config: {
|
||||
enabled: false
|
||||
},
|
||||
stylesheets: [style],
|
||||
backend({ ipc }) {
|
||||
ipc.handle('music-together:prompt', async (title: string, label: string) => prompt({
|
||||
title,
|
||||
label,
|
||||
type: 'input',
|
||||
...promptOptions()
|
||||
}));
|
||||
},
|
||||
renderer: {
|
||||
updateNext: false,
|
||||
ignoreChange: false,
|
||||
permission: 'playlist',
|
||||
popups: {} as {
|
||||
host: ReturnType<typeof createHostPopup>;
|
||||
guest: ReturnType<typeof createGuestPopup>;
|
||||
setting: ReturnType<typeof createSettingPopup>;
|
||||
},
|
||||
elements: {} as {
|
||||
setting: HTMLElement;
|
||||
icon: SVGElement;
|
||||
spinner: HTMLElement;
|
||||
},
|
||||
profiles: {},
|
||||
showPrompt: () => Promise.resolve(''),
|
||||
api: null,
|
||||
|
||||
/* events */
|
||||
videoChangeListener(event: CustomEvent<VideoDataChanged>) {
|
||||
if (event.detail.name === 'dataloaded' || this.updateNext) {
|
||||
if (this.connection?.mode === 'host') {
|
||||
const videoList: VideoData[] = this.queue?.flatItems.map((it) => ({
|
||||
videoId: it!.videoId,
|
||||
ownerId: this.connection!.id
|
||||
} satisfies VideoData)) ?? [];
|
||||
|
||||
this.queue?.setVideoList(videoList, false);
|
||||
this.queue?.syncQueueOwner();
|
||||
this.connection.broadcast('SYNC_QUEUE', {
|
||||
videoList
|
||||
});
|
||||
|
||||
this.updateNext = event.detail.name === 'dataloaded';
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
videoStateChangeListener() {
|
||||
if (this.connection?.mode !== 'guest') return;
|
||||
if (this.ignoreChange) return;
|
||||
if (this.permission !== 'all') return;
|
||||
|
||||
const state = this.playerApi?.getPlayerState();
|
||||
if (state !== 1 && state !== 2) return;
|
||||
|
||||
this.connection.broadcast('SYNC_PROGRESS', {
|
||||
// progress: this.playerApi?.getCurrentTime(),
|
||||
state: this.playerApi?.getPlayerState()
|
||||
// index: this.queue?.selectedIndex ?? 0,
|
||||
});
|
||||
},
|
||||
|
||||
/* connection */
|
||||
async onHost() {
|
||||
this.connection = new Connection();
|
||||
const wait = await this.connection.waitForReady().catch(() => null);
|
||||
if (!wait) return false;
|
||||
|
||||
if (!this.me) this.me = getDefaultProfile(this.connection.id);
|
||||
const rawItems = this.queue?.flatItems?.map((it) => ({
|
||||
videoId: it!.videoId,
|
||||
ownerId: this.connection!.id
|
||||
} satisfies VideoData)) ?? [];
|
||||
this.queue?.setOwner({
|
||||
id: this.connection.id,
|
||||
...this.me
|
||||
});
|
||||
this.queue?.setVideoList(rawItems, false);
|
||||
this.queue?.syncQueueOwner();
|
||||
this.queue?.initQueue();
|
||||
this.queue?.injection();
|
||||
|
||||
this.profiles = {};
|
||||
this.connection.onConnections((connection) => {
|
||||
if (!connection) {
|
||||
this.api?.openToast(t('plugins.music-together.toast.disconnected'));
|
||||
this.onStop();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!connection.open) {
|
||||
this.api?.openToast(t('plugins.music-together.toast.user-disconnected', {
|
||||
name: this.profiles[connection.peer]?.name
|
||||
}));
|
||||
this.putProfile(connection.peer, undefined);
|
||||
}
|
||||
});
|
||||
this.putProfile(this.connection.id, {
|
||||
id: this.connection.id,
|
||||
...this.me
|
||||
});
|
||||
|
||||
const listener = async (event: ConnectionEventUnion, conn?: DataConnection) => {
|
||||
this.ignoreChange = true;
|
||||
|
||||
switch (event.type) {
|
||||
case 'ADD_SONGS': {
|
||||
if (conn && this.permission === 'host-only') return;
|
||||
|
||||
await this.queue?.addVideos(event.payload.videoList, event.payload.index);
|
||||
await this.connection?.broadcast('ADD_SONGS', event.payload);
|
||||
break;
|
||||
}
|
||||
case 'REMOVE_SONG': {
|
||||
if (conn && this.permission === 'host-only') return;
|
||||
|
||||
this.queue?.removeVideo(event.payload.index);
|
||||
await this.connection?.broadcast('REMOVE_SONG', event.payload);
|
||||
break;
|
||||
}
|
||||
case 'MOVE_SONG': {
|
||||
if (conn && this.permission === 'host-only') {
|
||||
await this.connection?.broadcast('SYNC_QUEUE', {
|
||||
videoList: this.queue?.videoList ?? []
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
this.queue?.moveItem(event.payload.fromIndex, event.payload.toIndex);
|
||||
await this.connection?.broadcast('MOVE_SONG', event.payload);
|
||||
break;
|
||||
}
|
||||
case 'IDENTIFY': {
|
||||
if (!event.payload || !conn) {
|
||||
console.warn('Music Together [Host]: Received "IDENTIFY" event without payload or connection');
|
||||
break;
|
||||
}
|
||||
|
||||
this.api?.openToast(t('plugins.music-together.toast.user-connected', { name: event.payload.profile.name }));
|
||||
this.putProfile(conn.peer, event.payload.profile);
|
||||
break;
|
||||
}
|
||||
case 'SYNC_PROFILE': {
|
||||
await this.connection?.broadcast('SYNC_PROFILE', { profiles: this.profiles });
|
||||
|
||||
break;
|
||||
}
|
||||
case 'PERMISSION': {
|
||||
await this.connection?.broadcast('PERMISSION', this.permission);
|
||||
this.popups.guest.setPermission(this.permission);
|
||||
this.popups.host.setPermission(this.permission);
|
||||
this.popups.setting.setPermission(this.permission);
|
||||
break;
|
||||
}
|
||||
case 'SYNC_QUEUE': {
|
||||
await this.connection?.broadcast('SYNC_QUEUE', {
|
||||
videoList: this.queue?.videoList ?? []
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'SYNC_PROGRESS': {
|
||||
let permissionLevel = 0;
|
||||
if (this.permission === 'all') permissionLevel = 2;
|
||||
if (this.permission === 'playlist') permissionLevel = 1;
|
||||
if (this.permission === 'host-only') permissionLevel = 0;
|
||||
if (!conn) permissionLevel = 3;
|
||||
|
||||
if (permissionLevel >= 2) {
|
||||
if (typeof event.payload?.progress === 'number') {
|
||||
const currentTime = this.playerApi?.getCurrentTime() ?? 0;
|
||||
if (Math.abs(event.payload.progress - currentTime) > 3) this.playerApi?.seekTo(event.payload.progress);
|
||||
}
|
||||
if (this.playerApi?.getPlayerState() !== event.payload?.state) {
|
||||
if (event.payload?.state === 2) this.playerApi?.pauseVideo();
|
||||
if (event.payload?.state === 1) this.playerApi?.playVideo();
|
||||
}
|
||||
}
|
||||
if (permissionLevel >= 1) {
|
||||
if (typeof event.payload?.index === 'number') {
|
||||
const nowIndex = this.queue?.selectedIndex ?? 0;
|
||||
|
||||
if (nowIndex !== event.payload.index) {
|
||||
this.queue?.setIndex(event.payload.index);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
console.warn('Music Together [Host]: Unknown Event', event);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (event.after) {
|
||||
const now = event.after.shift();
|
||||
if (now) {
|
||||
now.after = event.after;
|
||||
await listener(now, conn);
|
||||
}
|
||||
}
|
||||
};
|
||||
this.connection.on(listener);
|
||||
this.queue?.on(listener);
|
||||
|
||||
setTimeout(() => {
|
||||
this.ignoreChange = false;
|
||||
}, 16); // wait 1 frame
|
||||
return true;
|
||||
},
|
||||
|
||||
async onJoin() {
|
||||
this.connection = new Connection();
|
||||
const wait = await this.connection.waitForReady().catch(() => null);
|
||||
if (!wait) return false;
|
||||
|
||||
this.profiles = {};
|
||||
|
||||
const id = await this.showPrompt(t('plugins.music-together.name'), t('plugins.music-together.dialog.enter-host'));
|
||||
if (typeof id !== 'string') return false;
|
||||
|
||||
const connection = await this.connection.connect(id).catch(() => false);
|
||||
if (!connection) return false;
|
||||
this.connection.onConnections((connection) => {
|
||||
if (!connection?.open) {
|
||||
this.api?.openToast(t('plugins.music-together.toast.disconnected'));
|
||||
this.onStop();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
let resolveIgnore: number | null = null;
|
||||
const listener = async (event: ConnectionEventUnion) => {
|
||||
this.ignoreChange = true;
|
||||
switch (event.type) {
|
||||
case 'ADD_SONGS': {
|
||||
await this.queue?.addVideos(event.payload.videoList, event.payload.index);
|
||||
break;
|
||||
}
|
||||
case 'REMOVE_SONG': {
|
||||
this.queue?.removeVideo(event.payload.index);
|
||||
break;
|
||||
}
|
||||
case 'MOVE_SONG': {
|
||||
this.queue?.moveItem(event.payload.fromIndex, event.payload.toIndex);
|
||||
break;
|
||||
}
|
||||
case 'IDENTIFY': {
|
||||
console.warn('Music Together [Guest]: Received "IDENTIFY" event from guest');
|
||||
break;
|
||||
}
|
||||
case 'SYNC_QUEUE': {
|
||||
if (Array.isArray(event.payload?.videoList)) {
|
||||
await this.queue?.setVideoList(event.payload.videoList);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'SYNC_PROFILE': {
|
||||
if (!event.payload) {
|
||||
console.warn('Music Together [Guest]: Received "SYNC_PROFILE" event without payload');
|
||||
break;
|
||||
}
|
||||
|
||||
Object.entries(event.payload.profiles).forEach(([id, profile]) => {
|
||||
this.putProfile(id, profile);
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'SYNC_PROGRESS': {
|
||||
if (typeof event.payload?.progress === 'number') {
|
||||
const currentTime = this.playerApi?.getCurrentTime() ?? 0;
|
||||
if (Math.abs(event.payload.progress - currentTime) > 3) this.playerApi?.seekTo(event.payload.progress);
|
||||
}
|
||||
if (this.playerApi?.getPlayerState() !== event.payload?.state) {
|
||||
if (event.payload?.state === 2) this.playerApi?.pauseVideo();
|
||||
if (event.payload?.state === 1) this.playerApi?.playVideo();
|
||||
}
|
||||
if (typeof event.payload?.index === 'number') {
|
||||
const nowIndex = this.queue?.selectedIndex ?? 0;
|
||||
|
||||
if (nowIndex !== event.payload.index) {
|
||||
this.queue?.setIndex(event.payload.index);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'PERMISSION': {
|
||||
if (!event.payload) {
|
||||
console.warn('Music Together [Guest]: Received "PERMISSION" event without payload');
|
||||
break;
|
||||
}
|
||||
|
||||
this.permission = event.payload;
|
||||
this.popups.guest.setPermission(this.permission);
|
||||
this.popups.host.setPermission(this.permission);
|
||||
this.popups.setting.setPermission(this.permission);
|
||||
|
||||
const permissionLabel = t(`plugins.music-together.menu.permission.${this.permission}`);
|
||||
|
||||
this.api?.openToast(t('plugins.music-together.toast.permission-changed', { permission: permissionLabel }));
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
console.warn('Music Together [Guest]: Unknown Event', event);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof resolveIgnore === 'number') clearTimeout(resolveIgnore);
|
||||
resolveIgnore = window.setTimeout(() => {
|
||||
this.ignoreChange = false;
|
||||
}, 16); // wait 1 frame
|
||||
};
|
||||
|
||||
this.connection.on(listener);
|
||||
this.queue?.on(async (event: ConnectionEventUnion) => {
|
||||
this.ignoreChange = true;
|
||||
switch (event.type) {
|
||||
case 'ADD_SONGS': {
|
||||
await this.connection?.broadcast('ADD_SONGS', event.payload);
|
||||
await this.connection?.broadcast('SYNC_QUEUE', undefined);
|
||||
break;
|
||||
}
|
||||
case 'REMOVE_SONG': {
|
||||
await this.connection?.broadcast('REMOVE_SONG', event.payload);
|
||||
break;
|
||||
}
|
||||
case 'MOVE_SONG': {
|
||||
await this.connection?.broadcast('MOVE_SONG', event.payload);
|
||||
await this.connection?.broadcast('SYNC_QUEUE', undefined);
|
||||
break;
|
||||
}
|
||||
case 'SYNC_PROGRESS': {
|
||||
if (this.permission === 'host-only') await this.connection?.broadcast('SYNC_QUEUE', undefined);
|
||||
else await this.connection?.broadcast('SYNC_PROGRESS', event.payload);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof resolveIgnore === 'number') clearTimeout(resolveIgnore);
|
||||
resolveIgnore = window.setTimeout(() => {
|
||||
this.ignoreChange = false;
|
||||
}, 16); // wait 1 frame
|
||||
});
|
||||
|
||||
if (!this.me) this.me = getDefaultProfile(this.connection.id);
|
||||
this.queue?.injection();
|
||||
this.queue?.setOwner({
|
||||
id: this.connection.id,
|
||||
...this.me
|
||||
});
|
||||
|
||||
const progress = Array.from(document.querySelectorAll<HTMLElement & {
|
||||
_update: (...args: unknown[]) => void
|
||||
}>('tp-yt-paper-progress'));
|
||||
const rollbackList = progress.map((progress) => {
|
||||
const original = progress._update;
|
||||
progress._update = (...args) => {
|
||||
const now = args[0];
|
||||
|
||||
if (this.permission === 'all' && typeof now === 'number') {
|
||||
const currentTime = this.playerApi?.getCurrentTime() ?? 0;
|
||||
if (Math.abs(now - currentTime) > 3) this.connection?.broadcast('SYNC_PROGRESS', {
|
||||
progress: now,
|
||||
state: this.playerApi?.getPlayerState()
|
||||
});
|
||||
}
|
||||
|
||||
original.call(progress, ...args);
|
||||
};
|
||||
|
||||
return () => {
|
||||
progress._update = original;
|
||||
};
|
||||
});
|
||||
this.rollbackInjector = () => {
|
||||
rollbackList.forEach((rollback) => rollback());
|
||||
};
|
||||
|
||||
this.connection.broadcast('IDENTIFY', {
|
||||
profile: {
|
||||
id: this.connection.id,
|
||||
handleId: this.me.handleId,
|
||||
name: this.me.name,
|
||||
thumbnail: this.me.thumbnail
|
||||
}
|
||||
});
|
||||
|
||||
this.connection.broadcast('SYNC_PROFILE', undefined);
|
||||
this.connection.broadcast('PERMISSION', undefined);
|
||||
|
||||
this.queue?.clear();
|
||||
this.queue?.syncQueueOwner();
|
||||
this.queue?.initQueue();
|
||||
|
||||
this.connection.broadcast('SYNC_QUEUE', undefined);
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
onStop() {
|
||||
this.connection?.disconnect();
|
||||
this.queue?.rollbackInjection();
|
||||
this.queue?.removeQueueOwner();
|
||||
if (this.rollbackInjector) {
|
||||
this.rollbackInjector();
|
||||
this.rollbackInjector = undefined;
|
||||
}
|
||||
|
||||
this.profiles = {};
|
||||
this.popups.host.setUsers(Object.values(this.profiles));
|
||||
this.popups.guest.setUsers(Object.values(this.profiles));
|
||||
|
||||
this.popups.host.dismiss();
|
||||
this.popups.guest.dismiss();
|
||||
this.popups.setting.dismiss();
|
||||
},
|
||||
|
||||
/* methods */
|
||||
putProfile(id: string, profile?: Profile) {
|
||||
if (profile === undefined) {
|
||||
delete this.profiles[id];
|
||||
} else {
|
||||
this.profiles[id] = profile;
|
||||
}
|
||||
|
||||
this.popups.host.setUsers(Object.values(this.profiles));
|
||||
this.popups.guest.setUsers(Object.values(this.profiles));
|
||||
},
|
||||
|
||||
showSpinner() {
|
||||
this.elements.icon.style.setProperty('display', 'none');
|
||||
this.elements.spinner.removeAttribute('hidden');
|
||||
this.elements.spinner.setAttribute('active', '');
|
||||
},
|
||||
|
||||
hideSpinner() {
|
||||
this.elements.icon.style.removeProperty('display');
|
||||
this.elements.spinner.removeAttribute('active');
|
||||
this.elements.spinner.setAttribute('hidden', '');
|
||||
},
|
||||
|
||||
initMyProfile() {
|
||||
const accountButton = document.querySelector<HTMLElement & {
|
||||
onButtonTap: () => void
|
||||
}>('ytmusic-settings-button');
|
||||
|
||||
accountButton?.onButtonTap();
|
||||
setTimeout(() => {
|
||||
accountButton?.onButtonTap();
|
||||
const renderer = document.querySelector<HTMLElement & { data: unknown }>('ytd-active-account-header-renderer');
|
||||
if (!accountButton || !renderer) {
|
||||
console.warn('Music Together: Cannot find account');
|
||||
this.me = getDefaultProfile(this.connection?.id ?? '');
|
||||
return;
|
||||
}
|
||||
|
||||
const accountData = renderer.data as RawAccountData;
|
||||
this.me = {
|
||||
handleId: accountData.channelHandle.runs[0].text,
|
||||
name: accountData.accountName.runs[0].text,
|
||||
thumbnail: accountData.accountPhoto.thumbnails[0].url
|
||||
};
|
||||
|
||||
if (this.me.thumbnail) {
|
||||
this.popups.host.setProfile(this.me.thumbnail);
|
||||
this.popups.guest.setProfile(this.me.thumbnail);
|
||||
this.popups.setting.setProfile(this.me.thumbnail);
|
||||
}
|
||||
}, 0);
|
||||
},
|
||||
/* hooks */
|
||||
|
||||
start({ ipc }) {
|
||||
this.ipc = ipc;
|
||||
this.showPrompt = async (title: string, label: string) => ipc.invoke('music-together:prompt', title, label) as Promise<string>;
|
||||
this.api = document.querySelector<HTMLElement & AppAPI>('ytmusic-app');
|
||||
|
||||
/* setup */
|
||||
document.querySelector('#right-content > ytmusic-settings-button')?.insertAdjacentHTML('beforebegin', settingHTML);
|
||||
const setting = document.querySelector<HTMLElement>('#music-together-setting-button');
|
||||
const icon = document.querySelector<SVGElement>('#music-together-setting-button > svg');
|
||||
const spinner = document.querySelector<HTMLElement>('#music-together-setting-button > tp-yt-paper-spinner-lite');
|
||||
if (!setting || !icon || !spinner) {
|
||||
console.warn('Music Together: Cannot inject html');
|
||||
console.log(setting, icon, spinner);
|
||||
return;
|
||||
}
|
||||
|
||||
this.elements = {
|
||||
setting,
|
||||
icon,
|
||||
spinner
|
||||
};
|
||||
|
||||
this.stateInterval = window.setInterval(() => {
|
||||
if (this.connection?.mode !== 'host') return;
|
||||
const index = this.queue?.selectedIndex ?? 0;
|
||||
|
||||
this.connection.broadcast('SYNC_PROGRESS', {
|
||||
progress: this.playerApi?.getCurrentTime(),
|
||||
state: this.playerApi?.getPlayerState(),
|
||||
index
|
||||
});
|
||||
}, 1000);
|
||||
|
||||
/* UI */
|
||||
const hostPopup = createHostPopup({
|
||||
onItemClick: (id) => {
|
||||
if (id === 'music-together-close') {
|
||||
this.onStop();
|
||||
this.api?.openToast(t('plugins.music-together.toast.closed'));
|
||||
hostPopup.dismiss();
|
||||
}
|
||||
|
||||
if (id === 'music-together-copy-id') {
|
||||
navigator.clipboard.writeText(this.connection?.id ?? '')
|
||||
.then(() => {
|
||||
this.api?.openToast(t('plugins.music-together.toast.id-copied'));
|
||||
hostPopup.dismiss();
|
||||
})
|
||||
.catch(() => {
|
||||
this.api?.openToast(t('plugins.music-together.toast.id-copy-failed'));
|
||||
hostPopup.dismiss();
|
||||
});
|
||||
}
|
||||
|
||||
if (id === 'music-together-permission') {
|
||||
if (this.permission === 'all') this.permission = 'host-only';
|
||||
else if (this.permission === 'playlist') this.permission = 'all';
|
||||
else if (this.permission === 'host-only') this.permission = 'playlist';
|
||||
this.connection?.broadcast('PERMISSION', this.permission);
|
||||
|
||||
hostPopup.setPermission(this.permission);
|
||||
guestPopup.setPermission(this.permission);
|
||||
settingPopup.setPermission(this.permission);
|
||||
|
||||
const permissionLabel = t(`plugins.music-together.menu.permission.${this.permission}`);
|
||||
this.api?.openToast(t('plugins.music-together.toast.permission-changed', { permission: permissionLabel }));
|
||||
const item = hostPopup.items.find((it) => it?.element.id === id);
|
||||
if (item?.type === 'item') {
|
||||
item.setText(t('plugins.music-together.menu.set-permission'));
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
const guestPopup = createGuestPopup({
|
||||
onItemClick: (id) => {
|
||||
if (id === 'music-together-disconnect') {
|
||||
this.onStop();
|
||||
this.api?.openToast(t('plugins.music-together.toast.disconnected'));
|
||||
guestPopup.dismiss();
|
||||
}
|
||||
}
|
||||
});
|
||||
const settingPopup = createSettingPopup({
|
||||
onItemClick: async (id) => {
|
||||
if (id === 'music-together-host') {
|
||||
settingPopup.dismiss();
|
||||
this.showSpinner();
|
||||
const result = await this.onHost();
|
||||
this.hideSpinner();
|
||||
|
||||
if (result) {
|
||||
navigator.clipboard.writeText(this.connection?.id ?? '')
|
||||
.then(() => {
|
||||
this.api?.openToast(t('plugins.music-together.toast.id-copied'));
|
||||
hostPopup.showAtAnchor(setting);
|
||||
}).catch(() => {
|
||||
this.api?.openToast(t('plugins.music-together.toast.id-copy-failed'));
|
||||
hostPopup.showAtAnchor(setting);
|
||||
});
|
||||
} else {
|
||||
this.api?.openToast(t('plugins.music-together.toast.host-failed'));
|
||||
}
|
||||
}
|
||||
|
||||
if (id === 'music-together-join') {
|
||||
settingPopup.dismiss();
|
||||
this.showSpinner();
|
||||
const result = await this.onJoin();
|
||||
this.hideSpinner();
|
||||
|
||||
if (result) {
|
||||
this.api?.openToast(t('plugins.music-together.toast.joined'));
|
||||
guestPopup.showAtAnchor(setting);
|
||||
} else {
|
||||
this.api?.openToast(t('plugins.music-together.toast.join-failed'));
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
this.popups = {
|
||||
host: hostPopup,
|
||||
guest: guestPopup,
|
||||
setting: settingPopup
|
||||
};
|
||||
setting.addEventListener('click', () => {
|
||||
let popup = settingPopup;
|
||||
if (this.connection?.mode === 'host') popup = hostPopup;
|
||||
if (this.connection?.mode === 'guest') popup = guestPopup;
|
||||
|
||||
if (popup.isShowing()) popup.dismiss();
|
||||
else popup.showAtAnchor(setting);
|
||||
});
|
||||
|
||||
/* account data getter */
|
||||
this.initMyProfile();
|
||||
},
|
||||
onPlayerApiReady(playerApi) {
|
||||
this.queue = new Queue({
|
||||
owner: {
|
||||
id: this.connection?.id ?? '',
|
||||
...this.me!
|
||||
},
|
||||
getProfile: (id) => this.profiles[id]
|
||||
});
|
||||
this.playerApi = playerApi;
|
||||
|
||||
this.playerApi.addEventListener('onStateChange', this.videoStateChangeListener);
|
||||
document.addEventListener('videodatachange', this.videoChangeListener);
|
||||
},
|
||||
stop() {
|
||||
const dividers = Array.from(document.querySelectorAll('.music-together-divider'));
|
||||
dividers.forEach((divider) => divider.remove());
|
||||
|
||||
this.elements.setting?.remove();
|
||||
this.onStop();
|
||||
if (typeof this.stateInterval === 'number') clearInterval(this.stateInterval);
|
||||
if (this.playerApi) this.playerApi.removeEventListener('onStateChange', this.videoStateChangeListener);
|
||||
if (this.videoChangeListener) document.removeEventListener('videodatachange', this.videoChangeListener);
|
||||
}
|
||||
}
|
||||
});
|
||||
37
src/plugins/music-together/queue/client.ts
Normal file
37
src/plugins/music-together/queue/client.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import { SHA1Hash } from './sha1hash';
|
||||
|
||||
export const extractToken = (cookie = document.cookie) => cookie.match(/SAPISID=([^;]+);/)?.[1] ?? cookie.match(/__Secure-3PAPISID=([^;]+);/)?.[1];
|
||||
|
||||
export const getHash = async (papisid: string, millis = Date.now(), origin: string = 'https://music.youtube.com') =>
|
||||
(await SHA1Hash(`${millis} ${papisid} ${origin}`)).toLowerCase();
|
||||
|
||||
export const getAuthorizationHeader = async (papisid: string, millis = Date.now(), origin: string = 'https://music.youtube.com') => {
|
||||
return `SAPISIDHASH ${millis}_${await getHash(papisid, millis, origin)}`;
|
||||
};
|
||||
|
||||
export const getClient = () => {
|
||||
return {
|
||||
hl: navigator.language.split('-')[0] ?? 'en',
|
||||
gl: navigator.language.split('-')[1] ?? 'US',
|
||||
deviceMake: '',
|
||||
deviceModel: '',
|
||||
userAgent: navigator.userAgent,
|
||||
clientName: 'WEB_REMIX',
|
||||
clientVersion: '1.20231208.05.02',
|
||||
osName: '',
|
||||
osVersion: '',
|
||||
platform: 'DESKTOP',
|
||||
timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||
locationInfo: {
|
||||
locationPermissionAuthorizationStatus: 'LOCATION_PERMISSION_AUTHORIZATION_STATUS_UNSUPPORTED',
|
||||
},
|
||||
musicAppInfo: {
|
||||
pwaInstallabilityStatus: 'PWA_INSTALLABILITY_STATUS_UNKNOWN',
|
||||
webDisplayMode: 'WEB_DISPLAY_MODE_BROWSER',
|
||||
storeDigitalGoodsApiSupportStatus: {
|
||||
playStoreDigitalGoodsApiSupportStatus: 'DIGITAL_GOODS_API_SUPPORT_STATUS_UNSUPPORTED',
|
||||
},
|
||||
},
|
||||
utcOffsetMinutes: -1 * (new Date()).getTimezoneOffset(),
|
||||
};
|
||||
};
|
||||
1
src/plugins/music-together/queue/index.ts
Normal file
1
src/plugins/music-together/queue/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './queue';
|
||||
474
src/plugins/music-together/queue/queue.ts
Normal file
474
src/plugins/music-together/queue/queue.ts
Normal file
@ -0,0 +1,474 @@
|
||||
import { getMusicQueueRenderer } from './song';
|
||||
import { mapQueueItem } from './utils';
|
||||
|
||||
import { ConnectionEventUnion } from '@/plugins/music-together/connection';
|
||||
import { t } from '@/i18n';
|
||||
|
||||
import type { Profile, QueueAPI, VideoData } from '../types';
|
||||
import type { QueueItem } from '@/types/datahost-get-state';
|
||||
|
||||
const getHeaderPayload = (() => {
|
||||
let payload: {
|
||||
items?: QueueItem[] | undefined;
|
||||
title: {
|
||||
runs: {
|
||||
text: string;
|
||||
}[];
|
||||
};
|
||||
subtitle: {
|
||||
runs: {
|
||||
text: string;
|
||||
}[];
|
||||
};
|
||||
buttons: {
|
||||
chipCloudChipRenderer: {
|
||||
style: {
|
||||
styleType: string;
|
||||
};
|
||||
text: {
|
||||
runs: {
|
||||
text: string;
|
||||
}[];
|
||||
};
|
||||
navigationEndpoint: {
|
||||
saveQueueToPlaylistCommand: unknown;
|
||||
};
|
||||
icon: {
|
||||
iconType: string;
|
||||
};
|
||||
accessibilityData: {
|
||||
accessibilityData: {
|
||||
label: string;
|
||||
};
|
||||
};
|
||||
isSelected: boolean;
|
||||
uniqueId: string;
|
||||
};
|
||||
}[];
|
||||
} | null = null;
|
||||
|
||||
return () => {
|
||||
if (!payload) {
|
||||
payload = {
|
||||
title: {
|
||||
runs: [
|
||||
{
|
||||
text: t('plugins.music-together.internal.track-source')
|
||||
}
|
||||
]
|
||||
},
|
||||
subtitle: {
|
||||
runs: [
|
||||
{
|
||||
text: t('plugins.music-together.name')
|
||||
}
|
||||
]
|
||||
},
|
||||
buttons: [
|
||||
{
|
||||
chipCloudChipRenderer: {
|
||||
style: {
|
||||
styleType: 'STYLE_TRANSPARENT'
|
||||
},
|
||||
text: {
|
||||
runs: [
|
||||
{
|
||||
text: t('plugins.music-together.internal.save')
|
||||
}
|
||||
]
|
||||
},
|
||||
navigationEndpoint: {
|
||||
saveQueueToPlaylistCommand: {}
|
||||
},
|
||||
icon: {
|
||||
iconType: 'ADD_TO_PLAYLIST'
|
||||
},
|
||||
accessibilityData: {
|
||||
accessibilityData: {
|
||||
label: t('plugins.music-together.internal.save')
|
||||
}
|
||||
},
|
||||
isSelected: false,
|
||||
uniqueId: t('plugins.music-together.internal.save')
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
return payload;
|
||||
};
|
||||
})();
|
||||
|
||||
export type QueueOptions = {
|
||||
videoList?: VideoData[];
|
||||
owner?: Profile;
|
||||
queue?: HTMLElement & QueueAPI;
|
||||
getProfile: (id: string) => Profile | undefined;
|
||||
}
|
||||
export type QueueEventListener = (event: ConnectionEventUnion) => void;
|
||||
|
||||
export class Queue {
|
||||
private queue: (HTMLElement & QueueAPI);
|
||||
private originalDispatch?: (obj: {
|
||||
type: string;
|
||||
payload?: { items?: QueueItem[] | undefined; };
|
||||
}) => void;
|
||||
private internalDispatch = false;
|
||||
private ignoreFlag = false;
|
||||
private listeners: QueueEventListener[] = [];
|
||||
private owner: Profile | null = null;
|
||||
private getProfile: (id: string) => Profile | undefined;
|
||||
|
||||
constructor(options: QueueOptions) {
|
||||
this.getProfile = options.getProfile;
|
||||
this.queue = options.queue ?? document.querySelector<HTMLElement & QueueAPI>('#queue')!;
|
||||
this.owner = options.owner ?? null;
|
||||
this._videoList = options.videoList ?? [];
|
||||
}
|
||||
|
||||
private _videoList: VideoData[] = [];
|
||||
|
||||
/* utils */
|
||||
get videoList() {
|
||||
return this._videoList;
|
||||
}
|
||||
|
||||
get selectedIndex() {
|
||||
return mapQueueItem((it) => it?.selected, this.queue.store.getState().queue.items).findIndex(Boolean) ?? 0;
|
||||
}
|
||||
|
||||
get rawItems() {
|
||||
return this.queue?.store.getState().queue.items;
|
||||
}
|
||||
|
||||
get flatItems() {
|
||||
return mapQueueItem((it) => it, this.rawItems);
|
||||
}
|
||||
|
||||
setOwner(owner: Profile) {
|
||||
this.owner = owner;
|
||||
}
|
||||
|
||||
/* public */
|
||||
async setVideoList(videoList: VideoData[], sync = true) {
|
||||
this._videoList = videoList;
|
||||
|
||||
if (sync) await this.syncVideo();
|
||||
}
|
||||
|
||||
async addVideos(videos: VideoData[], index?: number) {
|
||||
const response = await getMusicQueueRenderer(videos.map((it) => it.videoId));
|
||||
if (!response) return false;
|
||||
|
||||
const items = response.queueDatas.map((it) => it?.content).filter(Boolean);
|
||||
if (!items) return false;
|
||||
|
||||
this.internalDispatch = true;
|
||||
this._videoList.push(...videos);
|
||||
this.queue?.dispatch({
|
||||
type: 'ADD_ITEMS',
|
||||
payload: {
|
||||
nextQueueItemId: this.queue.store.getState().queue.nextQueueItemId,
|
||||
index: index ?? this.queue.store.getState().queue.items.length ?? 0,
|
||||
items,
|
||||
shuffleEnabled: false,
|
||||
shouldAssignIds: true
|
||||
}
|
||||
});
|
||||
this.internalDispatch = false;
|
||||
setTimeout(() => {
|
||||
this.initQueue();
|
||||
this.syncQueueOwner();
|
||||
}, 0);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
removeVideo(index: number) {
|
||||
this.internalDispatch = true;
|
||||
this._videoList.splice(index, 1);
|
||||
this.queue?.dispatch({
|
||||
type: 'REMOVE_ITEM',
|
||||
payload: index
|
||||
});
|
||||
this.internalDispatch = false;
|
||||
setTimeout(() => {
|
||||
this.initQueue();
|
||||
this.syncQueueOwner();
|
||||
}, 0);
|
||||
}
|
||||
|
||||
setIndex(index: number) {
|
||||
this.internalDispatch = true;
|
||||
this.queue?.dispatch({
|
||||
type: 'SET_INDEX',
|
||||
payload: index
|
||||
});
|
||||
this.internalDispatch = false;
|
||||
}
|
||||
|
||||
moveItem(fromIndex: number, toIndex: number) {
|
||||
this.internalDispatch = true;
|
||||
const data = this._videoList.splice(fromIndex, 1)[0];
|
||||
this._videoList.splice(toIndex, 0, data);
|
||||
this.queue?.dispatch({
|
||||
type: 'MOVE_ITEM',
|
||||
payload: {
|
||||
fromIndex,
|
||||
toIndex
|
||||
}
|
||||
});
|
||||
this.internalDispatch = false;
|
||||
setTimeout(() => {
|
||||
this.initQueue();
|
||||
this.syncQueueOwner();
|
||||
}, 0);
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.internalDispatch = true;
|
||||
this._videoList = [];
|
||||
this.queue?.dispatch({
|
||||
type: 'CLEAR'
|
||||
});
|
||||
this.internalDispatch = false;
|
||||
}
|
||||
|
||||
on(listener: QueueEventListener) {
|
||||
this.listeners.push(listener);
|
||||
}
|
||||
|
||||
off(listener: QueueEventListener) {
|
||||
this.listeners = this.listeners.filter((it) => it !== listener);
|
||||
}
|
||||
|
||||
rollbackInjection() {
|
||||
if (!this.queue) {
|
||||
console.error('Queue is not initialized!');
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.originalDispatch) this.queue.store.dispatch = this.originalDispatch;
|
||||
}
|
||||
|
||||
injection() {
|
||||
if (!this.queue) {
|
||||
console.error('Queue is not initialized!');
|
||||
return;
|
||||
}
|
||||
|
||||
this.originalDispatch = this.queue.store.dispatch;
|
||||
this.queue.store.dispatch = (event) => {
|
||||
if (!this.queue || !this.owner) {
|
||||
console.error('Queue is not initialized!');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.internalDispatch) {
|
||||
if (event.type === 'CLEAR') {
|
||||
this.ignoreFlag = true;
|
||||
}
|
||||
if (event.type === 'ADD_ITEMS') {
|
||||
if (this.ignoreFlag) {
|
||||
this.ignoreFlag = false;
|
||||
const videoList = mapQueueItem((it) => ({
|
||||
videoId: it!.videoId,
|
||||
ownerId: this.owner!.id
|
||||
} satisfies VideoData), event.payload!.items!);
|
||||
const index = this._videoList.length + videoList.length - 1;
|
||||
|
||||
if (videoList.length > 0) {
|
||||
this.broadcast({ // play
|
||||
type: 'ADD_SONGS',
|
||||
payload: {
|
||||
videoList
|
||||
},
|
||||
after: [
|
||||
{
|
||||
type: 'SYNC_PROGRESS',
|
||||
payload: {
|
||||
index
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
} else if ((event.payload as {
|
||||
items: unknown[];
|
||||
}).items.length === 1) {
|
||||
this.broadcast({ // add playlist
|
||||
type: 'ADD_SONGS',
|
||||
payload: {
|
||||
// index: (event.payload as any).index,
|
||||
videoList: mapQueueItem((it) => ({
|
||||
videoId: it!.videoId,
|
||||
ownerId: this.owner!.id
|
||||
} satisfies VideoData), event.payload!.items!)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.type === 'MOVE_ITEM') {
|
||||
this.broadcast({
|
||||
type: 'MOVE_SONG',
|
||||
payload: {
|
||||
fromIndex: (event.payload as {
|
||||
fromIndex: number;
|
||||
}).fromIndex,
|
||||
toIndex: (event.payload as {
|
||||
toIndex: number;
|
||||
}).toIndex
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (event.type === 'REMOVE_ITEM') {
|
||||
this.broadcast({
|
||||
type: 'REMOVE_SONG',
|
||||
payload: {
|
||||
index: event.payload as number
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (event.type === 'SET_INDEX') {
|
||||
this.broadcast({
|
||||
type: 'SYNC_PROGRESS',
|
||||
payload: {
|
||||
index: event.payload as number
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.type === 'SET_HEADER') event.payload = getHeaderPayload();
|
||||
if (event.type === 'ADD_STEERING_CHIPS') {
|
||||
event.type = 'CLEAR_STEERING_CHIPS';
|
||||
event.payload = undefined;
|
||||
}
|
||||
if (event.type === 'SET_PLAYER_UI_STATE') {
|
||||
if (event.payload as string === 'INACTIVE' && this.videoList.length > 0) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (event.type === 'HAS_SHOWN_AUTOPLAY') return;
|
||||
if (event.type === 'ADD_AUTOMIX_ITEMS') return;
|
||||
}
|
||||
|
||||
const fakeContext = {
|
||||
...this.queue,
|
||||
store: {
|
||||
...this.queue.store,
|
||||
dispatch: this.originalDispatch
|
||||
}
|
||||
};
|
||||
this.originalDispatch?.call(fakeContext, event);
|
||||
};
|
||||
}
|
||||
|
||||
/* sync */
|
||||
initQueue() {
|
||||
if (!this.queue) return;
|
||||
|
||||
this.internalDispatch = true;
|
||||
this.queue.dispatch({
|
||||
type: 'HAS_SHOWN_AUTOPLAY',
|
||||
payload: false
|
||||
});
|
||||
this.queue.dispatch({
|
||||
type: 'SET_HEADER',
|
||||
payload: getHeaderPayload(),
|
||||
});
|
||||
this.queue.dispatch({
|
||||
type: 'CLEAR_STEERING_CHIPS'
|
||||
});
|
||||
this.internalDispatch = false;
|
||||
}
|
||||
|
||||
async syncVideo() {
|
||||
const response = await getMusicQueueRenderer(this._videoList.map((it) => it.videoId));
|
||||
if (!response) return false;
|
||||
|
||||
const items = response.queueDatas.map((it) => it.content);
|
||||
|
||||
this.internalDispatch = true;
|
||||
this.queue?.dispatch({
|
||||
type: 'UPDATE_ITEMS',
|
||||
payload: {
|
||||
items: items,
|
||||
nextQueueItemId: this.queue.store.getState().queue.nextQueueItemId,
|
||||
shouldAssignIds: true,
|
||||
currentIndex: -1
|
||||
}
|
||||
});
|
||||
this.internalDispatch = false;
|
||||
setTimeout(() => {
|
||||
this.initQueue();
|
||||
this.syncQueueOwner();
|
||||
}, 0);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
syncQueueOwner() {
|
||||
const allQueue = document.querySelectorAll('#queue');
|
||||
|
||||
allQueue.forEach((queue) => {
|
||||
const list = Array.from(queue?.querySelectorAll<HTMLElement>('ytmusic-player-queue-item') ?? []);
|
||||
|
||||
list.forEach((item, index: number | undefined) => {
|
||||
if (typeof index !== 'number') return;
|
||||
|
||||
const id = this._videoList[index]?.ownerId;
|
||||
const data = this.getProfile(id);
|
||||
|
||||
const profile = item.querySelector<HTMLImageElement>('.music-together-owner') ?? document.createElement('img');
|
||||
profile.classList.add('music-together-owner');
|
||||
profile.dataset.id = id;
|
||||
profile.dataset.index = index.toString();
|
||||
|
||||
const name = item.querySelector<HTMLElement>('.music-together-name') ?? document.createElement('div');
|
||||
name.classList.add('music-together-name');
|
||||
name.textContent = data?.name ?? t('plugins.music-together.internal.unknown-user');
|
||||
|
||||
if (data) {
|
||||
profile.dataset.thumbnail = data.thumbnail ?? '';
|
||||
profile.dataset.name = data.name ?? '';
|
||||
profile.dataset.handleId = data.handleId ?? '';
|
||||
profile.dataset.id = data.id ?? '';
|
||||
|
||||
profile.src = data.thumbnail ?? '';
|
||||
profile.title = data.name ?? '';
|
||||
profile.alt = data.handleId ?? '';
|
||||
}
|
||||
|
||||
if (!profile.isConnected) item.append(profile);
|
||||
if (!name.isConnected) item.append(name);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
removeQueueOwner() {
|
||||
const allQueue = document.querySelectorAll('#queue');
|
||||
|
||||
allQueue.forEach((queue) => {
|
||||
const list = Array.from(queue?.querySelectorAll<HTMLElement>('ytmusic-player-queue-item') ?? []);
|
||||
|
||||
list.forEach((item) => {
|
||||
const profile = item.querySelector<HTMLImageElement>('.music-together-owner');
|
||||
const name = item.querySelector<HTMLElement>('.music-together-name');
|
||||
profile?.remove();
|
||||
name?.remove();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/* private */
|
||||
private broadcast(event: ConnectionEventUnion) {
|
||||
this.listeners.forEach((listener) => listener(event));
|
||||
}
|
||||
}
|
||||
7
src/plugins/music-together/queue/sha1hash.ts
Normal file
7
src/plugins/music-together/queue/sha1hash.ts
Normal file
@ -0,0 +1,7 @@
|
||||
export const SHA1Hash = async (str: string) => {
|
||||
const enc = new TextEncoder();
|
||||
const hash = await crypto.subtle.digest('SHA-1', enc.encode(str));
|
||||
return Array.from(new Uint8Array(hash))
|
||||
.map((v) => v.toString(16).padStart(2, '0'))
|
||||
.join('');
|
||||
};
|
||||
48
src/plugins/music-together/queue/song.ts
Normal file
48
src/plugins/music-together/queue/song.ts
Normal file
@ -0,0 +1,48 @@
|
||||
import { extractToken, getAuthorizationHeader, getClient } from './client';
|
||||
|
||||
type QueueRendererResponse = {
|
||||
queueDatas: {
|
||||
content: unknown;
|
||||
}[];
|
||||
responseContext: unknown;
|
||||
trackingParams: string;
|
||||
};
|
||||
|
||||
export const getMusicQueueRenderer = async (videoIds: string[]): Promise<QueueRendererResponse | null> => {
|
||||
const token = extractToken();
|
||||
if (!token) return null;
|
||||
|
||||
const response = await fetch(
|
||||
'https://music.youtube.com/youtubei/v1/music/get_queue?key=AIzaSyC9XL3ZjWddXya6X74dJoCTL-WEYFDNX30&prettyPrint=false',
|
||||
{
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({
|
||||
context: {
|
||||
client: getClient(),
|
||||
request: {
|
||||
useSsl: true,
|
||||
internalExperimentFlags: [],
|
||||
consistencyTokenJars: [],
|
||||
},
|
||||
user: {
|
||||
lockedSafetyMode: false,
|
||||
},
|
||||
},
|
||||
videoIds,
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Origin': 'https://music.youtube.com',
|
||||
'Authorization': await getAuthorizationHeader(token),
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const text = await response.text();
|
||||
try {
|
||||
return JSON.parse(text) as QueueRendererResponse;
|
||||
} catch {}
|
||||
|
||||
return null;
|
||||
};
|
||||
21
src/plugins/music-together/queue/utils.ts
Normal file
21
src/plugins/music-together/queue/utils.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import {
|
||||
ItemPlaylistPanelVideoRenderer,
|
||||
PlaylistPanelVideoWrapperRenderer,
|
||||
QueueItem
|
||||
} from '@/types/datahost-get-state';
|
||||
|
||||
export const mapQueueItem = <T>(map: (item?: ItemPlaylistPanelVideoRenderer) => T, array: QueueItem[]): T[] => array
|
||||
.map((item) => {
|
||||
if ('playlistPanelVideoWrapperRenderer' in item) {
|
||||
const keys = Object.keys(item.playlistPanelVideoWrapperRenderer!.primaryRenderer) as (keyof PlaylistPanelVideoWrapperRenderer['primaryRenderer'])[];
|
||||
return item.playlistPanelVideoWrapperRenderer!.primaryRenderer[keys[0]];
|
||||
}
|
||||
if ('playlistPanelVideoRenderer' in item) {
|
||||
return item.playlistPanelVideoRenderer;
|
||||
}
|
||||
|
||||
console.error('Music Together: Unknown item', item);
|
||||
return undefined;
|
||||
})
|
||||
.map(map);
|
||||
|
||||
160
src/plugins/music-together/style.css
Normal file
160
src/plugins/music-together/style.css
Normal file
@ -0,0 +1,160 @@
|
||||
.music-together-button {
|
||||
display: inline-flex;
|
||||
|
||||
cursor: pointer;
|
||||
margin-left: 8px;
|
||||
|
||||
& svg {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
fill: rgba(255, 255, 255, .5);
|
||||
}
|
||||
|
||||
&:hover svg:hover {
|
||||
fill: #fff;
|
||||
}
|
||||
}
|
||||
#right-content > .music-together-divider {
|
||||
width: 1px;
|
||||
height: 26px;
|
||||
margin-left: 16px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.music-together-divider {
|
||||
background-color: rgba(255, 255, 255, .15);
|
||||
}
|
||||
.music-together-divider.horizontal {
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
}
|
||||
.music-together-divider.vertical {
|
||||
width: 1px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.music-together-tool {
|
||||
position: absolute;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
opacity: 0;
|
||||
translate: 50%;
|
||||
pointer-events: none;
|
||||
|
||||
transition: all 0.225s ease-out;
|
||||
|
||||
&.open {
|
||||
position: unset;
|
||||
opacity: 1;
|
||||
translate: 0;
|
||||
pointer-events: all;
|
||||
}
|
||||
}
|
||||
|
||||
.music-together-spinner {
|
||||
|
||||
}
|
||||
|
||||
.music-together-popup {
|
||||
position: fixed;
|
||||
|
||||
z-index: 1000;
|
||||
}
|
||||
.music-together-popup-container {
|
||||
border-radius: 10px !important;
|
||||
}
|
||||
|
||||
.music-together-item {
|
||||
display: flex;
|
||||
height: 48px;
|
||||
align-items: center;
|
||||
padding: 0 8px;
|
||||
--iron-icon-fill-color: #fff;
|
||||
|
||||
&:not([is-disabled]) {
|
||||
cursor: pointer;
|
||||
}
|
||||
&:hover {
|
||||
background-color: var(--ytmusic-menu-item-hover-background-color, rgba(255,255,255,0.05));
|
||||
}
|
||||
}
|
||||
|
||||
.music-together-status {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
|
||||
padding: 16px;
|
||||
}
|
||||
.music-together-profile {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.music-together-profile.big {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.music-together-status-container {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
gap: 16px;
|
||||
}
|
||||
.music-together-status-item {
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 4px;
|
||||
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
}
|
||||
.music-together-user-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
|
||||
gap: 8px;
|
||||
padding-top: 16px;
|
||||
font-size: 14px;
|
||||
}
|
||||
.music-together-empty {
|
||||
width: 100%;
|
||||
|
||||
font-size: 14px;
|
||||
color: rgba(255, 255, 255, .5);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.music-together-owner {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
flex-shrink: 0;
|
||||
border-radius: 50%;
|
||||
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.music-together-name {
|
||||
display: none;
|
||||
|
||||
color: #fff;
|
||||
font-size: 14px;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
ytmusic-player-queue-item:hover .music-together-name {
|
||||
display: unset;
|
||||
}
|
||||
8
src/plugins/music-together/templates/item.html
Normal file
8
src/plugins/music-together/templates/item.html
Normal file
@ -0,0 +1,8 @@
|
||||
<div class="style-scope music-together-item">
|
||||
<div class="icon style-scope ytmusic-menu-service-item-renderer">
|
||||
<!-- icon -->
|
||||
</div>
|
||||
<div class="text style-scope ytmusic-menu-service-item-renderer">
|
||||
<!-- text -->
|
||||
</div>
|
||||
</div>
|
||||
5
src/plugins/music-together/templates/popup.html
Normal file
5
src/plugins/music-together/templates/popup.html
Normal file
@ -0,0 +1,5 @@
|
||||
<div class="music-together-popup">
|
||||
<tp-yt-paper-listbox class="style-scope ytmusic-menu-popup-renderer music-together-popup-container">
|
||||
|
||||
</tp-yt-paper-listbox>
|
||||
</div>
|
||||
7
src/plugins/music-together/templates/setting.html
Normal file
7
src/plugins/music-together/templates/setting.html
Normal file
@ -0,0 +1,7 @@
|
||||
<div id="music-together-setting-button" class="music-together-button style-scope ytmusic-nav-bar">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24">
|
||||
<path d="M0-240v-63q0-43 44-70t116-27q13 0 25 .5t23 2.5q-14 21-21 44t-7 48v65H0Zm240 0v-65q0-32 17.5-58.5T307-410q32-20 76.5-30t96.5-10q53 0 97.5 10t76.5 30q32 20 49 46.5t17 58.5v65H240Zm540 0v-65q0-26-6.5-49T754-397q11-2 22.5-2.5t23.5-.5q72 0 116 26.5t44 70.5v63H780Zm-455-80h311q-10-20-55.5-35T480-370q-55 0-100.5 15T325-320ZM160-440q-33 0-56.5-23.5T80-520q0-34 23.5-57t56.5-23q34 0 57 23t23 57q0 33-23 56.5T160-440Zm640 0q-33 0-56.5-23.5T720-520q0-34 23.5-57t56.5-23q34 0 57 23t23 57q0 33-23 56.5T800-440Zm-320-40q-50 0-85-35t-35-85q0-51 35-85.5t85-34.5q51 0 85.5 34.5T600-600q0 50-34.5 85T480-480Zm0-80q17 0 28.5-11.5T520-600q0-17-11.5-28.5T480-640q-17 0-28.5 11.5T440-600q0 17 11.5 28.5T480-560Zm1 240Zm-1-280Z"/>
|
||||
</svg>
|
||||
<tp-yt-paper-spinner-lite id="music-together-host-spinner" hidden class="loading-indicator style-scope music-together-spinner"></tp-yt-paper-spinner-lite>
|
||||
</div>
|
||||
<div class="music-together-divider"></div>
|
||||
23
src/plugins/music-together/templates/status.html
Normal file
23
src/plugins/music-together/templates/status.html
Normal file
@ -0,0 +1,23 @@
|
||||
<div class="music-together-status">
|
||||
<div class="music-together-status-container">
|
||||
<img class="music-together-profile big" alt="Profile Image">
|
||||
<div class="music-together-status-item">
|
||||
<ytmd-trans key="plugins.music-together.name"></ytmd-trans>
|
||||
<span id="music-together-status-label">
|
||||
<ytmd-trans key="plugins.music-together.menu.status.disconnected"></ytmd-trans>
|
||||
</span>
|
||||
<marquee id="music-together-permission-label">
|
||||
<ytmd-trans key="plugins.music-together.menu.permission.playlist" style="color: rgba(255, 255, 255, 0.75)"></ytmd-trans>
|
||||
</marquee>
|
||||
</div>
|
||||
</div>
|
||||
<div class="music-together-divider horizontal" style="margin: 16px 0;"></div>
|
||||
<div class="music-together-status-item">
|
||||
<ytmd-trans key="plugins.music-together.menu.connected-users"></ytmd-trans>
|
||||
</div>
|
||||
<div class="music-together-user-container">
|
||||
<span class="music-together-empty">
|
||||
<ytmd-trans key="plugins.music-together.menu.empty-user"></ytmd-trans>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
56
src/plugins/music-together/types.ts
Normal file
56
src/plugins/music-together/types.ts
Normal file
@ -0,0 +1,56 @@
|
||||
import { YoutubePlayer } from '@/types/youtube-player';
|
||||
import { GetState, QueueItem } from '@/types/datahost-get-state';
|
||||
|
||||
type StoreState = GetState;
|
||||
type Store = {
|
||||
dispatch: (obj: {
|
||||
type: string;
|
||||
payload?: {
|
||||
items?: QueueItem[];
|
||||
};
|
||||
}) => void;
|
||||
|
||||
getState: () => StoreState;
|
||||
replaceReducer: (param1: unknown) => unknown;
|
||||
subscribe: (callback: () => void) => unknown;
|
||||
}
|
||||
export type QueueAPI = {
|
||||
dispatch(obj: {
|
||||
type: string;
|
||||
payload?: unknown;
|
||||
}): void;
|
||||
getItems(): unknown[];
|
||||
store: Store;
|
||||
continuation?: string;
|
||||
autoPlaying?: boolean;
|
||||
};
|
||||
export type AppAPI = {
|
||||
queue_: QueueAPI;
|
||||
playerApi_: YoutubePlayer;
|
||||
openToast: (message: string) => void;
|
||||
|
||||
// TODO: Add more
|
||||
};
|
||||
|
||||
export type Profile = {
|
||||
id: string;
|
||||
handleId: string;
|
||||
name: string;
|
||||
thumbnail: string;
|
||||
};
|
||||
export type VideoData = {
|
||||
videoId: string;
|
||||
ownerId: string;
|
||||
};
|
||||
export type Permission = 'host-only' | 'playlist' | 'all';
|
||||
|
||||
export const getDefaultProfile = (connectionID: string, id: string = Date.now().toString()): Profile => {
|
||||
const name = `Guest ${id.slice(0, 4)}`;
|
||||
|
||||
return {
|
||||
id: connectionID,
|
||||
handleId: `#music-together:${id}`,
|
||||
name,
|
||||
thumbnail: `https://ui-avatars.com/api/?name=${name}&background=random`
|
||||
};
|
||||
};
|
||||
43
src/plugins/music-together/ui/guest.ts
Normal file
43
src/plugins/music-together/ui/guest.ts
Normal file
@ -0,0 +1,43 @@
|
||||
import { ElementFromHtml } from '@/plugins/utils/renderer';
|
||||
|
||||
import { t } from '@/i18n';
|
||||
|
||||
import { Popup } from '../element';
|
||||
import { createStatus } from '../ui/status';
|
||||
|
||||
import IconOff from '../icons/off.svg?raw';
|
||||
|
||||
|
||||
export type GuestPopupProps = {
|
||||
onItemClick: (id: string) => void;
|
||||
};
|
||||
export const createGuestPopup = (props: GuestPopupProps) => {
|
||||
const status = createStatus();
|
||||
status.setStatus('guest');
|
||||
|
||||
const result = Popup({
|
||||
data: [
|
||||
{
|
||||
type: 'custom',
|
||||
element: status.element,
|
||||
},
|
||||
{
|
||||
type: 'divider',
|
||||
},
|
||||
{
|
||||
type: 'item',
|
||||
id: 'music-together-disconnect',
|
||||
icon: ElementFromHtml(IconOff),
|
||||
text: t('plugins.music-together.menu.disconnect'),
|
||||
onClick: () => props.onItemClick('music-together-disconnect'),
|
||||
},
|
||||
],
|
||||
anchorAt: 'bottom-right',
|
||||
popupAt: 'top-right'
|
||||
});
|
||||
|
||||
return {
|
||||
...status,
|
||||
...result,
|
||||
};
|
||||
};
|
||||
60
src/plugins/music-together/ui/host.ts
Normal file
60
src/plugins/music-together/ui/host.ts
Normal file
@ -0,0 +1,60 @@
|
||||
import { t } from '@/i18n';
|
||||
import { ElementFromHtml } from '@/plugins/utils/renderer';
|
||||
|
||||
import { Popup } from '../element';
|
||||
import { createStatus } from '../ui/status';
|
||||
|
||||
import IconKey from '../icons/key.svg?raw';
|
||||
import IconOff from '../icons/off.svg?raw';
|
||||
import IconTune from '../icons/tune.svg?raw';
|
||||
|
||||
export type HostPopupProps = {
|
||||
onItemClick: (id: string) => void;
|
||||
};
|
||||
export const createHostPopup = (props: HostPopupProps) => {
|
||||
const status = createStatus();
|
||||
status.setStatus('host');
|
||||
|
||||
const result = Popup({
|
||||
data: [
|
||||
{
|
||||
type: 'custom',
|
||||
element: status.element,
|
||||
},
|
||||
{
|
||||
type: 'divider'
|
||||
},
|
||||
{
|
||||
id: 'music-together-copy-id',
|
||||
type: 'item',
|
||||
icon: ElementFromHtml(IconKey),
|
||||
text: t('plugins.music-together.menu.click-to-copy-id'),
|
||||
onClick: () => props.onItemClick('music-together-copy-id'),
|
||||
},
|
||||
{
|
||||
id: 'music-together-permission',
|
||||
type: 'item',
|
||||
icon: ElementFromHtml(IconTune),
|
||||
text: t('plugins.music-together.menu.set-permission', { permission: t('plugins.music-together.menu.permission.host-only') }),
|
||||
onClick: () => props.onItemClick('music-together-permission'),
|
||||
},
|
||||
{
|
||||
type: 'divider',
|
||||
},
|
||||
{
|
||||
type: 'item',
|
||||
id: 'music-together-close',
|
||||
icon: ElementFromHtml(IconOff),
|
||||
text: t('plugins.music-together.menu.close'),
|
||||
onClick: () => props.onItemClick('music-together-close'),
|
||||
},
|
||||
],
|
||||
anchorAt: 'bottom-right',
|
||||
popupAt: 'top-right',
|
||||
});
|
||||
|
||||
return {
|
||||
...status,
|
||||
...result,
|
||||
};
|
||||
};
|
||||
49
src/plugins/music-together/ui/setting.ts
Normal file
49
src/plugins/music-together/ui/setting.ts
Normal file
@ -0,0 +1,49 @@
|
||||
import { Popup } from '@/plugins/music-together/element';
|
||||
import { ElementFromHtml } from '@/plugins/utils/renderer';
|
||||
|
||||
import { createStatus } from './status';
|
||||
|
||||
import { t } from '@/i18n';
|
||||
|
||||
import IconMusicCast from '../icons/music-cast.svg?raw';
|
||||
import IconConnect from '../icons/connect.svg?raw';
|
||||
|
||||
export type SettingPopupProps = {
|
||||
onItemClick: (id: string) => void;
|
||||
};
|
||||
export const createSettingPopup = (props: SettingPopupProps) => {
|
||||
const status = createStatus();
|
||||
status.setStatus('disconnected');
|
||||
|
||||
const result = Popup({
|
||||
data: [
|
||||
{
|
||||
type: 'custom',
|
||||
element: status.element,
|
||||
},
|
||||
{
|
||||
type: 'divider',
|
||||
},
|
||||
{
|
||||
id: 'music-together-host',
|
||||
type: 'item',
|
||||
icon: ElementFromHtml(IconMusicCast),
|
||||
text: t('plugins.music-together.menu.host'),
|
||||
onClick: () => props.onItemClick('music-together-host'),
|
||||
},
|
||||
{
|
||||
type: 'item',
|
||||
icon: ElementFromHtml(IconConnect),
|
||||
text: t('plugins.music-together.menu.join'),
|
||||
onClick: () => props.onItemClick('music-together-join'),
|
||||
},
|
||||
],
|
||||
anchorAt: 'bottom-right',
|
||||
popupAt: 'top-right'
|
||||
});
|
||||
|
||||
return {
|
||||
...status,
|
||||
...result,
|
||||
};
|
||||
};
|
||||
84
src/plugins/music-together/ui/status.ts
Normal file
84
src/plugins/music-together/ui/status.ts
Normal file
@ -0,0 +1,84 @@
|
||||
import { ElementFromHtml } from '@/plugins/utils/renderer';
|
||||
import { t } from '@/i18n';
|
||||
|
||||
import statusHTML from '../templates/status.html?raw';
|
||||
|
||||
import type { Permission, Profile } from '../types';
|
||||
|
||||
export const createStatus = () => {
|
||||
const element = ElementFromHtml(statusHTML);
|
||||
const icon = document.querySelector<HTMLImageElement>('ytmusic-settings-button > tp-yt-paper-icon-button > tp-yt-iron-icon#icon img');
|
||||
|
||||
const profile = element.querySelector<HTMLImageElement>('.music-together-profile')!;
|
||||
const statusLabel = element.querySelector<HTMLSpanElement>('#music-together-status-label')!;
|
||||
const permissionLabel = element.querySelector<HTMLMarqueeElement>('#music-together-permission-label')!;
|
||||
|
||||
profile.src = icon?.src ?? '';
|
||||
|
||||
const setStatus = (status: 'disconnected' | 'host' | 'guest') => {
|
||||
if (status === 'disconnected') {
|
||||
statusLabel.textContent = t('plugins.music-together.menu.status.disconnected');
|
||||
statusLabel.style.color = 'rgba(255, 255, 255, 0.5)';
|
||||
}
|
||||
|
||||
if (status === 'host') {
|
||||
statusLabel.textContent = t('plugins.music-together.menu.status.host');
|
||||
statusLabel.style.color = 'rgba(255, 0, 0, 1)';
|
||||
}
|
||||
|
||||
if (status === 'guest') {
|
||||
statusLabel.textContent = t('plugins.music-together.menu.status.guest');
|
||||
statusLabel.style.color = 'rgba(255, 255, 255, 1)';
|
||||
}
|
||||
};
|
||||
|
||||
const setPermission = (permission: Permission) => {
|
||||
if (permission === 'host-only') {
|
||||
permissionLabel.textContent = t('plugins.music-together.menu.permission.host-only');
|
||||
permissionLabel.style.color = 'rgba(255, 255, 255, 0.5)';
|
||||
}
|
||||
|
||||
if (permission === 'playlist') {
|
||||
permissionLabel.textContent = t('plugins.music-together.menu.permission.playlist');
|
||||
permissionLabel.style.color = 'rgba(255, 255, 255, 0.75)';
|
||||
}
|
||||
|
||||
if (permission === 'all') {
|
||||
permissionLabel.textContent = t('plugins.music-together.menu.permission.all');
|
||||
permissionLabel.style.color = 'rgba(255, 255, 255, 1)';
|
||||
}
|
||||
};
|
||||
|
||||
const setProfile = (src: string) => {
|
||||
profile.src = src;
|
||||
};
|
||||
|
||||
const setUsers = (users: Profile[]) => {
|
||||
const container = element.querySelector<HTMLDivElement>('.music-together-user-container')!;
|
||||
const empty = element.querySelector<HTMLElement>('.music-together-empty')!;
|
||||
for (const child of Array.from(container.children)) {
|
||||
if (child !== empty) child.remove();
|
||||
}
|
||||
|
||||
if (users.length === 0) empty.style.display = 'block';
|
||||
else empty.style.display = 'none';
|
||||
|
||||
for (const user of users) {
|
||||
const img = document.createElement('img');
|
||||
img.classList.add('music-together-profile');
|
||||
img.src = user.thumbnail ?? '';
|
||||
img.title = user.name;
|
||||
img.alt = `${user.name} (${user.id})`;
|
||||
|
||||
container.append(img);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
element,
|
||||
setStatus,
|
||||
setUsers,
|
||||
setProfile,
|
||||
setPermission,
|
||||
};
|
||||
};
|
||||
@ -18,6 +18,8 @@ let useNativePiP = false;
|
||||
let menu: Element | null = null;
|
||||
const pipButton = ElementFromHtml(pipHTML);
|
||||
|
||||
let doneFirstLoad = false;
|
||||
|
||||
// Will also clone
|
||||
function replaceButton(query: string, button: Element) {
|
||||
const svg = button.querySelector('#icon svg')?.cloneNode(true);
|
||||
@ -61,11 +63,15 @@ const observer = new MutationObserver(() => {
|
||||
const menuUrl = $<HTMLAnchorElement>(
|
||||
'tp-yt-paper-listbox [tabindex="0"] #navigation-endpoint',
|
||||
)?.href;
|
||||
if (!menuUrl?.includes('watch?')) {
|
||||
if (!menuUrl?.includes('watch?') && doneFirstLoad) {
|
||||
return;
|
||||
}
|
||||
|
||||
menu.prepend(pipButton);
|
||||
|
||||
if (!doneFirstLoad) {
|
||||
setTimeout(() => (doneFirstLoad ||= true), 500);
|
||||
}
|
||||
});
|
||||
|
||||
const togglePictureInPicture = async () => {
|
||||
|
||||
@ -12,7 +12,7 @@
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
class="icon menu-icon style-scope ytmusic-menu-navigation-item-renderer"
|
||||
class="icon ytmd-menu-item style-scope ytmusic-menu-navigation-item-renderer"
|
||||
>
|
||||
<svg
|
||||
id="Layer_1"
|
||||
|
||||
@ -40,7 +40,6 @@ const audioCanPlayListener = (e: CustomEvent<Compressor>) => {
|
||||
const fftBins = new Float32Array(analyser.frequencyBinCount);
|
||||
|
||||
sourceNode.connect(analyser);
|
||||
analyser.connect(audioContext.destination);
|
||||
|
||||
const looper = () => {
|
||||
setTimeout(() => {
|
||||
|
||||
@ -1,4 +1,8 @@
|
||||
// Creates a DOM element from an HTML string
|
||||
/**
|
||||
* Creates a DOM element from an HTML string
|
||||
* @param html The HTML string
|
||||
* @returns The DOM element
|
||||
*/
|
||||
export const ElementFromHtml = (html: string): HTMLElement => {
|
||||
const template = document.createElement('template');
|
||||
html = html.trim(); // Never return a text node of whitespace as the result
|
||||
@ -6,3 +10,14 @@ export const ElementFromHtml = (html: string): HTMLElement => {
|
||||
|
||||
return template.content.firstElementChild as HTMLElement;
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a DOM element from a src string
|
||||
* @param src The source of the image
|
||||
* @returns The image element
|
||||
*/
|
||||
export const ImageElementFromSrc = (src: string): HTMLImageElement => {
|
||||
const image = document.createElement('img');
|
||||
image.src = src;
|
||||
return image;
|
||||
};
|
||||
|
||||
@ -127,7 +127,7 @@ export default (api: YoutubePlayer) => {
|
||||
|
||||
const waitingEvent = new Set<string>();
|
||||
// Name = "dataloaded" and abit later "dataupdated"
|
||||
api.addEventListener('videodatachange', (name: string, videoData) => {
|
||||
api.addEventListener('videodatachange', (name, videoData) => {
|
||||
videoEventDispatcher(name, videoData);
|
||||
|
||||
if (name === 'dataupdated' && waitingEvent.has(videoData.videoId)) {
|
||||
|
||||
28
src/tray.ts
28
src/tray.ts
@ -1,10 +1,12 @@
|
||||
import { Menu, nativeImage, Tray } from 'electron';
|
||||
|
||||
import youtubeMusicTrayIcon from '@assets/youtube-music-tray.png?asset&asarUnpack';
|
||||
import defaultTrayIconAsset from '@assets/youtube-music-tray.png?asset&asarUnpack';
|
||||
import pausedTrayIconAsset from '@assets/youtube-music-tray-paused.png?asset&asarUnpack';
|
||||
|
||||
import config from './config';
|
||||
|
||||
import { restart } from './providers/app-controls';
|
||||
import registerCallback from './providers/song-info';
|
||||
import getSongControls from './providers/song-controls';
|
||||
|
||||
import { t } from '@/i18n';
|
||||
@ -46,14 +48,18 @@ export const setUpTray = (app: Electron.App, win: Electron.BrowserWindow) => {
|
||||
|
||||
const { playPause, next, previous } = getSongControls(win);
|
||||
|
||||
const trayIcon = nativeImage.createFromPath(youtubeMusicTrayIcon).resize({
|
||||
const defaultTrayIcon = nativeImage.createFromPath(defaultTrayIconAsset).resize({
|
||||
width: 16,
|
||||
height: 16,
|
||||
});
|
||||
const pausedTrayIcon = nativeImage.createFromPath(pausedTrayIconAsset).resize({
|
||||
width: 16,
|
||||
height: 16,
|
||||
});
|
||||
|
||||
tray = new Tray(trayIcon);
|
||||
tray = new Tray(defaultTrayIcon);
|
||||
|
||||
tray.setToolTip('YouTube Music');
|
||||
tray.setToolTip(t('main.tray.tooltip.default'));
|
||||
|
||||
// MacOS only
|
||||
tray.setIgnoreDoubleClickEvents(true);
|
||||
@ -110,4 +116,18 @@ export const setUpTray = (app: Electron.App, win: Electron.BrowserWindow) => {
|
||||
|
||||
const trayMenu = Menu.buildFromTemplate(template);
|
||||
tray.setContextMenu(trayMenu);
|
||||
|
||||
registerCallback(songInfo => {
|
||||
if (typeof songInfo.isPaused === 'undefined') {
|
||||
tray.setImage(defaultTrayIcon);
|
||||
return;
|
||||
}
|
||||
|
||||
tray.setToolTip(t('main.tray.tooltip.with-song-info', {
|
||||
artist: songInfo.artist,
|
||||
title: songInfo.title,
|
||||
}));
|
||||
|
||||
tray.setImage(songInfo.isPaused ? pausedTrayIcon : defaultTrayIcon);
|
||||
})
|
||||
};
|
||||
|
||||
@ -256,5 +256,7 @@ export type VideoDataChangeValue = Record<string, unknown> & {
|
||||
export interface PlayerAPIEvents {
|
||||
videodatachange: {
|
||||
value: VideoDataChangeValue;
|
||||
} & ({ name: 'dataloaded' } | { name: 'dataupdated ' });
|
||||
name: 'dataloaded' | 'dataupdated';
|
||||
};
|
||||
onStateChange: number;
|
||||
}
|
||||
|
||||
@ -47,6 +47,7 @@ export interface PluginDef<
|
||||
name: () => string;
|
||||
authors?: Author[];
|
||||
description?: () => string;
|
||||
addedVersion?: string;
|
||||
config?: Config;
|
||||
|
||||
menu?: (
|
||||
|
||||
@ -262,7 +262,23 @@ export interface YoutubePlayer {
|
||||
showControls: () => void;
|
||||
hideControls: () => void;
|
||||
cancelPlayback: () => void;
|
||||
getProgressState: <Return>() => Return;
|
||||
getProgressState: () => {
|
||||
airingEnd: number;
|
||||
airingStart: number;
|
||||
allowSeeking: boolean;
|
||||
clipEnd: number;
|
||||
clipStart: number;
|
||||
current: number;
|
||||
displayedStart: number;
|
||||
duration: number;
|
||||
ingestionTime: number;
|
||||
isAtLiveHead: boolean;
|
||||
loaded: number;
|
||||
offset: number;
|
||||
seekableEnd: number;
|
||||
seekableStart: number;
|
||||
viewerLivestreamJoinMediaTime: number;
|
||||
};
|
||||
isInline: () => boolean;
|
||||
setInline: (isInline: boolean) => void;
|
||||
setLoopVideo: (value: boolean) => void;
|
||||
@ -320,6 +336,10 @@ export interface YoutubePlayer {
|
||||
getVolume: () => number;
|
||||
seekTo: (seconds: number) => void;
|
||||
getPlayerMode: <Return>() => Return;
|
||||
/**
|
||||
* 1: playing
|
||||
* 2: paused
|
||||
*/
|
||||
getPlayerState: () => number;
|
||||
getAvailablePlaybackRates: () => number[];
|
||||
getPlaybackQuality: () => string;
|
||||
@ -337,8 +357,8 @@ export interface YoutubePlayer {
|
||||
type: K,
|
||||
listener: (
|
||||
this: Document,
|
||||
name: PlayerAPIEvents[K]['name'],
|
||||
data: PlayerAPIEvents[K]['value'],
|
||||
name: K extends 'videodatachange' ? PlayerAPIEvents[K]['name'] : never,
|
||||
data: K extends 'videodatachange' ? PlayerAPIEvents[K]['value'] : never,
|
||||
) => void,
|
||||
options?: boolean | AddEventListenerOptions | undefined,
|
||||
) => void;
|
||||
@ -346,8 +366,8 @@ export interface YoutubePlayer {
|
||||
type: K,
|
||||
listener: (
|
||||
this: Document,
|
||||
name: PlayerAPIEvents[K]['name'],
|
||||
data: PlayerAPIEvents[K]['value'],
|
||||
name: K extends 'videodatachange' ? PlayerAPIEvents[K]['name'] : never,
|
||||
data: K extends 'videodatachange' ? PlayerAPIEvents[K]['value'] : never,
|
||||
) => void,
|
||||
options?: boolean | EventListenerOptions | undefined,
|
||||
) => void;
|
||||
|
||||
@ -24,10 +24,6 @@ ytmusic-app-layout {
|
||||
--ytmusic-nav-bar-height: 90px;
|
||||
}
|
||||
|
||||
ytmusic-search-box.ytmusic-nav-bar {
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
/* Blocking annoying elements */
|
||||
ytmusic-mealbar-promo-renderer {
|
||||
display: none !important;
|
||||
|
||||
5
src/youtube-music.d.ts
vendored
5
src/youtube-music.d.ts
vendored
@ -15,6 +15,11 @@ declare module '*.svg?inline' {
|
||||
|
||||
export default base64;
|
||||
}
|
||||
declare module '*.svg?raw' {
|
||||
const html: string;
|
||||
|
||||
export default html;
|
||||
}
|
||||
declare module '*.png' {
|
||||
const element: HTMLImageElement;
|
||||
|
||||
|
||||
@ -1,28 +0,0 @@
|
||||
require 'json'
|
||||
require 'open-uri'
|
||||
|
||||
cask "youtube-music" do
|
||||
desc "YouTube Music Desktop App"
|
||||
homepage "https://github.com/th-ch/youtube-music"
|
||||
|
||||
# Fetch the latest release version from GitHub API
|
||||
latest_release = JSON.parse(URI.open("https://api.github.com/repos/th-ch/youtube-music/releases/latest").read)['tag_name']
|
||||
version latest_release
|
||||
|
||||
base_url = "https://github.com/th-ch/youtube-music/releases/download/#{latest_release}/YouTube-Music-#{latest_release.delete_prefix('v')}"
|
||||
file_extension = Hardware::CPU.arm? ? "-arm64.dmg" : ".dmg"
|
||||
url "#{base_url}#{file_extension}"
|
||||
|
||||
# TODO checksum
|
||||
sha256 :no_check
|
||||
|
||||
app "YouTube Music.app"
|
||||
|
||||
postflight do
|
||||
print("Removing quarantine attribute from YouTube Music.app.\n")
|
||||
system "xattr -cr '/Applications/YouTube Music.app'"
|
||||
end
|
||||
|
||||
auto_updates true
|
||||
|
||||
end
|
||||
Reference in New Issue
Block a user