Compare commits

...

193 Commits

Author SHA1 Message Date
34cb79eeaf Bump version to 3.2.2 2024-01-05 23:33:42 +09:00
1c30a07031 chore(deps): update dependency playwright to v1.41.0-alpha-jan-5-2024 2024-01-05 23:30:16 +09:00
5dd5f41ef5 chore(deps): update dependency electron to v29.0.0-alpha.7
Change to electron v29 to use node.js v20.
2024-01-05 23:28:24 +09:00
45a3c11d51 chore(i18n): Translated using Weblate (Korean)
Currently translated at 100.0% (331 of 331 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/ko/
2024-01-05 14:24:44 +00:00
8bd3b4d3f0 fix(visualizer): fixed an issue with audio getting unusually loud 2024-01-05 23:23:39 +09:00
4e3cb5806d fix(music-together): modernize code 2024-01-05 23:11:26 +09:00
6563eb4ddd chore(i18n): Translated using Weblate (Spanish)
Currently translated at 100.0% (330 of 330 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/es/
2024-01-05 14:02:12 +00:00
895386f6f8 fix(music-together): typing 2024-01-05 23:01:55 +09:00
3810955e56 fix(skip-silences): fix audio distorted
fix #1141
2024-01-05 21:58:36 +09:00
59c521e53f fix: download button not working 2024-01-05 21:04:52 +09:00
25d266f8f9 feat(tray): Add song info and paused icon (#1592) 2024-01-05 20:56:47 +09:00
0c3c380591 chore(deps): update dependency rollup to v4.9.3 2024-01-05 20:56:17 +09:00
a20cfa30a1 chore(deps): update dependency vite to v5.0.11 2024-01-05 20:43:13 +09:00
fefe899393 chore(i18n): Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (328 of 328 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/zh_Hans/
2024-01-05 11:12:50 +01:00
55759e8d7a chore(i18n): Translated using Weblate (Italian)
Currently translated at 100.0% (328 of 328 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/it/
2024-01-05 11:12:50 +01:00
ddb561937f fix(deps): update dependency i18next to v23.7.16 2024-01-04 22:41:16 +09:00
198cb71a4c chore(deps): update dependency electron to v28.1.1 2024-01-04 22:38:45 +09:00
c34b880752 chore(deps): update dependency electron-vite to v2.0.0-beta.3 2024-01-04 22:38:30 +09:00
76944e3e41 fix(deps): update dependency i18next to v23.7.14 2024-01-03 17:35:30 +09:00
68cd76f2af chore(deps): update pnpm to v8.14.0 2024-01-03 17:35:23 +09:00
81145b52b7 fix(#1580): fix NEW badge doesn't show 2024-01-03 15:02:29 +09:00
2a19dab061 chore(i18n): Translated using Weblate (Vietnamese)
Currently translated at 3.3% (11 of 328 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/vi/
2024-01-02 10:52:22 +01:00
6958d59d4f chore(i18n): Translated using Weblate (Lithuanian)
Currently translated at 89.6% (294 of 328 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/lt/
2024-01-02 09:49:04 +01:00
8a51dfad87 chore(i18n): Translated using Weblate (Chinese (Simplified))
Currently translated at 89.6% (294 of 328 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/zh_Hans/
2024-01-02 09:49:04 +01:00
5bb4d9efbe chore(i18n): Translated using Weblate (Turkish)
Currently translated at 100.0% (328 of 328 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/tr/
2024-01-02 09:49:04 +01:00
927aa5f24b chore(i18n): Translated using Weblate (Portuguese)
Currently translated at 89.6% (294 of 328 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/pt/
2024-01-02 09:49:03 +01:00
d695bc93a1 chore(i18n): Translated using Weblate (Polish)
Currently translated at 89.3% (293 of 328 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/pl/
2024-01-02 09:49:03 +01:00
b05fb4ccbe chore(i18n): Translated using Weblate (Norwegian Bokmål)
Currently translated at 67.9% (223 of 328 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/nb_NO/
2024-01-02 09:49:03 +01:00
299eb7e7d6 chore(i18n): Translated using Weblate (Italian)
Currently translated at 89.3% (293 of 328 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/it/
2024-01-02 09:49:03 +01:00
ae26333224 chore(i18n): Translated using Weblate (French)
Currently translated at 85.9% (282 of 328 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/fr/
2024-01-02 09:49:03 +01:00
35176469b0 chore(i18n): Translated using Weblate (Spanish)
Currently translated at 100.0% (328 of 328 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/es/
2024-01-02 09:49:03 +01:00
4e74f9cbc5 chore(i18n): Translated using Weblate (Czech)
Currently translated at 94.8% (311 of 328 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/cs/
2024-01-02 09:49:02 +01:00
4091b36f36 chore(i18n): Translated using Weblate (English)
Currently translated at 100.0% (328 of 328 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/en/
2024-01-02 09:49:02 +01:00
b3f805fce6 chore(i18n): Translated using Weblate (Vietnamese)
Currently translated at 2.4% (8 of 328 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/vi/
2024-01-02 09:43:14 +01:00
b129a3e8d8 chore(i18n): Translated using Weblate (Chinese (Simplified))
Currently translated at 89.9% (295 of 328 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/zh_Hans/
2024-01-02 09:43:13 +01:00
64ea1fdb58 chore(i18n): Translated using Weblate (Turkish)
Currently translated at 100.0% (328 of 328 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/tr/
2024-01-02 09:43:13 +01:00
8fcf59ed0a chore(i18n): Translated using Weblate (Spanish)
Currently translated at 100.0% (328 of 328 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/es/
2024-01-02 09:43:12 +01:00
9811ca63de chore(i18n): Translated using Weblate (Czech)
Currently translated at 94.8% (311 of 328 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/cs/
2024-01-02 09:43:12 +01:00
9028f88299 chore(i18n): Translated using Weblate (English)
Currently translated at 100.0% (328 of 328 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/en/
2024-01-02 09:43:12 +01:00
fd47766d93 chore(deps): update dependency @typescript-eslint/eslint-plugin to v6.17.0 2024-01-02 17:34:04 +09:00
26b12c7208 chore(i18n): Added translation using Weblate (Vietnamese) 2024-01-02 07:19:07 +01:00
8da9b3454d chore(deps): update dependency esbuild to v0.19.11 2024-01-01 20:32:13 +09:00
205cbefc83 Update changelog for v3.2.1 2024-01-01 00:32:24 +00:00
0e94c72eef Bump version to 3.2.1 2024-01-01 09:22:46 +09:00
c055641351 chore(i18n): Translated using Weblate (Korean)
Currently translated at 100.0% (328 of 328 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/ko/
2024-01-01 01:19:52 +01:00
c0a3aa99de chore(i18n): Translated using Weblate (English)
Currently translated at 100.0% (328 of 328 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/en/
2024-01-01 01:19:52 +01:00
8a8976acef chore(i18n): Translated using Weblate (Czech)
Currently translated at 94.8% (311 of 328 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/cs/
2024-01-01 01:06:02 +01:00
e409165e1b chore(i18n): Translated using Weblate (English)
Currently translated at 100.0% (328 of 328 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/en/
2024-01-01 01:06:02 +01:00
b278140796 chore(i18n): Update translation files
Updated by "Remove blank strings" hook in Weblate.

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/
2024-01-01 01:01:00 +01:00
397056a54d chore(i18n): Translated using Weblate (Turkish)
Currently translated at 26.5% (87 of 328 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/tr/
2024-01-01 01:01:00 +01:00
edecd65419 chore(i18n): Translated using Weblate (Korean)
Currently translated at 100.0% (328 of 328 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/ko/
2024-01-01 01:01:00 +01:00
4d2d0b7bd6 chore(i18n): Translated using Weblate (Czech)
Currently translated at 85.6% (281 of 328 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/cs/
2024-01-01 01:01:00 +01:00
0ca4e34efd chore(i18n): Translated using Weblate (Czech)
Currently translated at 85.6% (281 of 328 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/cs/
2024-01-01 01:00:59 +01:00
43f3226c3a fix: fix #1574 2024-01-01 08:36:48 +09:00
0a6dbecc05 fix: fix #1575 2024-01-01 08:36:22 +09:00
f5aa179cd6 chore(i18n): Translated using Weblate
Co-authored-by: Allan Nordhøy <epost@anotheragency.no>
Co-authored-by: inson1 <vaclav.svarc01@seznam.cz>
Co-authored-by: Anonymous <noreply@weblate.org>
2024-01-01 04:53:59 +09:00
3140e91dda Update changelog for v3.2.0 2023-12-31 16:31:59 +00:00
022f8ff65c Merge branch 'master' of https://github.com/th-ch/youtube-music 2024-01-01 01:23:01 +09:00
5e63cc2e89 Bump version to 3.2.0 2024-01-01 01:22:48 +09:00
880ed99846 Revert "fix(deps): update dependency @xhayper/discord-rpc to v1.1.2"
This reverts commit 050d55c736.
2024-01-01 00:34:49 +09:00
222e78c85b fix(in-app-menu): fix in-app-menu tooltip position 2023-12-31 23:48:01 +09:00
050d55c736 fix(deps): update dependency @xhayper/discord-rpc to v1.1.2 2023-12-31 23:40:26 +09:00
13ef8560ff fix: pnpm build error 2023-12-31 23:40:09 +09:00
78d990c079 feat(album-color-theme): improve Album Color Theme style (#1571) 2023-12-31 23:04:44 +09:00
4d3e2c09da feat(menu): add more detail in Menu (#1570) 2023-12-31 20:56:24 +09:00
aa899d247a chore(plugins): change default config album-actions, music-together 2023-12-31 13:56:42 +09:00
ee0c512529 feat(music-together): Add new plugin Music Together (#1562)
* feat(music-together): test `peerjs`

* feat(music-together): replace `prompt` to `custom-electron-prompt`

* fix(music-together): fix

* test fix

* wow

* test

* feat(music-together): improve `onStart`

* fix: adblocker

* fix(adblock): fix crash with `peerjs`

* feat(music-together): add host UI

* feat(music-together): implement addSong, removeSong, syncQueue

* feat(music-together): inject panel

* feat(music-together): redesign music together panel

* feat(music-together): sync queue, profile

* feat(music-together): sync progress, song, state

* fix(music-together): fix some bug

* fix(music-together): fix sync queue

* feat(music-together): support i18n

* feat(music-together): improve sync queue

* feat(music-together): add profile in music item

* refactor(music-together): refactor structure

* feat(music-together): add permission

* fix(music-together): fix queue sync bug

* fix(music-together): fix some bugs

* fix(music-together): fix permission not working on guest mode

* fix(music-together): fix queue sync relate bugs

* fix(music-together): fix automix items not append using music together

* fix(music-together): fix

* feat(music-together): improve video injection

* fix(music-together): fix injection code

* fix(music-together): fix broadcast guest

* feat(music-together): add more permission

* fix(music-together): fix injector

* fix(music-together): fix guest add song logic

* feat(music-together): add popup close listener

* fix(music-together): fix connection issue

* fix(music-together): fix connection issue 2

* feat(music-together): reserve playlist

* fix(music-together): exclude automix songs

* fix(music-together): fix playlist index sync bug

* fix(music-together): fix connection failed error and sync index

* fix(music-together): fix host set index bug

* fix: apply fix from eslint

* feat(util): add `ImageElementFromSrc`

* chore(util): update jsdoc

* feat(music-together): add owner name

* chore(music-together): add translation

* feat(music-together): add progress sync

* chore(music-together): remove `console.log`

---------

Co-authored-by: JellyBrick <shlee1503@naver.com>
2023-12-31 13:52:15 +09:00
5f9b522307 chore(deps): update dependency rollup to v4.9.2 (#1567)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-12-31 04:05:49 +09:00
c207e29980 fix(deps): update dependency i18next to v23.7.13 (#1569)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-12-31 04:05:44 +09:00
df4d2d6b72 chore(ambient-mode): remove console.log 2023-12-31 02:28:31 +09:00
c3dd20cabd chore(i18n): Translated using Weblate (Korean)
Currently translated at 100.0% (296 of 296 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/ko/
2023-12-30 17:09:06 +00:00
7a6db95d1a chore(i18n): Translated using Weblate (Czech)
Currently translated at 80.4% (238 of 296 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/cs/
2023-12-30 17:09:05 +00:00
bc6825d63b chore(i18n): Translated using Weblate (Czech)
Currently translated at 76.8% (226 of 294 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/cs/
2023-12-29 16:14:02 +01:00
5e79e9e0f2 feat: Add new plugin Album actions (#1515)
Co-authored-by: JellyBrick <shlee1503@naver.com>
2023-12-30 00:13:56 +09:00
5e303c2ba8 fix(deps): update dependency i18next to v23.7.12 (#1564)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-12-29 22:26:41 +09:00
0bd9c16356 fix: Only apply scale factor on Windows (#1565) 2023-12-29 22:26:24 +09:00
f0f5d9da2f feat(ambient-mode): support ambient mode on Song section
resolve #1555
2023-12-29 21:46:27 +09:00
TC
f46c431f4c Move cask definition to separate repo 2023-12-29 11:26:55 +01:00
62410e9ee2 feat(in-app-menu): add show on hover 2023-12-29 17:52:31 +09:00
46f76f1408 chore(i18n): Translated using Weblate (Indonesian)
Currently translated at 3.7% (11 of 294 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/id/
2023-12-27 18:20:59 +01:00
5e071e16d8 chore(i18n): Translated using Weblate (Turkish)
Currently translated at 29.2% (86 of 294 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/tr/
2023-12-27 18:20:58 +01:00
c0238588bd chore(i18n): Translated using Weblate (Russian)
Currently translated at 78.5% (231 of 294 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/ru/
2023-12-27 18:20:58 +01:00
Nik
30002d660a chore(i18n): Translated using Weblate (Russian)
Currently translated at 78.5% (231 of 294 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/ru/
2023-12-27 18:20:58 +01:00
48eeb6bca3 fix: picture-in-picture icon 2023-12-28 02:17:42 +09:00
e67699fed5 fix: fixed an issue with the download button disappearing
- resolve #1551
2023-12-28 02:08:03 +09:00
8aeae45965 chore(i18n): Translated using Weblate (Ukrainian)
Currently translated at 77.2% (227 of 294 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/uk/
2023-12-27 13:09:36 +01:00
ce7491941b chore(i18n): Translated using Weblate (Russian)
Currently translated at 74.8% (220 of 294 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/ru/
2023-12-27 13:09:36 +01:00
1dce03c4f2 chore(i18n): Translated using Weblate (Polish)
Currently translated at 100.0% (294 of 294 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/pl/
2023-12-27 13:09:36 +01:00
62eae6d5d0 chore(deps): update dependency @typescript-eslint/eslint-plugin to v6.16.0 (#1556)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-12-27 18:37:06 +09:00
15b2b26b84 chore(deps): update pnpm to v8.13.1 (#1557)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-12-27 18:36:57 +09:00
9664c17c47 chore(deps): update dependency ws to v8.16.0 (#1559)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-12-27 18:36:51 +09:00
8067dad2fa fix(deps): update dependency youtubei.js to v8.1.0 (#1560)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-12-27 18:36:42 +09:00
4dcaa510d9 chore(i18n): Translated using Weblate (Indonesian)
Currently translated at 3.7% (11 of 294 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/id/
2023-12-25 12:42:54 +01:00
b6e918089d chore(i18n): Translated using Weblate (Turkish)
Currently translated at 29.2% (86 of 294 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/tr/
2023-12-25 12:42:54 +01:00
1c9e6b1bb8 chore(i18n): Translated using Weblate (Polish)
Currently translated at 97.9% (288 of 294 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/pl/
2023-12-25 12:42:54 +01:00
ebd304c252 fix(deps): update dependency node-html-parser to v6.1.12 (#1554)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-12-25 20:37:05 +09:00
36083c4173 Revert "fix(deps): update dependency @xhayper/discord-rpc to v1.1.2" (#1552) 2023-12-25 03:48:15 +09:00
a084b060d8 chore(deps): update dependency electron to v28.1.0 2023-12-25 03:32:28 +09:00
432c79b606 fix(deps): update dependency @xhayper/discord-rpc to v1.1.2 2023-12-25 03:32:07 +09:00
0f1f0ee933 chore(deps): update dependency eslint-plugin-prettier to v5.1.2 2023-12-25 03:31:59 +09:00
9b1a4b8d88 chore(i18n): Translated using Weblate (Chinese (Traditional))
Currently translated at 100.0% (294 of 294 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/zh_Hant/
2023-12-23 21:07:03 +01:00
1a7a665915 chore(i18n): Translated using Weblate (Czech)
Currently translated at 76.5% (225 of 294 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/cs/
2023-12-22 03:06:46 +01:00
623ecf7fb8 chore(i18n): Update translation files
Updated by "Remove blank strings" hook in Weblate.

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/
2023-12-20 17:44:47 +01:00
0dc9c6a1a9 chore(i18n): Translated using Weblate (Czech)
Currently translated at 72.7% (214 of 294 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/cs/
2023-12-20 17:44:47 +01:00
72c5eaa5ff chore(i18n): Translated using Weblate (Czech)
Currently translated at 82.3% (242 of 294 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/cs/
2023-12-20 13:54:56 +01:00
0f47b94b7d chore(i18n): Update translation files
Updated by "Remove blank strings" hook in Weblate.

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/
2023-12-20 13:53:36 +01:00
9abe15f1ad chore(i18n): Translated using Weblate (Czech)
Currently translated at 82.3% (242 of 294 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/cs/
2023-12-20 13:53:35 +01:00
96afda92c8 chore(i18n): Translated using Weblate (Czech)
Currently translated at 80.9% (238 of 294 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/cs/
2023-12-20 13:33:50 +01:00
5c6fd4a739 Update README-ko.md 2023-12-20 14:11:53 +02:00
23b87a876d chore(deps): update dependency eslint-plugin-prettier to v5.1.0 2023-12-20 13:59:40 +09:00
737fd05369 chore(deps): update dependency electron-vite to v2.0.0-beta.2 2023-12-20 13:59:28 +09:00
c5bcd89f16 chore(deps): update dependency @typescript-eslint/eslint-plugin to v6.15.0 2023-12-19 16:32:46 +09:00
377e1be0b2 fix(deps): update dependency @foobar404/wave to v2.0.5 2023-12-19 16:32:31 +09:00
a92049c0c9 chore(i18n): Translated using Weblate (Lithuanian)
Currently translated at 100.0% (294 of 294 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/lt/
2023-12-19 07:21:28 +01:00
27a2955bba fix: fix homebrew cask
- resolve #1514
2023-12-18 22:02:38 +09:00
cc940e2020 Update changelog for v3.1.1 2023-12-18 12:57:28 +00:00
203c80767d Bump version to 3.1.1 2023-12-18 21:49:31 +09:00
f564039438 chore: fix the example plugins in the README 2023-12-18 14:47:18 +02:00
d6566fb870 chore(i18n): Translated using Weblate (Lithuanian)
Currently translated at 84.3% (248 of 294 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/lt/
2023-12-18 13:46:47 +01:00
55ab17e789 chore(i18n): Translated using Weblate (Portuguese)
Currently translated at 100.0% (294 of 294 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/pt/
2023-12-18 13:43:08 +01:00
e0eeb720cd feat: upgrade to Vite v5 2023-12-18 21:10:47 +09:00
53ac7ff257 fix: electron-vite dev --watch 2023-12-18 20:00:00 +09:00
a4e2f10afa chore(deps): update dependency playwright to v1.41.0-alpha-dec-18-2023 2023-12-18 19:58:33 +09:00
c9c9d766e6 chore(deps): update dependency rollup to v4.9.1 2023-12-18 19:56:14 +09:00
b82d161180 chore: Update bug_report.yml 2023-12-18 19:47:51 +09:00
7cadacd8cf fix: vite dev mode
fix an issue caused by da3bc5aeb7
2023-12-18 19:07:43 +09:00
fc1a7cda62 chore(i18n): Translated using Weblate (Lithuanian)
Currently translated at 84.3% (248 of 294 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/lt/
2023-12-18 10:56:31 +01:00
da3bc5aeb7 fix: renderer plugin load timing
MAGIC OF JAVASCRIPT
2023-12-18 18:42:01 +09:00
ee98344064 chore(i18n): Added translation using Weblate (Lithuanian) 2023-12-17 21:43:08 +01:00
da10eabf99 chore(i18n): Translated using Weblate (Ukrainian)
Currently translated at 69.3% (204 of 294 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/uk/
2023-12-17 14:55:42 +01:00
ce3c63c386 chore(i18n): Translated using Weblate (Portuguese)
Currently translated at 73.8% (217 of 294 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/pt/
2023-12-17 14:55:42 +01:00
f8c3cff5a9 chore(i18n): Translated using Weblate (Korean)
Currently translated at 100.0% (294 of 294 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/ko/
2023-12-17 14:55:41 +01:00
08f369f8bc chore(i18n): Translated using Weblate (Portuguese)
Currently translated at 43.5% (128 of 294 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/pt/
2023-12-17 08:28:44 +01:00
1517230ede chore(i18n): Translated using Weblate (German)
Currently translated at 100.0% (294 of 294 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/de/
2023-12-17 08:28:44 +01:00
45aeadef86 fix(song-info-front): fix type 2023-12-17 16:05:26 +09:00
5ec7c346ca fix(song-info-front): fix parser to parse album name 2023-12-17 15:51:35 +09:00
4b68de1606 fix: update types 2023-12-16 22:39:57 +09:00
7710bcdacc chore(i18n): Translated using Weblate (Chinese (Traditional))
Currently translated at 100.0% (294 of 294 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/zh_Hant/
2023-12-16 11:58:32 +01:00
4c19bcc983 chore(i18n): Translated using Weblate (Turkish)
Currently translated at 26.1% (77 of 294 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/tr/
2023-12-16 03:33:06 +01:00
2f50b1423a chore(i18n): Translated using Weblate (French)
Currently translated at 95.9% (282 of 294 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/fr/
2023-12-16 03:33:06 +01:00
15768c9691 chore(deps): update dependency eslint to v8.56.0 2023-12-16 11:28:58 +09:00
442eb75b8d chore(i18n): Translated using Weblate (Portuguese)
Currently translated at 43.1% (127 of 294 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/pt/
2023-12-16 03:09:12 +01:00
8facd27125 chore(i18n): Translated using Weblate (Italian)
Currently translated at 100.0% (294 of 294 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/it/
2023-12-16 03:09:11 +01:00
a302cb6ebf chore(i18n): Translated using Weblate (Spanish)
Currently translated at 100.0% (294 of 294 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/es/
2023-12-16 03:09:11 +01:00
32422c7ba9 fix(deps): update dependency i18next to v23.7.11 2023-12-15 13:42:50 +09:00
ca131cb156 chore(deps): update dependency eslint-plugin-import to v2.29.1 2023-12-15 13:42:44 +09:00
5f8262ede1 chore(i18n): Translated using Weblate (Ukrainian)
Currently translated at 65.3% (192 of 294 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/uk/
2023-12-14 02:11:40 +01:00
88db9d3693 chore(i18n): Translated using Weblate (Chinese (Traditional))
Currently translated at 100.0% (294 of 294 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/zh_Hant/
2023-12-13 21:33:32 +01:00
0fa6f344f9 chore(i18n): Translated using Weblate (Polish)
Currently translated at 97.6% (287 of 294 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/pl/
2023-12-13 21:33:31 +01:00
d1642b3dfb chore(i18n): Translated using Weblate (Polish)
Currently translated at 97.6% (287 of 294 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/pl/
2023-12-13 21:33:31 +01:00
7599fce5f5 chore(i18n): Translated using Weblate (Spanish)
Currently translated at 97.6% (287 of 294 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/es/
2023-12-13 21:33:31 +01:00
9bd0dc9a7b chore(i18n): Translated using Weblate (Greek)
Currently translated at 23.8% (70 of 294 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/el/
2023-12-13 21:33:31 +01:00
dad7cafebc chore(i18n): Translated using Weblate (Russian)
Currently translated at 72.7% (214 of 294 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/ru/
2023-12-13 16:20:54 +01:00
4febb28201 chore(deps): update dependency rollup to v4.9.0 2023-12-13 21:13:41 +09:00
ced943bad3 chore(deps): update pnpm to v8.12.1 2023-12-13 21:12:07 +09:00
202b8717a2 chore(i18n): Translated using Weblate (Chinese (Traditional))
Currently translated at 100.0% (294 of 294 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/zh_Hant/
2023-12-13 13:10:46 +01:00
eba7026b89 chore(i18n): Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (294 of 294 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/zh_Hans/
2023-12-13 13:10:46 +01:00
6b2adca1f4 fix(downloader): fix download button i18n 2023-12-13 18:02:18 +09:00
0c9d3cc057 chore(i18n): Translated using Weblate (Ukrainian)
Currently translated at 42.8% (126 of 294 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/uk/
2023-12-13 05:07:02 +01:00
45ec11aaed chore(deps): update pnpm-lock.yaml 2023-12-13 13:06:10 +09:00
4d47fa3fac fix(deps): update dependency @electron/remote to v2.1.1 2023-12-13 12:24:55 +09:00
6322d5bb09 chore(i18n): Translated using Weblate (Indonesian)
Currently translated at 3.0% (9 of 294 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/id/
2023-12-12 21:56:36 +01:00
2e8a579049 chore(i18n): Translated using Weblate (Thai)
Currently translated at 1.0% (3 of 294 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/th/
2023-12-12 21:56:36 +01:00
9db7e99d7f chore(i18n): Translated using Weblate (Korean)
Currently translated at 100.0% (294 of 294 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/ko/
2023-12-12 21:56:36 +01:00
ef73989995 chore(i18n): Translated using Weblate (Czech)
Currently translated at 79.9% (235 of 294 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/cs/
2023-12-12 21:56:36 +01:00
61024084e9 chore(deps): update dependency ws to v8.15.1 2023-12-13 05:46:56 +09:00
1c71b2020f fix(deps): update dependency i18next to v23.7.9 2023-12-13 05:45:52 +09:00
3b2b545388 chore(i18n): Translated using Weblate (Russian)
Currently translated at 67.6% (199 of 294 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/ru/
2023-12-12 21:44:51 +01:00
fafc7f94fd chore(i18n): Translated using Weblate (Italian)
Currently translated at 100.0% (294 of 294 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/it/
2023-12-12 21:44:50 +01:00
5fa88b5ce9 fix: fix renderer plugin load timing
- fix #1522
2023-12-12 12:53:41 +09:00
a8fe4167db fix(pip): fix invalid position config 2023-12-12 11:11:41 +09:00
0e6b0ddc26 chore(i18n): Translated using Weblate (Korean)
Currently translated at 100.0% (294 of 294 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/ko/
2023-12-12 02:24:41 +01:00
af88db3865 chore(i18n): Translated using Weblate (Japanese)
Currently translated at 100.0% (294 of 294 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/ja/
2023-12-12 02:24:41 +01:00
0a1bef556c chore(i18n): Translated using Weblate (English)
Currently translated at 100.0% (294 of 294 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/en/
2023-12-12 02:24:41 +01:00
7030181dc1 chore(i18n): Translated using Weblate (English)
Currently translated at 100.0% (294 of 294 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/en/
2023-12-12 02:24:41 +01:00
98cb2f61ab chore(deps): update dependency playwright to v1.41.0-alpha-dec-12-2023 2023-12-12 10:23:59 +09:00
a601d0b3d2 chore(deps): update dependency rollup to v4.8.0 2023-12-12 10:22:23 +09:00
8d18923065 chore(i18n): Translated using Weblate (Norwegian Bokmål)
Currently translated at 75.1% (221 of 294 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/nb_NO/
2023-12-12 01:08:34 +01:00
4b6a7e0bd7 chore(i18n): Translated using Weblate (English)
Currently translated at 100.0% (294 of 294 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/en/
2023-12-12 01:08:34 +01:00
44f87853d6 chore(i18n): Translated using Weblate (Norwegian Bokmål)
Currently translated at 74.8% (220 of 294 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/nb_NO/
2023-12-12 01:02:37 +01:00
4ff47bee03 chore(i18n): Translated using Weblate (Norwegian Bokmål)
Currently translated at 74.1% (218 of 294 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/nb_NO/
2023-12-12 00:54:03 +01:00
7a2c28df21 chore(i18n): Translated using Weblate (English)
Currently translated at 100.0% (294 of 294 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/en/
2023-12-12 00:54:02 +01:00
f7241d1e4a chore(i18n): Added translation using Weblate (Indonesian) 2023-12-12 00:33:55 +01:00
2b5daf3a75 chore(deps): update dependency @typescript-eslint/eslint-plugin to v6.14.0 2023-12-12 06:43:20 +09:00
d7184cf75d chore(i18n): Translated using Weblate (Russian)
Currently translated at 54.0% (159 of 294 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/ru/
2023-12-11 19:59:59 +01:00
1313d18d44 chore(i18n): Translated using Weblate (Czech)
Currently translated at 80.9% (238 of 294 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/cs/
2023-12-11 19:59:58 +01:00
605e9481d9 chore(i18n): Translated using Weblate (English)
Currently translated at 100.0% (294 of 294 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/en/
2023-12-11 19:59:58 +01:00
cf4c89f825 chore(i18n): Added translation using Weblate (Thai) 2023-12-11 10:17:18 +01:00
f4f1d4d373 chore(i18n): Translated using Weblate (Norwegian Bokmål)
Currently translated at 46.2% (136 of 294 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/nb_NO/
2023-12-11 09:00:43 +01:00
6557b40924 Translated using Weblate (Spanish)
Currently translated at 100.0% (294 of 294 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/es/
2023-12-11 00:20:44 +01:00
e01a29c5d1 Translated using Weblate (Spanish)
Currently translated at 100.0% (294 of 294 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/es/
2023-12-11 00:20:44 +01:00
d9a7e16d63 Translated using Weblate (Italian)
Currently translated at 99.3% (292 of 294 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/it/
2023-12-11 00:20:44 +01:00
d170199d85 Update changelog for v3.1.0 2023-12-10 23:10:52 +00:00
89 changed files with 7765 additions and 1137 deletions

View File

@ -24,6 +24,11 @@ body:
placeholder: 2.0.0
validations:
required: true
- type: checkboxes
attributes:
label: Are you using the portable version of the YouTube Music Application?
options:
- label: I use the portable version of the YouTube Music Application.
- type: dropdown
attributes:
label: What operating system are you using?

View File

@ -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 cant be opened." when launching the app, run the following in the Terminal:
@ -290,9 +291,9 @@ import style from './style.css?inline'; // import style as inline
import { createPlugin } from '@/utils';
const builder = createPlugin({
export default createPlugin({
name: 'Plugin Label',
restartNeeded: true, // if value is true, ytmusic show restart dialog
restartNeeded: true, // if value is true, ytmusic will show a restart dialog
config: {
enabled: false,
}, // your custom config
@ -306,9 +307,9 @@ const builder = createPlugin({
```typescript
import { createPlugin } from '@/utils';
const builder = createPlugin({
export default createPlugin({
name: 'Plugin Label',
restartNeeded: true, // if value is true, ytmusic show restart dialog
restartNeeded: true, // if value is true, ytmusic will show the restart dialog
config: {
enabled: false,
}, // your custom config

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@ -2,8 +2,68 @@
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)
- Skip Disliked Songs [`#1505`](https://github.com/th-ch/youtube-music/pull/1505)
- chore(deps): update dependency @typescript-eslint/eslint-plugin to v6.13.2 [`#1452`](https://github.com/th-ch/youtube-music/pull/1452)
- fix: Homebrew latest release url parsing [`#1496`](https://github.com/th-ch/youtube-music/pull/1496)
- fix: in-player adblocker inject timing issue [`#1478`](https://github.com/th-ch/youtube-music/issues/1478)
- fix(package.json): fix RPM version `libuuid` issue [`#1508`](https://github.com/th-ch/youtube-music/issues/1508)
- Translated using Weblate (Polish) [`7b78ba6`](https://github.com/th-ch/youtube-music/commit/7b78ba67613f14be65a45751efeb06431b405a91)
- Translated using Weblate (French) [`ebc0879`](https://github.com/th-ch/youtube-music/commit/ebc087963b23265ff00528c8305d51597abf587a)
- Translated using Weblate (Chinese (Traditional)) [`020bdc0`](https://github.com/th-ch/youtube-music/commit/020bdc0811ea45ad6c2853c62a05ae6695c5c4f9)
#### [v3.0.2](https://github.com/th-ch/youtube-music/compare/v3.0.1...v3.0.2)
> 3 December 2023
- fix(adblocker): fix In-Player adblocker [`#1478`](https://github.com/th-ch/youtube-music/issues/1478)
- fix(menu): crash on linux [`#1477`](https://github.com/th-ch/youtube-music/issues/1477)
- fix: update pnpm-lock.yaml [`9e2c6b1`](https://github.com/th-ch/youtube-music/commit/9e2c6b1afa33b5708853c8328946e68ec45b09c3)

View File

@ -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: {

View File

@ -1,15 +1,18 @@
import { resolve } from 'node:path';
import { resolve, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import { defineConfig, defineViteConfig } from 'electron-vite';
import builtinModules from 'builtin-modules';
import viteResolve from 'vite-plugin-resolve';
import Inspect from 'vite-plugin-inspect';
import { pluginVirtualModuleGenerator } from './vite-plugins/plugin-importer';
import pluginLoader from './vite-plugins/plugin-loader';
import { pluginVirtualModuleGenerator } from './vite-plugins/plugin-importer.mjs';
import pluginLoader from './vite-plugins/plugin-loader.mjs';
import type { UserConfig } from 'vite';
import { i18nImporter } from './vite-plugins/i18n-importer';
import { i18nImporter } from './vite-plugins/i18n-importer.mjs';
const __dirname = dirname(fileURLToPath(import.meta.url));
const resolveAlias = {
'@': resolve(__dirname, './src'),

View File

@ -1,7 +1,7 @@
{
"name": "youtube-music",
"productName": "YouTube Music",
"version": "3.1.0",
"version": "3.2.2",
"description": "YouTube Music Desktop App - including custom plugins",
"main": "./dist/main/index.js",
"license": "MIT",
@ -77,8 +77,8 @@
"rpm"
]
},
"rpm":{
"depends":[
"rpm": {
"depends": [
"/usr/lib64/libuuid.so.1"
]
},
@ -102,20 +102,20 @@
"vite:inspect": "pnpm clean && electron-vite build --mode development && pnpm exec serve .vite-inspect",
"start": "electron-vite preview",
"start:debug": "cross-env ELECTRON_ENABLE_LOGGING=1 pnpm start",
"dev": "electron-vite dev",
"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,29 +123,34 @@
},
"pnpm": {
"overrides": {
"esbuild": "0.19.11",
"usocket": "1.0.1",
"rollup": "4.7.0",
"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": {
"@cliqz/adblocker-electron": "1.26.12",
"@cliqz/adblocker-electron-preload": "1.26.12",
"@electron-toolkit/tsconfig": "1.0.1",
"@electron/remote": "2.1.0",
"@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",
@ -161,48 +166,53 @@
"filenamify": "6.0.0",
"howler": "2.2.4",
"html-to-text": "9.0.5",
"i18next": "23.7.8",
"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-10-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.13.2",
"@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": "1.0.29",
"eslint": "8.55.0",
"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.0",
"eslint-plugin-prettier": "5.0.1",
"eslint-plugin-import": "2.29.1",
"eslint-plugin-prettier": "5.1.2",
"glob": "10.3.10",
"node-gyp": "10.0.1",
"playwright": "1.41.0-alpha-dec-10-2023",
"rollup": "4.7.0",
"playwright": "1.41.0-alpha-jan-5-2024",
"rollup": "4.9.3",
"typescript": "5.3.3",
"utf-8-validate": "6.0.3",
"vite": "4.5.1",
"vite": "5.0.11",
"vite-plugin-inspect": "0.8.1",
"vite-plugin-resolve": "2.5.1",
"ws": "8.15.0"
"ws": "8.16.0"
},
"auto-changelog": {
"hideCredit": true,
@ -210,5 +220,5 @@
"unreleased": true,
"output": "changelog.md"
},
"packageManager": "pnpm@8.12.0"
"packageManager": "pnpm@8.14.0"
}

20
patches/vudio@2.1.1.patch Normal file
View 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) {

1022
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -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",
@ -20,7 +20,7 @@
"main": {
"console": {
"did-finish-load": {
"dev-tools": "Dokončeno načítání. DevTools otevřeny"
"dev-tools": "Načítání dokončeno. Vývojářské nástroje se otevřely"
},
"i18n": {
"loaded": "i18n načteno"
@ -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é, use 'Alt' to show it (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,14 +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": "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í"
@ -148,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"
}
}
@ -166,13 +169,16 @@
},
"plugins": {
"enabled": "Povoleno",
"label": "Pluginy"
"label": "Pluginy",
"new": "NOVÉ"
},
"view": {
"label": "Zobrazení",
"submenu": {
"force-reload": "Vynutit znovu načtení",
"reset-zoom": "Actual velikost",
"reload": "Obnovit",
"reset-zoom": "Skutečná velikost",
"toggle-fullscreen": "Přepnout režim celé obrazovky",
"zoom-in": "Přiblížit",
"zoom-out": "Oddálit"
}
@ -184,7 +190,7 @@
"previous": "Minulý",
"quit": "Ukončit",
"restart": "Restartovat aplikaci",
"show": "Ukázat okno"
"show": "Zobrazit okno"
}
},
"plugins": {
@ -195,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 screens 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ů"
}
@ -234,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",
@ -270,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": {
@ -293,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"
}
}
},
@ -331,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 (interaktiv notifikace jsou dostupné na Windows)",
"menu": {
"interactive": "Interaktivní oznámení",
"interactive-settings": {
@ -402,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í",
@ -411,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": {
@ -419,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",
@ -433,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",
@ -449,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"
}
}
},
@ -462,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": {
@ -475,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": {
@ -495,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í",
@ -510,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"
}

View File

@ -44,7 +44,7 @@
},
"dialog": {
"hide-menu-enabled": {
"detail": "Menü ist versteckt, nutze 'Alt', um es zu zeigen (oder 'Escape' beim Verwenden des In-App-Menüs)",
"detail": "Das Menü ist versteckt, nutze 'Alt', um es zu aufzurufen (oder 'Escape' beim Verwenden des In-App-Menüs)",
"message": "Menü verstecken ist aktiviert",
"title": "Menü Verstecken Aktiviert"
},
@ -105,7 +105,7 @@
"label": "Proxy setzen",
"prompt": {
"label": "Proxy-Adresse eingeben: (leer lassen zum Ausschalten)",
"placeholder": "Beispiel: socks5://127.0.0.1:9999",
"placeholder": "Beispiel: SOCKS5://127.0.0.1:9999",
"title": "Proxy setzen"
}
},
@ -470,9 +470,9 @@
"save-window-size": "Fenstergröße speichern",
"use-native-pip": "Browsereigenes PiP verwenden"
},
"name": "Bild im Bild",
"name": "Bild-im-Bild",
"templates": {
"button": "Bild im Bild"
"button": "Bild-im-Bild"
}
},
"playback-speed": {
@ -519,7 +519,7 @@
"name": "Videoqualitätsänderer"
},
"shortcuts": {
"description": "Ermöglicht das Festlegen globaler Hotkeys für die Wiedergabe (Abspielen/Pause/Nächstes/Vorheriges) + Deaktivieren des Medien-OSD durch Überschreiben der Medientasten + Aktivieren von Strg/CMD + F zum Suchen + Aktivieren der Linux mpris-Unterstützung für Medientasten + Angepasste Tastenkürzel für fortgeschrittene Benutzer",
"description": "Ermöglicht das Festlegen globaler Hotkeys für die Wiedergabe (Abspielen/Pause/Nächster/Vorheriger) + Deaktivieren des Medien-OSD durch Überschreiben der Medientasten + Aktivieren von Strg/CMD + F zum Suchen + Aktivieren der Linux mpris-Unterstützung für Medientasten + Angepasste Tastenkürzel für fortgeschrittene Benutzer.",
"menu": {
"override-media-keys": "Medientasten überschreiben",
"set-keybinds": "Globale Liedsteuerung setzen"
@ -537,6 +537,10 @@
}
}
},
"skip-disliked-songs": {
"description": "Überspringt Lieder, die ihnen nicht gefallen",
"name": "Überspring Lieder, die ihnen nicht gefallen"
},
"skip-silences": {
"description": "Automatisch stille Abschnitte in Liedern überspringen",
"name": "Stille überspringen"

View File

@ -1,4 +1,11 @@
{
"common": {
"console": {
"plugins": {
"execute-failed": "Αποτυχία εκτέλεσης προσθέτου {{pluginName}}::{{contextName}}"
}
}
},
"language": {
"code": "el",
"local-name": "Ελληνικά",

View File

@ -44,7 +44,7 @@
},
"dialog": {
"hide-menu-enabled": {
"detail": "Menu is hidden, use 'Alt' to show it (or 'Escape' if using in-app-menu)",
"detail": "Menu is hidden, use 'Alt' to show it (or 'Escape' if using In-App Menu)",
"message": "Hide Menu is enabled",
"title": "Hide Menu Enabled"
},
@ -96,7 +96,7 @@
"advanced-options": {
"label": "Advanced options",
"submenu": {
"auto-reset-app-cache": "Reset App cache when app starts",
"auto-reset-app-cache": "Reset app cache when app starts",
"disable-hardware-acceleration": "Disable hardware acceleration",
"edit-config-json": "Edit config.json",
"override-user-agent": "Override User-Agent",
@ -105,7 +105,7 @@
"label": "Set proxy",
"prompt": {
"label": "Enter Proxy Address: (leave empty to disable)",
"placeholder": "Example: socks5://127.0.0.1:9999",
"placeholder": "Example: SOCKS5://127.0.0.1:9999",
"title": "Set proxy"
}
},
@ -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"
@ -241,7 +250,7 @@
"smoothness-transition": {
"label": "Smoothness transition",
"submenu": {
"during": "During {{interpolationTime}}s"
"during": "During {{interpolationTime}} s"
}
},
"use-fullscreen": {
@ -289,12 +298,12 @@
"menu": {
"advanced": "Advanced"
},
"name": "Crossfade [beta]",
"name": "Crossfade [Beta]",
"prompt": {
"options": {
"multi-input": {
"fade-in-duration": "Fade in duration (milliseconds)",
"fade-out-duration": "Fade out duration (milliseconds)",
"fade-in-duration": "Fade in duration (ms)",
"fade-out-duration": "Fade out duration (ms)",
"fade-scaling": {
"label": "Fade scaling",
"linear": "Linear",
@ -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"
@ -462,17 +517,17 @@
"keybind-options": {
"hotkey": "Hotkey"
},
"label": "Choose a hotkey for toggle Picture in Picture",
"title": "Picture in Picture Hotkey"
"label": "Choose a hotkey to toggle picture-in-picture",
"title": "Picture-in-picture Hotkey"
}
},
"save-window-position": "Save window position",
"save-window-size": "Save window size",
"use-native-pip": "Use browser native PiP"
},
"name": "Picture in Picture",
"name": "Picture-in-picture",
"templates": {
"button": "Picture in Picture"
"button": "Picture-in-picture"
}
},
"playback-speed": {
@ -519,7 +574,7 @@
"name": "Video Quality Changer"
},
"shortcuts": {
"description": "Allows setting global hotkeys for playback (play/pause/next/previous) + disable media osd by overriding media keys + enable Ctrl/CMD + F to search + enable linux mpris support for mediakeys + custom hotkeys for advanced users",
"description": "Allows setting global hotkeys for playback (play/pause/next/previous) and turning off media OSD by overriding media keys, turning on Ctrl/CMD + F to search, turning on Linux MPRIS support for media keys, and custom hotkeys for advanced users.",
"menu": {
"override-media-keys": "Override Media Keys",
"set-keybinds": "Set Global Song Controls"

View File

@ -16,12 +16,12 @@
"language": {
"code": "es",
"local-name": "Inglés",
"name": "Inglés"
"name": "Spanish"
},
"main": {
"console": {
"did-finish-load": {
"dev-tools": "Carga Finalizada. DevTools abiertas"
"dev-tools": "Carga finalizada. DevTools abiertos"
},
"i18n": {
"loaded": "i18n cargado"
@ -44,7 +44,7 @@
},
"dialog": {
"hide-menu-enabled": {
"detail": "El menú está oculto, use 'Alt' para mostrarlo (o 'Escape' si usa en-app-menu)",
"detail": "El menú está oculto, utiliza \"Alt\" para mostrarlo (o \"Escape\" si utilizas el menú integrado en la aplicación)",
"message": "Menu oculto esta deshabilitado",
"title": "Menú oculto activado"
},
@ -96,7 +96,7 @@
"advanced-options": {
"label": "Opciones avanzadas",
"submenu": {
"auto-reset-app-cache": "Restablecer la caché cuando se inicie la aplicación",
"auto-reset-app-cache": "Restablecer la caché de la aplicación al iniciarla",
"disable-hardware-acceleration": "Desactivar la aceleración por hardware",
"edit-config-json": "Editar config.json",
"override-user-agent": "sobrescribir User-Agent",
@ -105,7 +105,7 @@
"label": "Definir proxy",
"prompt": {
"label": "Introduzca la dirección del proxy: (déjela vacía para desactivarla)",
"placeholder": "Ejemplo: socks5://127.0.0.1:9999",
"placeholder": "Ejemplo: SOCKS5://127.0.0.1:9999",
"title": "Establecer proxy"
}
},
@ -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"
@ -241,7 +250,7 @@
"smoothness-transition": {
"label": "Transición suave",
"submenu": {
"during": "Durante {{interpolationTime}}s"
"during": "Durante {{interpolationTime}} s"
}
},
"use-fullscreen": {
@ -289,12 +298,12 @@
"menu": {
"advanced": "Avanzado"
},
"name": "Crossfade [beta]",
"name": "Crossfade [Beta]",
"prompt": {
"options": {
"multi-input": {
"fade-in-duration": "Duración del fundido (milliseconds)",
"fade-out-duration": "Duración del fundido de salida (milliseconds)",
"fade-in-duration": "Duración del fundido (ms)",
"fade-out-duration": "Duración del fundido de salida (ms)",
"fade-scaling": {
"label": "Escala de fundido",
"linear": "Lineal",
@ -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"
@ -462,17 +516,17 @@
"keybind-options": {
"hotkey": "Tecla de acceso rápido"
},
"label": "Elige una tecla de acceso rápido para alternar al modo \"Dos imágenes a la vez\" PiP",
"title": "Tecla de acceso rápido al modo \"Dos imágenes a la vez\" PiP"
"label": "Elige una tecla de acceso rápido para activar la función de imagen en imagen",
"title": "Tecla de acceso directo a imagen en imagen"
}
},
"save-window-position": "Guardar la posición de la ventana",
"save-window-size": "Guardar tamaño de la ventana",
"use-native-pip": "Utilizar \"Dos imágenes a la vez\" PiP nativo del navegador"
},
"name": "Imagen en Imagen",
"name": "Imagen en imagen",
"templates": {
"button": "Imagen en Imagen"
"button": "Imagen en imagen"
}
},
"playback-speed": {
@ -519,7 +573,7 @@
"name": "Ajustador de calidad de vídeo"
},
"shortcuts": {
"description": "Permite ajustar atajos de teclado generales para reproducción (reproducir/pausar/siguiente/previo) + deshabilita OSD de medios mediante la anulación de las teclas de medios + habilitar Ctrl/CMD + F para buscar + habilitar soporte mpris de linux para teclas de medios + atajos de teclado personalizados para usuarios avanzados",
"description": "Permite configurar teclas de acceso rápido globales para la reproducción (reproducir/pausa/siguiente/anterior) y desactivar el OSD multimedia anulando las teclas multimedia, activar Ctrl/CMD + F para buscar, activar la compatibilidad con MPRIS de Linux para las teclas multimedia y teclas de acceso rápido personalizadas para usuarios avanzados.",
"menu": {
"override-media-keys": "Anular teclas de medios",
"set-keybinds": "Configurar controles globales de canciones"

View File

@ -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",
@ -537,6 +537,9 @@
}
}
},
"skip-disliked-songs": {
"description": "Passer les musiques que je n'aime pas"
},
"skip-silences": {
"description": "Ignorer automatiquement les sections de silence dans les chansons",
"name": "Passer les silences"

View File

@ -0,0 +1,21 @@
{
"common": {
"console": {
"plugins": {
"execute-failed": "Gagal saat mengeksekusi plugin {{pluginName}}::{{contextName}}",
"executed-at-ms": "Plugin {{pluginName}}::{{contextName}} dieksekusi pada {{ms}}ms",
"initialize-failed": "Gagal dalam menginisialisasi plugin \"{{pluginName}}\"",
"load-all": "Memuat semua plugin",
"load-failed": "Gagal memuat plugin \"{{pluginName}}\"",
"loaded": "Plugin \"{{pluginName}}\" dimuat",
"unload-failed": "Gagal untuk memuat plugin \"{{pluginName}}\"",
"unloaded": "Plugin \"{{pluginName}}\" telah dikeluarkan"
}
}
},
"language": {
"code": "id",
"local-name": "Bahasa Indonesia",
"name": "Indonesian"
}
}

View File

@ -15,8 +15,8 @@
},
"language": {
"code": "it",
"local-name": "Inglese",
"name": "Inglese"
"local-name": "Italiano",
"name": "Italian"
},
"main": {
"console": {
@ -44,7 +44,7 @@
},
"dialog": {
"hide-menu-enabled": {
"detail": "Il menu è nascosto, utilizza 'Alt' per visualizzarlo (o 'Escape' se si utilizza il menu In-App)\"",
"detail": "Il menu è nascosto, utilizza 'Alt' per visualizzarlo (o 'Escape' se si utilizza il Menu In-App)\"",
"message": "'Nascondi menu' è attivo",
"title": "'Nascondi menu' attivo"
},
@ -96,7 +96,7 @@
"advanced-options": {
"label": "Opzioni avanzate",
"submenu": {
"auto-reset-app-cache": "Resetta la cache dell'app quando viene riavviata",
"auto-reset-app-cache": "Reimposta la cache dell'app quando viene riavviata",
"disable-hardware-acceleration": "Disabilita l'accelerazione hardware",
"edit-config-json": "Modificare config.json",
"override-user-agent": "Sovrascrivi User-Agent",
@ -105,7 +105,7 @@
"label": "Imposta il proxy",
"prompt": {
"label": "Inserisci l'indirizzo proxy: (lascia vuoto per disabilitare)",
"placeholder": "Esempio: socks5://127.0.0.1:9999",
"placeholder": "Esempio: SOKS5://127.0.0.1:9999",
"title": "Imposta il proxy"
}
},
@ -116,7 +116,7 @@
"auto-update": "Aggiornamento automatico",
"hide-menu": {
"dialog": {
"message": "Il menu verrà nascosto al prossimo avvio. Utilizzare [Alt] per mostrarlo (o backtick [`] se si utilizza il menu In-App)",
"message": "Il menu verrà nascosto al prossimo avvio. Utilizzare [Alt] per mostrarlo (o backtick [`] se si utilizza il Menu In-App)",
"title": "Nascondi menu abilitato"
},
"label": "Nascondi menu"
@ -132,7 +132,7 @@
}
},
"resume-on-start": "Riprendi a riprodurre l'ultimo brano all'avvio dell'app",
"single-instance-lock": "Blocco a istanza singola",
"single-instance-lock": "Permetti una sola istanza dell'app",
"start-at-login": "Avvia al login",
"starting-page": {
"label": "Pagina iniziale",
@ -169,15 +169,16 @@
}
},
"plugins": {
"enabled": "Abilitato",
"label": "Plugin"
"enabled": "Attivato",
"label": "Plugin",
"new": "NUOVO"
},
"view": {
"label": "Visualizzazione",
"submenu": {
"force-reload": "Forza l'aggiornamento",
"reload": "Aggiorna",
"reset-zoom": "Dimensione attuale",
"reset-zoom": "Ripristina dimensione",
"toggle-fullscreen": "Attiva/disattiva Schermo Intero",
"zoom-in": "Zoom in",
"zoom-out": "Zoom out"
@ -201,9 +202,13 @@
},
"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 Colore Album"
"name": "Tema abbinato a colore album"
},
"ambient-mode": {
"description": "Applica un effetto di illuminazione proiettando i colori delicati del video sullo sfondo dello schermo.",
@ -241,7 +246,7 @@
"smoothness-transition": {
"label": "Fluidità transizione",
"submenu": {
"during": "Per {{interpolationTime}}"
"during": "Per {{interpolationTime}} _s"
}
},
"use-fullscreen": {
@ -252,15 +257,15 @@
},
"audio-compressor": {
"description": "Attiva la compressione audio (abbassa il volume delle parti più alte e alza quello delle parti più basse del segnale)",
"name": "Compressore Audio"
"name": "Compressore audio"
},
"blur-nav-bar": {
"description": "Rende la barra di navigazione trasparente e sfuocata",
"name": "Sfuoca Barra di Navigazione"
"name": "Barra di navigazione trasparente"
},
"bypass-age-restrictions": {
"description": "Bypassa la verifica dell'età di YouTube",
"name": "Bypassa le Restrizioni d'Età"
"name": "Aggira i limiti d'età"
},
"captions-selector": {
"description": "Selettore sottotitolo per le tracce audio di YouTube",
@ -282,7 +287,7 @@
},
"compact-sidebar": {
"description": "Imposta sempre la barra laterale in modalità compatta",
"name": "Barra Laterale Compatta"
"name": "Barra laterale compatta"
},
"crossfade": {
"description": "Crossfade tra i brani",
@ -293,8 +298,8 @@
"prompt": {
"options": {
"multi-input": {
"fade-in-duration": "Durata dissolvenza in entrata (millisecondi)",
"fade-out-duration": "Durata dissolvenza in uscita (millisecondi)",
"fade-in-duration": "Durata dissolvenza in entrata (ms)",
"fade-out-duration": "Durata dissolvenza in uscita (ms)",
"fade-scaling": {
"label": "Transizione dissolvenza",
"linear": "Lineare",
@ -311,7 +316,7 @@
"menu": {
"apply-once": "Solo all'avvio"
},
"name": "Disattiva Autoplay"
"name": "Disattiva autoplay"
},
"discord": {
"backend": {
@ -399,7 +404,7 @@
},
"exponential-volume": {
"description": "Rende esponenziale il cursore del volume, in modo da facilitare la selezione di volumi più bassi.",
"name": "Volume Esponenziale"
"name": "Volume esponenziale"
},
"in-app-menu": {
"description": "Migliora l'aspetto delle barre del menu con un look scuro o basato sul colore dell'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,13 +431,58 @@
"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"
},
"no-google-login": {
"description": "Rimuovi i pulsanti di accesso e i link di Google dall'interfaccia",
"name": "Nessun Login di Google"
"name": "Nessun login di Google"
},
"notifications": {
"description": "Mostra una notifica quando viene riprodotto un brano (le notifiche interattive sono disponibili su Windows)",
@ -462,22 +512,22 @@
"keybind-options": {
"hotkey": "Hotkey"
},
"label": "Scegliere un'hotkey per attivare Picture-in-Picture",
"title": "Picture-in-Picture Hotkey"
"label": "Scegliere un'hotkey per attivare Picture-in-picture",
"title": "Picture-in-picture Hotkey"
}
},
"save-window-position": "Salva la posizione della finestra",
"save-window-size": "Salva la dimensione della finestra",
"use-native-pip": "Usa il PiP nativo del browser"
},
"name": "Picture in Picture",
"name": "Picture-in-Picture",
"templates": {
"button": "Picture in Picture"
"button": "Picture-in-Picture"
}
},
"playback-speed": {
"description": "Ascolto veloce, ascolto lento! Aggiunge un cursore che controlla la velocità di riproduzione del brano",
"name": "Velocità Riproduzione",
"name": "Velocità riproduzione",
"templates": {
"button": "Velocità"
}
@ -489,7 +539,7 @@
"custom-volume-steps": "Imposta incrementi di volume personalizzati",
"global-shortcuts": "Hotkey globali"
},
"name": "Volume Preciso",
"name": "Volume preciso",
"prompt": {
"global-shortcuts": {
"keybind-options": {
@ -519,7 +569,7 @@
"name": "Cambia qualità video"
},
"shortcuts": {
"description": "Consente di impostare tasti di scelta rapida globali per la riproduzione (riproduci/pausa/successivo/precedente) + disabilita l'OSD multimediale sovrascrivendo i tasti multimediali + abilita Ctrl/CMD + F per la ricerca + abilita il supporto Linux MPRIS per i tasti multimediali + tasti di scelta rapida personalizzati per utenti avanzati",
"description": "Consente di impostare tasti di scelta rapida globali per la riproduzione (riproduci/pausa/successivo/precedente) + disabilita l'OSD multimediale sovrascrivendo i tasti multimediali + abilita Ctrl/CMD + F per la ricerca + abilita il supporto Linux MPRIS per i tasti multimediali + tasti di scelta rapida personalizzati per utenti avanzati.",
"menu": {
"override-media-keys": "Ridefinisci i tasti multimediali",
"set-keybinds": "Imposta i controlli brano globali"
@ -537,21 +587,25 @@
}
}
},
"skip-disliked-songs": {
"description": "Salta i brani che non ti piacciono",
"name": "Salta i brani che non ti piacciono"
},
"skip-silences": {
"description": "Salta automaticamente le parti silenziose nei brani",
"name": "Salta Silenzi"
"name": "Salta silenzi"
},
"sponsorblock": {
"description": "Salta automaticamente le parti non musicali, come l'intro/outro delle canzoni o le parti dei video musicali in cui non viene riprodotto il brano",
"name": "SponsorBlock"
"name": "Blocco sponsor"
},
"taskbar-mediacontrol": {
"description": "Controlla riproduzione dalla taskbar di Windows",
"name": "Controlli Multimediali Taskbar"
"name": "Controlli multimediali sulla taskbar"
},
"touchbar": {
"description": "Aggiunge un widget TouchBar per gli utenti macOS",
"name": "Touch Bar"
"name": "Touch Bar (per MacOS)"
},
"tuna-obs": {
"description": "Integrazione con il plugin OBS Tuna",
@ -573,7 +627,7 @@
"label": "Modalità",
"submenu": {
"custom": "Brano/Video personalizzato",
"disabled": "Disabilitato",
"disabled": "Disattivato",
"native": "Brano/Video nativo"
}
}
@ -588,7 +642,7 @@
"menu": {
"visualizer-type": "Tipo di visualizzazione"
},
"name": "Visualizzatore"
"name": "Visualizzatore grafico"
}
}
}

View File

@ -105,7 +105,7 @@
"label": "プロキシ",
"prompt": {
"label": "プロキシのアドレスを入力: (空にすると無効化)",
"placeholder": "例: socks5://127.0.0.1:9999",
"placeholder": "例: SOCKS5://127.0.0.1:9999",
"title": "プロキシ"
}
},
@ -519,7 +519,7 @@
"name": "ビデオ品質チェンジャー"
},
"shortcuts": {
"description": "再生用のグローバル ホットキー (再生/一時停止/次/前) の設定 + メディア キーをオーバーライドしてメディア OSD を無効にする + Ctrl/CMD + F による検索を有効にする + メディアキーの Linux mpris サポートを有効にする + 上級ユーザー向けのカスタム ホットキー を可能にします",
"description": "再生用のグローバル ホットキー (再生/一時停止/次/前) の設定メディア キーをオーバーライドしてメディア OSD を無効にするCtrl/CMD + F による検索を有効にする メディアキーの Linux mpris サポートを有効にする 上級ユーザー向けのカスタム ホットキー を可能にします",
"menu": {
"override-media-keys": "メディアキーを上書き",
"set-keybinds": "グローバルソングコントロールを設定する"

View File

@ -105,7 +105,7 @@
"label": "프록시 설정",
"prompt": {
"label": "프록시 주소를 입력하세요: (비워두면 비활성화됨)",
"placeholder": "예제: socks5://127.0.0.1:9999",
"placeholder": "예제: SOCKS5://127.0.0.1:9999",
"title": "프록시 설정"
}
},
@ -170,7 +170,8 @@
},
"plugins": {
"enabled": "활성화",
"label": "확장"
"label": "확장",
"new": "NEW"
},
"view": {
"label": "보기",
@ -190,16 +191,24 @@
"previous": "이전",
"quit": "종료",
"restart": "앱 재시작",
"show": "창 표시"
"show": "창 표시",
"tooltip": {
"default": "유튜브 뮤직",
"with-song-info": "유튜브 뮤직: {{artist}} - {{title}}"
}
}
},
"plugins": {
"adblocker": {
"description": "모든 광고와 트래커를 즉시 차단합니다",
"menu": {
"blocker": "애드블록 타입"
"blocker": "광고 차단 타입"
},
"name": "애드블록"
"name": "광고 차단기"
},
"album-actions": {
"description": "좋아요, 싫어요 버튼을 추가하고, 결과를 재생 목록 또는 앨범의 모든 노래에 적용합니다.",
"name": "앨범 액션"
},
"album-color-theme": {
"description": "앨범 색상 팔레트를 기반으로 동적 테마 및 시각 효과를 적용합니다",
@ -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": "탐색"
@ -519,7 +574,7 @@
"name": "영상 품질 체인저"
},
"shortcuts": {
"description": "재생을 위한 전역 단축키 설정 허용 (재생/일시 정지/다음/이전) + 미디어 키를 재정의하여 미디어 OSD 비활성화 + Ctrl/CMD + F 검색 활성화 + 미디어 키에 대한 리눅스 MPRIS 지원 활성화 + 고급 사용자를 위한 사용자 지정 단축키 지원",
"description": "재생을 위한 전역 단축키 설정 허용 (재생/일시 정지/다음/이전), 미디어 키를 재정의하여 미디어 OSD 비활성화, Ctrl/CMD + F 검색 활성화, 미디어키 지원을 위해 리눅스 MPRIS 지원 활성화, 고급 사용자를 위한 사용자 지정 단축키 지원 추가.",
"menu": {
"override-media-keys": "미디어 키 재정의",
"set-keybinds": "전역 노래 제어 설정"

598
src/i18n/resources/lt.json Normal file
View File

@ -0,0 +1,598 @@
{
"common": {
"console": {
"plugins": {
"execute-failed": "Nepavyko įvykdyti įskiepio {{pluginName}}::{{contextName}}",
"executed-at-ms": "Įskiepis {{pluginName}}::{{contextName}} įvykdytas per {{ms}}ms",
"initialize-failed": "Nepavyko inicijuoti įskiepio \"{{pluginName}}\"",
"load-all": "Kraunama visus įskiepius",
"load-failed": "Nepavyko užkrauti įskiepio \"{{pluginName}}\"",
"loaded": "Įskiepis \"{{pluginName}}\" užkrautas",
"unload-failed": "Nepavyko iškrauti įskiepio \"{{pluginName}}\"",
"unloaded": "Įskiepis \"{{pluginName}}\" iškrautas"
}
}
},
"language": {
"code": "lt",
"local-name": "Lietuvių kalba",
"name": "Lithuanian"
},
"main": {
"console": {
"did-finish-load": {
"dev-tools": "Baigta krauti. \"DevTools\" atidaryta"
},
"i18n": {
"loaded": "\"i18n\" užkrauta"
},
"second-instance": {
"receive-command": "Gauta komanda per protokolą: \"{{command}}\""
},
"theme": {
"css-file-not-found": "CSS failas \"{{cssFile}}\" neegzistuoja, ignoruojama"
},
"unresponsive": {
"details": "Nereguojanti paklaida\n{{error}}"
},
"when-ready": {
"clearing-cache-after-20s": "Išvaloma programos talpykla"
},
"window": {
"tried-to-render-offscreen": "Langas bandė vaizduotis už ekrano ribų, langoDydis={{windowSize}}, ekranoDydis={{displaySize}}, pozicija={{position}}"
}
},
"dialog": {
"hide-menu-enabled": {
"detail": "Meniu yra paslėpta, naudokite 'Alt', kad ją parodyti (arba 'Escape' jei naudojama programos meniu)",
"message": "\"Paslėpti Meniu\" yra įjungta",
"title": "Įjungta \"Paslėpti Meniu\""
},
"need-to-restart": {
"buttons": {
"later": "Vėliau",
"restart-now": "Perkrauti Dabar"
},
"detail": "\"{{pluginName}}\" įskiepis reikalauja perkrovimą, kad veiktų",
"message": "\"{{pluginName}}\" reikia perkrovimo",
"title": "Reikiamas perkrovimas"
},
"unresponsive": {
"buttons": {
"quit": "Išeiti",
"relaunch": "Perleisti",
"wait": "Palaukti"
},
"detail": "Mes apgailestaujame dėl nepatogumų! prašau pasirinkti ką daryti:",
"message": "Programa Neatsako",
"title": "Langas Neatsako"
},
"update-available": {
"buttons": {
"disable": "Išjungti Atnaujinimus",
"download": "Atsisiųsti",
"ok": "Gerai"
},
"detail": "Nauja versija yra prieinama ir gali būti atsisiųsta {{downloadLink}}",
"message": "Nauja versija yra prieinama",
"title": "Prieinamas Atnaujinimas"
}
},
"menu": {
"about": "Apie",
"navigation": {
"label": "Navigacija",
"submenu": {
"copy-current-url": "Nukopijuoti dabartinį URL",
"go-back": "Grįžti Atgal",
"go-forward": "Eiti į priekį",
"quit": "Išeiti",
"restart": "Perkrauti programą"
}
},
"options": {
"label": "Nustatymai",
"submenu": {
"advanced-options": {
"label": "Išplėstiniai nustatymai",
"submenu": {
"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\"",
"restart-on-config-changes": "Perkrauti po config pasikeitimo",
"set-proxy": {
"label": "Nustatyti įgaliotajį serverį",
"prompt": {
"label": "Įvesti Įgaliotojo serverio adresą: (palikti tuščią, kad išjungti)",
"placeholder": "Pavyzdys: SOCKS5://127.0.0.1:9999",
"title": "Nustatyti įgaliotajį serverį"
}
},
"toggle-dev-tools": "Įjungti/Išjungti DevTools"
}
},
"always-on-top": "Visada viršuje",
"auto-update": "Automatinis Atnaujinimas",
"hide-menu": {
"dialog": {
"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"
},
"language": {
"dialog": {
"message": "Kalba bus pakeista po perkrovimo",
"title": "Kalba Pakeista"
},
"label": "Kalba",
"submenu": {
"to-help-translate": "Norite padėti išversti? Paspauskite čia"
}
},
"resume-on-start": "Tęsti paskutinę dainą, kai programa bus paleista",
"single-instance-lock": "Vienkartinis užraktas",
"start-at-login": "Pradėti nuo prisijungimo",
"starting-page": {
"label": "Pradžios puslapis",
"unset": "Nenustatyta"
},
"tray": {
"label": "Padėklas",
"submenu": {
"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": "Paleisti/Pristabdyti ant paspaudimo"
}
},
"visual-tweaks": {
"label": "Vizualiniai patobulinimai",
"submenu": {
"like-buttons": {
"default": "Numatytasis",
"force-show": "Priversti rodyti",
"hide": "Slėpti",
"label": "\"Patinka\" mygtukai"
},
"remove-upgrade-button": "Nerodyti \"Patobulinti\" mygtuko",
"theme": {
"label": "Tema",
"submenu": {
"import-css-file": "Įkelti pasirinktinį CSS failą",
"no-theme": "Be temos"
}
}
}
}
}
},
"plugins": {
"enabled": "Įjungta",
"label": "Įskiepiai"
},
"view": {
"label": "Vaizdas",
"submenu": {
"force-reload": "Priverstinai perkrauti",
"reload": "Perkrauti",
"reset-zoom": "Tikras dydis",
"toggle-fullscreen": "Įjungti/Išjungti Pilną Ekraną",
"zoom-in": "Priartinti",
"zoom-out": "Nutolinti"
}
}
},
"tray": {
"next": "Kitas",
"play-pause": "Paleisti/Pristabdyti",
"previous": "Ankstesnis",
"quit": "Išeiti",
"restart": "Perkrauti programą",
"show": "Rodyti langą"
}
},
"plugins": {
"adblocker": {
"description": "Blokuoti visas reklamas ir seklius",
"menu": {
"blocker": "Blokuotojas"
},
"name": "Reklamų blokuotojas"
},
"album-color-theme": {
"description": "Pritaiko dinamišką temą ir vizualinius efektus pagal albumo spalvų paletę",
"name": "Albumo Spalvų Tema"
},
"ambient-mode": {
"description": "Pritaiko apšvietimo efektą, perteikdamas švelnias vaizdo įrašo spalvas į ekrano foną.",
"menu": {
"blur-amount": {
"label": "Suliejimo kiekis",
"submenu": {
"pixels": "{{blurAmount}} pikseliai"
}
},
"buffer": {
"label": "Buferis",
"submenu": {
"buffer": "{{buffer}}"
}
},
"opacity": {
"label": "Skaidrumas",
"submenu": {
"percent": "{{opacity}}%"
}
},
"quality": {
"label": "Kokybė",
"submenu": {
"pixels": "{{quality}} pikseliai"
}
},
"size": {
"label": "Dydis",
"submenu": {
"percent": "{{size}}%"
}
},
"smoothness-transition": {
"label": "Perliejimo švelnumas",
"submenu": {
"during": "Per {{interpolationTime}}s"
}
},
"use-fullscreen": {
"label": "Naudojamas visas ekranas"
}
},
"name": "Aplinkos rėžimas"
},
"audio-compressor": {
"description": "Pritaikyti garso kompresiją (sumažina garsiausių signalo dalių garsumą ir padidina švelniausių dalių garsumą)",
"name": "Garso Kompresorius"
},
"blur-nav-bar": {
"description": "Padaro navigacijos lentą permatomą ir susiliejusią",
"name": "Sulieti Navigacijos Lentą"
},
"bypass-age-restrictions": {
"description": "Apeiti \"Youtube\" amžiaus patikrinimą",
"name": "Apeiti Amžiaus Apribojimus"
},
"captions-selector": {
"description": "„YouTube Music“ Garso takelių antraščių parinkiklis",
"menu": {
"autoload": "Automatiškai pasirinkti paskutinę naudotą antraštę",
"disable-captions": "Pagal numatytuosius nustatymus išjungti antraštės"
},
"name": "Antraščių parinkiklis",
"prompt": {
"selector": {
"label": "Dabartinė antraščių kalba: {{language}}",
"none": "Joks",
"title": "Pasirinkti antraščių kalbą"
}
},
"templates": {
"title": "Atidaryti antraščių parinkiklį"
}
},
"compact-sidebar": {
"description": "Visada nustatyti šoninę juostą kompaktiniame rėžime",
"name": "Kompaktinė šoninė juosta"
},
"crossfade": {
"description": "Perliejimas tarp dainų",
"menu": {
"advanced": "Išplėstinė"
},
"name": "Perliejimas [Beta]",
"prompt": {
"options": {
"multi-input": {
"fade-in-duration": "Išblukimo trukmė (ms)",
"fade-out-duration": "Išnykimo trukmė (ms)",
"fade-scaling": {
"label": "Išblukimo stiprumas",
"linear": "Linijinis",
"logarithmic": "Logaritminis"
},
"seconds-before-end": "Pradėti lieti dainas N sekundžių prieš pabaigą"
},
"title": "Perliejimo nustatymai"
}
}
},
"disable-autoplay": {
"description": "Pradeda dainą pristabdytame rėžime",
"menu": {
"apply-once": "Pritaiko tik per programos paleidimą"
},
"name": "Išjungti Automatinį leidimą"
},
"discord": {
"backend": {
"already-connected": "Bandyta prisijungti naudojant aktyvų ryšį",
"connected": "Prisijungta prie \"Discord\"",
"disconnected": "Atsijungta nuo \"Discord\""
},
"description": "Parodyk savo draugams ko tu klausaisi su \"Turtingas Buvimas\" (Rich Presence)",
"menu": {
"auto-reconnect": "Automatiškai prisijungti",
"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",
"hide-github-button": "Slėpti \"GitHub\" nuorodos mygtuką",
"play-on-youtube-music": "Leisti ant \"Youtube Music\"",
"set-inactivity-timeout": "Nustatyti neveiklumo laiką"
},
"name": "\"Discord\" Turtingas Buvimas (Rich Presence)",
"prompt": {
"set-inactivity-timeout": {
"label": "Įveskite neveiklumo skirtąjį laiką sekundėmis:",
"title": "Nustatyti neveiklumo laiką"
}
}
},
"downloader": {
"backend": {
"dialog": {
"error": {
"buttons": {
"ok": "Gerai"
},
"message": "Aaa! Apgailestaujame, nepavyko atsisiųsti…",
"title": "Paklaida atsisiunčiant!"
},
"start-download-playlist": {
"buttons": {
"ok": "Gerai"
},
"detail": "({{playlistSize}} dainos)",
"message": "Atsisiunčiama {{playlistTitle}} grojaraštį",
"title": "Prasidėjo atsisiuntimas"
}
},
"feedback": {
"conversion-progress": "Konversija: {{percent}}%",
"converting": "Konvertuojama…",
"done": "Baigta: {{filePath}}",
"download-info": "Atsiunčiama {{artist}} - {{title}} {{videoId}}",
"download-progress": "Atsisiuntimas: {{percent}}%",
"downloading": "Atsisiunčiama…",
"downloading-counter": "Atsisiunčiama {{current}}/{{total}}…",
"downloading-playlist": "Atsisiunčiamas grojaraštis \"{{playlistTitle}}\" - {{playlistSize}} dainų {{playlistId}}",
"error-while-downloading": "Paklaida atsisiunčiant \"{{author}} - {{title}}\": {{error}}",
"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ą 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 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}}",
"video-id-not-found": "Vaizdo įrašas nerastas",
"writing-id3": "Rašoma ID3 žymes…"
}
},
"description": "Atsisiunčia MP3 / šaltinio garsą tiesiogiai iš sąsajos",
"menu": {
"choose-download-folder": "Pasirinkti atsisiuntimų aplanką",
"download-playlist": "Atsisiųsti grojaraštį",
"presets": "Iš anksto nustatyti nustatymai",
"skip-existing": "Praleisti egzistuojančius failus"
},
"name": "Atsiuntėjas",
"renderer": {
"can-not-update-progress": "Nepavyko atnaujinti eigos"
},
"templates": {
"button": "Atsisiųsti"
}
},
"exponential-volume": {
"description": "Padaro garsumo slankiklį eksponentinį, kad būtų lengviau pasirinkti mažesnį garsumą.",
"name": "Eksponentinis garsas"
},
"in-app-menu": {
"description": "Duoda meniu lentoms įmantrią, tamsią ar albumo spalvos išvaizdą",
"menu": {
"hide-dom-window-controls": "Slėpti DOM lango kontroles"
},
"name": "Programos Meniu"
},
"last-fm": {
"description": "Pridėkite Last.fm scrobble palaikymą",
"name": "Last.fm"
},
"lumiastream": {
"description": "Prideda \"Lumia Stream\" palaikymą",
"name": "Lumia Stream [Beta]"
},
"lyrics-genius": {
"description": "Prideda daugumai dainių žodžių tekstus",
"menu": {
"romanized-lyrics": "Romanizuoti dainų tekstai"
},
"name": "\"Genius\" Žodžių tekstai",
"renderer": {
"fetched-lyrics": "Gauti žodžiai iš „Genius“"
}
},
"navigation": {
"description": "Kitas/Ankstenis navigacijos rodyklės tiesiogiai integruotos sąsajoje, kaip tavo mėgstamiausioje naršyklėje",
"name": "Navigacija"
},
"no-google-login": {
"description": "Pašalinti \"Google\" prisijungimo mygtukus ir nuorodas iš sąsjos",
"name": "Be \"Google\" Prisijungimo"
},
"notifications": {
"description": "Rodyti pranešimą, kai pradeda groti daina (interaktyvūs pranešimai pasiekiami sistemoje \"Windows\")",
"menu": {
"interactive": "Interaktyvūs pranešimai",
"interactive-settings": {
"label": "Interaktyvūs nustatymai",
"submenu": {
"hide-button-text": "Paslėpti mygtuko tekstą",
"refresh-on-play-pause": "Atnaujinti ant Paleidimo/Pristabdymo",
"tray-controls": "Atidaryti/Uždaryti ant padėklo paspaudimo"
}
},
"priority": "Pranešimų pirminybė",
"toast-style": "Skrudintas stilius",
"unpause-notification": "Rodyti pranešimus po dainos paleidimo"
},
"name": "Pranešimai"
},
"picture-in-picture": {
"description": "Leidžia pakeisti programą į \"picture-in-picture\" rėžimą",
"menu": {
"always-on-top": "Visada ant viršaus",
"hotkey": {
"label": "Spartusis klavišas",
"prompt": {
"keybind-options": {
"hotkey": "Spartusis klavišas"
},
"label": "Pasirinkti spartujį klaviša, kad įjungti/išjungti \"picture-in-picture\"",
"title": "\"Picture-in-picture\" Spartusis klavišas"
}
},
"save-window-position": "Išsaugoti lango poziciją",
"save-window-size": "Išsaugoti lango dydį",
"use-native-pip": "Naudoti naršyklės savajį PiP"
},
"name": "Picture-in-picture",
"templates": {
"button": "Picture-in-picture"
}
},
"playback-speed": {
"description": "Klausyk greitai, klausyk lėtai! Prideda slankiklį, kuris valdo dainos greitį",
"name": "Atkūrimo Greitis",
"templates": {
"button": "Greitis"
}
},
"precise-volume": {
"description": "Tiksliai valdykite garsumą naudodami pelės ratuką / sparčiuosius klavišus, naudodami pritaikytą HUD ir pritaikomus garsumo žingsnius",
"menu": {
"arrows-shortcuts": "Vietiniai rodyklių klavišai valdikliai",
"custom-volume-steps": "Nustatykite Pasirinktinius Garsumo Žingsnius",
"global-shortcuts": "Pasauliniai spartieji klavišai"
},
"name": "Tikslus Garsas",
"prompt": {
"global-shortcuts": {
"keybind-options": {
"decrease": "Pamažinti Garsą",
"increase": "Padidinti Garsą"
},
"label": "Pasirinkti Pasaulinius garso klavišus:",
"title": "Pasauliniai Garso Klavišai"
},
"volume-steps": {
"label": "Pasirinkti Garso Didinimo/Mažinimo Žingsnius",
"title": "Garso Žingsniai"
}
}
},
"quality-changer": {
"backend": {
"dialog": {
"quality-changer": {
"detail": "Dabartinė Kokybė: {{quality}}",
"message": "Pasirinkite Vaizdo Kokybę:",
"title": "Pasirinkite Vaizdo Kokybę"
}
}
},
"description": "Leidžia pakeisti vaizdo kokybę su mygtuku ant vaizdo perdangos",
"name": "Vaizdo Kokybės Pakeitėjas"
},
"shortcuts": {
"description": "Leidžia nustatyti visuotinius atkūrimo sparčiuosius klavišus (paleisti / pristabdyti / kitą / ankstesnį) ir išjungti medijos OSD nepaisant medijos klavišų, įjungti Ctrl / CMD + F ieškoti, įjungti Linux MPRIS palaikymą medijos klavišams ir pasirinktinius sparčiuosius klavišus pažengusiems vartotojams.",
"menu": {
"override-media-keys": "Perrašyti Medijos klavišus",
"set-keybinds": "Nustatyti Pasaulines Dainų Kontroles"
},
"name": "Spartieji klavišai (& MPRIS)",
"prompt": {
"keybind": {
"keybind-options": {
"next": "Kitas",
"play-pause": "Paleisti / Pristabdyti",
"previous": "Ankstesnis"
},
"label": "Pasirinkti Pasaulinius Klavišus Dainų Kontroliavimui:",
"title": "Pasauliniai Klavišai"
}
}
},
"skip-disliked-songs": {
"description": "Praleidžia nepatinkančias dainas",
"name": "Praleisti Nepatinkančias Dainas"
},
"skip-silences": {
"description": "Automatiškai praleisti tylos dalis dainose",
"name": "Praleisti Tylumas"
},
"sponsorblock": {
"description": "Automatiškai praleidžia ne muzikines dalis, pvz., įžangą/užvedimą arba muzikinių vaizdo įrašų dalis, kuriose daina negrojama",
"name": "Rėmėjų blokuotojas"
},
"taskbar-mediacontrol": {
"description": "Valdykite atkūrimą iš „Windows“ užduočių juostos",
"name": "Užduočių juostos medijos valdymas"
},
"touchbar": {
"description": "Pridedamas jutiklinės juostos valdiklis MacOS vartotojams",
"name": "TouchBar"
},
"tuna-obs": {
"description": "Integracija su OBS papildiniu \"Tuna\"",
"name": "Tuna OBS"
},
"video-toggle": {
"description": "Pridedamas mygtukas, skirtas perjungti vaizdo įrašo/dainos režimą. taip pat galite pasirinktinai pašalinti visą vaizdo įrašo skirtuką",
"menu": {
"align": {
"label": "Lygiavimas",
"submenu": {
"left": "Kairė",
"middle": "Vidurys",
"right": "Dešinė"
}
},
"force-hide": "Priverstinai pašalinti vaizdo įrašo skirtuką",
"mode": {
"label": "Rėžimas",
"submenu": {
"custom": "Pasirinktinis perjungimas",
"disabled": "Išjungta",
"native": "Vietinis perjungimas"
}
}
},
"name": "Vaizdo įrašo perjungimas",
"templates": {
"button": "Daina"
}
},
"visualizer": {
"description": "Prie grotuvo pridedamas vizualizatorius",
"menu": {
"visualizer-type": "Vizualizatoriaus tipas"
},
"name": "Vizualizatorius"
}
}
}

View File

@ -1,25 +1,160 @@
{
"common": {
"console": {
"plugins": {
"execute-failed": "Klarte ikke å kjøre programtillegg {{pluginName}}::{{contextName}}",
"executed-at-ms": "Det tok {{ms}} ms å kjøre {{pluginName}}::{{contextName}}",
"initialize-failed": "Klarte ikke å igangsette «{{pluginName}}»",
"load-all": "Laster inn alle programtillegg",
"load-failed": "Klarte ikke å laste inn {{pluginName}}-programtillegget",
"loaded": "Lastet inn {{pluginName}}-programtillegget",
"unload-failed": "Kunne ikke skru av {{pluginName}}-programtillegget",
"unloaded": "{{pluginName}}-programtillegg avskrudd"
}
}
},
"language": {
"code": "nb_NO",
"local-name": "Norsk bokmål",
"name": "Norwegian Bokmål"
},
"main": {
"console": {
"did-finish-load": {
"dev-tools": "Innlasting fullført. Utviklerverktøy åpnet."
},
"i18n": {
"loaded": "språkstøtte innlastet"
},
"second-instance": {
"receive-command": "Mottok kommando over protokoll: \"{{command}}\""
},
"theme": {
"css-file-not-found": "CSS-filen «{{cssFile}}» finnes ikke. Ignorerer."
},
"unresponsive": {
"details": "Svarer ikke\n{{error}}"
},
"when-ready": {
"clearing-cache-after-20s": "Tømmer programhurtiglager"
},
"window": {
"tried-to-render-offscreen": "Prøvde å tegne vindu utenfor skjermen. Størrelse={{windowSize}}, skjermstørrelse={{displaySize}}, posisjon={{position}}"
}
},
"dialog": {
"hide-menu-enabled": {
"detail": "Menyen er skjult. Bruk «Alt» for å vise den, (ller «Esc» for å bruke menyen i programmet).",
"message": "Meny skjult",
"title": "Meny vist"
},
"need-to-restart": {
"buttons": {
"later": "Senere",
"restart-now": "Start på ny nå"
},
"detail": "{{pluginName}}-programtillegget krever programomstart før bruk.",
"message": "{{pluginName}}-programtillegget krever programomstart.",
"title": "Programomstart kreves"
},
"unresponsive": {
"buttons": {
"quit": "Avslutt",
"relaunch": "Start igjen",
"wait": "Vent"
},
"detail": "Velg blandt følgende:",
"message": "Programmet svarer ikke",
"title": "Vinduet svarer ikke"
},
"update-available": {
"buttons": {
"disable": "Skru av oppgraderinger",
"download": "Last ned",
"ok": "OK"
},
"detail": "En ny versjon er tilgjengelig og kan lastes ned fra {{downloadLink}}",
"message": "En ny versjon er tilgjengelig",
"title": "Oppgradering tilgjengelig"
}
},
"menu": {
"about": "Om",
"navigation": {
"label": "Navigasjon",
"submenu": {
"copy-current-url": "Kopier nåværende nettadresse",
"go-back": "Tilbake",
"go-forward": "Forover",
"quit": "Avslutt",
"restart": "Programomstart"
}
},
"options": {
"label": "Alternativer",
"submenu": {
"advanced-options": {
"label": "Avanserte alternativer",
"submenu": {
"auto-reset-app-cache": "Tilbakestill programhurtiglager når programmet startes",
"disable-hardware-acceleration": "Skru av maskinvareakselerasjon",
"edit-config-json": "Rediger config.json",
"override-user-agent": "Overstyr brukeragent",
"restart-on-config-changes": "Omstart ved oppsettsendringer",
"set-proxy": {
"label": "Sett mellomtjener",
"prompt": {
"label": "Skriv inn mellomtjeneradresse: (la stå tom for å skru av)",
"placeholder": "Eksempel: socks5://127.0.0.1:9999",
"title": "Sett mellomtjener"
}
},
"toggle-dev-tools": "Skru av/på utviklerverktøy"
}
},
"always-on-top": "Alltid på toppen",
"auto-update": "Auto-oppdatering",
"hide-menu": {
"dialog": {
"message": "Menyen vil bli skjult ved neste programstart. Bruk [Alt] for å vise den, eller gravistegn [`] hvis du bruker menyen i programmet).",
"title": "Skjuler meny"
},
"label": "Skjul meny"
},
"language": {
"dialog": {
"message": "Nytt språk vises når programmet startes på ny",
"title": "Språk endret"
},
"label": "Språk",
"submenu": {
"to-help-translate": "Klikk her for å bistå oversettelsen"
}
},
"resume-on-start": "Gjenoppta siste spor ved programstart",
"single-instance-lock": "Sperr én instans",
"start-at-login": "Start ved innlogging",
"starting-page": {
"label": "Startside",
"unset": "Opphev"
},
"tray": {
"label": "Systemkurv",
"submenu": {
"disabled": "Avskrudd"
"disabled": "Avskrudd",
"play-pause-on-click": "Spill av/pause ved klikk"
}
},
"visual-tweaks": {
"label": "Visuelle tilpasninger",
"submenu": {
"like-buttons": {
"default": "Forvalg",
"force-show": "Tving visning",
"hide": "Skjul",
"label": "Begunstningsknapper"
},
"remove-upgrade-button": "Fjern oppgraderingsknapp",
"theme": {
"label": "Drakt",
"submenu": {
@ -32,8 +167,28 @@
}
},
"plugins": {
"enabled": "Påskrudd",
"label": "Programtillegg"
},
"view": {
"label": "Vis",
"submenu": {
"force-reload": "Tving gjeninnlasting",
"reload": "Gjeninnlast",
"reset-zoom": "Faktisk størrelse",
"toggle-fullscreen": "Veksle fullskjermsvisning",
"zoom-in": "Forstørr",
"zoom-out": "Forminsk"
}
}
},
"tray": {
"next": "Neste",
"play-pause": "Spill av/pause",
"previous": "Forrige",
"quit": "Avslutt",
"restart": "Programomstart",
"show": "Vis vindu"
}
},
"plugins": {
@ -49,6 +204,7 @@
"name": "Albumsfargedrakt"
},
"ambient-mode": {
"description": "Ifører lyseffekt ved å hente myke farger fra videoen inn i skjermens bakgrunn.",
"menu": {
"blur-amount": {
"label": "Tilsløringsmengde",
@ -56,6 +212,12 @@
"pixels": "{{blurAmount}} piksler"
}
},
"buffer": {
"label": "Mellomlager",
"submenu": {
"buffer": "{{buffer}}"
}
},
"opacity": {
"label": "Dekkevne",
"submenu": {
@ -73,10 +235,38 @@
"submenu": {
"percent": "{{size}}%"
}
},
"smoothness-transition": {
"label": "Mykhetsovergang",
"submenu": {
"during": "I løpet av {{interpolationTime}} s"
}
},
"use-fullscreen": {
"label": "Ved bruk av fullskjerm"
}
}
},
"name": "Omgivelsesmodus"
},
"audio-compressor": {
"description": "Anvend kompresjon for lyd (senker lydstyrken for de kraftigste delene av signalet og øker nivået i de svakeste)",
"name": "Lydkompressor"
},
"blur-nav-bar": {
"description": "Gjør navigeringsbjelken gjennomsiktig og tilslørt",
"name": "Tilslør navigasjonsfelt"
},
"bypass-age-restrictions": {
"description": "Omgå YouTube sin aldersgrenser",
"name": "Omgå aldersgrense"
},
"captions-selector": {
"description": "Undertekstverktøy for lydspor i YouTube Music",
"menu": {
"autoload": "Auto-velg sist brukte undertekst",
"disable-captions": "Ingen undertekst som forvalg"
},
"name": "Undertekstvelger",
"prompt": {
"selector": {
"label": "Nåværende tekstingsspråk: {{language}}",
@ -88,27 +278,63 @@
"title": "Åpne undertekstvelger"
}
},
"compact-sidebar": {
"description": "Alltid sett sidefeltet i kompakt modus",
"name": "Kompakt sidefelt"
},
"crossfade": {
"description": "Overgang mellom spor",
"menu": {
"advanced": "Avansert"
},
"name": "Overgang [Beta]",
"prompt": {
"options": {
"multi-input": {
"fade-in-duration": "Inntoningsvarighet (ms)",
"fade-out-duration": "Uttoningsvarighet (ms)",
"fade-scaling": {
"label": "Overgangsskalering",
"linear": "Lineær",
"logarithmic": "Logaritmisk"
}
}
},
"seconds-before-end": "Overgang antall sekunder før slutt"
},
"title": "Overgangsalternativer"
}
}
},
"disable-autoplay": {
"description": "Merkerer sporstart i «pauset» modus",
"menu": {
"apply-once": "Har kun innvirkning ved oppstart"
},
"name": "Skru av autospilling"
},
"discord": {
"backend": {
"already-connected": "Forsøkte å koble til med aktiv tilkobling",
"connected": "Tilkoblet Discord",
"disconnected": "Frakoblet fra Discord"
},
"description": "Vis venne dine hva du lytter til med rik tilstedeværelse",
"menu": {
"auto-reconnect": "Automatisk retilkobling",
"clear-activity": "Tøm aktivitet",
"clear-activity-after-timeout": "Tøm aktivitet etter tidsavbrudd",
"connected": "Tilkoblet",
"disconnected": "Frakoblet",
"hide-duration-left": "Skjul gjenværende tid",
"hide-github-button": "Skjul GitHub-lenkeknapp",
"play-on-youtube-music": "Spill på YouTube Music",
"set-inactivity-timeout": "Sett tid før tidsavbrudd"
},
"name": "Rik tilstedeværelse for Discord",
"prompt": {
"set-inactivity-timeout": {
"label": "Skriv inn antall sekunder for inaktivitetstidsavbrudd:",
"title": "Sett inaktivitetstidsavbrudd"
}
}
},
"downloader": {
@ -117,12 +343,16 @@
"error": {
"buttons": {
"ok": "OK"
}
},
"message": "Nedlasting mislyktes …",
"title": "Feil i nedlastning."
},
"start-download-playlist": {
"buttons": {
"ok": "OK"
},
"detail": "({{playlistSize}} spor)",
"message": "Laster ned {{playlistTitle}}-spillelisten …",
"title": "Nedlasting startet"
}
},
@ -130,42 +360,147 @@
"conversion-progress": "Konvertering: {{percent}}%",
"converting": "Konverterer …",
"done": "Ferdig: {{filePath}}",
"download-info": "Laster ned {{artist}} — {{title}} [{{videoId}} …",
"download-progress": "Nedlastet: {{percent}}%",
"downloading": "Laster ned …",
"downloading-counter": "Laster ned {{current}}/{{total}} …",
"downloading-playlist": "Laster ned {{playlistTitle}}-spillelisten — {{playlistSize}} spor ({{playlistId}})",
"error-while-downloading": "Kunne ikke laste ned «{{author}} — {{title}}»: {{error}}",
"folder-already-exists": "{{playlistFolder}}-mappen finnes allerede",
"getting-playlist-info": "Henter spillelisteinfo …",
"loading": "Laster inn …",
"playlist-has-only-one-song": "Spillelisten har kun ett element. Laster ned direkte.",
"playlist-id-not-found": "Fant ingen spilleliste-ID",
"playlist-is-empty": "Tom spilleliste",
"playlist-is-mix-or-private": "Kunne ikke hente spillelisteinfo. Forsikre deg om at den ikke er privat eller «Mikset for deg».\n\n{{error}}",
"preparing-file": "Forbereder fil …",
"saving": "Lagrer …"
"saving": "Lagrer …",
"trying-to-get-playlist-id": "Prøver å hente spilleliste-ID: {{playlistId}}",
"video-id-not-found": "Fant ikke videoen",
"writing-id3": "Skriver ID3-tagger …"
}
},
"description": "Laster ned MP3/kildelyd direkte fra grensesnittet",
"menu": {
"choose-download-folder": "Velg nedlastningsmappe",
"download-playlist": "Last ned spilleliste",
"presets": "Forhåndsinnstillinger",
"skip-existing": "Hopp over eksisterende filer"
},
"name": "Nedlaster",
"renderer": {
"can-not-update-progress": "Kan ikke oppdatere framdrift"
},
"templates": {
"button": "Last ned"
}
},
"exponential-volume": {
"description": "Gjør lydstyrkekontrollen eksponentiell, slik at det er enklere velge lavere lydstyrker.",
"name": "Eksponentiell lydstyrke"
},
"in-app-menu": {
"description": "Gir menybjelkene stilig, mørk, eller albumfarget utseende",
"menu": {
"hide-dom-window-controls": "Skjul DOM-vinduskontroller"
},
"name": "Meny i programmet"
},
"last-fm": {
"description": "Legg til lyttestatistikkstøtte for Last.fm",
"name": "Last.fm"
},
"lumiastream": {
"description": "Legger til Lumia Stream-støtte",
"name": "Lumia Stream [Beta]"
},
"lyrics-genius": {
"description": "Gir sangtekststøtte for de fleste spor",
"menu": {
"romanized-lyrics": "Romaniserte sangtekster"
},
"name": "Sangtekster fra Genius",
"renderer": {
"fetched-lyrics": "Henter sangtekster for Genius"
}
},
"navigation": {
"description": "Direkte integrering av neste/tilbake-navigasjonspilene i grensesnittet, som din favorittnettleser",
"name": "Navigasjon"
},
"no-google-login": {
"description": "Fjern Google-innloggingsknapper og lenker fra grensesnittet",
"name": "Ingen Google-innlogging"
},
"notifications": {
"description": "Vis en merknad når et spor starter avspilling (interaktive merknader er tilgjengelig på Windows)",
"menu": {
"interactive": "Interaktive merknader",
"interactive-settings": {
"label": "Interaktive innstillinger",
"submenu": {
"hide-button-text": "Skjul knappetekst",
"refresh-on-play-pause": "Gjenoppfrisk ved avspilling/pause",
"tray-controls": "Åpne/lukk med klikk i systemkurven"
}
},
"priority": "Merknadsprioritet",
"unpause-notification": "Vis merknad ved oppheving av pause"
},
"name": "Merknader"
},
"picture-in-picture": {
"description": "Tillater å bytte programmet til bilde-i-bilde modus",
"menu": {
"save-window-position": "Lagre vindusposisjon"
"always-on-top": "Alltid på toppen",
"hotkey": {
"label": "Hurtigtast",
"prompt": {
"keybind-options": {
"hotkey": "Hurtigtast"
},
"label": "Velg en hurtigtast for veksling av bilde-i-bilde",
"title": "Hurtigtast for bilde-i-bilde"
}
},
"save-window-position": "Lagre vindusposisjon",
"save-window-size": "Lagre vindusstørrelse",
"use-native-pip": "Bruk nettleserens innebygde bilde-i-bilde"
},
"name": "Bilde-i-bilde",
"templates": {
"button": "Bilde-i-bilde"
}
},
"playback-speed": {
"description": "Legger til glidebryter som kontrollerer avspillingshastighet",
"name": "Avspillingshastighet",
"templates": {
"button": "Hastighet"
}
},
"precise-volume": {
"name": "Presis lydstyrkejustering"
"description": "Kontroller lydstyrken presist ved bruk av musehjul/hurtigtaster, med egendefinert skjermoverlag og tilpassbare steg",
"menu": {
"arrows-shortcuts": "Kontroller for lokale piltaster",
"custom-volume-steps": "Sett egendefinerte lydstyrkesteg",
"global-shortcuts": "Hurtigtaster i hele programmet"
},
"name": "Presis lydstyrkejustering",
"prompt": {
"global-shortcuts": {
"keybind-options": {
"decrease": "Senk lydstyrke",
"increase": "Øk lydstyrke"
},
"label": "Velg tastetilknytninger for lydstyrkejusteringskontroller i hele programmet",
"title": "Tastetilknytninger for lydstyrkejusteringskontroller i hele programmet"
},
"volume-steps": {
"label": "Velg steg for økning/senking av lydstyrke",
"title": "Lydstyrkesteg"
}
}
},
"quality-changer": {
"backend": {
@ -176,30 +511,85 @@
"title": "Velg videokvalitet"
}
}
},
"description": "Tillat endring av videokvalitet med en knapp i videooverlaget",
"name": "Vindukvalitetsvelger"
},
"shortcuts": {
"description": "Tillater bruk av hurtigtaster for hele programmet til avspilling (spill/pause/neste/forrige) + skru av media-videooverlag ved å overstyre mediataster + skru på Ctrl+CMD+F for å søke, pluss å egge til MPRIS støtte på linux for mediataster pluss egendefinerte hurtigtaster for avanserte brukere",
"menu": {
"override-media-keys": "Overstyr mediataster",
"set-keybinds": "Sett kontroller for spor i hele programmet"
},
"name": "Snarveier (og MPRIS)",
"prompt": {
"keybind": {
"keybind-options": {
"next": "Neste",
"play-pause": "Spill av/pause",
"previous": "Forrige"
},
"label": "Velg kontroller for spor i hele programmet:",
"title": "Tastaturtilknytninger i hele programmet"
}
}
},
"skip-disliked-songs": {
"description": "Hopper over mislikte spor",
"name": "Hopp over mislikte spor"
},
"skip-silences": {
"description": "Hopp over stille deler av spor",
"name": "Hopp over pauser"
},
"sponsorblock": {
"description": "Hopper over ikke-musikalske deler, som intro/sluttsats, eller deler av musikkvideoer der ingen musikk spilles",
"name": "SponsorBlock"
},
"taskbar-mediacontrol": {
"description": "Kontroller avspilling fra din Windows-oppgavelinje",
"name": "Oppgavelinje-mediakontroll"
},
"touchbar": {
"description": "Legger til et pekefelt-miniprogram for macOS-brukere",
"name": "Pekefelt"
},
"tuna-obs": {
"description": "Integrasjon med Tuna-programtillegget i OBS",
"name": "OBS Tuna"
},
"video-toggle": {
"description": "Leger til en knapp for å bytte mellom video/spormodus. Kan også alternativt fjerne hele videofanen.",
"menu": {
"align": {
"label": "Justering",
"submenu": {
"left": "Venstre",
"middle": "Midten",
"right": "Høyre"
}
},
"force-hide": "Påtving fjerning av videofane",
"mode": {
"label": "Modus"
"label": "Modus",
"submenu": {
"custom": "Egendefinert veksling",
"disabled": "Avskrudd",
"native": "Innebygd veksling"
}
}
},
"name": "Videoveksling",
"templates": {
"button": "Spor"
}
},
"visualizer": {
"description": "Legger til en visualisator i avspilleren",
"menu": {
"visualizer-type": "Visualisatortype"
},
"name": "Visualisator"
}
}
}

View File

@ -44,7 +44,7 @@
},
"dialog": {
"hide-menu-enabled": {
"detail": "Menu jest ukryte, użyj 'Alt' aby je pokazać (lub 'Escape' jeśli używasz in-app-menu)",
"detail": "Menu jest ukryte, użyj przycisku [Alt] aby je pokazać (lub [Escape], jeśli używasz menu w aplikacji)",
"message": "Ukrywanie menu jest włączone",
"title": "Ukrywanie Menu Włączone"
},
@ -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"
}
},
@ -116,7 +116,7 @@
"auto-update": "Automatyczne aktualizacje",
"hide-menu": {
"dialog": {
"message": "Menu będzie ukryte po następnym uruchomieniu, użyj [Alt] aby je pokazać (lub [`] jeśli używasz in-app-menu)",
"message": "Menu będzie ukryte po następnym uruchomieniu, użyj przycisku [Alt] aby je pokazać (lub [`], jeśli używasz menu w aplikacji)",
"title": "Ukrywanie menu włączone"
},
"label": "Ukryj menu"
@ -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",
@ -406,7 +406,7 @@
"menu": {
"hide-dom-window-controls": "Ukryj kontrolki okna DOM"
},
"name": "In-App menu"
"name": "Menu w aplikacji"
},
"last-fm": {
"description": "Dodanie obsługi scrobblingu dla Last.fm",
@ -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",
@ -463,7 +463,7 @@
"hotkey": "Klawisz skrótu"
},
"label": "Wybierz klawisz skrótu do przełączania trybu obrazu w obrazie",
"title": "Klawisz skrótu Obraz w obrazie"
"title": "Klawisz skrótu obrazu w obrazie"
}
},
"save-window-position": "Zapisz pozycję okna",
@ -519,7 +519,7 @@
"name": "Zmieniacz jakości wideo"
},
"shortcuts": {
"description": "Umożliwia ustawienie globalnych skrótów klawiszowych do odtwarzania (odtwarzanie/pauza/następny/poprzedni) + wyłączanie OSD multimediów poprzez zastąpienie klawiszy multimediów + włączanie Ctrl/CMD + F w celu wyszukiwania + włączanie obsługi linux mpris dla klawiszy multimediów + niestandardowe skróty klawiszowe dla zaawansowanych użytkowników",
"description": "Umożliwia ustawienie globalnych skrótów klawiszowych do odtwarzania (odtwarzanie/pauza/następny/poprzedni) + wyłączanie OSD multimediów poprzez zastąpienie klawiszy multimediów, włączając kombinację klawiszy Ctrl/CMD + F w celu wyszukiwania, obsługę Linux MPRIS dla klawiszy multimediów oraz niestandardowe skróty klawiszowe dla zaawansowanych użytkowników.",
"menu": {
"override-media-keys": "Zastąp klawisze multimediów",
"set-keybinds": "Ustaw globalne sterowanie utworem"
@ -537,6 +537,10 @@
}
}
},
"skip-disliked-songs": {
"description": "Pomija nieulubione piosenki (zaznaczone łapką w dół)",
"name": "Pomijanie nieulubionych piosenek"
},
"skip-silences": {
"description": "Automatycznie pomijaj sekcje bez dźwięku w utworach",
"name": "Pomiń ciszę"
@ -547,7 +551,7 @@
},
"taskbar-mediacontrol": {
"description": "Steruj odtwarzaniem z paska zadań systemu Windows",
"name": "Kontrola odtwarzania z paska zadań"
"name": "Kontroler odtwarzania z paska zadań"
},
"touchbar": {
"description": "Dodaje widżet do paska dotykowego dla użytkowników systemu macOS",

View File

@ -31,10 +31,28 @@
},
"theme": {
"css-file-not-found": "Arquivo CSS \"{{cssFile}}\" não existe, ignorando"
},
"unresponsive": {
"details": "Erro sem resposta!\n{{error}}"
},
"when-ready": {
"clearing-cache-after-20s": "Limpando o cache do aplicativo"
},
"window": {
"tried-to-render-offscreen": "Janela tentou desenhar fora do ecrã, windowSize={{windowSize}}, displaySize={{displaySize}}, position={{position}}"
}
},
"dialog": {
"hide-menu-enabled": {
"detail": "O menu está oculto, use a tecla 'Alt' para mostra-lo (ou 'Esc' se estiver usando o menu do aplicativo)",
"message": "Ocultar menu está ativado",
"title": "Ocultar menu ativado"
},
"need-to-restart": {
"buttons": {
"later": "Depois",
"restart-now": "Reiniciar agora"
},
"detail": "O plugin {{pluginName}} precisa ser reiniciado para ter efeito",
"message": "\"{{pluginName}}\" precisa ser reiniciado",
"title": "É necessário reiniciar"
@ -47,7 +65,7 @@
},
"detail": "Lamentamos o inconveniente! Por favor escolha o que fazer:",
"message": "A aplicação não está respondendo",
"title": "Janela não está respondendo"
"title": "A janela não está respondendo"
},
"update-available": {
"buttons": {
@ -81,11 +99,13 @@
"auto-reset-app-cache": "Reiniciar cache do aplicativo quando o aplicativo abrir",
"disable-hardware-acceleration": "Desabilitar aceleração por hardware",
"edit-config-json": "Editar config.json",
"override-user-agent": "Substituir User-Agent",
"restart-on-config-changes": "Reinicie as alterações de configurações feitas",
"set-proxy": {
"label": "Definir proxy",
"prompt": {
"label": "Inserir Endereço do Proxy: (deixe em branco para desativar)",
"placeholder": "Exemplo: socks5://127.0.0.1:9999",
"placeholder": "Exemplo: SOCKS5://127.0.0.1:9999",
"title": "Definir proxy"
}
},
@ -176,10 +196,403 @@
"plugins": {
"adblocker": {
"description": "Bloquear todos os anúncios e rastreamento automaticamente",
"name": "Adblocker"
"menu": {
"blocker": "Bloqueador"
},
"name": "Bloqueador de anúncios"
},
"album-color-theme": {
"description": "\"Aplica um tema dinâmico e efeitos visuais com base na paleta de cores do álbum."
"description": "Aplica um tema dinâmico e efeitos visuais com base na paleta de cores do álbum",
"name": "Tema de cores do álbum"
},
"ambient-mode": {
"description": "Aplica um efeito de iluminação lançando cores suaves do vídeo no fundo da tela.",
"menu": {
"blur-amount": {
"label": "Quantidade de desfoque",
"submenu": {
"pixels": "{{blurAmount}} píxeis"
}
},
"buffer": {
"label": "Buffer",
"submenu": {
"buffer": "{{buffer}}"
}
},
"opacity": {
"label": "Opacidade",
"submenu": {
"percent": "{{opacity}}%"
}
},
"quality": {
"label": "Qualidade",
"submenu": {
"pixels": "{{quality}} píxeis"
}
},
"size": {
"label": "Tamanho",
"submenu": {
"percent": "{{size}}%"
}
},
"smoothness-transition": {
"label": "Transição suave",
"submenu": {
"during": "Durante {{interpolationTime}}s"
}
},
"use-fullscreen": {
"label": "Tela cheia"
}
},
"name": "Modo Ambiente"
},
"audio-compressor": {
"description": "Aplicar compressão ao áudio (diminui o volume das partes mais altas do sinal e aumenta o volume das partes mais suaves)",
"name": "Compressor de áudio"
},
"blur-nav-bar": {
"description": "Torna a barra de navegação transparente e desfocada",
"name": "Barra de navegação desfocada"
},
"bypass-age-restrictions": {
"description": "Ignorar a verificação de idade do YouTube",
"name": "Ignorar restrições de idade"
},
"captions-selector": {
"description": "Seletor de legenda para faixas de áudio do YouTube Music",
"menu": {
"autoload": "Selecionar automaticamente a última legenda usada",
"disable-captions": "Sem legendas por padrão"
},
"name": "Seletor de legendas",
"prompt": {
"selector": {
"label": "Idioma da legenda atual: {{language}}",
"none": "Nenhuma",
"title": "Selecione o idioma da legenda"
}
},
"templates": {
"title": "Seletor de legendas aberto"
}
},
"compact-sidebar": {
"description": "Definir sempre a barra lateral no modo compacto",
"name": "Barra lateral compacta"
},
"crossfade": {
"description": "Transição entre músicas",
"menu": {
"advanced": "Avançado"
},
"name": "Transição entre músicas [Beta]",
"prompt": {
"options": {
"multi-input": {
"fade-in-duration": "Duração da transição no início (ms)",
"fade-out-duration": "Duração da transição no final (ms)",
"fade-scaling": {
"label": "Escala da transição",
"linear": "Linear",
"logarithmic": "Logarítmica"
},
"seconds-before-end": "Realizar transição N segundos antes do final"
},
"title": "Opções de transição"
}
}
},
"disable-autoplay": {
"description": "Faz a música começar no modo \"pausado\"",
"menu": {
"apply-once": "Aplicar apenas na inicialização"
},
"name": "Desativar reprodução automática"
},
"discord": {
"backend": {
"already-connected": "Tentativa de conexão com conexão já ativa",
"connected": "Conectado ao Discord",
"disconnected": "Desconectado do Discord"
},
"description": "Mostre aos seus amigos o que você ouve com Rich Presence",
"menu": {
"auto-reconnect": "Reconexão automática",
"clear-activity": "Limpar atividade",
"clear-activity-after-timeout": "Limpar atividade após o tempo limite",
"connected": "Conectado",
"disconnected": "Desconectado",
"hide-duration-left": "Ocultar duração restante",
"hide-github-button": "Ocultar botão de link do GitHub",
"play-on-youtube-music": "Reproduzir no YouTube Music",
"set-inactivity-timeout": "Definir tempo limite de inatividade"
},
"name": "Discord Rich Presence",
"prompt": {
"set-inactivity-timeout": {
"label": "Insira o tempo limite de inatividade em segundos:",
"title": "Definir tempo limite de inatividade"
}
}
},
"downloader": {
"backend": {
"dialog": {
"error": {
"buttons": {
"ok": "OK"
},
"message": "Poxa! Desculpe, o download falhou…",
"title": "Erro no download!"
},
"start-download-playlist": {
"buttons": {
"ok": "OK"
},
"detail": "({{playlistSize}} músicas)",
"message": "Baixando lista de reprodução {{playlistTitle}}",
"title": "Download iniciado"
}
},
"feedback": {
"conversion-progress": "Conversão: {{percent}}%",
"converting": "Convertendo…",
"done": "Finalizado: {{filePath}}",
"download-info": "Baixando {{artist}} - {{title}} {{videoId}}",
"download-progress": "Baixando: {{percent}}%",
"downloading": "Baixando…",
"downloading-counter": "Baixando {{current}}/{{total}}…",
"downloading-playlist": "Baixando lista de reprodução \"{{playlistTitle}}\" - {{playlistSize}} músicas ({{playlistId}})",
"error-while-downloading": "Erro ao baixar \"{{author}} - {{title}}\": {{error}}",
"folder-already-exists": "A pasta {{playlistFolder}} já existe",
"getting-playlist-info": "Obtendo informações da lista de reprodução…",
"loading": "Carregando…",
"playlist-has-only-one-song": "A lista de reprodução possui apenas um item, baixando-o diretamente",
"playlist-id-not-found": "ID da lista de reprodução não encontrado",
"playlist-is-empty": "Lista de reprodução vazia",
"playlist-is-mix-or-private": "Erro ao obter informações da lista de reprodução: tenha certeza que ela não está privada ou não seja \"Mixtapes criadas para você\"\n\n{{error}}",
"preparing-file": "Preparando arquivos…",
"saving": "Salvando…",
"trying-to-get-playlist-id": "Tentando pegar ID da lista de reprodução: {{playlistId}}",
"video-id-not-found": "Vídeo não encontrado",
"writing-id3": "Escrevendo tags ID3…"
}
},
"description": "Baixa MP3 / fonte de áudio diretamente da interface",
"menu": {
"choose-download-folder": "Escolha a pasta de download",
"download-playlist": "Baixar lista de reprodução",
"presets": "Predefinições",
"skip-existing": "Ignorar arquivos existentes"
},
"name": "Downloader",
"renderer": {
"can-not-update-progress": "Não é possível atualizar o progresso"
},
"templates": {
"button": "Baixar"
}
},
"exponential-volume": {
"description": "Torna o controle deslizante de volume exponencial, facilitando a seleção de volumes mais baixos.",
"name": "Volume Exponencial"
},
"in-app-menu": {
"description": "Dá às barras de menu uma aparência sofisticada, escura ou com a cor do álbum",
"menu": {
"hide-dom-window-controls": "Ocultar controles da janela DOM"
},
"name": "Menu no aplicativo"
},
"last-fm": {
"description": "Adiciona suporte de scrobbling para Last.fm",
"name": "Last.fm"
},
"lumiastream": {
"description": "Adiciona suporte Lumia Stream",
"name": "Lumia Stream [Beta]"
},
"lyrics-genius": {
"description": "Adiciona suporte a letras para a maioria das músicas",
"menu": {
"romanized-lyrics": "Letras romanizadas"
},
"name": "Letras Genius",
"renderer": {
"fetched-lyrics": "Buscar letras no Genius"
}
},
"navigation": {
"description": "Setas de navegação Próximo/Voltar integradas diretamente na interface, como no seu navegador favorito",
"name": "Navegação"
},
"no-google-login": {
"description": "Remove os botões e links de login do Google da interface",
"name": "Sem login do Google"
},
"notifications": {
"description": "Exibir uma notificação quando uma música começar a tocar (notificações interativas estão disponíveis no Windows)",
"menu": {
"interactive": "Notificações interativas",
"interactive-settings": {
"label": "Configurações interativas",
"submenu": {
"hide-button-text": "Ocultar texto do botão",
"refresh-on-play-pause": "Atualizar ao reproduzir/pausar",
"tray-controls": "Abrir/Fechar no clique da bandeja"
}
},
"priority": "Prioridade de notificação",
"toast-style": "Estilo de alerta",
"unpause-notification": "Mostrar notificação ao despausar"
},
"name": "Notificações"
},
"picture-in-picture": {
"description": "Permite mudar o aplicativo para o modo picture-in-picture",
"menu": {
"always-on-top": "Sempre no topo",
"hotkey": {
"label": "Tecla de atalho",
"prompt": {
"keybind-options": {
"hotkey": "Tecla de atalho"
},
"label": "Escolha uma tecla de atalho para alternar o picture-in-picture",
"title": "Tecla de atalho picture-in-picture"
}
},
"save-window-position": "Salvar posição da janela",
"save-window-size": "Salvar tamanho da janela",
"use-native-pip": "Use PiP nativo do navegador"
},
"name": "Picture-in-picture",
"templates": {
"button": "Picture-in-picture"
}
},
"playback-speed": {
"description": "Ouça rápido, ouça devagar! Adiciona um controle deslizante que controla a velocidade da música",
"name": "Velocidade de reprodução",
"templates": {
"button": "Velocidade"
}
},
"precise-volume": {
"description": "Controle o volume com precisão usando a roda do mouse/teclas de atalho, com um HUD personalizado e etapas de volume personalizáveis",
"menu": {
"arrows-shortcuts": "Controles locais das teclas de seta",
"custom-volume-steps": "Definir etapas de volume personalizadas",
"global-shortcuts": "Teclas de atalho globais"
},
"name": "Volume preciso",
"prompt": {
"global-shortcuts": {
"keybind-options": {
"decrease": "Diminuir o volume",
"increase": "Aumentar o volume"
},
"label": "Escolha atalhos de teclado de volume global:",
"title": "Atalhos de teclado de volume global"
},
"volume-steps": {
"label": "Escolha as etapas de aumento/diminuição de volume",
"title": "Etapas de volume"
}
}
},
"quality-changer": {
"backend": {
"dialog": {
"quality-changer": {
"detail": "Qualidade atual: {{quality}}",
"message": "Escolha a qualidade do vídeo:",
"title": "Escolha a qualidade do vídeo"
}
}
},
"description": "Permite alterar a qualidade do vídeo com um botão na sobreposição de vídeo",
"name": "Trocador de qualidade do vídeo"
},
"shortcuts": {
"description": "Permite definir teclas de atalho globais para reprodução (reproduzir/pausar/próximo/anterior) e desligar o OSD de mídia substituindo as teclas de mídia, ativando Ctrl/CMD + F para pesquisar, ativando o suporte Linux MPRIS para teclas de mídia e teclas de atalho personalizadas para usuários avançados.",
"menu": {
"override-media-keys": "Substituir teclas de mídia",
"set-keybinds": "Definir controles globais de música"
},
"name": "Atalhos (& MPRIS)",
"prompt": {
"keybind": {
"keybind-options": {
"next": "Próximo",
"play-pause": "Reproduzir/Pausar",
"previous": "Anterior"
},
"label": "Escolha atalhos de teclado globais para controle de músicas:",
"title": "Atalhos globais do teclado"
}
}
},
"skip-disliked-songs": {
"description": "Pula músicas com 'não gostei'",
"name": "Pular músicas com 'não gostei'"
},
"skip-silences": {
"description": "Pular automaticamente seções de silêncio nas músicas",
"name": "Pular silêncios"
},
"sponsorblock": {
"description": "Ignora automaticamente partes não musicais, como introdução/final ou partes de videoclipes onde a música não está tocando",
"name": "SponsorBlock (bloqueador de patrocínios)"
},
"taskbar-mediacontrol": {
"description": "Controle a reprodução na barra de tarefas do Windows",
"name": "Controle de mídia da barra de tarefas"
},
"touchbar": {
"description": "Adiciona um widget TouchBar para usuários do macOS",
"name": "Barra de toque"
},
"tuna-obs": {
"description": "Integração com o plugin Tuna do OBS",
"name": "Tuna OBS"
},
"video-toggle": {
"description": "Adiciona um botão para alternar entre o modo Vídeo/Música. Também pode, opcionalmente, remover toda a guia de vídeo",
"menu": {
"align": {
"label": "Alinhamento",
"submenu": {
"left": "Esquerda",
"middle": "Centro",
"right": "Direita"
}
},
"force-hide": "Forçar remoção da guia de vídeo",
"mode": {
"label": "Modo",
"submenu": {
"custom": "Alternar personalizado",
"disabled": "Desabilitado",
"native": "Alternar nativo"
}
}
},
"name": "Botão de Alternar Vídeo",
"templates": {
"button": "Música"
}
},
"visualizer": {
"description": "Adiciona um visualizador ao player",
"menu": {
"visualizer-type": "Tipo de visualizador"
},
"name": "Visualizer"
}
}
}

View File

@ -7,7 +7,9 @@
"initialize-failed": "Ошибка инициализации плагина \"{{pluginName}}\"",
"load-all": "Загружаем все плагины",
"load-failed": "Ошибка загрузки плагина \"{{pluginName}}\"",
"loaded": "Плагин \"{{pluginName}}\" загружен"
"loaded": "Плагин \"{{pluginName}}\" загружен",
"unload-failed": "Ошибка выгрузки плагина \"{{pluginName}}\"",
"unloaded": "Плагин \"{{pluginName}}\" выгружен"
}
}
},
@ -17,45 +19,149 @@
"name": "Russian"
},
"main": {
"console": {
"did-finish-load": {
"dev-tools": "Загрузка завершена. DevTools открыт"
},
"i18n": {
"loaded": "i18n загружен"
},
"second-instance": {
"receive-command": "Получена команда по протоколу: \"{{command}}\""
},
"theme": {
"css-file-not-found": "CSS файл \"{{cssFile}}\" не существует, игнорирую"
},
"unresponsive": {
"details": "Приложение не отвечает\n{{error}}"
},
"when-ready": {
"clearing-cache-after-20s": "Очищаю кеш приложения"
},
"window": {
"tried-to-render-offscreen": "Окно попробовало открыться за пределами экрана, размер окна={{windowSize}}, разрешение экрана={{displaySize}}, местоположение={{position}}"
}
},
"dialog": {
"hide-menu-enabled": {
"detail": "Меню скрыто, 'Alt' чтобы показать его ('Escape' если используете внутреннее меню приложения)",
"message": "Скрытие меню включено",
"title": "Включено скрытие меню"
},
"need-to-restart": {
"buttons": {
"later": "Позже",
"restart-now": "Перезапустить сейчас"
},
"detail": "Перезагрузите приложение для включения плагина {{pluginName}}",
"message": "{{pluginName}} нуждается в перезагрузке",
"title": "Нужна перезагрузка"
},
"unresponsive": {
"buttons": {
"quit": "Выйти",
"relaunch": "Перезапустить",
"wait": "Подождать"
},
"detail": "Извиняемся за неувязку! пожалуйста выберите что вы хотите сделать:",
"message": "Приложение не отвечает",
"title": "Окно не отвечает"
},
"update-available": {
"buttons": {
"download": "Download",
"disable": "Отключить обновления",
"download": "Скачать",
"ok": "OK"
}
},
"detail": "Новая версия доступна для загрузки на {{downloadLink}}",
"message": "Доступная новая версия",
"title": "Доступно обновление"
}
},
"menu": {
"about": "О приложении",
"navigation": {
"label": "Navigation"
"label": "Навигация",
"submenu": {
"copy-current-url": "Скопировать текущий URL",
"go-back": "Вернуться",
"go-forward": "Вперед",
"quit": "Выйти",
"restart": "Перезапустить приложение"
}
},
"options": {
"label": "Options",
"label": "Настройки",
"submenu": {
"advanced-options": {
"label": "Расширенные настройки",
"submenu": {
"auto-reset-app-cache": "Очищать кеш при запуске приложения",
"disable-hardware-acceleration": "Отключить аппаратное ускорение",
"edit-config-json": "Редактировать config.json",
"override-user-agent": "Переопределить User-Agent",
"restart-on-config-changes": "Перезапускать при изменениях конфига",
"set-proxy": {
"label": "Set proxy",
"label": "Задать прокси",
"prompt": {
"title": "Set proxy"
"label": "Введите прокси адрес (оставьте поле пустым для отключения)",
"placeholder": "Пример: SOCKS5://127.0.0.1:9999",
"title": "Задать прокси"
}
}
},
"toggle-dev-tools": "Включить DevTools"
}
},
"auto-update": "Auto Update",
"start-at-login": "Start at login",
"always-on-top": "Поверх остальных окон",
"auto-update": "Автообновление",
"hide-menu": {
"dialog": {
"message": "Меню будет скрыто при следующем запуске, используйте [Alt] чтобы показать его (или [`] если используйте меню внутри приложения)",
"title": "Скрытие меню включено"
},
"label": "Скрыть меню"
},
"language": {
"dialog": {
"message": "Язык будет сменён после перезапуска",
"title": "Язык изменён"
},
"label": "Язык",
"submenu": {
"to-help-translate": "Хотите помочь с переводом? Кликните сюда"
}
},
"resume-on-start": "Продолжать последнюю песню при запуске приложения",
"single-instance-lock": "Запрет запуска нескольких экземпляров",
"start-at-login": "Запуск при включении компьютера",
"starting-page": {
"label": "Стартовая страница",
"unset": "Не настроено"
},
"tray": {
"label": "Tray"
"label": "Трей",
"submenu": {
"disabled": "Отключено",
"enabled-and-hide-app": "Включено и скрывать приложение",
"enabled-and-show-app": "Включено и показывать приложение",
"play-pause-on-click": "Пауза/продолжить при нажатии"
}
},
"visual-tweaks": {
"label": "Визуальные настройки",
"submenu": {
"like-buttons": {
"default": "Default"
"default": "Default",
"force-show": "Всегда показывать",
"hide": "Скрывать",
"label": "Кнопка лайка"
},
"remove-upgrade-button": "Убрать кнопку Youtube Premium",
"theme": {
"label": "Theme",
"label": "Тема",
"submenu": {
"no-theme": "No theme"
"import-css-file": "Импортировать кастомный CSS файл",
"no-theme": "Без темы"
}
}
}
@ -63,11 +169,28 @@
}
},
"plugins": {
"label": "Plugins"
"enabled": "Включено",
"label": "Плагины"
},
"view": {
"label": "View"
"label": "Вид",
"submenu": {
"force-reload": "Принудительная перезагрузка",
"reload": "Перезагрузить",
"reset-zoom": "Настоящий размер",
"toggle-fullscreen": "Включить полноэкранный режим",
"zoom-in": "Приблизить",
"zoom-out": "Отдалить"
}
}
},
"tray": {
"next": "Следующий",
"play-pause": "Пауза/Продолжить",
"previous": "Предыдущий",
"quit": "Выйти",
"restart": "Перезагрузить приложение",
"show": "Показать окно"
}
},
"plugins": {
@ -118,7 +241,7 @@
"smoothness-transition": {
"label": "Плавный переход",
"submenu": {
"during": "В течение {{interpolationTime}}s"
"during": "В течение {{interpolationTime}} s"
}
},
"use-fullscreen": {
@ -141,21 +264,78 @@
},
"captions-selector": {
"description": "Выбор субтитров для аудиотреков в YouTube Music",
"menu": {
"autoload": "Автоматически выбирать последние использованные субтитры",
"disable-captions": "Без субтитров по умолчанию"
},
"name": "Выбор субтитров",
"prompt": {
"selector": {
"none": "None"
"label": "Текущий язык субтитров:{{language}}",
"none": "Нет",
"title": "Выберите язык субтитров"
}
},
"templates": {
"title": "Открыть выбор субтитров"
}
},
"compact-sidebar": {
"description": "Боковая панель всегда в компактном режиме",
"name": "Компактная боковая панель"
},
"crossfade": {
"description": "Плавный переход между песнями",
"menu": {
"advanced": "Расширенное"
},
"name": "Плавный переход [бета]",
"prompt": {
"options": {
"multi-input": {
"fade-in-duration": "Длительность постепенного появления (мс)",
"fade-out-duration": "Длительность затухания (мс)",
"fade-scaling": {
"linear": "Linear"
}
}
"label": "Сила перехода",
"linear": "Линейный",
"logarithmic": "Логарифмический"
},
"seconds-before-end": "Плавных переход за N секунд перед концом"
},
"title": "Настройки плавного перехода"
}
}
},
"disable-autoplay": {
"description": "Запускает песню сразу на паузе",
"menu": {
"apply-once": "Применимо только при запуске"
},
"name": "Отключить Автопроигрыш"
},
"discord": {
"backend": {
"already-connected": "Попытка подключения при уже существующем соединении",
"connected": "Подключено к Discord",
"disconnected": "Отключено от Discord"
},
"description": "Покажите своим друзьям что вы слушаете с помощью Rich Presence",
"menu": {
"auto-reconnect": "Авто переподключение",
"clear-activity": "Очистить активность",
"clear-activity-after-timeout": "Очищать активность после таймаута",
"connected": "Подключено",
"disconnected": "Отключено",
"hide-duration-left": "Скрыть сколько осталось времени",
"hide-github-button": "Скрыть ссылку на GitHub",
"play-on-youtube-music": "Воспроизвести на YouTube Music",
"set-inactivity-timeout": "Поставить таймер неактивности"
},
"name": "Discord Rich Presence",
"prompt": {
"set-inactivity-timeout": {
"label": "Введите таймер неактивности в секундах:",
"title": "Задать таймер неактивности"
}
}
},
@ -166,33 +346,92 @@
"buttons": {
"ok": "OK"
},
"message": "Ой-ой! Прошу простить, загрузка дала сбой…",
"title": "Error in download!"
},
"start-download-playlist": {
"buttons": {
"ok": "OK"
}
},
"detail": "({{playlistSize}} песен)",
"message": "Скачиваем плейлист {{playlistTitle}}",
"title": "Загрузка началась"
}
},
"feedback": {
"download-progress": "Download: {{percent}}%"
"conversion-progress": "Конвертация: {{percent}}%",
"converting": "Конвертация…",
"done": "Выполнено: {{filePath}}",
"download-info": "Скачиваем {{artist}}-{{title}} {{videoId}}",
"download-progress": "Download: {{percent}}%",
"downloading": "Скачиваем…",
"downloading-counter": "Скачиваем {{current}}/{{total}}…",
"downloading-playlist": "Скачиваем плейлист \"{{playlistTitle}}\" - {{playlistSize}} песен {{playlistId}}",
"error-while-downloading": "Ошибка при скачивании \"{{author}} - {{title}}\": {{error}}",
"folder-already-exists": "Папка {{playlistFolder}} уже существует",
"getting-playlist-info": "Получаем информацию о плейлисте…",
"loading": "Загрузка…",
"playlist-has-only-one-song": "В плейлисте только одна песня, скачиваем напрямую",
"playlist-id-not-found": "Плейлист не найден",
"playlist-is-empty": "Плейлист пуст",
"playlist-is-mix-or-private": "Ошибка при получении данных о плейлисте: убедитесь что он не приватный или \"Только для вас\" плейлист\n\n{{error}}",
"preparing-file": "Подготавливаем файл…",
"saving": "Сохраняем…",
"trying-to-get-playlist-id": "Пытаемся получить ID плейлиста: {{playlistId}}",
"video-id-not-found": "Видео не найдено",
"writing-id3": "Пишем ID3 теги…"
}
},
"description": "Скачивать MP3 / исходное аудио напрямую из интерфейса",
"menu": {
"choose-download-folder": "Выберите папку для загрузок",
"download-playlist": "Скачать плейлист",
"presets": "Пресеты",
"skip-existing": "Пропускать уже существующие файлы"
},
"name": "Загрузчик",
"renderer": {
"can-not-update-progress": "Невозможно обновить прогресс"
},
"templates": {
"button": "Download"
}
},
"exponential-volume": {
"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": {
@ -207,7 +446,7 @@
"name": "SponsorBlock"
},
"touchbar": {
"name": "TouchBar"
"name": "Тачбар"
},
"tuna-obs": {
"name": "Tuna OBS"
@ -220,13 +459,27 @@
"right": "Right"
}
},
"force-hide": "Скрыть обложку",
"mode": {
"label": "Mode"
"label": "Mode",
"submenu": {
"custom": "Кастомный переключатель",
"disabled": "Отключен",
"native": "Стандартный переключатель"
}
}
},
"name": "Переключатель видео",
"templates": {
"button": "Song"
}
},
"visualizer": {
"description": "Заменяет обложку визуализатором музыки",
"menu": {
"visualizer-type": "Вид визуализации"
},
"name": "Визуалайзер"
}
}
}

View File

@ -0,0 +1,7 @@
{
"language": {
"code": "th",
"local-name": "ภาษาไทย",
"name": "Thai"
}
}

View File

@ -1,58 +1,166 @@
{
"common": {
"console": {
"plugins": {
"execute-failed": "{{pluginName}}::{{contextName}} eklentisi çalıştırılamadı",
"executed-at-ms": "{{pluginName}}::{{contextName}} eklentisi {{ms}}ms'de çalıştırıldı",
"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",
"unload-failed": "\"{{pluginName}}\" eklentisi çıkartılamadı",
"unloaded": "\"{{pluginName}}\" eklentisi kaldırıldı"
}
}
},
"language": {
"code": "tr",
"local-name": "Türkçe",
"name": "Turkish"
},
"main": {
"console": {
"did-finish-load": {
"dev-tools": "Yükleme tamamlandı. Geliştirici araçlarııldı"
},
"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"
}
}
@ -61,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": {
@ -101,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ı"
}
}
},
@ -155,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": {
@ -172,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"
}
}
}

View File

@ -1,7 +1,464 @@
{
"common": {
"console": {
"plugins": {
"execute-failed": "Не вдалося запустити плагін {{pluginName}}::{{contextName}}",
"executed-at-ms": "Плагін {{pluginName}}::{{contextName}} запущено за {{ms}}ms",
"initialize-failed": "Не вдалося ініціалізувати плагін \"{{pluginName}}\"",
"load-all": "Завантаження всіх плагінів",
"load-failed": "Не вдалося завантажити плагін \"{{pluginName}}\"",
"loaded": "Плагін \"{{pluginName}}\" завантажено",
"unload-failed": "Не вдалося вивантажити плагін \"{{pluginName}}\"",
"unloaded": "Плагін \"{{pluginName}}\" вивантажено"
}
}
},
"language": {
"code": "uk",
"local-name": "Українська",
"name": "Ukrainian"
},
"main": {
"console": {
"did-finish-load": {
"dev-tools": "Завантаження завершено. Відкрито DevTools"
},
"i18n": {
"loaded": "i18n завантажено"
},
"second-instance": {
"receive-command": "Отримано команду за протоколом: \"{{command}}\""
},
"theme": {
"css-file-not-found": "Файл CSS \"{{cssFile}}\" не існує, ігноруємо"
},
"unresponsive": {
"details": "Невідповідна помилка!\n{{error}}"
},
"when-ready": {
"clearing-cache-after-20s": "Очищення кешу програми"
},
"window": {
"tried-to-render-offscreen": "Вікно намагалися відобразитися поза екраном, windowSize={{windowSize}}, displaySize={{displaySize}}, position={{position}}"
}
},
"dialog": {
"hide-menu-enabled": {
"detail": "Меню приховано, використовуйте \"Alt\", щоб показати його (або \"Escape\", якщо ви використовуєте меню в додатку)",
"message": "Приховане меню увімкнено",
"title": "Увімкнути Приховане Меню"
},
"need-to-restart": {
"buttons": {
"later": "Пізніше",
"restart-now": "Перезапустити зараз"
},
"detail": "\"{{pluginName}}\" плагін потребує перезапуску, щоб набути чинності",
"message": "\"{{pluginName}}\" необхідно перезапустити",
"title": "Потрібен перезапуск"
},
"unresponsive": {
"buttons": {
"quit": "Вийти",
"relaunch": "Перезапустити",
"wait": "Чекати"
},
"detail": "Перепрошуємо за незручності! Будь ласка, оберіть, що робити:",
"message": "Програма не відповідає",
"title": "Вікно не відповідає"
},
"update-available": {
"buttons": {
"disable": "Вимкнути оновлення",
"download": "Завантажити",
"ok": "Так"
},
"detail": "Доступна нова версія, яку можна завантажити за посиланням {{downloadLink}}",
"message": "Доступна нова версія",
"title": "Доступне оновлення"
}
},
"menu": {
"about": "Про нас",
"navigation": {
"label": "Навігація",
"submenu": {
"copy-current-url": "Копіювати поточну URL-адресу",
"go-back": "Назад",
"go-forward": "Вперед",
"quit": "Вихід",
"restart": "Перезапустити програму"
}
},
"options": {
"label": "Параметри",
"submenu": {
"advanced-options": {
"label": "Додаткові опції",
"submenu": {
"auto-reset-app-cache": "Очищення кешу програми при її запуску",
"disable-hardware-acceleration": "Вимкнути апаратне прискорення",
"edit-config-json": "Редагувати config.json",
"override-user-agent": "Змінити User-Agent",
"restart-on-config-changes": "Перезапуск після зміни конфігурації",
"set-proxy": {
"label": "Задати проксі",
"prompt": {
"label": "Введіть адресу проксі-сервера: (залиште порожнім, щоб вимкнути)",
"placeholder": "Приклад: SOCKS5://127.0.0.1:9999",
"title": "Задати проксі"
}
},
"toggle-dev-tools": "Перемкнути DevTools"
}
},
"always-on-top": "Завжди зверху",
"auto-update": "Автооновлення",
"hide-menu": {
"dialog": {
"message": "Меню буде приховано при наступному запуску, використовуйте [Alt], щоб показати його (або клавішу [`], якщо ви використовуєте вбудоване меню)",
"title": "Приховане меню Увімкнено"
},
"label": "Приховане меню"
},
"language": {
"dialog": {
"message": "Мова буде змінена після перезапуску",
"title": "Мову змінено"
},
"label": "Мова",
"submenu": {
"to-help-translate": "Хочете допомогти з перекладом? Клацніть тут"
}
},
"resume-on-start": "Відновлювати останню пісню при запуску програми",
"single-instance-lock": "Заблокувати єдиним інстансом",
"start-at-login": "Запустити при вході в систему",
"starting-page": {
"label": "Початкова сторінка",
"unset": "Не задано"
},
"tray": {
"label": "Таця",
"submenu": {
"disabled": "Вимкнено",
"enabled-and-hide-app": "Запустити та приховати програму",
"enabled-and-show-app": "Запустити та відобразити додаток",
"play-pause-on-click": "Відтворення/Пауза за натисканням"
}
},
"visual-tweaks": {
"label": "Візуальні налаштування",
"submenu": {
"like-buttons": {
"default": "За замовчуванням",
"force-show": "Завжди показувати",
"hide": "Приховати",
"label": "Як кнопки"
},
"remove-upgrade-button": "Прибрати кнопку оновлення",
"theme": {
"label": "Тема",
"submenu": {
"import-css-file": "Імпортувати власний CSS файл",
"no-theme": "Без теми"
}
}
}
}
}
},
"plugins": {
"enabled": "Увімкнено",
"label": "Плагіни"
},
"view": {
"label": "Вид",
"submenu": {
"force-reload": "Примусове перезавантаження",
"reload": "Перезавантажити",
"reset-zoom": "Фактичний розмір",
"toggle-fullscreen": "Включити повноекранний режим",
"zoom-in": "Збільшити",
"zoom-out": "Зменшити"
}
}
},
"tray": {
"next": "Далі",
"play-pause": "Відтворення/Пауза",
"previous": "Попередній",
"quit": "Вихід",
"restart": "Перезапустити програму",
"show": "Показати вікно"
}
},
"plugins": {
"adblocker": {
"description": "Блокувати всю рекламу та відстеження з коробки",
"menu": {
"blocker": "Блокувальник"
},
"name": "Блокувальник реклами"
},
"album-color-theme": {
"description": "Застосовує динамічну тему та візуальні ефекти на основі колірної палітри альбому",
"name": "Кольорова тема альбому"
},
"ambient-mode": {
"description": "Застосовує ефект освітлення, накладаючи ніжні кольори з відео на фон екрана.",
"menu": {
"blur-amount": {
"label": "Обсяг розмиття",
"submenu": {
"pixels": "{{blurAmount}} пікселів"
}
},
"buffer": {
"label": "Буфер",
"submenu": {
"buffer": "{{buffer}}"
}
},
"opacity": {
"label": "Прозорість",
"submenu": {
"percent": "{{opacity}}%"
}
},
"quality": {
"label": "Якість",
"submenu": {
"pixels": "{{quality}} пікселів"
}
},
"size": {
"label": "Розмір",
"submenu": {
"percent": "{{size}}%"
}
},
"smoothness-transition": {
"label": "Плавність переходу",
"submenu": {
"during": "Протягом {{interpolationTime}} с"
}
},
"use-fullscreen": {
"label": "Повноекранний режим"
}
},
"name": "Режим навколишнього середовища"
},
"audio-compressor": {
"description": "Застосувати стиснення аудіо (зменшити гучність найгучніших фрагментів сигналу та збільшити гучність тихих фрагментів)",
"name": "Аудіокомпресор"
},
"blur-nav-bar": {
"description": "Робить панель навігації прозорою та розмитою",
"name": "Розмиття панелі навігації"
},
"bypass-age-restrictions": {
"description": "Обхід перевірки віку на YouTube",
"name": "Обхід вікових обмежень"
},
"captions-selector": {
"description": "Вибір субтитрів для аудіодоріжок YouTube Music",
"menu": {
"autoload": "Автоматичний вибір останніх використаних субтитрів",
"disable-captions": "За замовчуванням субтитри відсутні"
},
"name": "Вибір субтитрів",
"prompt": {
"selector": {
"label": "Поточна мова субтитрів: {{language}}",
"none": "Нема",
"title": "Виберіть мову субтитрів"
}
},
"templates": {
"title": "Відкрити селектор субтитрів"
}
},
"compact-sidebar": {
"description": "Бічна панель завжди в компактному режимі",
"name": "Компактна бічна панель"
},
"crossfade": {
"description": "Плавний перехід між піснями",
"menu": {
"advanced": "Розширене"
},
"name": "Плавний перехід[бета-версія]",
"prompt": {
"options": {
"multi-input": {
"fade-in-duration": "Тривалість згасання (мс)",
"fade-out-duration": "Тривалість згасання (ms)",
"fade-scaling": {
"label": "Згладжування масштабування",
"linear": "Лінійна",
"logarithmic": "Логарифмічна"
},
"seconds-before-end": "Плавний перехід за N секунд до кінця"
},
"title": "Параметри плавного переходу"
}
}
},
"disable-autoplay": {
"description": "Запуск пісні в режимі \"пауза\"",
"menu": {
"apply-once": "Застосовується тільки при запуску"
},
"name": "Вимкнути автовідтворення"
},
"discord": {
"backend": {
"already-connected": "Спроба підключення при активному з'єднанні",
"connected": "Під'єднано до Discord",
"disconnected": "Від'єднано від Discord"
},
"description": "Покажіть друзям, що ви слухаєте за допомогою Rich Presence",
"menu": {
"auto-reconnect": "Автоматичне перепідключення",
"clear-activity": "Очистити активність",
"clear-activity-after-timeout": "Очистити активність після тайм-ауту",
"connected": "Під'єднано",
"disconnected": "Від'єднано",
"hide-duration-left": "Приховати тривалість, яка залишилася",
"hide-github-button": "Приховати посилання на GitHub",
"play-on-youtube-music": "Слухати на YouTube Music",
"set-inactivity-timeout": "Встановити тайм-аут бездіяльності"
},
"name": "Discord Rich Presence",
"prompt": {
"set-inactivity-timeout": {
"label": "Введіть тайм-аут бездіяльності в секундах:",
"title": "Встановлення тайм-ауту бездіяльності"
}
}
},
"downloader": {
"backend": {
"dialog": {
"error": {
"buttons": {
"ok": "Добре"
},
"message": "Йой! При завантаженні виникла помилка…",
"title": "Помилка при завантаженні!"
},
"start-download-playlist": {
"buttons": {
"ok": "OK"
},
"detail": "({{playlistSize}} пісень)",
"message": "Завантаження списку відтворення {{playlistTitle}}",
"title": "Завантаження розпочато"
}
},
"feedback": {
"conversion-progress": "Конвертація: {{percent}}%",
"converting": "Конвертація…",
"done": "Готово: {{filePath}}",
"download-info": "Завантаження {{artist}} - {{title}} [{{videoId}}]",
"download-progress": "Завантажено: {{percent}}%",
"downloading": "Завантаження…",
"downloading-counter": "Завантажено {{current}}/{{total}}…",
"downloading-playlist": "Завантаження плейлисту \"{{playlistTitle}}\" - {{playlistSize}} пісень ({{playlistId}})",
"error-while-downloading": "Помилка завантаження \"{{author}} - {{title}}\": {{error}}",
"folder-already-exists": "Папка {{playlistFolder}} вже існує",
"getting-playlist-info": "Отримання інформації про плейлист…",
"loading": "Завантаження…",
"playlist-has-only-one-song": "Плейлист має лише один елемент, завантаження якого відбувається безпосередньо",
"playlist-id-not-found": "Ідентифікатор плейлиста не знайдено",
"playlist-is-empty": "Плейлист порожній",
"playlist-is-mix-or-private": "Помилка при отриманні інформації про плейлист: переконайтеся, що це не приватний плейлист або плейлист \"Мікс для вас\"\n\n{{error}}",
"preparing-file": "Готуємо файл…",
"saving": "Збереження…",
"trying-to-get-playlist-id": "Спроба отримати ідентифікатора плейлиста: {{playlistId}}",
"video-id-not-found": "Відео не знайдено",
"writing-id3": "Пишемо ID3-теги…"
}
},
"description": "Завантажує MP3 / джерело аудіо безпосередньо з інтерфейсу",
"menu": {
"choose-download-folder": "Оберіть папку для завантаження",
"download-playlist": "Завантажити плейлист",
"presets": "Попередні налаштування",
"skip-existing": "Пропустити наявні файли"
},
"name": "Завантажувач",
"renderer": {
"can-not-update-progress": "Неможливо оновити прогрес"
},
"templates": {
"button": "Завантажити"
}
},
"exponential-volume": {
"description": "Робить регулятор гучності експоненціальним, що полегшує вибір тихих рівнів гучності.",
"name": "Експоненціальний обсяг"
},
"in-app-menu": {
"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": "Зображення в зображенні"
}
}
}

View 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"
}
}

View File

@ -1,7 +1,648 @@
{
"common": {
"console": {
"plugins": {
"execute-failed": "执行插件 {{pluginName}}::{{contextName}} 失败",
"executed-at-ms": "插件 {{pluginName}}::{{contextName}} 已在 {{ms}} 毫秒内执行",
"initialize-failed": "初始化插件 “{{pluginName}}” 失败",
"load-all": "正在加载所有插件",
"load-failed": "加载插件 “{{pluginName}}” 失败",
"loaded": "插件 “{{pluginName}}” 已载入",
"unload-failed": "卸载插件 “{{pluginName}}” 失败",
"unloaded": "插件 “{{pluginName}}” 已卸载"
}
}
},
"language": {
"code": "zh-CN",
"local-name": "簡體中文",
"local-name": "简体中文",
"name": "Simplified Chinese"
},
"main": {
"console": {
"did-finish-load": {
"dev-tools": "加载完毕。开发人员工具已启动"
},
"i18n": {
"loaded": "i18n 已载入"
},
"second-instance": {
"receive-command": "已从协议接收到以下指令:“{{command}}”"
},
"theme": {
"css-file-not-found": "CSS 文件 “{{cssFile}}” 不存在,将被忽略"
},
"unresponsive": {
"details": "无响应错误!\n{{error}}"
},
"when-ready": {
"clearing-cache-after-20s": "正在清理应用缓存"
},
"window": {
"tried-to-render-offscreen": "窗口试图于屏幕外绘制, windowSize={{windowSize}}, displaySize={{displaySize}}, position={{position}}"
}
},
"dialog": {
"hide-menu-enabled": {
"detail": "菜单已隐藏,按下 Alt 键来显示(若使用应用内菜单则按 Esc 键)",
"message": "隐藏菜单已被启用",
"title": "隐藏菜单已启用"
},
"need-to-restart": {
"buttons": {
"later": "稍后",
"restart-now": "现在重启"
},
"detail": "“{{pluginName}}” 插件需要重启程序后生效",
"message": "“{{pluginName}}” 需要重启",
"title": "需要重启"
},
"unresponsive": {
"buttons": {
"quit": "退出",
"relaunch": "重启",
"wait": "等待"
},
"detail": "对产生的不便我们表示抱歉!请选择接下来要做什么:",
"message": "应用程序无响应",
"title": "窗口无响应"
},
"update-available": {
"buttons": {
"disable": "禁用更新",
"download": "下载",
"ok": "好的"
},
"detail": "新版本现已可用,可从 {{downloadLink}} 下载",
"message": "新版本可用",
"title": "更新可用"
}
},
"menu": {
"about": "关于",
"navigation": {
"label": "导航",
"submenu": {
"copy-current-url": "复制当前 URL",
"go-back": "后退",
"go-forward": "前进",
"quit": "退出",
"restart": "重启应用"
}
},
"options": {
"label": "选项",
"submenu": {
"advanced-options": {
"label": "高级选项",
"submenu": {
"auto-reset-app-cache": "启动时重置应用缓存",
"disable-hardware-acceleration": "禁用硬件加速",
"edit-config-json": "编辑 config.json",
"override-user-agent": "覆盖 User-Agent",
"restart-on-config-changes": "配置改变时重启",
"set-proxy": {
"label": "设置代理",
"prompt": {
"label": "输入代理地址(留空以禁用)",
"placeholder": "例如: SOCKS5://127.0.0.1:9999",
"title": "设置代理服务器"
}
},
"toggle-dev-tools": "切换开发人员工具"
}
},
"always-on-top": "保持置顶",
"auto-update": "自动更新",
"hide-menu": {
"dialog": {
"message": "菜单将在下次启动时隐藏,按下 Alt 键以显示(若使用应用内菜单则按 ` 键)",
"title": "隐藏菜单已启用"
},
"label": "隐藏菜单"
},
"language": {
"dialog": {
"message": "语言会在应用重启后更改",
"title": "语言已更改"
},
"label": "语言",
"submenu": {
"to-help-translate": "想要协助翻译?点击此处"
}
},
"resume-on-start": "应用启动时继续上次播放的歌曲",
"single-instance-lock": "单例模式",
"start-at-login": "系统登录时启动",
"starting-page": {
"label": "启动页面",
"unset": "取消设定"
},
"tray": {
"label": "托盘",
"submenu": {
"disabled": "已禁用",
"enabled-and-hide-app": "启用并隐藏应用",
"enabled-and-show-app": "启用并显示应用",
"play-pause-on-click": "点击时播放/暂停"
}
},
"visual-tweaks": {
"label": "视觉调整",
"submenu": {
"like-buttons": {
"default": "默认",
"force-show": "强制显示",
"hide": "隐藏",
"label": "点赞按钮"
},
"remove-upgrade-button": "移除升级按钮",
"theme": {
"label": "主题",
"submenu": {
"import-css-file": "导入自定义 CSS 文件",
"no-theme": "无主题"
}
}
}
}
}
},
"plugins": {
"enabled": "已启用",
"label": "插件",
"new": "新建"
},
"view": {
"label": "视图",
"submenu": {
"force-reload": "强制刷新",
"reload": "刷新",
"reset-zoom": "实际缩放大小",
"toggle-fullscreen": "切换全屏",
"zoom-in": "放大",
"zoom-out": "缩小"
}
}
},
"tray": {
"next": "下一首",
"play-pause": "播放/暂停",
"previous": "上一首",
"quit": "退出",
"restart": "重启应用",
"show": "显示窗口"
}
},
"plugins": {
"adblocker": {
"description": "屏蔽所有广告与跟踪器",
"menu": {
"blocker": "屏蔽器"
},
"name": "广告屏蔽器"
},
"album-actions": {
"description": "添加作用于播放列表或专辑中所有歌曲的全局“点赞/取消点赞”与“喜欢/取消喜欢”按钮。",
"name": "专辑操作"
},
"album-color-theme": {
"description": "根据专辑封面配色动态改变主题与视觉效果",
"name": "专辑配色主题"
},
"ambient-mode": {
"description": "将视频中的浅配色作为光效投射到背景中,以增加沉浸感。",
"menu": {
"blur-amount": {
"label": "模糊等级",
"submenu": {
"pixels": "{{blurAmount}} 像素"
}
},
"buffer": {
"label": "缓冲",
"submenu": {
"buffer": "{{buffer}}"
}
},
"opacity": {
"label": "不透明度",
"submenu": {
"percent": "{{opacity}}%"
}
},
"quality": {
"label": "质量",
"submenu": {
"pixels": "{{quality}} 像素"
}
},
"size": {
"label": "大小",
"submenu": {
"percent": "{{size}}%"
}
},
"smoothness-transition": {
"label": "平滑过渡",
"submenu": {
"during": "持续 {{interpolationTime}} 秒"
}
},
"use-fullscreen": {
"label": "使用全屏"
}
},
"name": "沉浸模式"
},
"audio-compressor": {
"description": "对音频应用压缩(压低响亮部分,提升柔和部分)",
"name": "音频压缩器"
},
"blur-nav-bar": {
"description": "让导航栏透明及模糊",
"name": "模糊导航栏"
},
"bypass-age-restrictions": {
"description": "绕过 YouTube 年龄验证",
"name": "绕过年龄验证"
},
"captions-selector": {
"description": "YouTube Music 音轨字幕选择器",
"menu": {
"autoload": "自动选择上次使用的字幕",
"disable-captions": "默认无字幕"
},
"name": "字幕选择器",
"prompt": {
"selector": {
"label": "当前字幕语言: {{language}}",
"none": "无",
"title": "选择字幕语言"
}
},
"templates": {
"title": "开启字幕选择器"
}
},
"compact-sidebar": {
"description": "始终将侧边栏设为紧凑模式",
"name": "紧凑式侧边栏"
},
"crossfade": {
"description": "在歌曲间启用交叉淡化效果",
"menu": {
"advanced": "高级"
},
"name": "交叉淡化 [Beta]",
"prompt": {
"options": {
"multi-input": {
"fade-in-duration": "淡入持续时间(毫秒)",
"fade-out-duration": "淡出持续时间(毫秒)",
"fade-scaling": {
"label": "淡化尺度",
"linear": "线性",
"logarithmic": "对数"
},
"seconds-before-end": "歌曲结束前持续交叉淡化时长"
},
"title": "交叉淡化选项"
}
}
},
"disable-autoplay": {
"description": "让曲目开始时处于 “暂停” 模式",
"menu": {
"apply-once": "仅在程序启动时应用"
},
"name": "禁用自动播放"
},
"discord": {
"backend": {
"already-connected": "已尝试使用活跃网络进行连接",
"connected": "已连接到 Discord",
"disconnected": "已和 Discord 断开连接"
},
"description": "使用 Rich Presence 与好友分享正在收听的音乐",
"menu": {
"auto-reconnect": "自动重连",
"clear-activity": "清除状态",
"clear-activity-after-timeout": "超时后清除状态",
"connected": "已连接",
"disconnected": "已断开连接",
"hide-duration-left": "隐藏剩余时长",
"hide-github-button": "隐藏 GitHub 链接按钮",
"play-on-youtube-music": "转至 YouTube Music 播放",
"set-inactivity-timeout": "设置非活跃时长"
},
"name": "Discord Rich Presence 状态显示",
"prompt": {
"set-inactivity-timeout": {
"label": "设置非活跃状态超时时长,以秒为单位:",
"title": "设置非活跃超时"
}
}
},
"downloader": {
"backend": {
"dialog": {
"error": {
"buttons": {
"ok": "好的"
},
"message": "啊!抱歉,下载失败…",
"title": "下载时发生错误!"
},
"start-download-playlist": {
"buttons": {
"ok": "好的"
},
"detail": "({{playlistSize}} 首歌曲)",
"message": "正在下载播放列表 {{playlistTitle}}",
"title": "下载已开始"
}
},
"feedback": {
"conversion-progress": "转换进度: {{percent}}%",
"converting": "正在转换…",
"done": "完成下载: {{filePath}}",
"download-info": "正在下载 {{artist}} - {{title}} [{{videoId}}",
"download-progress": "下载: {{percent}}%",
"downloading": "下载中…",
"downloading-counter": "当前正下载 {{current}}/{{total}}…",
"downloading-playlist": "当前正在下载播放列表 \"{{playlistTitle}}\" - 共有 {{playlistSize}} 首歌曲 ({{playlistId}})",
"error-while-downloading": "下载 \"{{author}} - {{title}}\" 时出错: {{error}}",
"folder-already-exists": "文件夹 {{playlistFolder}} 已存在",
"getting-playlist-info": "正在获取播放列表信息…",
"loading": "加载中…",
"playlist-has-only-one-song": "播放列表只有一首歌曲,将直接下载",
"playlist-id-not-found": "未找到播放列表 ID",
"playlist-is-empty": "播放列表是空的",
"playlist-is-mix-or-private": "获取播放列表信息时出错:请确认目标不是私有或“为你推荐”列表\n\n{{error}}",
"preparing-file": "正在准备文件…",
"saving": "正在保存…",
"trying-to-get-playlist-id": "正尝试获取播放列表 ID {{playlistId}}",
"video-id-not-found": "视频未找到",
"writing-id3": "正在写入 ID3 标签…"
}
},
"description": "在界面内直接下载 MP3 / 源音频",
"menu": {
"choose-download-folder": "选择下载文件夹",
"download-playlist": "下载播放列表",
"presets": "预设",
"skip-existing": "跳过已存在的文件"
},
"name": "下载器",
"renderer": {
"can-not-update-progress": "无法更新进度"
},
"templates": {
"button": "下载"
}
},
"exponential-volume": {
"description": "让音量滑块指数化以便选择更低的音量。",
"name": "指数化音量"
},
"in-app-menu": {
"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": "罗马化字幕"
},
"name": "Genius 歌词",
"renderer": {
"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": "导航"
},
"no-google-login": {
"description": "从界面内移除 Google 登录按钮和链接",
"name": "无 Google 登录"
},
"notifications": {
"description": "歌曲开始时显示通知(交互式通知仅适用于 Windows",
"menu": {
"interactive": "交互式通知",
"interactive-settings": {
"label": "通知交互设定",
"submenu": {
"hide-button-text": "隐藏按钮文字",
"refresh-on-play-pause": "播放/暂停时刷新",
"tray-controls": "点击托盘时打开/关闭"
}
},
"priority": "通知优先级",
"toast-style": "弹出通知样式",
"unpause-notification": "取消暂停时显示通知"
},
"name": "通知"
},
"picture-in-picture": {
"description": "允许应用切换到画中画模式",
"menu": {
"always-on-top": "保持置顶",
"hotkey": {
"label": "热键",
"prompt": {
"keybind-options": {
"hotkey": "热键"
},
"label": "选择切换画中画模式的热键",
"title": "画中画热键"
}
},
"save-window-position": "记住窗口位置",
"save-window-size": "记住窗口尺寸",
"use-native-pip": "使用浏览器原生画中画功能"
},
"name": "画中画",
"templates": {
"button": "画中画"
}
},
"playback-speed": {
"description": "快慢随心!为歌曲添加速度控制滑块",
"name": "回放速度",
"templates": {
"button": "速度"
}
},
"precise-volume": {
"description": "启用自定义的音量弹窗以及可变步长,以便采用滚轮/热键精确控制音量",
"menu": {
"arrows-shortcuts": "本地方向键控件",
"custom-volume-steps": "设置自定义音量步长",
"global-shortcuts": "全局热键"
},
"name": "精确音量",
"prompt": {
"global-shortcuts": {
"keybind-options": {
"decrease": "降低音量",
"increase": "提升音量"
},
"label": "选择全局音量键绑定:",
"title": "全局音量键绑定"
},
"volume-steps": {
"label": "选择音量增加/减弱步长",
"title": "音量步长"
}
}
},
"quality-changer": {
"backend": {
"dialog": {
"quality-changer": {
"detail": "当前质量: {{quality}}",
"message": "选择视频画质:",
"title": "选择视频画质"
}
}
},
"description": "允许在视频上显示切换画质按钮",
"name": "视频画质切换器"
},
"shortcuts": {
"description": "允许为音频回放(播放/暂停/上一曲/下一曲)设置全局热键,兼具覆盖物理键以禁用 OSD、启用 Ctrl/CMD + F 搜索、为物理键启用 Linux MPRIS 支持及自定义热键等高级功能。",
"menu": {
"override-media-keys": "覆盖物理媒体热键",
"set-keybinds": "设置全局歌曲控件"
},
"name": "快捷键(以及 MPRIS",
"prompt": {
"keybind": {
"keybind-options": {
"next": "下一曲",
"play-pause": "播放/暂停",
"previous": "上一曲"
},
"label": "为歌曲控制选择全局按键绑定:",
"title": "全局按键绑定"
}
}
},
"skip-disliked-songs": {
"description": "自动跳过不喜欢的歌曲",
"name": "跳过不喜欢的歌曲"
},
"skip-silences": {
"description": "自动跳过音乐中的无声片段",
"name": "跳过无声片段"
},
"sponsorblock": {
"description": "自动跳过非音乐部分,如 MV 的介绍/结语以及歌曲未开始的部分",
"name": "SponsorBlock"
},
"taskbar-mediacontrol": {
"description": "从 Windows 任务栏控制音乐回放",
"name": "任务栏媒体控件"
},
"touchbar": {
"description": "为 macOS 用户启用 TouchBar 支持",
"name": "TouchBar"
},
"tuna-obs": {
"description": "与 OBS 的 Tuna 插件集成",
"name": "Tuna OBS"
},
"video-toggle": {
"description": "增加视频/音频模式间的切换按钮。兼具移除整个视频页面的功能",
"menu": {
"align": {
"label": "位置对齐",
"submenu": {
"left": "左对齐",
"middle": "居中",
"right": "右对齐"
}
},
"force-hide": "强制移除视频页",
"mode": {
"label": "模式",
"submenu": {
"custom": "自定义开关样式",
"disabled": "禁用",
"native": "原生开关样式"
}
}
},
"name": "视频切换开关",
"templates": {
"button": "歌曲"
}
},
"visualizer": {
"description": "在播放器中添加可视化效果",
"menu": {
"visualizer-type": "可视化类型"
},
"name": "可视化效果"
}
}
}

View File

@ -2,14 +2,14 @@
"common": {
"console": {
"plugins": {
"execute-failed": "外掛 {{pluginName}} 無法執行::{{contextName}}",
"execute-failed": "外掛 {{pluginName}} 無法執行::{{contextName}}",
"executed-at-ms": "外掛 {{pluginName}}::{{contextName}} 用了 {{ms}} 毫秒來執行",
"initialize-failed": "初始化外掛「{{pluginName}}」失敗",
"load-all": "正在載入所有外掛",
"load-failed": "載入外掛「{{pluginName}}」失敗",
"loaded": "外掛「{{pluginName}}」已載入",
"unload-failed": "移除外掛「{{pluginName}}」失敗",
"unloaded": "外掛「{{pluginName}}」已移除"
"unloaded": "外掛「{{pluginName}}」已移除"
}
}
},
@ -44,18 +44,18 @@
},
"dialog": {
"hide-menu-enabled": {
"detail": "選單已隱藏,使用 Alt 鍵來重新顯示(或是使用 Esc 鍵)",
"message": "隱藏選單已經啟用",
"title": "隱藏選單已啟用"
"detail": "選單已隱藏,使用 Alt 鍵來重新顯示(或是使用 Esc 鍵)",
"message": "隱藏選單已經啟用",
"title": "隱藏選單已啟用"
},
"need-to-restart": {
"buttons": {
"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": {
@ -100,12 +100,12 @@
"disable-hardware-acceleration": "關閉硬體加速",
"edit-config-json": "編輯 config.json",
"override-user-agent": "覆寫使用者代理",
"restart-on-config-changes": "設定檔變更時重啟程式",
"restart-on-config-changes": "設定檔更動時自動重啟應用程式",
"set-proxy": {
"label": "設定代理伺服器",
"prompt": {
"label": "輸入代理伺服器位置:(留空以停用本設定)",
"placeholder": "範例:socks5://127.0.0.1:9999",
"placeholder": "範例:SOCKS5://127.0.0.1:9999",
"title": "設定代理伺服器"
}
},
@ -116,10 +116,10 @@
"auto-update": "自動更新",
"hide-menu": {
"dialog": {
"message": "選單會在程式下次啟動時隱藏,使用 Alt 鍵來重新顯示(或是使用 ` 鍵)",
"title": "隱藏選單已啟用"
"message": "選單會在程式下次啟動時隱藏,使用 Alt 鍵來重新顯示(或是使用 ` 鍵)",
"title": "隱藏選單已啟用"
},
"label": "隱藏選單"
"label": "隱藏選單"
},
"language": {
"dialog": {
@ -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": "預設",
@ -202,8 +202,8 @@
"name": "廣告阻擋"
},
"album-color-theme": {
"description": "依歌曲專輯色調動態改變主題顏色",
"name": "動態專輯主題色調"
"description": "依歌曲色調自動更改應用程式主題",
"name": "隨歌曲色調變更主題"
},
"ambient-mode": {
"description": "將影片內容的淺色畫面投放至螢幕背景, 讓觀眾在觀賞影片時更有臨場感。",
@ -329,13 +329,13 @@
"hide-duration-left": "隱藏音樂剩餘時間狀態",
"hide-github-button": "隱藏Github頁面按鈕",
"play-on-youtube-music": "顯示Play on YouTube Music按鈕",
"set-inactivity-timeout": "設置歌曲暫停後幾秒清除狀態"
"set-inactivity-timeout": "設定閒置狀態時長"
},
"name": "Discord狀態",
"prompt": {
"set-inactivity-timeout": {
"label": "設多少秒後清除狀態:",
"title": "設置歌曲暫停後幾秒清除狀態"
"label": "設多少秒後清除狀態:",
"title": "設定閒置狀態時長"
}
}
},
@ -354,7 +354,7 @@
"ok": "完成"
},
"detail": "({{playlistSize}} 首歌曲)",
"message": "正在下載 {{playlistTitle}} 播放清單",
"message": "正在下載播放清單 {{playlistTitle}}",
"title": "開始下載"
}
},
@ -366,30 +366,30 @@
"download-progress": "下載進度: {{percent}}%",
"downloading": "下載中…",
"downloading-counter": "正在下載第 {{current}}/{{total}}…",
"downloading-playlist": "正在下載 \"{{playlistTitle}}\" 播放清單 - 共 {{playlistSize}} 首歌 ({{playlistId}})",
"downloading-playlist": "正在下載播放清單 \"{{playlistTitle}}\" - 共 {{playlistSize}} 首歌 ({{playlistId}})",
"error-while-downloading": "無法下載 \"{{author}} - {{title}}\": {{error}}",
"folder-already-exists": "資料夾 {{playlistFolder}} 已經存在",
"getting-playlist-info": "正在獲取播放清單資訊…",
"loading": "載入中…",
"playlist-has-only-one-song": "播放清單內只有一首歌曲, 直接下載",
"playlist-id-not-found": "沒有找到播放清單ID",
"playlist-is-empty": "播放清單為空",
"playlist-has-only-one-song": "播放清單內只有一首歌曲, 直接下載",
"playlist-id-not-found": "沒有找到播放清單 ID",
"playlist-is-empty": "播放清單是空的",
"playlist-is-mix-or-private": "獲取播放清單資訊時發生錯誤: 請確認非私人播放清單或是\"為你推薦的合輯\"\n\n{{error}}",
"preparing-file": "正在準備檔案…",
"saving": "儲存中…",
"trying-to-get-playlist-id": "正在嘗試獲取播放清單ID: {{playlistId}}",
"video-id-not-found": "未找到影片",
"writing-id3": "正在寫入ID3標籤…"
"trying-to-get-playlist-id": "正在嘗試獲取播放清單 ID: {{playlistId}}",
"video-id-not-found": "未找到影片",
"writing-id3": "正在寫入 ID3 標籤…"
}
},
"description": "在應用程式內下載MP3原始音樂檔",
"description": "在應用程式內下載 MP3原始音樂檔",
"menu": {
"choose-download-folder": "選擇下載位置",
"download-playlist": "下載播放清單",
"presets": "預設格式",
"skip-existing": "跳過已存在的檔案"
},
"name": "下載",
"name": "歌曲下載",
"renderer": {
"can-not-update-progress": "無法更新進度"
},
@ -398,22 +398,22 @@
}
},
"exponential-volume": {
"description": "使音量滑桿指數增加,以便更容易選擇較低的音量。",
"name": "指數音量調整"
"description": "使音量滑桿指數,以便更容易選擇較低的音量。",
"name": "指數音量調整"
},
"in-app-menu": {
"description": "優化選單欄外觀, 選單欄顏色將會隨主題變色或預設黑色",
"description": "使選單列變更為黑色或隨主題變色",
"menu": {
"hide-dom-window-controls": "隱藏DOM視窗控制"
},
"name": "程式內選單"
"name": "程式內選單"
},
"last-fm": {
"description": "新增對Last.fm的scrobbling支援",
"name": "Last.fm"
},
"lumiastream": {
"description": "新增對Lumia Stream的支援",
"description": "新增對 Lumia Stream 的支援",
"name": "Lumia Stream [Beta]"
},
"lyrics-genius": {
@ -432,7 +432,7 @@
},
"no-google-login": {
"description": "移除Google登入按鈕及連結",
"name": "用Google登入"
"name": "用Google登入"
},
"notifications": {
"description": "在歌曲播放時發送一個系統通知 (可互動通知僅限Windows)",
@ -453,7 +453,7 @@
"name": "歌曲播放通知"
},
"picture-in-picture": {
"description": "允許應用程式切換至畫中畫模式",
"description": "允許應用程式切換至子母畫面模式",
"menu": {
"always-on-top": "永遠顯示在最上層",
"hotkey": {
@ -462,17 +462,17 @@
"keybind-options": {
"hotkey": "快捷鍵"
},
"label": "選擇一個快捷鍵來切換畫中畫模式",
"title": "畫中畫模式快捷鍵"
"label": "選擇一個快捷鍵來切換子母畫面模式",
"title": "子母畫面模式快捷鍵"
}
},
"save-window-position": "記住視窗位置",
"save-window-size": "記住視窗大小",
"use-native-pip": "使用瀏覽器原生畫中畫模式"
"use-native-pip": "使用瀏覽器原生子母畫面模式"
},
"name": "畫中畫模式",
"name": "子母畫面",
"templates": {
"button": "畫中畫模式"
"button": "子母畫面"
}
},
"playback-speed": {
@ -483,13 +483,13 @@
}
},
"precise-volume": {
"description": "讓你可使用滑鼠滾輪/快捷鍵控制音量",
"description": "讓你可使用滑鼠滾輪/快捷鍵控制音量,並顯示目前音量大小",
"menu": {
"arrows-shortcuts": "本地方向鍵控制",
"custom-volume-steps": "自訂音量層級",
"arrows-shortcuts": "方向鍵音量控制",
"custom-volume-steps": "自訂音量調整時層級",
"global-shortcuts": "全域快捷鍵"
},
"name": "精確音量控制",
"name": "進階音量控制",
"prompt": {
"global-shortcuts": {
"keybind-options": {
@ -500,8 +500,8 @@
"title": "全域音量控制快捷鍵"
},
"volume-steps": {
"label": "設定音量一次增加/降低的層級",
"title": "音量層級設定"
"label": "設定音量一次增加/降低的層級",
"title": "自訂音量調整時層級"
}
}
},
@ -515,11 +515,11 @@
}
}
},
"description": "允許在影片畫面內調整影片畫質",
"name": "影片畫質更改器"
"description": "允許在影片內進行畫質更改",
"name": "允許變更影片畫質"
},
"shortcuts": {
"description": "使用全域快捷鍵控制音樂 (播放/暫停/下一首/上一首) + 透過覆寫媒體快捷鍵停用媒體OSD + 允許Ctrl/CMD + F來搜尋 + 支援Linux MPRIS媒體快捷鍵 + 更多自訂快捷鍵給進階使用者",
"description": "使用全域快捷鍵控制音樂 (播放/暫停/下一首/上一首) + 透過覆寫媒體快捷鍵停用媒體OSD + 允許Ctrl/CMD + F來搜尋 + 支援Linux MPRIS媒體快捷鍵 + 更多自訂快捷鍵給進階使用者",
"menu": {
"override-media-keys": "覆寫媒體快捷鍵",
"set-keybinds": "設定全域歌曲控制"
@ -537,9 +537,13 @@
}
}
},
"skip-disliked-songs": {
"description": "自動跳過不喜歡的歌曲",
"name": "自動跳過不喜歡的歌曲"
},
"skip-silences": {
"description": "自動跳過音樂無聲片段",
"name": "跳過無聲"
"name": "自動跳過無聲片段"
},
"sponsorblock": {
"description": "自動跳過贊助片段",
@ -550,11 +554,11 @@
"name": "工作列媒體控制"
},
"touchbar": {
"description": "為macOS使用者新增支援觸控列",
"name": "觸控列"
"description": "為macOS使用者新增觸控列支援",
"name": "觸控列 (Touchbar) 支援"
},
"tuna-obs": {
"description": "與OBS外掛Tuna連接",
"description": "與 OBSTuna 外掛連接",
"name": "Tuna OBS"
},
"video-toggle": {
@ -578,7 +582,7 @@
}
}
},
"name": "影片切換",
"name": "歌曲/影片切換",
"templates": {
"button": "歌曲"
}

View File

@ -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);
@ -402,55 +402,16 @@ async function createMainWindow() {
removeContentSecurityPolicy();
win.webContents.on('dom-ready', async () => {
win.webContents.on('dom-ready', () => {
if (useInlineMenu && is.windows()) {
win.setTitleBarOverlay({
...defaultTitleBarOverlayOptions,
height: Math.floor(
defaultTitleBarOverlayOptions.height! *
win.webContents.getZoomFactor(),
win.webContents.getZoomFactor(),
),
});
}
// Inject index.html file as string using insertAdjacentHTML
// In dev mode, get string from process.env.VITE_DEV_SERVER_URL, else use fs.readFileSync
if (is.dev() && process.env.ELECTRON_RENDERER_URL) {
// HACK: to make vite work with electron renderer (supports hot reload)
await win.webContents.executeJavaScript(`
console.log('Loading vite from dev server');
const viteScript = document.createElement('script');
viteScript.type = 'module';
viteScript.src = '${process.env.ELECTRON_RENDERER_URL}/@vite/client';
const rendererScript = document.createElement('script');
rendererScript.type = 'module';
rendererScript.src = '${process.env.ELECTRON_RENDERER_URL}/renderer.ts';
document.body.appendChild(viteScript);
document.body.appendChild(rendererScript);
0
`);
} else {
const rendererPath = path.join(__dirname, '..', 'renderer');
const indexHTML = parse(
fs.readFileSync(path.join(rendererPath, 'index.html'), 'utf-8'),
);
const scriptSrc = indexHTML.querySelector('script')!;
const scriptPath = path.join(
rendererPath,
scriptSrc.getAttribute('src')!,
);
const scriptString = fs.readFileSync(scriptPath, 'utf-8');
await win.webContents.executeJavaScriptInIsolatedWorld(
0,
[
{
code: scriptString + ';0',
url: url.pathToFileURL(scriptPath).toString(),
},
],
true,
);
}
});
win.webContents.loadURL(urlToLoad);
@ -629,6 +590,48 @@ app.whenReady().then(async () => {
}
}
ipcMain.on('get-renderer-script', (event) => {
// Inject index.html file as string using insertAdjacentHTML
// In dev mode, get string from process.env.VITE_DEV_SERVER_URL, else use fs.readFileSync
if (is.dev() && process.env.ELECTRON_RENDERER_URL) {
// HACK: to make vite work with electron renderer (supports hot reload)
event.returnValue = [null, `
console.log('${LoggerPrefix}', 'Loading vite from dev server');
(async () => {
await new Promise((resolve) => {
if (document.readyState === 'loading') {
console.log('${LoggerPrefix}', 'Waiting for DOM to load');
document.addEventListener('DOMContentLoaded', () => resolve(), { once: true });
} else {
resolve();
}
});
const viteScript = document.createElement('script');
viteScript.type = 'module';
viteScript.src = '${process.env.ELECTRON_RENDERER_URL}/@vite/client';
const rendererScript = document.createElement('script');
rendererScript.type = 'module';
rendererScript.src = '${process.env.ELECTRON_RENDERER_URL}/renderer.ts';
document.body.appendChild(viteScript);
document.body.appendChild(rendererScript);
})();
0
`];
} else {
const rendererPath = path.join(__dirname, '..', 'renderer');
const indexHTML = parse(
fs.readFileSync(path.join(rendererPath, 'index.html'), 'utf-8'),
);
const scriptSrc = indexHTML.querySelector('script')!;
const scriptPath = path.join(
rendererPath,
scriptSrc.getAttribute('src')!,
);
const scriptString = fs.readFileSync(scriptPath, 'utf-8');
event.returnValue = [url.pathToFileURL(scriptPath).toString(), scriptString + ';0'];
}
});
mainWindow = await createMainWindow();
await setApplicationMenu(mainWindow);
await refreshMenu(mainWindow);
@ -684,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>;
@ -714,7 +719,7 @@ app.whenReady().then(async () => {
break;
}
default: {
case 0: {
break;
}
}

View File

@ -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);

View File

@ -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();

View File

@ -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 () {

View 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);
});
});
},
},
});

View 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>

View 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>

View 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>

View 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>

View File

@ -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));
},
},
});

View File

@ -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;
}

View File

@ -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;

View File

@ -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;
}

View File

@ -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) {
@ -77,7 +75,7 @@ export const onRendererLoad = ({
ipc.on('downloader-feedback', (feedback: string) => {
if (progress) {
progress.innerHTML = feedback || 'Download';
progress.innerHTML = feedback || t('plugins.downloader.templates.button');
} else {
console.warn(
LoggerPrefix,

View File

@ -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();

View File

@ -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 {

View 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);
});
}
}

View 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
};
};

View 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

View 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

View 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

View 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

View 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

View 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);
}
}
});

View 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(),
};
};

View File

@ -0,0 +1 @@
export * from './queue';

View 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));
}
}

View 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('');
};

View 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;
};

View 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);

View 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;
}

View 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>

View 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>

View 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>

View 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>

View 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`
};
};

View 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,
};
};

View 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,
};
};

View 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,
};
};

View 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,
};
};

View File

@ -107,13 +107,15 @@ export const onMainLoad = async ({
window.on('move', () => {
if (config.isInPiP && !config.useNativePiP) {
setConfig({ 'pip-position': window.getPosition() as [number, number] });
const [x, y] = window.getPosition();
setConfig({ 'pip-position': [x, y] });
}
});
window.on('resize', () => {
if (config.isInPiP && !config.useNativePiP) {
setConfig({ 'pip-size': window.getSize() as [number, number] });
const [width, height] = window.getSize();
setConfig({ 'pip-size': [width, height] });
}
});
};

View File

@ -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 () => {

View File

@ -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"

View File

@ -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(() => {

View File

@ -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;
};

View File

@ -1,4 +1,4 @@
import { contextBridge, ipcRenderer, IpcRendererEvent } from 'electron';
import { contextBridge, ipcRenderer, IpcRendererEvent, webFrame } from 'electron';
import is from 'electron-is';
import config from './config';
@ -53,3 +53,19 @@ contextBridge.exposeInMainWorld(
'ELECTRON_RENDERER_URL',
process.env.ELECTRON_RENDERER_URL,
);
const [path, script] = ipcRenderer.sendSync('get-renderer-script') as [string | null, string];
let blocked = true;
if (path) {
webFrame.executeJavaScriptInIsolatedWorld(0, [
{
code: script,
url: path,
},
], true, () => blocked = false);
} else {
webFrame.executeJavaScript(script, true, () => blocked = false);
}
// HACK: Wait for the script to be executed
while (blocked);

View File

@ -2,7 +2,7 @@ import { singleton } from './decorators';
import type { YoutubePlayer } from '@/types/youtube-player';
import type { GetState } from '@/types/datahost-get-state';
import type { VideoDataChangeValue } from '@/types/player-api-events';
import type { AlbumDetails, VideoDataChangeValue } from '@/types/player-api-events';
import type { SongInfo } from './song-info';
import type { VideoDataChanged } from '@/types/video-data-changed';
@ -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)) {
@ -155,7 +155,10 @@ export default (api: YoutubePlayer) => {
const data = api.getPlayerResponse();
data.videoDetails.album =
videoData?.Hd?.playerOverlays?.playerOverlayRenderer?.browserMediaSession?.browserMediaSessionRenderer?.album.runs?.at(
(
Object.entries(videoData)
.find(([, value]) => value && Object.hasOwn(value, 'playerOverlays')) as [string, AlbumDetails | undefined]
)?.[1]?.playerOverlays?.playerOverlayRenderer?.browserMediaSession?.browserMediaSessionRenderer?.album?.runs?.at(
0,
)?.text;
data.videoDetails.elapsedSeconds = 0;

View File

@ -21,37 +21,6 @@ let isPluginLoaded = false;
let isApiLoaded = false;
let firstDataLoaded = false;
const observer = new MutationObserver(() => {
const playerApi = document.querySelector<Element & YoutubePlayer>(
'#movie_player',
);
if (playerApi) {
observer.disconnect();
// Inject song-info provider
setupSongInfo(playerApi);
const dataLoadedListener = (name: string) => {
if (!firstDataLoaded && name === 'dataloaded') {
firstDataLoaded = true;
playerApi.removeEventListener('videodatachange', dataLoadedListener);
}
};
playerApi.addEventListener('videodatachange', dataLoadedListener);
if (isPluginLoaded && !isApiLoaded) {
api = playerApi;
isApiLoaded = true;
onApiLoaded();
}
}
});
observer.observe(document.documentElement, {
childList: true,
subtree: true,
});
async function listenForApiLoad() {
if (!isApiLoaded) {
api = document.querySelector('#movie_player');
@ -176,14 +145,16 @@ const defineYTMDTransElements = () => {
);
};
(async () => {
const preload = async () => {
await loadI18n();
await setLanguage(window.mainConfig.get('options.language') ?? 'en');
window.i18n = {
t: i18t.bind(i18next),
};
defineYTMDTransElements();
};
const main = async () => {
await loadAllRendererPlugins();
isPluginLoaded = true;
@ -226,4 +197,48 @@ const defineYTMDTransElements = () => {
console.log(JSON.parse(log));
});
}
})();
};
const initObserver = async () => {
// check document.documentElement is ready
await new Promise<void>((resolve) => {
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => resolve(), { once: true });
} else {
resolve();
}
});
const observer = new MutationObserver(() => {
const playerApi = document.querySelector<Element & YoutubePlayer>(
'#movie_player',
);
if (playerApi) {
observer.disconnect();
// Inject song-info provider
setupSongInfo(playerApi);
const dataLoadedListener = (name: string) => {
if (!firstDataLoaded && name === 'dataloaded') {
firstDataLoaded = true;
playerApi.removeEventListener('videodatachange', dataLoadedListener);
}
};
playerApi.addEventListener('videodatachange', dataLoadedListener);
if (isPluginLoaded && !isApiLoaded) {
api = playerApi;
isApiLoaded = true;
onApiLoaded();
}
}
});
observer.observe(document.documentElement, {
childList: true,
subtree: true,
});
};
initObserver().then(preload).then(main);

View File

@ -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);
})
};

View File

@ -246,8 +246,6 @@ export type VideoDataChangeValue = Record<string, unknown> & {
title: string;
author: string;
Hd?: AlbumDetails;
playlistId: string;
isUpcoming: boolean;
loading: boolean;
@ -258,5 +256,7 @@ export type VideoDataChangeValue = Record<string, unknown> & {
export interface PlayerAPIEvents {
videodatachange: {
value: VideoDataChangeValue;
} & ({ name: 'dataloaded' } | { name: 'dataupdated ' });
name: 'dataloaded' | 'dataupdated';
};
onStateChange: number;
}

View File

@ -47,6 +47,7 @@ export interface PluginDef<
name: () => string;
authors?: Author[];
description?: () => string;
addedVersion?: string;
config?: Config;
menu?: (

View File

@ -170,17 +170,13 @@ export interface YoutubePlayer {
channelUnsubscribed: <Parameters extends unknown[], Return>(
...params: Parameters
) => Return;
togglePictureInPicture: <Parameters extends unknown[], Return>(
...params: Parameters
) => Return;
togglePictureInPicture: () => void;
supportsGaplessAudio: () => boolean;
supportsGaplessShorts: () => boolean;
enqueueVideoByPlayerVars: <Parameters extends unknown[], Return>(
...params: Parameters
) => Return;
clearQueue: <Parameters extends unknown[], Return>(
...params: Parameters
) => Return;
clearQueue: () => void;
getAudioTrack: <Parameters extends unknown[], Return>(
...params: Parameters
) => Return;
@ -266,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;
@ -284,14 +296,10 @@ export interface YoutubePlayer {
handleGlobalKeyDown: () => void;
handleGlobalKeyUp: () => void;
wakeUpControls: () => void;
cueVideoById: (videoId: string) => void;
loadVideoById: (videoId: string) => void;
cueVideoByUrl: <Parameters extends unknown[], Return>(
...params: Parameters
) => Return;
loadVideoByUrl: <Parameters extends unknown[], Return>(
...params: Parameters
) => Return;
cueVideoById: (videoId: string, startSeconds: number, suggestedQuality: string) => void;
loadVideoById: (videoId: string, startSeconds: number, suggestedQuality: string) => void;
cueVideoByUrl: (mediaContentUrl: string, startSeconds: number, suggestedQuality: string, playerType: string) => void;
loadVideoByUrl: (mediaContentUrl: string, startSeconds: number, suggestedQuality: string, playerType: string) => void;
/**
* Note: This doesn't resume playback, it plays from the start.
*/
@ -328,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;
@ -345,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;
@ -354,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;
@ -363,9 +375,7 @@ export interface YoutubePlayer {
addCueRange: <Parameters extends unknown[], Return>(
...params: Parameters
) => Return;
removeCueRange: <Parameters extends unknown[], Return>(
...params: Parameters
) => Return;
removeCueRange: (range: unknown[]) => void;
setSize: (size: { width: number; height: number }) => void;
destroy: <Parameters extends unknown[], Return>(
...params: Parameters
@ -383,12 +393,8 @@ export interface YoutubePlayer {
getVideoUrl: () => string;
getMediaReferenceTime: () => number;
getSize: () => { width: number; height: number };
logImaAdEvent: <Parameters extends unknown[], Return>(
...params: Parameters
) => Return;
preloadVideoById: <Parameters extends unknown[], Return>(
...params: Parameters
) => Return;
logImaAdEvent: (eventType: unknown, breakType: unknown) => void;
preloadVideoById: (videoId: string, startSeconds: number, suggestedQuality: string) => void;
setAccountLinkState: <Parameters extends unknown[], Return>(
...params: Parameters
) => Return;

View File

@ -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;

View File

@ -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;

View File

@ -1,9 +1,9 @@
{
"extends": "@electron-toolkit/tsconfig/tsconfig.node.json",
"compilerOptions": {
"target": "ESNext",
"target": "esnext",
"lib": ["dom", "dom.iterable", "es2022"],
"module": "CommonJS",
"module": "esnext",
"types": ["electron-vite/node"],
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
@ -21,5 +21,8 @@
}
},
"exclude": ["./dist"],
"include": ["electron.vite.config.ts", "./src/**/*"]
"include": [
"electron.vite.config.mts",
"./src/**/*"
]
}

View File

@ -1,4 +1,5 @@
import { basename, relative, resolve, extname } from 'node:path';
import { basename, relative, resolve, extname, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import { globSync } from 'glob';
import { Project } from 'ts-morph';
@ -7,6 +8,7 @@ const snakeToCamel = (text: string) =>
text.replace(/-(\w)/g, (_, letter: string) => letter.toUpperCase());
export const i18nImporter = () => {
const __dirname = dirname(fileURLToPath(import.meta.url));
const project = new Project({
tsConfigFilePath: resolve(__dirname, '..', 'tsconfig.json'),
skipAddingFilesFromTsConfig: true,

View File

@ -1,4 +1,5 @@
import { basename, relative, resolve, extname } from 'node:path';
import { basename, relative, resolve, extname, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import { globSync } from 'glob';
import { Project } from 'ts-morph';
@ -9,6 +10,7 @@ const snakeToCamel = (text: string) =>
export const pluginVirtualModuleGenerator = (
mode: 'main' | 'preload' | 'renderer',
) => {
const __dirname = dirname(fileURLToPath(import.meta.url));
const project = new Project({
tsConfigFilePath: resolve(__dirname, '..', 'tsconfig.json'),
skipAddingFilesFromTsConfig: true,

View File

@ -1,5 +1,6 @@
import { readFile } from 'node:fs/promises';
import { resolve, basename } from 'node:path';
import { resolve, basename, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import { createFilter } from 'vite';
import { Project, ts, ObjectLiteralExpression, VariableDeclarationKind } from 'ts-morph';
@ -17,6 +18,8 @@ export default function (mode: 'backend' | 'preload' | 'renderer' | 'none'): Plu
async load(id) {
if (!pluginFilter(id)) return null;
const __dirname = dirname(fileURLToPath(import.meta.url));
const project = new Project({
tsConfigFilePath: resolve(__dirname, '..', 'tsconfig.json'),
skipAddingFilesFromTsConfig: true,

View File

@ -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