Compare commits

...

306 Commits

Author SHA1 Message Date
27f4c0393e Bump version to 3.6.2 2024-10-16 20:43:19 +09:00
9bc42f836f fix: trustedTypes issue
- Close #2339
2024-10-16 20:40:35 +09:00
11b11ed966 fix(deps): update dependency serve to v14.2.4 (#2515)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-16 20:15:01 +09:00
5f79b7e788 chore(i18n): Translated using Weblate (Icelandic)
Currently translated at 94.7% (376 of 397 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/is/
2024-10-15 22:15:39 +02:00
7d1d806797 chore(i18n): Translated using Weblate (Indonesian)
Currently translated at 100.0% (397 of 397 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/id/
2024-10-15 22:15:39 +02:00
836cedb0f3 chore(i18n): Translated using Weblate (Ukrainian)
Currently translated at 100.0% (397 of 397 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/uk/
2024-10-15 22:15:39 +02:00
70349e13cc chore(i18n): Translated using Weblate (Russian)
Currently translated at 99.7% (396 of 397 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/ru/
2024-10-15 22:15:39 +02:00
6d16b74471 fix(deps): update dependency hono to v4.6.5 (#2509)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-15 22:33:46 +09:00
c211780c33 chore(deps): update dependency vite to v5.4.9 (#2500)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-15 13:46:51 +09:00
2173ba0234 fix(api-server): properly implement next api call (#2505) 2024-10-15 13:34:19 +09:00
3a4cbc543b chore(deps): update dependency electron to v33 (#2507)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-15 13:30:11 +09:00
2fef7f0246 chore(deps): update dependency typescript-eslint to v8.9.0 (#2503)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-15 05:03:21 +09:00
12d693921e chore(i18n): Translated using Weblate (Chinese (Traditional Han script))
Currently translated at 100.0% (397 of 397 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/zh_Hant/
2024-10-14 19:24:56 +02:00
d1b4879f51 chore(deps): update dependency discord-api-types to v0.37.102 (#2501)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-14 23:32:47 +09:00
f14939fcd2 Update changelog for v3.6.1 2024-10-14 13:42:17 +00:00
4e66b0cedd fix: remove snap arm 2024-10-14 22:23:37 +09:00
11fe54d640 fix(style): fix youtube music player layout not aligned issue 2024-10-14 22:01:26 +09:00
446529f738 fix: remove pacman 2024-10-14 21:59:56 +09:00
ac6e9deeb9 chore(i18n): Translated using Weblate (Spanish)
Currently translated at 100.0% (397 of 397 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/es/
2024-10-14 14:42:58 +02:00
100873163f fix: remove "compression": "maximum" 2024-10-14 21:34:33 +09:00
e141e18bac fix: fix build 2024-10-14 21:27:42 +09:00
f4da0c2c95 fix: enum value must be one of xz,lzo, got 'maximum' 2024-10-14 21:17:15 +09:00
7507bce3cc fix: fix org.electronjs.Electron2.BaseApp/x86_64/20.08 2024-10-14 21:03:10 +09:00
2b970fade8 fix: fix org.freedesktop.Sdk/x86_64/20.08 2024-10-14 20:51:36 +09:00
35f0e43082 fix: remove duplicate flatpak 2024-10-14 20:38:25 +09:00
ae4410a613 fix(flatpak): remove armhf/arm64 2024-10-14 20:32:05 +09:00
5534174016 fix: fix flatpak build 2024-10-14 20:27:33 +09:00
15cf6c77c3 fix: fix flatpak release 2024-10-14 20:12:54 +09:00
18a8fc462d fix: fix pnpm-lock 2024-10-14 20:01:02 +09:00
d3acb4945a chore(flatpak-builder): Add more details when failing 2024-10-14 19:57:41 +09:00
32d3c58b44 fix: fix release workflow 2024-10-14 19:33:37 +09:00
bd8c2eb390 fix: configuration has an unknown property 'AppImage' 2024-10-14 19:26:24 +09:00
a81fa9c0d1 fix: apt update 2024-10-14 19:22:02 +09:00
1d0f7d7a48 fix: fix release 2024-10-14 19:20:04 +09:00
b6687307df Bump version to 3.6.1 2024-10-14 19:16:25 +09:00
7e07a44f68 fix(downloader): fix #2371 2024-10-14 18:27:04 +09:00
5ca66530ee fix(ytm-bugs): incorrect video ratio
- Close #2459
2024-10-14 18:16:22 +09:00
95acbe2b65 fix: fix conflict with adguard 2024-10-14 18:06:08 +09:00
534aeb163a fix(api-server): fix init/authentication error
- Close #2497
2024-10-14 17:51:59 +09:00
6abcbee290 chore(i18n): Translated using Weblate (English)
Currently translated at 100.0% (397 of 397 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/en/
2024-10-14 09:54:33 +02:00
4457a043a4 chore(i18n): Translated using Weblate (English)
Currently translated at 100.0% (397 of 397 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/en/
2024-10-14 09:54:05 +02:00
410a052fea chore(i18n): Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (397 of 397 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/pt_BR/
2024-10-14 09:51:36 +02:00
e4287085a1 chore(i18n): Translated using Weblate (Filipino)
Currently translated at 87.1% (346 of 397 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/fil/
2024-10-14 09:51:36 +02:00
b6cefef8fb fix(api-server): Various fixes and improvements (#2496) 2024-10-14 16:48:11 +09:00
9d7e2a06bc fix(deps): update dependency electron-debug to v4.1.0 (#2499)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-14 16:45:19 +09:00
d516fc2153 fix(renderer): fix force like buttons display logic (#2493) 2024-10-14 04:27:23 +09:00
77bfe8e218 fix: RSS feed CORS issue
- Close #1620
2024-10-14 04:04:17 +09:00
0fcbe38837 fix(deps): update dependency i18next to v23.16.0 (#2492)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-14 03:46:38 +09:00
b85a40f683 Update changelog for v3.6.0 2024-10-13 13:57:28 +00:00
8020d61715 Bump version to 3.6.0 2024-10-13 22:48:28 +09:00
cb1381bbb3 fix: apply fix from eslint 2024-10-13 22:45:11 +09:00
f42f20f770 chore(i18n): Translated using Weblate (Persian)
Currently translated at 49.1% (195 of 397 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/fa/
2024-10-13 15:40:48 +02:00
9fb1dbfde0 chore(i18n): Translated using Weblate (Arabic)
Currently translated at 26.9% (107 of 397 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/ar/
2024-10-13 15:40:47 +02:00
4f1ebab45d chore(i18n): Translated using Weblate (Catalan)
Currently translated at 100.0% (397 of 397 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/ca/
2024-10-13 15:34:42 +02:00
95644ea513 chore(i18n): Translated using Weblate (Korean)
Currently translated at 100.0% (397 of 397 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/ko/
2024-10-13 15:34:41 +02:00
2fcddc8d2d chore(i18n): Translated using Weblate (Spanish)
Currently translated at 99.7% (396 of 397 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/es/
2024-10-13 15:34:41 +02:00
6505a93645 fix(api-server): fix i18n 2024-10-13 21:56:17 +09:00
11c25efd47 fix(taskbar-mediacontrol): fix icon color
- Close #2485
2024-10-13 21:45:20 +09:00
a97bc8da5a fix(README): fix eslint path 2024-10-13 21:42:00 +09:00
782116b31b fix: pin deps version 2024-10-13 19:14:40 +09:00
708d4b5480 chore(deps): Bump pnpm/action-setup to v4 2024-10-13 19:11:03 +09:00
9ba8913da7 feat(api-server): remote control api (#1909)
Co-authored-by: JellyBrick <shlee1503@naver.com>
Co-authored-by: Angelos Bouklis <angelbouklis.official@gmail.com>
Co-authored-by: Angelos Bouklis <me@arjix.dev>
Co-authored-by: Angelos Bouklis <53124886+ArjixWasTaken@users.noreply.github.com>
2024-10-13 19:10:12 +09:00
d07dae2542 fix: fix pnpm-lock.yaml 2024-10-13 18:24:36 +09:00
e7f213c4dc chore(eslint): apply eslint-plugin-prettier
- Close #2438
2024-10-13 18:24:01 +09:00
4cbf6c015b chore(deps): update playwright monorepo to v1.48.0 (#2489)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-13 18:05:01 +09:00
8acb93225b fix(synced-lyrics): fix text extract logic 2024-10-13 18:03:37 +09:00
8153955ccf fix(synced-lyrics): Fix 2 issues (#2441) 2024-10-13 18:00:03 +09:00
1de1cbac65 chore(deps): update dependency typescript to v5.6.3 (#2486)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-13 17:14:17 +09:00
825aac1dab chore(deps): update dependency electron to v32.2.0 (#2487)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-13 17:14:08 +09:00
d48aa7ad39 chore(deps): update dependency del-cli to v6 (#2475)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-13 17:12:05 +09:00
65ad09a02e chore(deps): update dependency typescript-eslint to v8.8.1 (#2477)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-13 17:11:56 +09:00
08aae09446 fix(deps): update dependency solid-js to v1.9.2 (#2480)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-13 17:11:25 +09:00
88f54a389f Revert "chore(deps): update dependency electron-builder to v25" (#2488) 2024-10-13 17:11:11 +09:00
4c23b1f970 chore(deps): update dependency electron-builder to v25 (#2406)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-13 17:09:34 +09:00
a979f1c8ea fix(deps): update dependency deepmerge-ts to v7.1.3 (#2481)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-13 17:09:28 +09:00
ead448ed98 chore(i18n): Translated using Weblate (Persian)
Currently translated at 51.0% (195 of 382 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/fa/
2024-10-11 06:56:26 +02:00
fade340e80 chore(i18n): Translated using Weblate (Arabic)
Currently translated at 28.0% (107 of 382 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/ar/
2024-10-11 06:56:25 +02:00
8cc8160f70 chore(i18n): Translated using Weblate (Persian)
Currently translated at 22.5% (86 of 382 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/fa/
2024-10-10 14:39:44 +02:00
ee354ff678 chore(i18n): Added translation using Weblate (Persian) 2024-10-10 01:45:13 +02:00
ca7ccc7b3e chore(i18n): Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (382 of 382 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/pt_BR/
2024-10-09 17:15:40 +02:00
f6b2766ec2 fix(deps): update dependency ts-morph to v24 (#2474)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-07 12:42:06 +09:00
4a5f811485 fix(deps): update dependency i18next to v23.15.2 (#2471)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-05 19:57:18 +09:00
a092da7ba5 chore(deps): update eslint monorepo to v9.12.0 (#2470)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-05 15:32:49 +09:00
b587a16419 chore(deps): update dependency @stylistic/eslint-plugin-js to v2.9.0 (#2469)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-05 15:30:15 +09:00
4cf4f19ccc chore(deps): bump micromatch from 4.0.5 to 4.0.8 (#2465)
Bumps [micromatch](https://github.com/micromatch/micromatch) from 4.0.5 to 4.0.8.
- [Release notes](https://github.com/micromatch/micromatch/releases)
- [Changelog](https://github.com/micromatch/micromatch/blob/master/CHANGELOG.md)
- [Commits](https://github.com/micromatch/micromatch/compare/4.0.5...4.0.8)

---
updated-dependencies:
- dependency-name: micromatch
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-05 15:06:59 +09:00
5b84c9efce chore(deps): bump braces from 3.0.2 to 3.0.3 (#2466)
Bumps [braces](https://github.com/micromatch/braces) from 3.0.2 to 3.0.3.
- [Changelog](https://github.com/micromatch/braces/blob/master/CHANGELOG.md)
- [Commits](https://github.com/micromatch/braces/compare/3.0.2...3.0.3)

---
updated-dependencies:
- dependency-name: braces
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-05 15:06:52 +09:00
98bbbfd851 fix(deps): update dependency electron-updater to v6.3.9 (#2468)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-05 15:04:15 +09:00
da2dbcacc4 fix(deps): update dependency deepmerge-ts to v7.1.1 (#2467)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-05 15:02:19 +09:00
7b6235694b chore(deps): update dependency typescript-eslint to v8.8.0 (#2457)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-05 15:02:09 +09:00
4ad8e7b9dc chore(deps): update dependency @babel/runtime to v7.25.7 (#2462)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-05 15:02:01 +09:00
51ecfff86b chore(deps): update dependency rollup to v4.24.0 (#2458)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-05 15:00:04 +09:00
2c21a03201 chore(deps): update dependency eslint-plugin-import to v2.31.0 (#2464)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-05 14:59:44 +09:00
4c2cb8dac9 chore(i18n): Translated using Weblate (Portuguese (Brazil))
Currently translated at 73.2% (280 of 382 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/pt_BR/
2024-10-04 15:15:48 +02:00
9edcd2c32e chore(i18n): Translated using Weblate (Filipino)
Currently translated at 86.9% (332 of 382 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/fil/
2024-10-03 02:15:41 +00:00
0829fd2167 chore(deps): update dependency rollup to v4.22.5 (#2448)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-30 07:24:04 +09:00
494e58296b chore(deps): update dependency typescript-eslint to v8.7.0 (#2450)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-30 07:20:57 +09:00
151da2d786 fix(deps): update dependency solid-js to v1.9.1 (#2451)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-30 07:20:46 +09:00
d7f5f28091 chore(deps): update dependency vite to v5.4.8 (#2449)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-28 15:52:06 +09:00
358fb3b084 chore(deps): update dependency discord-api-types to v0.37.101 (#2440)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-28 15:51:54 +09:00
af79ba266d chore(deps): update dependency esbuild to v0.24.0 (#2439)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-28 15:51:44 +09:00
45f419f41a chore(deps): update eslint monorepo to v9.11.1 (#2442)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-28 15:35:42 +09:00
5189d6cfee chore(deps): update dependency @types/howler to v2.2.12 (#2443)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-28 15:35:30 +09:00
102034b58c chore(i18n): Translated using Weblate (Polish)
Currently translated at 100.0% (382 of 382 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/pl/
2024-09-24 18:15:47 +02:00
50d92bc004 chore(i18n): Translated using Weblate (English)
Currently translated at 99.7% (381 of 382 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/en/
2024-09-24 18:15:46 +02:00
f2716e1dc8 chore(i18n): Translated using Weblate (Polish)
Currently translated at 100.0% (382 of 382 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/pl/
2024-09-23 17:26:28 +02:00
9d436ed0a8 chore(i18n): Translated using Weblate (Polish)
Currently translated at 100.0% (382 of 382 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/pl/
2024-09-23 17:26:28 +02:00
31d472e289 chore(deps): update dependency vite to v5.4.7 (#2434)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-22 08:17:56 +09:00
7f9a2b3011 chore(deps): update playwright monorepo to v1.47.2 (#2436)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-22 08:17:35 +09:00
f54b346dce chore(deps): update eslint monorepo to v9.11.0 (#2437)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-22 08:17:12 +09:00
dbff62bc5b fix(deps): update dependency youtubei.js to v10.5.0 (#2431)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-22 08:13:18 +09:00
87f43e3237 chore(deps): update dependency rollup to v4.22.4 (#2430)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-22 08:13:00 +09:00
49cdcbdcc2 chore(deps): update dependency electron to v32.1.2 (#2433)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-22 08:12:50 +09:00
bcff26c85b chore(i18n): Translated using Weblate (Portuguese (Brazil))
Currently translated at 72.2% (276 of 382 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/pt_BR/
2024-09-21 03:40:55 +00:00
bcfb33b4b1 chore(i18n): Translated using Weblate (Portuguese (Brazil))
Currently translated at 72.2% (276 of 382 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/pt_BR/
2024-09-21 03:40:54 +00:00
b172d8e509 chore(i18n): Added translation using Weblate (Portuguese (Brazil)) 2024-09-20 04:55:25 +02:00
f10d146272 chore(i18n): Translated using Weblate (Italian)
Currently translated at 100.0% (382 of 382 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/it/
2024-09-19 23:40:54 +02:00
6105821a94 feat: ESLint Flat Config (v9 support #2229) (#2426)
* Flat config

* undo accidental formatting changes

* remove extra newline
2024-09-19 21:19:41 +09:00
7c983df6f4 chore(deps): update dependency electron to v32.1.1 2024-09-19 07:39:25 +09:00
cbbddefcf8 chore(i18n): Translated using Weblate (Ukrainian)
Currently translated at 97.3% (372 of 382 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/uk/
2024-09-18 15:41:01 +00:00
8d49c67fcb Update changelog for v3.5.3 2024-09-17 12:41:36 +00:00
c94b22b82c Bump version to 3.5.3 2024-09-17 21:32:22 +09:00
cab3cb49f0 chore(deps): update dependency electron to v32.1.0 2024-09-17 21:31:29 +09:00
e42084f008 fix(deps): update dependency @floating-ui/dom to v1.6.11 2024-09-17 21:30:11 +09:00
adca273ec3 chore(deps): update dependency typescript to v5.6.2 2024-09-17 21:28:03 +09:00
91dceb3c22 chore(deps): update playwright monorepo to v1.47.1 2024-09-17 21:27:47 +09:00
216e76f4a1 fix(music-together): fix ? operator 2024-09-17 21:09:30 +09:00
178bfa483f fix: openToast to toastService 2024-09-17 21:05:35 +09:00
10ecf5d2fe fix(src/index): fix typo 2024-09-17 20:40:23 +09:00
7099b81296 fix(src/index): ignore eslint error 2024-09-17 20:40:23 +09:00
1f15376b00 chore(deps): update dependency vite to v5.4.6 2024-09-17 20:39:32 +09:00
02b7a39753 chore(deps): update dependency eslint to v8.57.1 2024-09-17 20:38:38 +09:00
6edc84a8bd chore(deps): update dependency rollup to v4.21.3 2024-09-17 20:38:19 +09:00
11a0d39064 fix(song-info-front): fix nullable issue 2024-09-17 20:27:11 +09:00
d5a5ed35b6 fix: fix trustedHTML issue
- (Maybe) fix #2339, caused by YouTube's A/B testing
2024-09-17 20:05:22 +09:00
dbb9e95b32 fix(deps): update dependency i18next to v23.15.1 2024-09-17 18:27:27 +09:00
d4c8a4320d chore(deps): update typescript-eslint monorepo to v8.6.0 2024-09-17 18:27:05 +09:00
68d4f38e41 chore(i18n): Translated using Weblate (Hungarian)
Currently translated at 94.7% (362 of 382 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/hu/
2024-09-15 22:09:12 +00:00
2204784e89 chore(i18n): Translated using Weblate (Icelandic)
Currently translated at 92.9% (355 of 382 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/is/
2024-09-15 22:09:12 +00:00
c3b995b0a8 chore(i18n): Translated using Weblate (Hebrew)
Currently translated at 6.0% (23 of 382 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/he/
2024-09-11 22:09:24 +02:00
ed0a344077 chore(i18n): Translated using Weblate (Polish)
Currently translated at 95.2% (364 of 382 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/pl/
2024-09-09 22:09:15 +00:00
199d912823 Update changelog for v3.5.2 2024-09-07 12:27:30 +00:00
089eff3152 Revert "chore(deps): update dependency electron-builder to v25"
This reverts commit fe4c89c349.
2024-09-07 20:22:27 +09:00
306ee8dba5 Bump version to 3.5.2 2024-09-07 19:09:15 +09:00
a4992bafb2 fix(synced-lyric): fix timestamp
- Close #2323
- Close #2379
2024-09-07 19:06:52 +09:00
5b004eedff fix(song-info): fix regex 2024-09-07 18:40:24 +09:00
2a66076d31 fix(synced-lyric): optimize logic 2024-09-07 18:37:37 +09:00
f48e46d29c chore(deps): update dependency eslint-plugin-import to v2.30.0 2024-09-07 18:29:22 +09:00
20296f5463 chore(deps): update dependency rollup to v4.21.2 2024-09-07 18:29:16 +09:00
fe4c89c349 chore(deps): update dependency electron-builder to v25 2024-09-07 18:29:09 +09:00
3640527c8c fix(synced-lyrics): LRCLIB returns 0 results if album is undefined
- inspired by #2381
2024-09-07 18:28:13 +09:00
3326582a16 chore(deps): update dependency electron to v32 2024-09-07 18:21:41 +09:00
5dcb9fe9ba fix(deps): update dependency i18next to v23.14.0 2024-09-07 18:19:15 +09:00
336fa1e6fd fix(deps): update dependency youtubei.js to v10.4.0 2024-09-07 18:19:09 +09:00
3679a109d6 chore(deps): update dependency esbuild to v0.23.1 2024-09-07 18:15:01 +09:00
5290ed3de2 chore(deps): update playwright monorepo to v1.47.0 2024-09-07 18:14:40 +09:00
fe5195714f chore(deps): update typescript-eslint monorepo to v8.4.0 (#2401)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-07 18:14:27 +09:00
8eb846262d chore(deps): update dependency @total-typescript/ts-reset to v0.6.1 (#2396)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-07 18:09:00 +09:00
e9a1c2a91f chore(deps): update dependency electron to v31.5.0 (#2397)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-07 18:08:41 +09:00
2d1f78b383 chore(deps): update dependency eslint-import-resolver-typescript to v3.6.3 (#2376)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-07 18:06:07 +09:00
1899064fd3 chore(deps): update dependency discord-api-types to v0.37.100 (#2394)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-07 18:06:00 +09:00
e0280e5fe2 fix(deps): update dependency electron-updater to v6.3.4 (#2395)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-07 18:05:54 +09:00
d577e0fba6 chore(deps): update dependency @babel/runtime to v7.25.6 (#2388)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-07 17:26:24 +09:00
37577c2f7f chore(deps): update dependency vite-plugin-inspect to v0.8.7 (#2389)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-07 17:26:16 +09:00
c880f0a4eb chore(i18n): Translated using Weblate (Portuguese)
Currently translated at 100.0% (382 of 382 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/pt/
2024-09-05 21:09:15 +02:00
4875955914 chore(deps): update dependency discord-api-types to v0.37.99 (#2374)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-04 23:47:50 +09:00
5b28c780bd chore(deps): update dependency vite to v5.4.3 (#2377)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-04 23:47:42 +09:00
4db2674b15 chore(i18n): Translated using Weblate (Greek)
Currently translated at 52.6% (201 of 382 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/el/
2024-09-02 23:09:12 +00:00
Sen
a454a0163f chore(i18n): Translated using Weblate (Dutch)
Currently translated at 74.0% (283 of 382 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/nl/
2024-08-31 21:09:31 +02:00
61eb28e780 fix: incorrect regex when splitting artistName (#2378)
Fixes the incorrect regex used to split a string in the form of `Name1 & Name2` or `Name1, Name2`.

Previously the regex was actually splitting on `Name1 &, Name2`...
2024-08-29 07:48:18 +09:00
e165e64952 chore(i18n): Translated using Weblate (Hungarian)
Currently translated at 93.7% (358 of 382 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/hu/
2024-08-28 13:09:22 +00:00
f380822e11 chore(deps): update dependency @babel/runtime to v7.25.4 (#2373)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-28 12:22:29 +09:00
6b1995145a synced-lyrics: make the lyrics search more reliable (#2343)
* fix: comparing multiple artists on a single track

* fix: get song info on startup

* chore: also split on commas

* chore: re-apply .toLowerCase() on the artist names

* chore: remove redundant code

* chore: attempt at improving the initial videodata

* oops

* eureka!

* stuff
2024-08-28 12:21:58 +09:00
9317e99f43 fix(deps): update dependency solid-js to v1.8.22 (#2354)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-28 12:20:37 +09:00
66d05d8683 chore(deps): update typescript-eslint monorepo to v8.3.0 (#2350)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-28 12:20:26 +09:00
545a3a4bb6 fix(deps): update dependency electron-debug to v4.0.1 (#2349)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-28 12:20:07 +09:00
04a6d16dbe chore(deps): update dependency electron to v31.4.0 (#2356)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-28 12:19:53 +09:00
3c36477b1e fix: hide native-controls on linux when in-app-menu is used (#2366) 2024-08-28 12:19:30 +09:00
c5c191492e fix: detect the upgrade btn using the icon (#2364) 2024-08-23 07:13:55 +09:00
b12e2f607c chore(i18n): Translated using Weblate (Filipino)
Currently translated at 86.6% (331 of 382 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/fil/
2024-08-20 03:09:10 +00:00
1d77ad6de4 fix: exclude build-id files from rpm (#2361) 2024-08-19 07:46:05 +09:00
25bd26d7f3 chore(i18n): Translated using Weblate (Chinese (Traditional))
Currently translated at 100.0% (382 of 382 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/zh_Hant/
2024-08-17 12:09:10 +02:00
d11d0abe73 chore(i18n): Translated using Weblate (Japanese)
Currently translated at 100.0% (382 of 382 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/ja/
2024-08-17 12:09:10 +02:00
8a643c465d chore(i18n): Translated using Weblate (Estonian)
Currently translated at 18.8% (72 of 382 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/et/
2024-08-15 22:09:31 +00:00
233673b8d8 chore(i18n): Translated using Weblate (Estonian)
Currently translated at 14.1% (54 of 382 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/et/
2024-08-13 21:09:13 +02:00
5a448fab31 chore(i18n): Translated using Weblate (Catalan)
Currently translated at 100.0% (382 of 382 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/ca/
2024-08-13 21:09:12 +02:00
42e8262cda fix(deps): update dependency i18next to v23.12.3 (#2352)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-13 11:57:27 +09:00
f64769b1d3 fix(deps): update dependency @floating-ui/dom to v1.6.10 (#2340)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-12 12:12:08 +09:00
e12998761b fix(deps): update dependency electron-updater to v6.3.3 (#2347)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-12 12:11:55 +09:00
2e20fa83b8 chore(i18n): Translated using Weblate (German)
Currently translated at 99.2% (379 of 382 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/de/
2024-08-11 10:09:24 +00:00
5149757af3 chore(i18n): Translated using Weblate (German)
Currently translated at 99.2% (379 of 382 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/de/
2024-08-11 10:09:24 +00:00
655741f108 chore(i18n): Translated using Weblate (German)
Currently translated at 96.3% (368 of 382 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/de/
2024-08-10 11:41:12 +02:00
4e58571ad0 chore(i18n): Translated using Weblate (German)
Currently translated at 96.3% (368 of 382 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/de/
2024-08-10 11:41:12 +02:00
1e4a615b47 chore(i18n): Translated using Weblate (German)
Currently translated at 94.7% (362 of 382 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/de/
2024-08-10 11:39:20 +02:00
dedcf0c9ff fix(deps): update dependency solid-js to v1.8.20 (#2345)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-10 10:58:28 +09:00
a84a7d236a chore(deps): update dependency vite to v5.4.0 (#2342)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-10 10:54:05 +09:00
e56b4b21f0 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-08-09 16:09:15 +02:00
361f9e42bd chore(i18n): Translated using Weblate (Malay)
Currently translated at 19.6% (75 of 382 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/ms/
2024-08-09 16:09:13 +02:00
918736d2ca chore(i18n): Translated using Weblate (German)
Currently translated at 94.2% (360 of 382 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/de/
2024-08-09 16:09:12 +02:00
8f3d5b08ac chore(i18n): Translated using Weblate (Russian)
Currently translated at 97.3% (372 of 382 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/ru/
2024-08-08 00:09:19 +02:00
4ca327d801 chore(deps): update typescript-eslint monorepo to v8.0.1 (#2335)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-07 23:03:13 +09:00
8d0aa057ad fix(deps): update dependency @floating-ui/dom to v1.6.9 (#2337)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-07 23:02:23 +09:00
b7ffee089b chore(deps): update playwright monorepo to v1.46.0 (#2336)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-07 23:02:12 +09:00
d72b994f66 chore(README): Translation README to Russian and adding Synced Lyrics to main README (#2338)
* Translation of README.md to Russian and adding Synced Lyrics to default README.md

* Syntax fixes

* Sorting of Synced Lyrics Alphabetically
2024-08-07 23:01:49 +09:00
e6dafdb068 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-08-06 11:09:21 +02:00
14886dd4bd chore(i18n): Translated using Weblate (Catalan)
Currently translated at 100.0% (382 of 382 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/ca/
2024-08-06 11:09:17 +02:00
c9f2f88bac chore(i18n): Translated using Weblate (Russian)
Currently translated at 97.3% (372 of 382 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/ru/
2024-08-06 11:09:17 +02:00
1eabbc0bbe chore(i18n): Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (382 of 382 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/zh_Hans/
2024-08-05 09:09:19 +02:00
32a572b35a fix(downloader): fix playabilityStatus 2024-08-04 16:33:28 +09:00
59a5679cbb fix(cache): use cacheNoArgs for better performance 2024-08-04 16:31:07 +09:00
ac51f798c3 fix(synced-lyric): fix album_name 2024-08-04 15:49:30 +09:00
7599cc694a Revert "fix(MPRIS): Prevents player to start with invalid MPRIS interface (#1996)"
This reverts commit eaf9d310aa.

Fix #2225
2024-08-04 15:26:14 +09:00
53595654b1 chore(adblocker): add comment 2024-08-04 14:50:14 +09:00
7656c41dbc fix(adblocker/inplayer): fix Response.prototype.json
fix #2310
2024-08-04 14:36:10 +09:00
ff0c5b87c9 chore(i18n): Translated using Weblate (Estonian)
Currently translated at 2.8% (11 of 382 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/et/
2024-08-03 22:09:19 +00:00
506c95740a chore(i18n): Translated using Weblate (Vietnamese)
Currently translated at 99.4% (380 of 382 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/vi/
2024-08-03 22:09:18 +00:00
575a42e28a chore(i18n): Translated using Weblate (Indonesian)
Currently translated at 100.0% (382 of 382 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/id/
2024-08-03 22:09:17 +00:00
dcdc6a825f chore(i18n): Translated using Weblate (Chinese (Traditional))
Currently translated at 100.0% (382 of 382 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/zh_Hant/
2024-08-03 22:09:16 +00:00
0a41bb1cd6 chore(i18n): Translated using Weblate (Chinese (Traditional))
Currently translated at 100.0% (382 of 382 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/zh_Hant/
2024-08-03 22:09:16 +00:00
72a4736dc9 chore(i18n): Translated using Weblate (Greek)
Currently translated at 47.3% (181 of 382 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/el/
2024-08-03 22:09:15 +00:00
f2b1e6b6bf chore(deps): update dependency rollup to v4.20.0 (#2326)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-03 18:47:56 +09:00
bc0e28ad6d chore(i18n): Added translation using Weblate (Estonian) 2024-08-02 23:59:14 +02:00
8ebae91c02 chore(i18n): Translated using Weblate (Hungarian)
Currently translated at 93.7% (358 of 382 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/hu/
2024-08-02 23:59:13 +02:00
954ad90733 chore(i18n): Translated using Weblate (Chinese (Traditional))
Currently translated at 100.0% (382 of 382 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/zh_Hant/
2024-08-02 23:59:13 +02:00
5af0643788 chore(i18n): Translated using Weblate (French)
Currently translated at 100.0% (382 of 382 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/fr/
2024-08-02 23:59:13 +02:00
be633ac1f2 chore(i18n): Translated using Weblate (Chinese (Traditional))
Currently translated at 99.7% (381 of 382 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/zh_Hant/
2024-08-02 21:58:41 +02:00
0d047c1fd5 chore(i18n): Translated using Weblate (Chinese (Traditional))
Currently translated at 99.7% (381 of 382 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/zh_Hant/
2024-08-02 21:58:41 +02:00
e8b1aca629 chore(i18n): Translated using Weblate (Turkish)
Currently translated at 100.0% (382 of 382 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/tr/
2024-08-02 21:58:41 +02:00
5b9bacf390 chore(i18n): Translated using Weblate (Filipino)
Currently translated at 85.3% (326 of 382 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/fil/
2024-08-02 14:09:23 +02:00
ccd16f4a5f chore(i18n): Translated using Weblate (Chinese (Traditional))
Currently translated at 93.4% (357 of 382 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/zh_Hant/
2024-08-02 14:09:22 +02:00
02e519dab3 chore(i18n): Translated using Weblate (Spanish)
Currently translated at 100.0% (382 of 382 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/es/
2024-08-02 14:09:21 +02:00
473ea78f12 synced-lyrics: fix album name? 2024-08-01 15:46:53 +03:00
e7f366b770 Update changelog for v3.5.1 2024-08-01 11:49:06 +00:00
66816ac42d Bump version to 3.5.1 2024-08-01 20:39:44 +09:00
08b985f2ab chore(i18n): Translated using Weblate (Korean)
Currently translated at 100.0% (382 of 382 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/ko/
2024-08-01 13:38:36 +02:00
747bde2136 fix(synced-lyrics): fix lyric load
fix #2295
2024-08-01 20:19:08 +09:00
eabc28b39f fix(wait-for-element): add 100ms timeout for improve performance 2024-08-01 20:19:08 +09:00
3537dc19ee chore(i18n): Translated using Weblate (Hungarian)
Currently translated at 97.8% (358 of 366 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/hu/
2024-08-01 10:57:05 +00:00
6afeb60557 chore(i18n): Translated using Weblate (Vietnamese)
Currently translated at 100.0% (366 of 366 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/vi/
2024-08-01 10:57:04 +00:00
71115dedee chore(i18n): Translated using Weblate (Italian)
Currently translated at 100.0% (366 of 366 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/it/
2024-08-01 10:57:04 +00:00
8750b54f76 fix(synced-lyrics): fix i18n 2024-08-01 19:56:32 +09:00
482a1c5073 fix(deps): update dependency youtubei.js to v10.3.0 (#2306)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-01 19:27:58 +09:00
c8d516c40b fix: Window gets stuck offscreen in some instances (#2303)
Improved offscreen detection logic, fixes th-ch#1894
2024-08-01 19:27:01 +09:00
c1ad168c32 fix: Incorrect window size on multi-monitor scaled displays (#2302)
see discussion on th-ch#2258
2024-08-01 19:26:37 +09:00
5f5be5d02f chore(deps): update dependency rollup to v4.19.2 (#2304)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-01 19:25:59 +09:00
61ef56dccc chore: enable sourcemaps in dev 2024-08-01 11:02:21 +03:00
a73b5acc75 chore(deps): update typescript-eslint monorepo to v8 (major) (#2297)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-01 16:37:08 +09:00
877573532c ts-fix: disambiguate ElectronStore typings 2024-08-01 10:10:52 +03:00
7b033b5caf fix(ambient-mode): fix ambient-mode not working for videos after restart (#2294)
* Fix Ambient Mode not working for videos after restart (#2255)

This should fix https://github.com/th-ch/youtube-music/issues/1641

* fix: fix waitForElement

---------

Co-authored-by: craftgeil <80261988+craftgeil@users.noreply.github.com>
2024-07-31 22:08:45 +09:00
8924ec29d3 fix(deps): update dependency @xhayper/discord-rpc to v1.2.0 (#2291)
* fix(deps): update dependency @xhayper/discord-rpc to v1.2.0

* fix: discord-rpc

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: JellyBrick <shlee1503@naver.com>
2024-07-31 21:57:37 +09:00
23e688aaf8 Update changelog for v3.5.0 2024-07-31 12:04:22 +00:00
9b3cbe8e01 Bump version to 3.5.0 2024-07-31 20:53:02 +09:00
67a89e8ed4 chore(i18n): Translated using Weblate (Korean)
Currently translated at 100.0% (366 of 366 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/ko/
2024-07-31 13:51:24 +02:00
464a2b94ea fix: bump electron-builder version to 6.3.2 2024-07-31 20:37:38 +09:00
9357a15116 fix(synced-lyrics): fix type error 2024-07-31 20:32:34 +09:00
ee820bb01c Delete test-results directory 2024-07-31 19:55:51 +09:00
6b81735811 chore(i18n): Translated using Weblate (French)
Currently translated at 100.0% (357 of 357 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/fr/
2024-07-31 12:54:43 +02:00
8ce91b143a plugin: Synced Lyrics (#2207)
* Added Plugin File

* Added Logic

* Known issue

* Finished Backend part

* Before cleanup

* Added Style
Removed log

* Fixed time and visibility issues

* Changed lyrics style

* Changed way lyrics are selected

* Fix

* Added style lyrics options

* Cleanup

* Fix lyrics styling
Changed how lyrics status are changed

* Moved code to make file more readable

* Change Tab Size

* Fixed issue with overlapping lyrics

* Removed debug console.log

* Added style adaptation for music videos

* Changed file indent

* Revered back to original pnpm file

* Removed unnecessary option

* Fix lyrics status bug
Removed leftover logs

* Started to implement fetching for genius lyrics

* feat(synced-lyrics): add `addedVersion` field

* Made changes according to feedbacks

* fix: add a delay of 300ms to the current time

- Since the transition takes 300ms, we need to add a delay of 300ms to the current time

* Removed test about genius.com scraping

* Removed 300ms delay

* chore: cleaned up the code

* Specified path and variable

* chore: always enable lyrics tab

* chore: use SolidJS to render the lyrics

* chore: remove useless signal

* chore: feature-parity with original PR (+some nice stuff)

* recreate lock file

* show json decode error

* feat(synced-lyrics): improve ui
- Change type assertion code
- Replace span to `yt-formatted-string`
- Add refetch button

* chore: make the lyric styling a solidjs effect

* feat: i18n

* chore: apply suggestion

---------

Co-authored-by: Su-Yong <simssy2205@gmail.com>
Co-authored-by: JellyBrick <shlee1503@naver.com>
Co-authored-by: Angelos Bouklis <53124886+ArjixWasTaken@users.noreply.github.com>
2024-07-31 19:54:21 +09:00
116dbad9bc chore(deps): update dependency electron to v31.3.1 (#2290)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-31 19:52:47 +09:00
977af3d617 chore(deps): update typescript-eslint monorepo to v7.18.0 (#2292)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-31 19:52:38 +09:00
6da8defc73 chore(i18n): Translated using Weblate (Sinhala)
Currently translated at 4.4% (16 of 357 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/si/
2024-07-29 01:09:20 +00:00
0e93a963e1 chore(i18n): Translated using Weblate (Korean)
Currently translated at 100.0% (357 of 357 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/ko/
2024-07-29 01:09:19 +00:00
1e98b2e75a fix(deps): update dependency youtubei.js to v10.2.0 (#2285)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-29 01:30:01 +09:00
6f5f13a840 chore(deps): update dependency electron to v31.3.0 (#2282)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-28 14:10:07 +00:00
822bcedadf chore(deps): update typescript-eslint monorepo to v7.17.0 (#2283)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-28 14:09:48 +00:00
2b6aea82c3 fix(deps): update dependency solid-js to v1.8.19 (#2280)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-28 19:21:49 +09:00
4f4efb407e fix(deps): update dependency @xhayper/discord-rpc to v1.1.4 (#2279)
* fix(deps): update dependency @xhayper/discord-rpc to v1.1.4

* Update and rename @xhayper__discord-rpc@1.1.2.patch to @xhayper__discord-rpc@1.1.4.patch

* Update package.json

* fix: update pnpm-lock.yaml

* fix: use Listening instead of Playing

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: JellyBrick <shlee1503@naver.com>
2024-07-28 19:21:35 +09:00
6159e0e652 chore(deps): update dependency @babel/runtime to v7.25.0 (#2281)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-28 19:07:01 +09:00
3957e06174 fix(deps): update dependency @floating-ui/dom to v1.6.8 (#2278)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-28 19:06:26 +09:00
c78f823b9b Fix: Incorrect window size on scaled displays (#2258)
fix th-ch#1716
2024-07-28 19:02:08 +09:00
1be3bb360e chore(deps): update dependency vite-plugin-resolve to v2.5.2 (#2276)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-28 15:06:22 +09:00
ba2afd2652 chore(deps): update playwright monorepo to v1.45.3 (#2277)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-28 15:06:07 +09:00
5e283c9ea5 fix(deps): update dependency deepmerge-ts to v7.1.0 (#2263)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-28 14:59:23 +09:00
ddb1c56111 chore(deps): update dependency typescript to v5.5.4 (#2274)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-28 14:59:13 +09:00
ebd167f3f2 chore(deps): update dependency vite to v5.3.5 (#2275)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-28 14:55:57 +09:00
178a62b9d3 fix(deps): update dependency i18next to v23.12.2 (#2260)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-28 14:46:26 +09:00
f98a2cf766 chore(deps): update dependency discord-api-types to v0.37.93 (#2273)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-28 14:46:06 +09:00
fdbe6f7331 chore(deps): update dependency rollup to v4.19.1 (#2261)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-28 14:45:54 +09:00
57c2cdc91e chore(i18n): Translated using Weblate (Portuguese)
Currently translated at 100.0% (357 of 357 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/pt/
2024-07-26 18:09:17 +02:00
0f5074f8ab chore(i18n): Translated using Weblate (French)
Currently translated at 100.0% (357 of 357 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/fr/
2024-07-26 18:09:13 +02:00
661396226d fix(deps): update dependency custom-electron-prompt to v1.5.8 (#2262)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-26 12:42:17 +09:00
36f27fe2e6 chore(i18n): Translated using Weblate (Polish)
Currently translated at 100.0% (357 of 357 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/pl/
2024-07-25 15:09:16 +02:00
adf1ce4bc7 chore(i18n): Translated using Weblate (Vietnamese)
Currently translated at 100.0% (357 of 357 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/vi/
2024-07-22 14:09:21 +02:00
43b4b8df5e chore(i18n): Translated using Weblate (German)
Currently translated at 100.0% (357 of 357 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/de/
2024-07-22 14:09:20 +02:00
4a8440c281 chore(i18n): Translated using Weblate (Catalan)
Currently translated at 100.0% (357 of 357 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/ca/
2024-07-20 18:09:24 +02:00
32fe9fcffe chore(i18n): Translated using Weblate (French)
Currently translated at 100.0% (357 of 357 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/fr/
2024-07-20 18:09:22 +02:00
a9896845da chore(i18n): Added translation using Weblate (Catalan) 2024-07-19 17:02:32 +02:00
a59aa07334 chore(i18n): Translated using Weblate (Filipino)
Currently translated at 83.7% (299 of 357 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/fil/
2024-07-19 10:09:32 +02:00
e07d7395e7 chore(i18n): Translated using Weblate (Spanish)
Currently translated at 100.0% (357 of 357 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/es/
2024-07-19 10:09:31 +02:00
9bb6f32ece chore(i18n): Translated using Weblate (Czech)
Currently translated at 96.3% (344 of 357 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/cs/
2024-07-19 10:09:30 +02:00
ccb19a0dc9 feat(adblocker): add new option AdSpeedup (#2235)
* Ad speedup code

* And ad-speedup translations

* Update index.ts

* fix error

* Update build.yml

* add AdSpeedup as adBlock option

* remove it as own plugin

* remove console.log

* add semicolons
2024-07-18 13:16:46 +09:00
64fb6c2597 fix: disable multi-plane format for software video (#2254) 2024-07-18 13:15:26 +09:00
73c3e355fe chore(deps): update dependency eslint-plugin-prettier to v5.2.1 (#2253)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-18 03:42:41 +09:00
fc7a504643 chore(deps): update dependency vite to v5.3.4 (#2243)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-18 03:42:34 +09:00
764dc0f895 chore(deps): update typescript-eslint monorepo to v7.16.1 (#2239)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-18 03:42:26 +09:00
9f33f49ec4 chore(deps): update playwright monorepo to v1.45.2 (#2244)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-18 03:32:19 +09:00
87ae6d29bb chore(deps): update dependency vite-plugin-inspect to v0.8.5 (#2252)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-18 03:31:55 +09:00
093c8e3ca6 fix(deps): update dependency semver to v7.6.3 (#2250)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-18 03:29:19 +09:00
fec26a010d chore(deps): update dependency electron to v31.2.1 (#2241)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-17 18:08:17 +09:00
5d8aaccc55 chore(i18n): Translated using Weblate (Hebrew)
Currently translated at 4.5% (16 of 355 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/he/
2024-07-17 08:09:14 +00:00
cda03078a9 chore(i18n): Translated using Weblate (Turkish)
Currently translated at 100.0% (355 of 355 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/tr/
2024-07-16 02:46:56 +02:00
9c139b96f4 chore(i18n): Translated using Weblate (French)
Currently translated at 98.8% (351 of 355 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/fr/
2024-07-16 02:46:55 +02:00
9b2816c156 chore(i18n): Translated using Weblate (Finnish)
Currently translated at 80.8% (287 of 355 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/fi/
2024-07-15 19:09:19 +00:00
b1b8847134 chore(i18n): Translated using Weblate (Chinese (Traditional))
Currently translated at 100.0% (355 of 355 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/zh_Hant/
2024-07-15 19:09:19 +00:00
bf9e698288 chore(i18n): Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (355 of 355 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/zh_Hans/
2024-07-15 19:09:18 +00:00
28e8a1c5dd chore(i18n): Translated using Weblate (Polish)
Currently translated at 99.7% (354 of 355 strings)

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/pl/
2024-07-15 19:09:17 +00:00
18e0b1b863 Update changelog for v3.4.1 2024-07-14 15:26:58 +00:00
159 changed files with 11374 additions and 2836 deletions

View File

@ -1 +0,0 @@
.eslintrc.js

View File

@ -1,80 +0,0 @@
module.exports = {
extends: [
'eslint:recommended',
'plugin:import/recommended',
'plugin:import/typescript',
'plugin:@typescript-eslint/eslint-recommended',
'plugin:@typescript-eslint/recommended',
'plugin:@typescript-eslint/recommended-requiring-type-checking',
],
plugins: ['prettier', '@typescript-eslint', 'import'],
parser: '@typescript-eslint/parser',
parserOptions: {
project: './tsconfig.json',
tsconfigRootDir: __dirname,
sourceType: 'module',
ecmaVersion: 'latest'
},
rules: {
'arrow-parens': ['error', 'always'],
'object-curly-spacing': ['error', 'always'],
'@typescript-eslint/no-floating-promises': 'off',
'@typescript-eslint/no-misused-promises': ['off', { checksVoidReturn: false }],
'@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }],
"@typescript-eslint/no-non-null-assertion": "off",
'import/first': 'error',
'import/newline-after-import': 'error',
'import/no-default-export': 'off',
'import/no-duplicates': 'error',
'import/no-unresolved': ['error', { ignore: ['^virtual:', '\\?inline$', '\\?raw$', '\\?asset&asarUnpack'] }],
'import/order': [
'error',
{
'groups': ['builtin', 'external', ['internal', 'index', 'sibling'], 'parent', 'type'],
'newlines-between': 'always-and-inside-groups',
'alphabetize': {order: 'ignore', caseInsensitive: false}
}
],
'import/prefer-default-export': 'off',
'camelcase': ['error', {properties: 'never'}],
'class-methods-use-this': 'off',
'lines-around-comment': [
'error',
{
beforeBlockComment: false,
afterBlockComment: false,
beforeLineComment: false,
afterLineComment: false,
},
],
'max-len': 'off',
'no-mixed-operators': 'error',
'no-multi-spaces': ['error', {ignoreEOLComments: true}],
'no-tabs': 'error',
'no-void': 'error',
'no-empty': 'off',
'prefer-promise-reject-errors': 'off',
'quotes': ['error', 'single', {
avoidEscape: true,
allowTemplateLiterals: false,
}],
'quote-props': ['error', 'consistent'],
'semi': ['error', 'always'],
},
env: {
browser: true,
node: true,
es6: true,
},
ignorePatterns: ['dist', 'node_modules'],
root: true,
settings: {
'import/parsers': {
'@typescript-eslint/parser': ['.ts']
},
'import/resolver': {
typescript: {},
exports: {},
},
},
};

View File

@ -21,7 +21,7 @@ jobs:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Install pnpm - name: Install pnpm
uses: pnpm/action-setup@v2 uses: pnpm/action-setup@v4
with: with:
version: 9 version: 9
run_install: false run_install: false
@ -62,6 +62,12 @@ jobs:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: | run: |
sudo snap install snapcraft --classic sudo snap install snapcraft --classic
sudo apt update
sudo apt install -y flatpak flatpak-builder
sudo flatpak remote-add --if-not-exists --system flathub https://flathub.org/repo/flathub.flatpakrepo
sudo flatpak install -y flathub org.freedesktop.Platform/x86_64/20.08
sudo flatpak install -y flathub org.freedesktop.Sdk/x86_64/20.08
sudo flatpak install -y flathub org.electronjs.Electron2.BaseApp/x86_64/20.08
pnpm release:linux pnpm release:linux
- name: Build and release on Windows - name: Build and release on Windows
@ -90,7 +96,7 @@ jobs:
fetch-depth: 0 fetch-depth: 0
- name: Install pnpm - name: Install pnpm
uses: pnpm/action-setup@v2 uses: pnpm/action-setup@v4
with: with:
version: 9 version: 9
run_install: false run_install: false

View File

@ -4,7 +4,7 @@
[![GitHub release](https://img.shields.io/github/release/th-ch/youtube-music.svg?style=for-the-badge&logo=youtube-music)](https://github.com/th-ch/youtube-music/releases/) [![GitHub release](https://img.shields.io/github/release/th-ch/youtube-music.svg?style=for-the-badge&logo=youtube-music)](https://github.com/th-ch/youtube-music/releases/)
[![GitHub license](https://img.shields.io/github/license/th-ch/youtube-music.svg?style=for-the-badge)](https://github.com/th-ch/youtube-music/blob/master/LICENSE) [![GitHub license](https://img.shields.io/github/license/th-ch/youtube-music.svg?style=for-the-badge)](https://github.com/th-ch/youtube-music/blob/master/LICENSE)
[![eslint code style](https://img.shields.io/badge/code_style-eslint-5ed9c7.svg?style=for-the-badge)](https://github.com/th-ch/youtube-music/blob/master/.eslintrc.js) [![eslint code style](https://img.shields.io/badge/code_style-eslint-5ed9c7.svg?style=for-the-badge)](https://github.com/th-ch/youtube-music/blob/master/eslint.config.mjs)
[![Build status](https://img.shields.io/github/actions/workflow/status/th-ch/youtube-music/build.yml?branch=master&style=for-the-badge&logo=youtube-music)](https://GitHub.com/th-ch/youtube-music/releases/) [![Build status](https://img.shields.io/github/actions/workflow/status/th-ch/youtube-music/build.yml?branch=master&style=for-the-badge&logo=youtube-music)](https://GitHub.com/th-ch/youtube-music/releases/)
[![GitHub All Releases](https://img.shields.io/github/downloads/th-ch/youtube-music/total?style=for-the-badge&logo=youtube-music)](https://GitHub.com/th-ch/youtube-music/releases/) [![GitHub All Releases](https://img.shields.io/github/downloads/th-ch/youtube-music/total?style=for-the-badge&logo=youtube-music)](https://GitHub.com/th-ch/youtube-music/releases/)
[![AUR](https://img.shields.io/aur/version/youtube-music-bin?color=blueviolet&style=for-the-badge&logo=youtube-music)](https://aur.archlinux.org/packages/youtube-music-bin) [![AUR](https://img.shields.io/aur/version/youtube-music-bin?color=blueviolet&style=for-the-badge&logo=youtube-music)](https://aur.archlinux.org/packages/youtube-music-bin)
@ -21,7 +21,7 @@
</a> </a>
</div> </div>
Read this in other languages: [🇰🇷](./docs/readme/README-ko.md), [🇮🇸](./docs/readme/README-is.md), [🇨🇱 🇪🇸](./docs/readme/README-es.md) Read this in other languages: [🇰🇷](./docs/readme/README-ko.md), [🇮🇸](./docs/readme/README-is.md), [🇨🇱 🇪🇸](./docs/readme/README-es.md), [🇷🇺](./docs/readme/README-ru.md)
**Electron wrapper around YouTube Music featuring:** **Electron wrapper around YouTube Music featuring:**
@ -141,6 +141,8 @@ Read this in other languages: [🇰🇷](./docs/readme/README-ko.md), [🇮🇸]
- [**SponsorBlock**](https://github.com/ajayyy/SponsorBlock): Automatically Skips non-music parts like intro/outro or - [**SponsorBlock**](https://github.com/ajayyy/SponsorBlock): Automatically Skips non-music parts like intro/outro or
parts of music videos where the song isn't playing parts of music videos where the song isn't playing
- **Synced Lyrics**: Provides synced lyrics to songs, using providers like [LRClib](https://lrclib.net).
- **Taskbar Media Control**: Control playback from - **Taskbar Media Control**: Control playback from
your [Windows taskbar](https://user-images.githubusercontent.com/78568641/111916130-24a35e80-8a82-11eb-80c8-5021c1aa27f4.png) your [Windows taskbar](https://user-images.githubusercontent.com/78568641/111916130-24a35e80-8a82-11eb-80c8-5021c1aa27f4.png)
@ -159,6 +161,7 @@ Read this in other languages: [🇰🇷](./docs/readme/README-ko.md), [🇮🇸]
- **Visualizer**: Different music visualizers - **Visualizer**: Different music visualizers
## Translation ## Translation
You can help with translation on [Hosted Weblate](https://hosted.weblate.org/projects/youtube-music/). You can help with translation on [Hosted Weblate](https://hosted.weblate.org/projects/youtube-music/).

View File

@ -2,8 +2,245 @@
All notable changes to this project will be documented in this file. Dates are displayed in UTC. All notable changes to this project will be documented in this file. Dates are displayed in UTC.
#### [v3.6.1](https://github.com/th-ch/youtube-music/compare/v3.6.0...v3.6.1)
- fix(api-server): Various fixes and improvements [`#2496`](https://github.com/th-ch/youtube-music/pull/2496)
- fix(deps): update dependency electron-debug to v4.1.0 [`#2499`](https://github.com/th-ch/youtube-music/pull/2499)
- fix(renderer): fix force like buttons display logic [`#2493`](https://github.com/th-ch/youtube-music/pull/2493)
- fix(deps): update dependency i18next to v23.16.0 [`#2492`](https://github.com/th-ch/youtube-music/pull/2492)
- fix(downloader): fix #2371 [`#2371`](https://github.com/th-ch/youtube-music/issues/2371)
- fix(ytm-bugs): incorrect video ratio [`#2459`](https://github.com/th-ch/youtube-music/issues/2459)
- fix(api-server): fix init/authentication error [`#2497`](https://github.com/th-ch/youtube-music/issues/2497)
- fix: RSS feed CORS issue [`#1620`](https://github.com/th-ch/youtube-music/issues/1620)
- chore(flatpak-builder): Add more details when failing [`d3acb49`](https://github.com/th-ch/youtube-music/commit/d3acb4945a8dcde6598c53d8207bbf16eda8c739)
- chore(i18n): Translated using Weblate (Filipino) [`e428708`](https://github.com/th-ch/youtube-music/commit/e4287085a11f30d141148ab0432cc684819fd0d0)
- Bump version to 3.6.1 [`b668730`](https://github.com/th-ch/youtube-music/commit/b6687307dfe7ef765517019093c8db3c2ad14417)
#### [v3.6.0](https://github.com/th-ch/youtube-music/compare/v3.5.3...v3.6.0)
> 13 October 2024
- feat(api-server): remote control api [`#1909`](https://github.com/th-ch/youtube-music/pull/1909)
- chore(deps): update playwright monorepo to v1.48.0 [`#2489`](https://github.com/th-ch/youtube-music/pull/2489)
- fix(`synced-lyrics`): Fix 2 issues [`#2441`](https://github.com/th-ch/youtube-music/pull/2441)
- chore(deps): update dependency typescript to v5.6.3 [`#2486`](https://github.com/th-ch/youtube-music/pull/2486)
- chore(deps): update dependency electron to v32.2.0 [`#2487`](https://github.com/th-ch/youtube-music/pull/2487)
- chore(deps): update dependency del-cli to v6 [`#2475`](https://github.com/th-ch/youtube-music/pull/2475)
- chore(deps): update dependency typescript-eslint to v8.8.1 [`#2477`](https://github.com/th-ch/youtube-music/pull/2477)
- fix(deps): update dependency solid-js to v1.9.2 [`#2480`](https://github.com/th-ch/youtube-music/pull/2480)
- Revert "chore(deps): update dependency electron-builder to v25" [`#2488`](https://github.com/th-ch/youtube-music/pull/2488)
- chore(deps): update dependency electron-builder to v25 [`#2406`](https://github.com/th-ch/youtube-music/pull/2406)
- fix(deps): update dependency deepmerge-ts to v7.1.3 [`#2481`](https://github.com/th-ch/youtube-music/pull/2481)
- fix(deps): update dependency ts-morph to v24 [`#2474`](https://github.com/th-ch/youtube-music/pull/2474)
- fix(deps): update dependency i18next to v23.15.2 [`#2471`](https://github.com/th-ch/youtube-music/pull/2471)
- chore(deps): update eslint monorepo to v9.12.0 [`#2470`](https://github.com/th-ch/youtube-music/pull/2470)
- chore(deps): update dependency @stylistic/eslint-plugin-js to v2.9.0 [`#2469`](https://github.com/th-ch/youtube-music/pull/2469)
- chore(deps): bump micromatch from 4.0.5 to 4.0.8 [`#2465`](https://github.com/th-ch/youtube-music/pull/2465)
- chore(deps): bump braces from 3.0.2 to 3.0.3 [`#2466`](https://github.com/th-ch/youtube-music/pull/2466)
- fix(deps): update dependency electron-updater to v6.3.9 [`#2468`](https://github.com/th-ch/youtube-music/pull/2468)
- fix(deps): update dependency deepmerge-ts to v7.1.1 [`#2467`](https://github.com/th-ch/youtube-music/pull/2467)
- chore(deps): update dependency typescript-eslint to v8.8.0 [`#2457`](https://github.com/th-ch/youtube-music/pull/2457)
- chore(deps): update dependency @babel/runtime to v7.25.7 [`#2462`](https://github.com/th-ch/youtube-music/pull/2462)
- chore(deps): update dependency rollup to v4.24.0 [`#2458`](https://github.com/th-ch/youtube-music/pull/2458)
- chore(deps): update dependency eslint-plugin-import to v2.31.0 [`#2464`](https://github.com/th-ch/youtube-music/pull/2464)
- chore(deps): update dependency rollup to v4.22.5 [`#2448`](https://github.com/th-ch/youtube-music/pull/2448)
- chore(deps): update dependency typescript-eslint to v8.7.0 [`#2450`](https://github.com/th-ch/youtube-music/pull/2450)
- fix(deps): update dependency solid-js to v1.9.1 [`#2451`](https://github.com/th-ch/youtube-music/pull/2451)
- chore(deps): update dependency vite to v5.4.8 [`#2449`](https://github.com/th-ch/youtube-music/pull/2449)
- chore(deps): update dependency discord-api-types to v0.37.101 [`#2440`](https://github.com/th-ch/youtube-music/pull/2440)
- chore(deps): update dependency esbuild to v0.24.0 [`#2439`](https://github.com/th-ch/youtube-music/pull/2439)
- chore(deps): update eslint monorepo to v9.11.1 [`#2442`](https://github.com/th-ch/youtube-music/pull/2442)
- chore(deps): update dependency @types/howler to v2.2.12 [`#2443`](https://github.com/th-ch/youtube-music/pull/2443)
- chore(deps): update dependency vite to v5.4.7 [`#2434`](https://github.com/th-ch/youtube-music/pull/2434)
- chore(deps): update playwright monorepo to v1.47.2 [`#2436`](https://github.com/th-ch/youtube-music/pull/2436)
- chore(deps): update eslint monorepo to v9.11.0 [`#2437`](https://github.com/th-ch/youtube-music/pull/2437)
- fix(deps): update dependency youtubei.js to v10.5.0 [`#2431`](https://github.com/th-ch/youtube-music/pull/2431)
- chore(deps): update dependency rollup to v4.22.4 [`#2430`](https://github.com/th-ch/youtube-music/pull/2430)
- chore(deps): update dependency electron to v32.1.2 [`#2433`](https://github.com/th-ch/youtube-music/pull/2433)
- feat: ESLint Flat Config (v9 support #2229) [`#2426`](https://github.com/th-ch/youtube-music/pull/2426)
- fix(taskbar-mediacontrol): fix icon color [`#2485`](https://github.com/th-ch/youtube-music/issues/2485)
- chore(eslint): apply eslint-plugin-prettier [`#2438`](https://github.com/th-ch/youtube-music/issues/2438)
- fix: apply fix from eslint [`cb1381b`](https://github.com/th-ch/youtube-music/commit/cb1381bbb394e2bbb404f44817ef96411dabc8a9)
- chore(i18n): Translated using Weblate (Portuguese (Brazil)) [`bcff26c`](https://github.com/th-ch/youtube-music/commit/bcff26c85b18258806f3960309776bc860c3a54e)
- chore(i18n): Translated using Weblate (Persian) [`ead448e`](https://github.com/th-ch/youtube-music/commit/ead448ed98095339557903eb0f84c4a6d0f32058)
#### [v3.5.3](https://github.com/th-ch/youtube-music/compare/v3.5.2...v3.5.3)
> 17 September 2024
- fix: fix `trustedHTML` issue [`#2339`](https://github.com/th-ch/youtube-music/issues/2339)
- chore(deps): update dependency rollup to v4.21.3 [`6edc84a`](https://github.com/th-ch/youtube-music/commit/6edc84a8bd6c7e009041117ba0d2004783eb3a47)
- chore(deps): update typescript-eslint monorepo to v8.6.0 [`d4c8a43`](https://github.com/th-ch/youtube-music/commit/d4c8a4320d733f7bddc4dcd1de93644790e71d66)
- chore(deps): update dependency eslint to v8.57.1 [`02b7a39`](https://github.com/th-ch/youtube-music/commit/02b7a39753528cfd8c0d107d6d2ec6ef78c5afe7)
#### [v3.5.2](https://github.com/th-ch/youtube-music/compare/v3.5.1...v3.5.2)
> 7 September 2024
- chore(deps): update typescript-eslint monorepo to v8.4.0 [`#2401`](https://github.com/th-ch/youtube-music/pull/2401)
- chore(deps): update dependency @total-typescript/ts-reset to v0.6.1 [`#2396`](https://github.com/th-ch/youtube-music/pull/2396)
- chore(deps): update dependency electron to v31.5.0 [`#2397`](https://github.com/th-ch/youtube-music/pull/2397)
- chore(deps): update dependency eslint-import-resolver-typescript to v3.6.3 [`#2376`](https://github.com/th-ch/youtube-music/pull/2376)
- chore(deps): update dependency discord-api-types to v0.37.100 [`#2394`](https://github.com/th-ch/youtube-music/pull/2394)
- fix(deps): update dependency electron-updater to v6.3.4 [`#2395`](https://github.com/th-ch/youtube-music/pull/2395)
- chore(deps): update dependency @babel/runtime to v7.25.6 [`#2388`](https://github.com/th-ch/youtube-music/pull/2388)
- chore(deps): update dependency vite-plugin-inspect to v0.8.7 [`#2389`](https://github.com/th-ch/youtube-music/pull/2389)
- chore(deps): update dependency discord-api-types to v0.37.99 [`#2374`](https://github.com/th-ch/youtube-music/pull/2374)
- chore(deps): update dependency vite to v5.4.3 [`#2377`](https://github.com/th-ch/youtube-music/pull/2377)
- fix: incorrect regex when splitting artistName [`#2378`](https://github.com/th-ch/youtube-music/pull/2378)
- chore(deps): update dependency @babel/runtime to v7.25.4 [`#2373`](https://github.com/th-ch/youtube-music/pull/2373)
- synced-lyrics: make the lyrics search more reliable [`#2343`](https://github.com/th-ch/youtube-music/pull/2343)
- fix(deps): update dependency solid-js to v1.8.22 [`#2354`](https://github.com/th-ch/youtube-music/pull/2354)
- chore(deps): update typescript-eslint monorepo to v8.3.0 [`#2350`](https://github.com/th-ch/youtube-music/pull/2350)
- fix(deps): update dependency electron-debug to v4.0.1 [`#2349`](https://github.com/th-ch/youtube-music/pull/2349)
- chore(deps): update dependency electron to v31.4.0 [`#2356`](https://github.com/th-ch/youtube-music/pull/2356)
- fix: hide native-controls on linux when in-app-menu is used [`#2366`](https://github.com/th-ch/youtube-music/pull/2366)
- fix: detect the upgrade btn using the icon [`#2364`](https://github.com/th-ch/youtube-music/pull/2364)
- fix: exclude build-id files from rpm [`#2361`](https://github.com/th-ch/youtube-music/pull/2361)
- fix(deps): update dependency i18next to v23.12.3 [`#2352`](https://github.com/th-ch/youtube-music/pull/2352)
- fix(deps): update dependency @floating-ui/dom to v1.6.10 [`#2340`](https://github.com/th-ch/youtube-music/pull/2340)
- fix(deps): update dependency electron-updater to v6.3.3 [`#2347`](https://github.com/th-ch/youtube-music/pull/2347)
- fix(deps): update dependency solid-js to v1.8.20 [`#2345`](https://github.com/th-ch/youtube-music/pull/2345)
- chore(deps): update dependency vite to v5.4.0 [`#2342`](https://github.com/th-ch/youtube-music/pull/2342)
- chore(deps): update typescript-eslint monorepo to v8.0.1 [`#2335`](https://github.com/th-ch/youtube-music/pull/2335)
- fix(deps): update dependency @floating-ui/dom to v1.6.9 [`#2337`](https://github.com/th-ch/youtube-music/pull/2337)
- chore(deps): update playwright monorepo to v1.46.0 [`#2336`](https://github.com/th-ch/youtube-music/pull/2336)
- chore(README): Translation README to Russian and adding Synced Lyrics to main README [`#2338`](https://github.com/th-ch/youtube-music/pull/2338)
- chore(deps): update dependency rollup to v4.20.0 [`#2326`](https://github.com/th-ch/youtube-music/pull/2326)
- fix(synced-lyric): fix timestamp [`#2323`](https://github.com/th-ch/youtube-music/issues/2323) [`#2379`](https://github.com/th-ch/youtube-music/issues/2379)
- Revert "fix(MPRIS): Prevents player to start with invalid MPRIS interface (#1996)" [`#2225`](https://github.com/th-ch/youtube-music/issues/2225)
- fix(adblocker/inplayer): fix Response.prototype.json [`#2310`](https://github.com/th-ch/youtube-music/issues/2310)
- chore(deps): update dependency eslint-plugin-import to v2.30.0 [`f48e46d`](https://github.com/th-ch/youtube-music/commit/f48e46d29cf09c76c5172fd56d2d0f705616e4e3)
- Revert "chore(deps): update dependency electron-builder to v25" [`089eff3`](https://github.com/th-ch/youtube-music/commit/089eff3152903c8b55ad3e5571b944062a647e27)
- chore(deps): update dependency electron-builder to v25 [`fe4c89c`](https://github.com/th-ch/youtube-music/commit/fe4c89c349bb9f4f54d95c2018943095ccfdab0c)
#### [v3.5.1](https://github.com/th-ch/youtube-music/compare/v3.5.0...v3.5.1)
> 1 August 2024
- fix(deps): update dependency youtubei.js to v10.3.0 [`#2306`](https://github.com/th-ch/youtube-music/pull/2306)
- fix: Window gets stuck offscreen in some instances [`#2303`](https://github.com/th-ch/youtube-music/pull/2303)
- fix: Incorrect window size on multi-monitor scaled displays [`#2302`](https://github.com/th-ch/youtube-music/pull/2302)
- chore(deps): update dependency rollup to v4.19.2 [`#2304`](https://github.com/th-ch/youtube-music/pull/2304)
- chore(deps): update typescript-eslint monorepo to v8 (major) [`#2297`](https://github.com/th-ch/youtube-music/pull/2297)
- fix(ambient-mode): fix ambient-mode not working for videos after restart [`#2294`](https://github.com/th-ch/youtube-music/pull/2294)
- fix(deps): update dependency @xhayper/discord-rpc to v1.2.0 [`#2291`](https://github.com/th-ch/youtube-music/pull/2291)
- fix(synced-lyrics): fix lyric load [`#2295`](https://github.com/th-ch/youtube-music/issues/2295)
- fix(ambient-mode): fix ambient-mode not working for videos after restart (#2294) [`#1641`](https://github.com/th-ch/youtube-music/issues/1641)
- fix(synced-lyrics): fix i18n [`8750b54`](https://github.com/th-ch/youtube-music/commit/8750b54f766c735ff039c6be454427f17d4737e2)
- ts-fix: disambiguate ElectronStore typings [`8775735`](https://github.com/th-ch/youtube-music/commit/877573532c1b68af861a3fdc44d093f3097d36ab)
- chore(i18n): Translated using Weblate (Hungarian) [`3537dc1`](https://github.com/th-ch/youtube-music/commit/3537dc19eecce7f7deb2478942f70d3c7b72148d)
#### [v3.5.0](https://github.com/th-ch/youtube-music/compare/v3.4.1...v3.5.0)
> 31 July 2024
- plugin: Synced Lyrics [`#2207`](https://github.com/th-ch/youtube-music/pull/2207)
- chore(deps): update dependency electron to v31.3.1 [`#2290`](https://github.com/th-ch/youtube-music/pull/2290)
- chore(deps): update typescript-eslint monorepo to v7.18.0 [`#2292`](https://github.com/th-ch/youtube-music/pull/2292)
- fix(deps): update dependency youtubei.js to v10.2.0 [`#2285`](https://github.com/th-ch/youtube-music/pull/2285)
- chore(deps): update dependency electron to v31.3.0 [`#2282`](https://github.com/th-ch/youtube-music/pull/2282)
- chore(deps): update typescript-eslint monorepo to v7.17.0 [`#2283`](https://github.com/th-ch/youtube-music/pull/2283)
- fix(deps): update dependency solid-js to v1.8.19 [`#2280`](https://github.com/th-ch/youtube-music/pull/2280)
- fix(deps): update dependency @xhayper/discord-rpc to v1.1.4 [`#2279`](https://github.com/th-ch/youtube-music/pull/2279)
- chore(deps): update dependency @babel/runtime to v7.25.0 [`#2281`](https://github.com/th-ch/youtube-music/pull/2281)
- fix(deps): update dependency @floating-ui/dom to v1.6.8 [`#2278`](https://github.com/th-ch/youtube-music/pull/2278)
- Fix: Incorrect window size on scaled displays [`#2258`](https://github.com/th-ch/youtube-music/pull/2258)
- chore(deps): update dependency vite-plugin-resolve to v2.5.2 [`#2276`](https://github.com/th-ch/youtube-music/pull/2276)
- chore(deps): update playwright monorepo to v1.45.3 [`#2277`](https://github.com/th-ch/youtube-music/pull/2277)
- fix(deps): update dependency deepmerge-ts to v7.1.0 [`#2263`](https://github.com/th-ch/youtube-music/pull/2263)
- chore(deps): update dependency typescript to v5.5.4 [`#2274`](https://github.com/th-ch/youtube-music/pull/2274)
- chore(deps): update dependency vite to v5.3.5 [`#2275`](https://github.com/th-ch/youtube-music/pull/2275)
- fix(deps): update dependency i18next to v23.12.2 [`#2260`](https://github.com/th-ch/youtube-music/pull/2260)
- chore(deps): update dependency discord-api-types to v0.37.93 [`#2273`](https://github.com/th-ch/youtube-music/pull/2273)
- chore(deps): update dependency rollup to v4.19.1 [`#2261`](https://github.com/th-ch/youtube-music/pull/2261)
- fix(deps): update dependency custom-electron-prompt to v1.5.8 [`#2262`](https://github.com/th-ch/youtube-music/pull/2262)
- feat(adblocker): add new option AdSpeedup [`#2235`](https://github.com/th-ch/youtube-music/pull/2235)
- fix: disable multi-plane format for software video [`#2254`](https://github.com/th-ch/youtube-music/pull/2254)
- chore(deps): update dependency eslint-plugin-prettier to v5.2.1 [`#2253`](https://github.com/th-ch/youtube-music/pull/2253)
- chore(deps): update dependency vite to v5.3.4 [`#2243`](https://github.com/th-ch/youtube-music/pull/2243)
- chore(deps): update typescript-eslint monorepo to v7.16.1 [`#2239`](https://github.com/th-ch/youtube-music/pull/2239)
- chore(deps): update playwright monorepo to v1.45.2 [`#2244`](https://github.com/th-ch/youtube-music/pull/2244)
- chore(deps): update dependency vite-plugin-inspect to v0.8.5 [`#2252`](https://github.com/th-ch/youtube-music/pull/2252)
- fix(deps): update dependency semver to v7.6.3 [`#2250`](https://github.com/th-ch/youtube-music/pull/2250)
- chore(deps): update dependency electron to v31.2.1 [`#2241`](https://github.com/th-ch/youtube-music/pull/2241)
- chore(i18n): Translated using Weblate (Catalan) [`4a8440c`](https://github.com/th-ch/youtube-music/commit/4a8440c281c341977ab3687982cec8cbc5af6cf7)
- Update changelog for v3.4.1 [`18e0b1b`](https://github.com/th-ch/youtube-music/commit/18e0b1b86341b13f1cbc713bfbd7b5d7a45ee392)
- fix(synced-lyrics): fix type error [`9357a15`](https://github.com/th-ch/youtube-music/commit/9357a15116a8526d22ba6142c0a02f31688743f2)
#### [v3.4.1](https://github.com/th-ch/youtube-music/compare/v3.4.0...v3.4.1)
> 15 July 2024
- fix(mpris): fix mpris position [`#2225`](https://github.com/th-ch/youtube-music/issues/2225)
- fix(deb): fix depends [`#1983`](https://github.com/th-ch/youtube-music/issues/1983)
- fix: fix touchbar icon [`#2183`](https://github.com/th-ch/youtube-music/issues/2183)
- fix: fix "Starting page" [`#1822`](https://github.com/th-ch/youtube-music/issues/1822)
- fix: fix album actions [`#2202`](https://github.com/th-ch/youtube-music/issues/2202)
- fix: fix playback slider [`#2045`](https://github.com/th-ch/youtube-music/issues/2045)
- chore(i18n): Translated using Weblate (Spanish) [`91bee48`](https://github.com/th-ch/youtube-music/commit/91bee4880ed2c6fdd887814a2620877d89bea311)
- Bump version to 3.4.1 [`02e2fb6`](https://github.com/th-ch/youtube-music/commit/02e2fb6a83844f439f760e72cdcb935b86000df2)
#### [v3.4.0](https://github.com/th-ch/youtube-music/compare/v3.3.12...v3.4.0)
> 14 July 2024
- fix(deps): update dependency i18next to v23.12.1 [`#2230`](https://github.com/th-ch/youtube-music/pull/2230)
- feat(downloader): New option to download on finish [`#1964`](https://github.com/th-ch/youtube-music/pull/1964)
- chore(deps): update typescript-eslint monorepo to v8.0.0-alpha.42 [`#2228`](https://github.com/th-ch/youtube-music/pull/2228)
- chore(deps): update dependency eslint to v9.7.0 [`#2226`](https://github.com/th-ch/youtube-music/pull/2226)
- chore(deps): update dependency @babel/runtime to v7.24.8 [`#2221`](https://github.com/th-ch/youtube-music/pull/2221)
- chore(deps): update dependency node-gyp to v10.2.0 [`#2216`](https://github.com/th-ch/youtube-music/pull/2216)
- chore(deps): update dependency ws to v8.18.0 [`#2217`](https://github.com/th-ch/youtube-music/pull/2217)
- chore(deps): update dependency glob to v11 [`#2219`](https://github.com/th-ch/youtube-music/pull/2219)
- chore(deps): update dependency esbuild to v0.23.0 [`#2215`](https://github.com/th-ch/youtube-music/pull/2215)
- chore(deps): update dependency electron to v31.2.0 [`#2214`](https://github.com/th-ch/youtube-music/pull/2214)
- fix(deps): update dependency youtubei.js to v10.1.0 [`#2218`](https://github.com/th-ch/youtube-music/pull/2218)
- chore(deps): update playwright monorepo to v1.45.1 [`#2212`](https://github.com/th-ch/youtube-music/pull/2212)
- chore(deps): update typescript-eslint monorepo to v8.0.0-alpha.41 [`#2213`](https://github.com/th-ch/youtube-music/pull/2213)
- chore(deps): update dependency rollup to v4.18.1 [`#2210`](https://github.com/th-ch/youtube-music/pull/2210)
- chore(deps): update dependency eslint to v9.6.0 [`#2192`](https://github.com/th-ch/youtube-music/pull/2192)
- chore(deps): update dependency vite to v5.3.3 [`#2211`](https://github.com/th-ch/youtube-music/pull/2211)
- chore(deps): update dependency glob to v10.4.5 [`#2205`](https://github.com/th-ch/youtube-music/pull/2205)
- chore(deps): update dependency discord-api-types to v0.37.92 [`#2204`](https://github.com/th-ch/youtube-music/pull/2204)
- fix(deps): update dependency solid-js to v1.8.18 [`#2189`](https://github.com/th-ch/youtube-music/pull/2189)
- chore(deps): update dependency typescript to v5.5.3 [`#2206`](https://github.com/th-ch/youtube-music/pull/2206)
- chore(deps): update dependency electron to v31.1.0 [`#2190`](https://github.com/th-ch/youtube-music/pull/2190)
- chore(deps): update typescript-eslint monorepo to v8.0.0-alpha.40 [`#2193`](https://github.com/th-ch/youtube-music/pull/2193)
- fix(deps): update dependency @floating-ui/dom to v1.6.7 [`#2196`](https://github.com/th-ch/youtube-music/pull/2196)
- chore(deps): update dependency vite to v5.3.2 [`#2188`](https://github.com/th-ch/youtube-music/pull/2188)
- chore(deps): update dependency discord-api-types to v0.37.91 [`#2187`](https://github.com/th-ch/youtube-music/pull/2187)
- chore(deps): update typescript-eslint monorepo to v8.0.0-alpha.34 [`#2184`](https://github.com/th-ch/youtube-music/pull/2184)
- fix(deps): update dependency @floating-ui/dom to v1.6.6 [`#2182`](https://github.com/th-ch/youtube-music/pull/2182)
- chore(deps): update playwright monorepo to v1.45.0 [`#2181`](https://github.com/th-ch/youtube-music/pull/2181)
- fix(deps): update dependency ts-morph to v23 [`#2180`](https://github.com/th-ch/youtube-music/pull/2180)
- chore(deps): update dependency electron-vite to v2.3.0 [`#2178`](https://github.com/th-ch/youtube-music/pull/2178)
- fix(deps): update dependency conf to v13.0.1 [`#2175`](https://github.com/th-ch/youtube-music/pull/2175)
- chore(deps): update dependency glob to v10.4.2 [`#2168`](https://github.com/th-ch/youtube-music/pull/2168)
- chore(deps): update dependency discord-api-types to v0.37.90 [`#2167`](https://github.com/th-ch/youtube-music/pull/2167)
- chore(deps): update dependency typescript to v5.5.2 [`#2173`](https://github.com/th-ch/youtube-music/pull/2173)
- chore(deps): update dependency electron to v31.0.2 [`#2170`](https://github.com/th-ch/youtube-music/pull/2170)
- chore(deps): update dependency ws to v8.17.1 [`#2164`](https://github.com/th-ch/youtube-music/pull/2164)
- chore(deps): update dependency eslint to v9.5.0 [`#2162`](https://github.com/th-ch/youtube-music/pull/2162)
- fix(deps): update dependency youtubei.js to v10 [`#2136`](https://github.com/th-ch/youtube-music/pull/2136)
- chore(deps): update dependency discord-api-types to v0.37.89 [`#2153`](https://github.com/th-ch/youtube-music/pull/2153)
- chore(deps): update dependency vite to v5.3.1 [`#2154`](https://github.com/th-ch/youtube-music/pull/2154)
- fix(deps): update dependency electron-store to v10 [`#2157`](https://github.com/th-ch/youtube-music/pull/2157)
- fix(deps): update dependency conf to v13 [`#2156`](https://github.com/th-ch/youtube-music/pull/2156)
- chore(deps): update dependency electron to v31.0.1 [`#2148`](https://github.com/th-ch/youtube-music/pull/2148)
- chore(deps): update dependency discord-api-types to v0.37.88 [`#2138`](https://github.com/th-ch/youtube-music/pull/2138)
- chore(deps): update typescript-eslint monorepo to v8.0.0-alpha.30 [`#2139`](https://github.com/th-ch/youtube-music/pull/2139)
- chore(deps): update dependency electron to v31 [`#2141`](https://github.com/th-ch/youtube-music/pull/2141)
- chore(deps): update dependency esbuild to v0.21.5 [`#2135`](https://github.com/th-ch/youtube-music/pull/2135)
- chore(deps): update typescript-eslint monorepo to v8.0.0-alpha.29 [`#2132`](https://github.com/th-ch/youtube-music/pull/2132)
- fix: rollback eslint version to v8 [`45931a2`](https://github.com/th-ch/youtube-music/commit/45931a25b08ab8a406f9e102486585311fd14bf9)
- chore(i18n): Translated using Weblate (Filipino) [`8a20566`](https://github.com/th-ch/youtube-music/commit/8a20566e0f2736f72d46282188ada69df1d7076a)
- chore(i18n): Translated using Weblate (Slovenian) [`40f0b9b`](https://github.com/th-ch/youtube-music/commit/40f0b9b852dcd9146e1c1e6c741b5baaf55ac079)
#### [v3.3.12](https://github.com/th-ch/youtube-music/compare/v3.3.11...v3.3.12) #### [v3.3.12](https://github.com/th-ch/youtube-music/compare/v3.3.11...v3.3.12)
> 8 June 2024
- hotfix: Revert "chore(deps): update dependencies `@cliqz/adblocker-electron`, `@cliqz/adblocker-electron-preload`" [`3c4abc1`](https://github.com/th-ch/youtube-music/commit/3c4abc14187e51f7e47c1ae71b3513f6d8c9912a) - hotfix: Revert "chore(deps): update dependencies `@cliqz/adblocker-electron`, `@cliqz/adblocker-electron-preload`" [`3c4abc1`](https://github.com/th-ch/youtube-music/commit/3c4abc14187e51f7e47c1ae71b3513f6d8c9912a)
- Update changelog for v3.3.11 [`de22444`](https://github.com/th-ch/youtube-music/commit/de224444c2a6d9030aa22a3b263ceacbc4b41914) - Update changelog for v3.3.11 [`de22444`](https://github.com/th-ch/youtube-music/commit/de224444c2a6d9030aa22a3b263ceacbc4b41914)
- Bump version to 3.3.12 [`89ed7d2`](https://github.com/th-ch/youtube-music/commit/89ed7d2345001fea59514944f4c1d56d2b7bd888) - Bump version to 3.3.12 [`89ed7d2`](https://github.com/th-ch/youtube-music/commit/89ed7d2345001fea59514944f4c1d56d2b7bd888)

374
docs/readme/README-ru.md Normal file
View File

@ -0,0 +1,374 @@
<div align="center">
# YouTube Music
[![GitHub release](https://img.shields.io/github/release/th-ch/youtube-music.svg?style=for-the-badge&logo=youtube-music)](https://github.com/th-ch/youtube-music/releases/)
[![GitHub license](https://img.shields.io/github/license/th-ch/youtube-music.svg?style=for-the-badge)](https://github.com/th-ch/youtube-music/blob/master/LICENSE)
[![eslint code style](https://img.shields.io/badge/code_style-eslint-5ed9c7.svg?style=for-the-badge)](https://github.com/th-ch/youtube-music/blob/master/.eslintrc.js)
[![Build status](https://img.shields.io/github/actions/workflow/status/th-ch/youtube-music/build.yml?branch=master&style=for-the-badge&logo=youtube-music)](https://GitHub.com/th-ch/youtube-music/releases/)
[![GitHub All Releases](https://img.shields.io/github/downloads/th-ch/youtube-music/total?style=for-the-badge&logo=youtube-music)](https://GitHub.com/th-ch/youtube-music/releases/)
[![AUR](https://img.shields.io/aur/version/youtube-music-bin?color=blueviolet&style=for-the-badge&logo=youtube-music)](https://aur.archlinux.org/packages/youtube-music-bin)
[![Known Vulnerabilities](https://snyk.io/test/github/th-ch/youtube-music/badge.svg)](https://snyk.io/test/github/th-ch/youtube-music)
</div>
![Screenshot](web/screenshot.jpg "Screenshot")
<div align="center">
<a href="https://github.com/th-ch/youtube-music/releases/latest">
<img src="web/youtube-music.svg" width="400" height="100" alt="YouTube Music SVG">
</a>
</div>
**Клиент для YouTube Music основанный на Electron с поддержкой:**
- Нативный вид приложения, нацелен на сохранение оригинального интерфейса
- Фреймворк для пользовательских плагинов: изменяйте YouTube Music под ваши нужды (внешний вид, контент, возможности), включайте/выключайте плагины в один клик
## Демо-изображение
| Экран плеера (цветовая тема альбома & режим Ambient) |
|:---------------------------------------------------------------------------------------------------------:|
|![Screenshot1](https://github.com/th-ch/youtube-music/assets/16558115/53efdf73-b8fa-4d7b-a235-b96b91ea77fc)|
## Содержание
- [Возможности](#features)
- [Доступные плагины](#available-plugins)
- [Перевод](#translation)
- [Скачать](#download)
- [Arch Linux](#arch-linux)
- [MacOS](#macos)
- [Windows](#windows)
- [Как установить без подключения к интернету? (в Windows)](#how-to-install-without-a-network-connection-in-windows)
- [Темы](#themes)
- [Для разработчиков](#dev)
- [Создайте свои собственные плагины](#build-your-own-plugins)
- [Создание плагина](#creating-a-plugin)
- [Примеры использования](#common-use-cases)
- [Сборка](#build)
- [Предварительный просмотр](#production-preview)
- [Тестирование](#tests)
- [Лицензия](#license)
- [Часто задаваемые вопросы](#faq)
## Возможности:
- **Авто-подтверждение при паузе** (Всегда включено): отключает всплывающие уведомление ["Продолжить просмотр?"](https://user-images.githubusercontent.com/61631665/129977894-01c60740-7ec6-4bf0-9a2c-25da24491b0e.png),
которое приостанавливает воспроизведение через определённое время
- И больше ...
## Доступные плагины:
- **Блокировщик рекламы**: Блокирует всю рекламу и трекеры
- **Действия с альбомом**: Добавляет кнопки "Убрать дизлайк", "Дизлайк", "Лайк", "Убрать лайк" и применяет их действия ко всем трекам в плейлисте или альбоме
- **Цветовая тема альбома**: Применяет динамическую тему и эффекты, основываясь на цветовой палитре альбома
- **Режим Ambient**: Применяет световой эффект, проецируя нежные цвета из видео на задний фон вашего экрана
- **Нормализация аудио**: Применяет нормализацию к аудио (уменьшает громкость громких частей трека и повышает громкость тихих частей трека)
- **Размытие панели навигации**: Делает панель навигации прозрачной и размытой
- **Обход возрастных ограничений**: Обходит проверку возраста YouTube
- **Выбор субтитров**: Включить субтитры
- **Компактная боковая панель**: Всегда показывать боковую панель компактно
- **Плавный переход**: Плавный переход между треками
- **Отключить автопроигрыш**: Каждый трек начинается в режиме паузы
- **[Discord](https://discord.com/) Rich Presence**: Показывает вашим друзьям, что вы слушаете с помощью [Rich Presence](https://user-images.githubusercontent.com/28219076/104362104-a7a0b980-5513-11eb-9744-bb89eabe0016.png)
- **Загрузчик**: Загрузка MP3 [напрямую из интерфейса](https://user-images.githubusercontent.com/61631665/129977677-83a7d067-c192-45e1-98ae-b5a4927393be.png) [(youtube-dl)](https://github.com/ytdl-org/youtube-dl)
- **Расширенная громкость**: Делает слайдер громкости [расширенным](https://greasyfork.org/en/scripts/397686-youtube-music-fix-volume-ratio/) облегчая выбор громкости
- **Меню в приложении**: [Придаёт панели меню красивый тёмный вид](https://user-images.githubusercontent.com/78568641/112215894-923dbf00-8c29-11eb-95c3-3ce15db27eca.png)
> (посмотрите [этот пост,](https://github.com/th-ch/youtube-music/issues/410#issuecomment-952060709) если у вас есть проблемы с доступом к меню после включения этого плагина и опции "Скрыть меню")
- **Скробблер**: Добавляет поддержку скробблинга [Last.fm](https://www.last.fm/) и [ListenBrainz](https://listenbrainz.org/)
- **Lumia Stream**: Добавляет поддержку [Lumia Stream](https://lumiastream.com/)
- **Тесты песен Genius**: Добавляет поддержку текстов для большинства песен
- **Music Together**: Делитесь плейлистом с другими. Когда ведущий воспроизводит трек, все остальные будут слушать этот же трек.
- **Навигация**: Кнопки Назад/Вперед интегрированы в интерфейс, как в вашем любимом браузере
- **Без входа в систему Google**: Убирает из интерфейса кнопки и ссылки для входа через Google
- **Уведомления**: Показывает уведомление, когда трек начинает играть ([интерактивные уведомления](https://user-images.githubusercontent.com/78568641/114102651-63ce0e00-98d0-11eb-9dfe-c5a02bb54f9c.png) доступны только для Windows)
- **Картинка в картинке**: Позволяет переключить приложение в режим "картинка в картинке"
- **Скорость воспроизведения**: Слушайте быстрее, слушайте медленнее! [Добавляет слайдер для контроля скорости трека](https://user-images.githubusercontent.com/61631665/129976003-e55db5ba-bf42-448c-a059-26a009775e68.png)
- **Точная громкость**: Точечно управляйте громкостью с помощью колеса мыши/горячих клавиш, с кастомным интерфейсом и настраиваемыми шагами громкости
- **Ярлыки (и MPRIS)**: Позволяет настроить глобальные горячие клавиши управления воспроизведением (плей/пауза/следующий/предыдущий) + отключает [отображение медиа на экране,](https://user-images.githubusercontent.com/84923831/128601225-afa38c1f-dea8-4209-9f72-0f84c1dd8b54.png) переопределяя клавиши управления + включает Ctrl/CMD + F для поиска + включает поддержку linux mpris для клавиш управления медиа + [настраиваемые сочетания клавиш](https://github.com/Araxeus/youtube-music/blob/1e591d6a3df98449bcda6e63baab249b28026148/providers/song-controls.js#L13-L50) для [продвинутых пользователей](https://github.com/th-ch/youtube-music/issues/106#issuecomment-952156902)
- **Пропускать непонравившиеся треки**: Пропускает непонравившиеся треки
- **Пропуск тишины**: Автоматически пропускает тихие моменты в песнях
- [**SponsorBlock**](https://github.com/ajayyy/SponsorBlock): Автоматически пропускает немузыкальные части, такие как интро/аутро музыкальных видео, где трек не играет
- **Управление воспроизведением из панели задач**: Управляйте воспроизведением из [панели задач Windows](https://user-images.githubusercontent.com/78568641/111916130-24a35e80-8a82-11eb-80c8-5021c1aa27f4.png)
- **TouchBar**: Кастомная раскладка TouchBar для MacOS
- **Tuna OBS**: Интеграция с [OBS](https://obsproject.com/) плагином [Tuna](https://obsproject.com/forum/resources/tuna.843/)
- **Изменение качества видео**: Позволяет менять качество видео [кнопкой](https://user-images.githubusercontent.com/78568641/138574366-70324a5e-2d64-4f6a-acdd-dc2a2b9cecc5.png) на медиаплеере видео
- **Переключатель видео**: Добавляет
[кнопку](https://user-images.githubusercontent.com/28893833/173663950-63e6610e-a532-49b7-9afa-54cb57ddfc15.png) переключения режимов Трек/Видео. Также может удалять вкладку "Видео" полностью
- **Визуализатор**: Различные визуализаторы музыки
- **Synced Lyrics**:
Предоставляет синхронизированные слова для песен из таких источников, как [LRClib](https://lrclib.net).
## Перевод
Вы можете помочь с переводом на ваш язык на [Hosted Weblate](https://hosted.weblate.org/projects/youtube-music/).
<a href="https://hosted.weblate.org/engage/youtube-music/">
<img src="https://hosted.weblate.org/widget/youtube-music/i18n/multi-auto.svg" alt="translation status" />
<img src="https://hosted.weblate.org/widget/youtube-music/i18n/287x66-black.png" alt="translation status 2" />
</a>
## Скачать
Вы можете посмотреть [latest release,](https://github.com/th-ch/youtube-music/releases/latest) чтобы быстро найти новую версию.
### Arch Linux
Установите пакет [`youtube-music-bin`](https://aur.archlinux.org/packages/youtube-music-bin) из AUR. Инструкции по установке из AUR можете найти на этой [вики-странице](https://wiki.archlinux.org/index.php/Arch_User_Repository#Installing_packages).
### macOS
Вы можете установить приложение с помощью Homebrew (сморите [cask definition](https://github.com/th-ch/homebrew-youtube-music)):
```bash
brew install th-ch/youtube-music/youtube-music
```
Если вы устанавливаете приложение вручную и получаете ошибку "is damaged and cant be opened.", запустите в терминале следующую команду:
```bash
xattr -cr /Applications/YouTube\ Music.app
```
### Windows
Вы можете использовать [пакетный менеджер Scoop](https://scoop.sh) для установки пакета `youtube-music` из [`extras` bucket](https://github.com/ScoopInstaller/Extras).
```bash
scoop bucket add extras
scoop install extras/youtube-music
```
Также для установки вы можете использовать [Winget](https://learn.microsoft.com/en-us/windows/package-manager/winget/), официальный пакетный менеджер командной строки Windows 11, для установки пакета `th-ch.YouTubeMusic`.
*К сведению: SmartScreen защитника Windows может блокировать установку, так как она от "неизвестного издателя". Это также применимо к методу ручной установки, когда вы пытаетесь запустить исполняемый файл(.exe) после загрузки здесь, на GitHub (тот же файл).*
```bash
winget install th-ch.YouTubeMusic
```
#### Установка без подключения к Интернету? (в Windows)
- Скачайте файл `*.nsis.7z` из _архетиктура вашего устройства_ на [release page](https://github.com/th-ch/youtube-music/releases/latest).
- `x64` для 64-bit Windows
- `ia32` для 32-bit Windows
- `arm64` для ARM64 Windows
- Скачайте установщик в release page. (`*-Setup.exe`)
- Поместите их в **одной директории**.
- Запустите установщик.
## Темы
Вы можете загрузить файл CSS для смены внешнего вида приложения (Настройки > Визуальные настройки > Тема).
Некоторые предустановленные темы доступны здесь: https://github.com/kerichdev/themes-for-ytmdesktop-player.
## Для разработчиков
```bash
git clone https://github.com/th-ch/youtube-music
cd youtube-music
pnpm install --frozen-lockfile
pnpm dev
```
## Создайте свои собственные плагины
Используя плагины вы можете:
- Манипулировать приложением - `BrowserWindow` из electron проброшен обработчику плагинов
- Изменять внешний вид, манипулируя HTML/CSS
### Создание плагина
Создайте директорию в `src/plugins/YOUR-PLUGIN-NAME`:
- `index.ts`: основной файл плагина
```typescript
import style from './style.css?inline'; // import style as inline
import { createPlugin } from '@/utils';
export default createPlugin({
name: 'Plugin Label',
restartNeeded: true, // if value is true, ytmusic show restart dialog
config: {
enabled: false,
}, // your custom config
stylesheets: [style], // your custom style,
menu: async ({ getConfig, setConfig }) => {
// All *Config methods are wrapped Promise<T>
const config = await getConfig();
return [
{
label: 'menu',
submenu: [1, 2, 3].map((value) => ({
label: `value ${value}`,
type: 'radio',
checked: config.value === value,
click() {
setConfig({ value });
},
})),
},
];
},
backend: {
start({ window, ipc }) {
window.maximize();
// you can communicate with renderer plugin
ipc.handle('some-event', () => {
return 'hello';
});
},
// it fired when config changed
onConfigChange(newConfig) { /* ... */ },
// it fired when plugin disabled
stop(context) { /* ... */ },
},
renderer: {
async start(context) {
console.log(await context.ipc.invoke('some-event'));
},
// Only renderer available hook
onPlayerApiReady(api: YoutubePlayer, context: RendererContext) {
// set plugin config easily
context.setConfig({ myConfig: api.getVolume() });
},
onConfigChange(newConfig) { /* ... */ },
stop(_context) { /* ... */ },
},
preload: {
async start({ getConfig }) {
const config = await getConfig();
},
onConfigChange(newConfig) {},
stop(_context) {},
},
});
```
### Примеры использования
- Кастомный CSS: создайте файл `style.css` в той же директории, затем:
```typescript
// index.ts
import style from './style.css?inline'; // import style as inline
import { createPlugin } from '@/utils';
export default createPlugin({
name: 'Plugin Label',
restartNeeded: true, // if value is true, ytmusic will show a restart dialog
config: {
enabled: false,
}, // your custom config
stylesheets: [style], // your custom style
renderer() {} // define renderer hook
});
```
- Если вы хотите изменить HTML:
```typescript
import { createPlugin } from '@/utils';
export default createPlugin({
name: 'Plugin Label',
restartNeeded: true, // if value is true, ytmusic will show the restart dialog
config: {
enabled: false,
}, // your custom config
renderer() {
// Remove the login button
document.querySelector(".sign-in-link.ytmusic-nav-bar").remove();
} // define renderer hook
});
```
- обмен между фронтом и бэком может быть выполнен с помощью модуля ipcMain из electron. Смотрите файл `index.ts` и
пример в плагине `sponsorblock`.
## Сборка
1. Склонируйте репозиторий
2. Следуйте [этой инструкции,](https://pnpm.io/installation) чтобы установить `pnpm`
3. Запустите `pnpm install --frozen-lockfile` для установки зависимостей
4. Запустите `pnpm build:OS`
- `pnpm dist:win` - Windows
- `pnpm dist:linux` - Linux (amd64)
- `pnpm dist:linux:deb-arm64` - Linux (arm64 for Debian)
- `pnpm dist:linux:rpm-arm64` - Linux (arm64 for Fedora)
- `pnpm dist:mac` - macOS (amd64)
- `pnpm dist:mac:arm64` - macOS (arm64)
Сборка приложения для macOS, Linux, и Windows,
используя [electron-builder](https://github.com/electron-userland/electron-builder).
## Предварительный просмотр
```bash
pnpm start
```
## Тестирование
```bash
pnpm test
```
Использует [Playwright](https://playwright.dev/) для тестирования приложения.
## Лицензия
MIT © [th-ch](https://github.com/th-ch/youtube-music)
## Часто задаваемые вопросы
### Почему меня приложения не отображается?
Если опция `Скрыть меню` включена - вы можете отобразить меню с помощью клавиши <kbd>alt</kbd> (или <kbd>\`</kbd> [обратный апостроф], если используете плагин "Меню в приложении")

View File

@ -1,17 +1,17 @@
import { resolve, dirname, join } from 'node:path'; import { resolve, dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url'; import { fileURLToPath } from 'node:url';
import { UserConfig } from 'vite';
import { defineConfig, defineViteConfig } from 'electron-vite'; import { defineConfig, defineViteConfig } from 'electron-vite';
import builtinModules from 'builtin-modules'; import builtinModules from 'builtin-modules';
import viteResolve from 'vite-plugin-resolve'; import viteResolve from 'vite-plugin-resolve';
import Inspect from 'vite-plugin-inspect'; import Inspect from 'vite-plugin-inspect';
import solidPlugin from 'vite-plugin-solid';
import { pluginVirtualModuleGenerator } from './vite-plugins/plugin-importer.mjs'; import { pluginVirtualModuleGenerator } from './vite-plugins/plugin-importer.mjs';
import pluginLoader from './vite-plugins/plugin-loader.mjs'; import pluginLoader from './vite-plugins/plugin-loader.mjs';
import type { UserConfig } from 'vite';
import { i18nImporter } from './vite-plugins/i18n-importer.mjs'; import { i18nImporter } from './vite-plugins/i18n-importer.mjs';
import solidPlugin from 'vite-plugin-solid';
const __dirname = dirname(fileURLToPath(import.meta.url)); const __dirname = dirname(fileURLToPath(import.meta.url));
@ -51,8 +51,12 @@ export default defineConfig({
}; };
if (mode === 'development') { if (mode === 'development') {
commonConfig.build!.sourcemap = 'inline';
commonConfig.plugins?.push( commonConfig.plugins?.push(
Inspect({ build: true, outputDir: join(__dirname, '.vite-inspect/backend') }), Inspect({
build: true,
outputDir: join(__dirname, '.vite-inspect/backend'),
}),
); );
return commonConfig; return commonConfig;
} }
@ -95,8 +99,12 @@ export default defineConfig({
}; };
if (mode === 'development') { if (mode === 'development') {
commonConfig.build!.sourcemap = 'inline';
commonConfig.plugins?.push( commonConfig.plugins?.push(
Inspect({ build: true, outputDir: join(__dirname, '.vite-inspect/preload') }), Inspect({
build: true,
outputDir: join(__dirname, '.vite-inspect/preload'),
}),
); );
return commonConfig; return commonConfig;
} }
@ -142,8 +150,12 @@ export default defineConfig({
}; };
if (mode === 'development') { if (mode === 'development') {
commonConfig.build!.sourcemap = 'inline';
commonConfig.plugins?.push( commonConfig.plugins?.push(
Inspect({ build: true, outputDir: join(__dirname, '.vite-inspect/renderer') }), Inspect({
build: true,
outputDir: join(__dirname, '.vite-inspect/renderer'),
}),
); );
return commonConfig; return commonConfig;
} }

80
eslint.config.mjs Normal file
View File

@ -0,0 +1,80 @@
//@ts-check
import eslint from '@eslint/js';
import prettier from 'eslint-plugin-prettier/recommended';
import stylistic from '@stylistic/eslint-plugin-js';
import tsEslint from 'typescript-eslint';
import * as importPlugin from 'eslint-plugin-import';
export default tsEslint.config(
eslint.configs.recommended,
tsEslint.configs.eslintRecommended,
...tsEslint.configs.recommendedTypeChecked,
prettier,
{ ignores: ['dist', 'node_modules', '*.config.*js', '*.test.*js'] },
{
plugins: {
stylistic,
importPlugin
},
languageOptions: {
parser: tsEslint.parser,
parserOptions: {
project: true,
sourceType: 'module',
ecmaVersion: 'latest'
}
},
rules: {
'stylistic/arrow-parens': ['error', 'always'],
'stylistic/object-curly-spacing': ['error', 'always'],
'prettier/prettier': ['error', { singleQuote: true, semi: true, tabWidth: 2, trailingComma: 'all', quoteProps: 'preserve' }],
'@typescript-eslint/no-floating-promises': 'off',
'@typescript-eslint/no-misused-promises': ['off', { checksVoidReturn: false }],
'@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }],
'@typescript-eslint/no-non-null-assertion': 'off',
'importPlugin/first': 'error',
'importPlugin/newline-after-import': 'off',
'importPlugin/no-default-export': 'off',
'importPlugin/no-duplicates': 'error',
'importPlugin/no-unresolved': ['error', { ignore: ['^virtual:', '\\?inline$', '\\?raw$', '\\?asset&asarUnpack'] }],
'importPlugin/order': ['error', {
'groups': ['builtin', 'external', ['internal', 'index', 'sibling'], 'parent', 'type'],
'newlines-between': 'always-and-inside-groups',
'alphabetize': { order: 'ignore', caseInsensitive: false }
}],
'importPlugin/prefer-default-export': 'off',
'camelcase': ['error', { properties: 'never' }],
'class-methods-use-this': 'off',
'stylistic/lines-around-comment': ['error', {
beforeBlockComment: false,
afterBlockComment: false,
beforeLineComment: false,
afterLineComment: false,
}],
'stylistic/max-len': 'off',
'stylistic/no-mixed-operators': 'warn', // prettier does not support no-mixed-operators
'stylistic/no-multi-spaces': ['error', { ignoreEOLComments: true }],
'stylistic/no-tabs': 'error',
'no-void': 'error',
'no-empty': 'off',
'prefer-promise-reject-errors': 'off',
'stylistic/quotes': ['error', 'single', {
avoidEscape: true,
allowTemplateLiterals: false,
}],
'stylistic/quote-props': ['error', 'consistent'],
'stylistic/semi': ['error', 'always'],
},
settings: {
'import/parsers': {
'@typescript-eslint/parser': ['.ts']
},
'import/resolver': {
typescript: {},
exports: {},
},
},
},
);

View File

@ -1,7 +1,7 @@
{ {
"name": "youtube-music", "name": "youtube-music",
"productName": "YouTube Music", "productName": "YouTube Music",
"version": "3.4.1", "version": "3.6.2",
"description": "YouTube Music Desktop App - including custom plugins", "description": "YouTube Music Desktop App - including custom plugins",
"main": "./dist/main/index.js", "main": "./dist/main/index.js",
"license": "MIT", "license": "MIT",
@ -40,7 +40,8 @@
] ]
} }
], ],
"icon": "assets/generated/icons/mac/icon.icns" "icon": "assets/generated/icons/mac/icon.icns",
"compression": "maximum"
}, },
"win": { "win": {
"icon": "assets/generated/icons/win/icon.ico", "icon": "assets/generated/icons/win/icon.ico",
@ -61,7 +62,8 @@
"arm64" "arm64"
] ]
} }
] ],
"compression": "maximum"
}, },
"nsisWeb": { "nsisWeb": {
"runAfterFinish": false "runAfterFinish": false
@ -70,13 +72,67 @@
"icon": "assets/generated/icons/png", "icon": "assets/generated/icons/png",
"category": "AudioVideo", "category": "AudioVideo",
"target": [ "target": [
"AppImage", {
"snap", "target": "AppImage",
"freebsd", "arch": [
"deb", "x64",
"rpm" "arm64",
"armv7l"
]
},
{
"target": "flatpak",
"arch": [
"x64"
]
},
{
"target": "deb",
"arch": [
"x64",
"arm64",
"armv7l"
]
},
{
"target": "rpm",
"arch": [
"x64",
"arm64"
]
},
{
"target": "snap",
"arch": [
"x64"
]
},
{
"target": "freebsd",
"arch": [
"x64",
"arm64",
"armv7l"
]
},
{
"target": "tar.gz",
"arch": [
"x64",
"arm64",
"armv7l"
]
}
] ]
}, },
"appImage": {
"description": "YouTube Music Desktop App bundled with custom plugins (and built-in ad blocker / downloader)",
"category": "AudioVideo"
},
"flatpak": {
"description": "YouTube Music Desktop App bundled with custom plugins (and built-in ad blocker / downloader)",
"category": "AudioVideo"
},
"deb": { "deb": {
"depends": [ "depends": [
"libgtk-3-0", "libgtk-3-0",
@ -94,6 +150,10 @@
"rpm": { "rpm": {
"depends": [ "depends": [
"/usr/lib64/libuuid.so.1" "/usr/lib64/libuuid.so.1"
],
"fpm": [
"--rpm-rpmbuild-define",
"_build_id_links none"
] ]
}, },
"snap": { "snap": {
@ -116,7 +176,7 @@
"vite:inspect": "pnpm clean && electron-vite build --mode development && pnpm exec serve .vite-inspect", "vite:inspect": "pnpm clean && electron-vite build --mode development && pnpm exec serve .vite-inspect",
"start": "electron-vite preview", "start": "electron-vite preview",
"start:debug": "cross-env ELECTRON_ENABLE_LOGGING=1 pnpm start", "start:debug": "cross-env ELECTRON_ENABLE_LOGGING=1 pnpm start",
"dev": "electron-vite dev --watch", "dev": "cross-env NODE_OPTIONS=--enable-source-maps electron-vite dev --watch",
"dev:debug": "cross-env ELECTRON_ENABLE_LOGGING=1 pnpm dev", "dev:debug": "cross-env ELECTRON_ENABLE_LOGGING=1 pnpm dev",
"clean": "del-cli dist && del-cli pack && del-cli .vite-inspect", "clean": "del-cli dist && del-cli pack && del-cli .vite-inspect",
"dist": "pnpm clean && pnpm build && pnpm electron-builder --win --mac --linux -p never", "dist": "pnpm clean && pnpm build && pnpm electron-builder --win --mac --linux -p never",
@ -135,7 +195,7 @@
"typecheck": "tsc -p tsconfig.json --noEmit" "typecheck": "tsc -p tsconfig.json --noEmit"
}, },
"engines": { "engines": {
"node": ">=18.0.0", "node": ">=18",
"pnpm": ">=8" "pnpm": ">=8"
}, },
"pnpm": { "pnpm": {
@ -145,12 +205,12 @@
"xml2js": "0.6.2", "xml2js": "0.6.2",
"node-fetch": "3.3.2", "node-fetch": "3.3.2",
"@electron/universal": "2.0.1", "@electron/universal": "2.0.1",
"@babel/runtime": "7.24.8" "@babel/runtime": "7.25.7"
}, },
"patchedDependencies": { "patchedDependencies": {
"vudio@2.1.1": "patches/vudio@2.1.1.patch", "vudio@2.1.1": "patches/vudio@2.1.1.patch",
"@xhayper/discord-rpc@1.1.2": "patches/@xhayper__discord-rpc@1.1.2.patch", "app-builder-lib@24.13.3": "patches/app-builder-lib@24.13.3.patch",
"app-builder-lib@24.13.3": "patches/app-builder-lib@24.13.3.patch" "@malept/flatpak-bundler": "patches/@malept__flatpak-bundler.patch"
} }
}, },
"dependencies": { "dependencies": {
@ -160,82 +220,95 @@
"@electron/remote": "2.1.2", "@electron/remote": "2.1.2",
"@ffmpeg.wasm/core-mt": "0.12.0", "@ffmpeg.wasm/core-mt": "0.12.0",
"@ffmpeg.wasm/main": "0.12.0", "@ffmpeg.wasm/main": "0.12.0",
"@floating-ui/dom": "1.6.7", "@floating-ui/dom": "1.6.11",
"@foobar404/wave": "2.0.5", "@foobar404/wave": "2.0.5",
"@hono/node-server": "1.13.2",
"@hono/swagger-ui": "0.4.1",
"@hono/zod-openapi": "0.16.4",
"@hono/zod-validator": "0.4.1",
"@jellybrick/electron-better-web-request": "1.0.4", "@jellybrick/electron-better-web-request": "1.0.4",
"@jellybrick/mpris-service": "2.1.4", "@jellybrick/mpris-service": "2.1.4",
"@xhayper/discord-rpc": "1.1.2", "@jimp/plugin-invert": "0.22.12",
"@skyra/jaro-winkler": "1.1.1",
"@xhayper/discord-rpc": "1.2.0",
"async-mutex": "0.5.0", "async-mutex": "0.5.0",
"butterchurn": "3.0.0-beta.4", "butterchurn": "3.0.0-beta.4",
"butterchurn-presets": "3.0.0-beta.4", "butterchurn-presets": "3.0.0-beta.4",
"color": "4.2.3", "color": "4.2.3",
"conf": "13.0.1", "conf": "13.0.1",
"custom-electron-prompt": "1.5.7", "custom-electron-prompt": "1.5.8",
"dbus-next": "0.10.2", "dbus-next": "0.10.2",
"deepmerge-ts": "7.0.3", "deepmerge-ts": "7.1.3",
"electron-debug": "4.0.0", "electron-debug": "4.1.0",
"electron-is": "3.0.0", "electron-is": "3.0.0",
"electron-localshortcut": "3.2.1", "electron-localshortcut": "3.2.1",
"electron-store": "10.0.0", "electron-store": "10.0.0",
"electron-unhandled": "4.0.1", "electron-unhandled": "4.0.1",
"electron-updater": "6.2.1", "electron-updater": "6.3.9",
"fast-average-color": "9.4.0", "fast-average-color": "9.4.0",
"fast-equals": "5.0.1", "fast-equals": "5.0.1",
"filenamify": "6.0.0", "filenamify": "6.0.0",
"hono": "4.6.5",
"howler": "2.2.4", "howler": "2.2.4",
"html-to-text": "9.0.5", "html-to-text": "9.0.5",
"i18next": "23.12.1", "i18next": "23.16.0",
"jimp": "1.6.0",
"keyboardevent-from-electron-accelerator": "2.0.0", "keyboardevent-from-electron-accelerator": "2.0.0",
"keyboardevents-areequal": "0.2.2", "keyboardevents-areequal": "0.2.2",
"node-html-parser": "6.1.13", "node-html-parser": "6.1.13",
"node-id3": "0.2.6", "node-id3": "0.2.6",
"peerjs": "1.5.4", "peerjs": "1.5.4",
"semver": "7.6.2", "semver": "7.6.3",
"serve": "14.2.3", "serve": "14.2.4",
"simple-youtube-age-restriction-bypass": "github:organization/Simple-YouTube-Age-Restriction-Bypass#v2.5.9", "simple-youtube-age-restriction-bypass": "github:organization/Simple-YouTube-Age-Restriction-Bypass#v2.5.9",
"solid-floating-ui": "0.3.1", "solid-floating-ui": "0.3.1",
"solid-js": "1.8.18", "solid-js": "1.9.2",
"solid-styled-components": "0.28.5", "solid-styled-components": "0.28.5",
"solid-transition-group": "0.2.3", "solid-transition-group": "0.2.3",
"ts-morph": "23.0.0", "ts-morph": "24.0.0",
"vudio": "2.1.1", "vudio": "2.1.1",
"x11": "2.3.0", "x11": "2.3.0",
"youtubei.js": "10.1.0" "youtubei.js": "10.5.0",
"zod": "3.23.8"
}, },
"devDependencies": { "devDependencies": {
"@playwright/test": "1.45.1", "@eslint/js": "9.12.0",
"@total-typescript/ts-reset": "0.5.1", "@playwright/test": "1.48.0",
"@stylistic/eslint-plugin-js": "2.9.0",
"@total-typescript/ts-reset": "0.6.1",
"@types/color": "3.0.6", "@types/color": "3.0.6",
"@types/electron-localshortcut": "3.1.3", "@types/electron-localshortcut": "3.1.3",
"@types/howler": "2.2.11", "@types/eslint__js": "8.42.3",
"@types/howler": "2.2.12",
"@types/html-to-text": "9.0.4", "@types/html-to-text": "9.0.4",
"@types/semver": "7.5.8", "@types/semver": "7.5.8",
"@typescript-eslint/eslint-plugin": "7.16.0", "@types/trusted-types": "2.0.7",
"@typescript-eslint/parser": "7.16.0",
"bufferutil": "4.0.8", "bufferutil": "4.0.8",
"builtin-modules": "4.0.0", "builtin-modules": "4.0.0",
"cross-env": "7.0.3", "cross-env": "7.0.3",
"del-cli": "5.1.0", "del-cli": "6.0.0",
"discord-api-types": "0.37.92", "discord-api-types": "0.37.102",
"electron": "31.2.0", "electron": "33.0.0",
"electron-builder": "24.13.3", "electron-builder": "24.13.3",
"electron-devtools-installer": "3.2.0", "electron-devtools-installer": "3.2.0",
"electron-vite": "2.3.0", "electron-vite": "2.3.0",
"esbuild": "0.23.0", "esbuild": "0.24.0",
"eslint": "8.57.0", "eslint": "9.12.0",
"eslint-config-prettier": "9.1.0",
"eslint-import-resolver-exports": "1.0.0-beta.5", "eslint-import-resolver-exports": "1.0.0-beta.5",
"eslint-import-resolver-typescript": "3.6.1", "eslint-import-resolver-typescript": "3.6.3",
"eslint-plugin-import": "2.29.1", "eslint-plugin-import": "2.31.0",
"eslint-plugin-prettier": "5.1.3", "eslint-plugin-prettier": "5.2.1",
"glob": "11.0.0", "glob": "11.0.0",
"node-gyp": "10.2.0", "node-gyp": "10.2.0",
"playwright": "1.45.1", "playwright": "1.48.0",
"rollup": "4.18.1", "rollup": "4.24.0",
"typescript": "5.5.3", "typescript": "5.6.3",
"typescript-eslint": "8.9.0",
"utf-8-validate": "6.0.4", "utf-8-validate": "6.0.4",
"vite": "5.3.3", "vite": "5.4.9",
"vite-plugin-inspect": "0.8.4", "vite-plugin-inspect": "0.8.7",
"vite-plugin-resolve": "2.5.1", "vite-plugin-resolve": "2.5.2",
"vite-plugin-solid": "2.10.2", "vite-plugin-solid": "2.10.2",
"ws": "8.18.0" "ws": "8.18.0"
}, },

View File

@ -0,0 +1,29 @@
diff --git a/index.js b/index.js
index 5968fcf47b69094993b0f861c03f5560e4a6a9b7..0fe16d4f40612c0abfa57898909ce0083f56944c 100644
--- a/index.js
+++ b/index.js
@@ -56,19 +56,23 @@ function getOptionsWithDefaults (options, manifest) {
async function spawnWithLogging (options, command, args, allowFail) {
return new Promise((resolve, reject) => {
logger(`$ ${command} ${args.join(' ')}`)
+ const output = []
const child = childProcess.spawn(command, args, { cwd: options['working-dir'] })
child.stdout.on('data', (data) => {
+ output.push(data)
logger(`1> ${data}`)
})
child.stderr.on('data', (data) => {
+ output.push(data)
logger(`2> ${data}`)
})
child.on('error', (error) => {
+ logger(`error - ${error.message} ${error.stack}`)
reject(error)
})
child.on('close', (code) => {
if (!allowFail && code !== 0) {
- reject(new Error(`${command} failed with status code ${code}`))
+ reject(new Error(`${command} ${args.join(' ')} failed with status code ${code} ${output.join(' ')}`))
}
resolve(code === 0)
})

View File

@ -1,17 +0,0 @@
diff --git a/package.json b/package.json
index 40db5dfbd8a4455ce2987d8115eca9882e1f9f14..414fc6986b9c0cc288908eb0107b90c4bfd916b2 100644
--- a/package.json
+++ b/package.json
@@ -25,11 +25,7 @@
},
"dependencies": {
"axios": "^1.6.2",
- "ws": "^8.15.1"
- },
- "optionalDependencies": {
- "bufferutil": "^4.0.8",
- "utf-8-validate": "^6.0.3"
+ "ws": "^8.16.0"
},
"devDependencies": {
"@types/node": "^14.*",

3408
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -1,9 +1,8 @@
import Store from 'electron-store';
import { deepmergeCustom } from 'deepmerge-ts'; import { deepmergeCustom } from 'deepmerge-ts';
import defaultConfig from './defaults'; import defaultConfig from './defaults';
import store from './store'; import store, { IStore } from './store';
import plugins from './plugins'; import plugins from './plugins';
import { restart } from '@/providers/app-controls'; import { restart } from '@/providers/app-controls';
@ -62,20 +61,19 @@ type Join<K, P> = K extends string | number
type Paths<T, D extends number = 10> = [D] extends [never] type Paths<T, D extends number = 10> = [D] extends [never]
? never ? never
: T extends object : T extends object
? { ? {
[K in keyof T]-?: K extends string | number [K in keyof T]-?: K extends string | number
? `${K}` | Join<K, Paths<T[K], Prev[D]>> ? `${K}` | Join<K, Paths<T[K], Prev[D]>>
: never; : never;
}[keyof T] }[keyof T]
: ''; : '';
type SplitKey<K> = K extends `${infer A}.${infer B}` ? [A, B] : [K, string]; type SplitKey<K> = K extends `${infer A}.${infer B}` ? [A, B] : [K, string];
type PathValue<T, K extends string> = SplitKey<K> extends [ type PathValue<T, K extends string> =
infer A extends keyof T, SplitKey<K> extends [infer A extends keyof T, infer B extends string]
infer B extends string, ? PathValue<T[A], B>
] : T;
? PathValue<T[A], B>
: T;
const get = <Key extends Paths<typeof defaultConfig>>(key: Key) => const get = <Key extends Paths<typeof defaultConfig>>(key: Key) =>
store.get(key) as PathValue<typeof defaultConfig, typeof key>; store.get(key) as PathValue<typeof defaultConfig, typeof key>;
@ -86,7 +84,7 @@ export default {
setPartial, setPartial,
setMenuOption, setMenuOption,
edit: () => store.openInEditor(), edit: () => store.openInEditor(),
watch(cb: Parameters<Store['onDidAnyChange']>[0]) { watch(cb: Parameters<IStore['onDidAnyChange']>[0]) {
store.onDidAnyChange(cb); store.onDidAnyChange(cb);
}, },
plugins, plugins,

View File

@ -1,12 +1,14 @@
import Store from 'electron-store'; import Store from 'electron-store';
import Conf from 'conf';
import defaults from './defaults'; import defaults from './defaults';
import { DefaultPresetList, type Preset } from '@/plugins/downloader/types'; import { DefaultPresetList, type Preset } from '@/plugins/downloader/types';
// prettier-ignore
export type IStore = InstanceType<typeof import('conf/dist/source/index').default<Record<string, unknown>>>;
const migrations = { const migrations = {
'>=3.3.0'(store: Conf<Record<string, unknown>>) { '>=3.3.0'(store: IStore) {
const lastfmConfig = store.get('plugins.lastfm') as { const lastfmConfig = store.get('plugins.lastfm') as {
enabled?: boolean; enabled?: boolean;
token?: string; token?: string;
@ -16,21 +18,21 @@ const migrations = {
secret?: string; secret?: string;
}; };
if (lastfmConfig) { if (lastfmConfig) {
let scrobblerConfig = store.get( let scrobblerConfig = store.get('plugins.scrobbler') as
'plugins.scrobbler', | {
) as {
enabled?: boolean;
scrobblers?: {
lastfm?: {
enabled?: boolean; enabled?: boolean;
token?: string; scrobblers?: {
sessionKey?: string; lastfm?: {
apiRoot?: string; enabled?: boolean;
apiKey?: string; token?: string;
secret?: string; sessionKey?: string;
}; apiRoot?: string;
}; apiKey?: string;
} | undefined; secret?: string;
};
};
}
| undefined;
if (!scrobblerConfig) { if (!scrobblerConfig) {
scrobblerConfig = { scrobblerConfig = {
@ -56,7 +58,7 @@ const migrations = {
store.delete('plugins.lastfm'); store.delete('plugins.lastfm');
} }
}, },
'>=3.0.0'(store: Conf<Record<string, unknown>>) { '>=3.0.0'(store: IStore) {
const discordConfig = store.get('plugins.discord') as Record< const discordConfig = store.get('plugins.discord') as Record<
string, string,
unknown unknown
@ -78,14 +80,14 @@ const migrations = {
} }
} }
}, },
'>=2.1.3'(store: Conf<Record<string, unknown>>) { '>=2.1.3'(store: IStore) {
const listenAlong = store.get('plugins.discord.listenAlong'); const listenAlong = store.get('plugins.discord.listenAlong');
if (listenAlong !== undefined) { if (listenAlong !== undefined) {
store.set('plugins.discord.playOnYouTubeMusic', listenAlong); store.set('plugins.discord.playOnYouTubeMusic', listenAlong);
store.delete('plugins.discord.listenAlong'); store.delete('plugins.discord.listenAlong');
} }
}, },
'>=2.1.0'(store: Conf<Record<string, unknown>>) { '>=2.1.0'(store: IStore) {
const originalPreset = store.get('plugins.downloader.preset') as const originalPreset = store.get('plugins.downloader.preset') as
| string | string
| undefined; | undefined;
@ -110,7 +112,7 @@ const migrations = {
store.delete('plugins.downloader.ffmpegArgs'); store.delete('plugins.downloader.ffmpegArgs');
} }
}, },
'>=1.20.0'(store: Conf<Record<string, unknown>>) { '>=1.20.0'(store: IStore) {
store.delete('plugins.visualizer'); // default value is now in the plugin store.delete('plugins.visualizer'); // default value is now in the plugin
if (store.get('plugins.notifications.toastStyle') === undefined) { if (store.get('plugins.notifications.toastStyle') === undefined) {
@ -125,14 +127,14 @@ const migrations = {
store.set('options.likeButtons', 'force'); store.set('options.likeButtons', 'force');
} }
}, },
'>=1.17.0'(store: Conf<Record<string, unknown>>) { '>=1.17.0'(store: IStore) {
store.delete('plugins.picture-in-picture'); // default value is now in the plugin store.delete('plugins.picture-in-picture'); // default value is now in the plugin
if (store.get('plugins.video-toggle.mode') === undefined) { if (store.get('plugins.video-toggle.mode') === undefined) {
store.set('plugins.video-toggle.mode', 'custom'); store.set('plugins.video-toggle.mode', 'custom');
} }
}, },
'>=1.14.0'(store: Conf<Record<string, unknown>>) { '>=1.14.0'(store: IStore) {
if ( if (
typeof store.get('plugins.precise-volume.globalShortcuts') !== 'object' typeof store.get('plugins.precise-volume.globalShortcuts') !== 'object'
) { ) {
@ -144,12 +146,12 @@ const migrations = {
store.set('plugins.video-toggle.enabled', true); store.set('plugins.video-toggle.enabled', true);
} }
}, },
'>=1.13.0'(store: Conf<Record<string, unknown>>) { '>=1.13.0'(store: IStore) {
if (store.get('plugins.discord.listenAlong') === undefined) { if (store.get('plugins.discord.listenAlong') === undefined) {
store.set('plugins.discord.listenAlong', true); store.set('plugins.discord.listenAlong', true);
} }
}, },
'>=1.12.0'(store: Conf<Record<string, unknown>>) { '>=1.12.0'(store: IStore) {
const options = store.get('plugins.shortcuts') as const options = store.get('plugins.shortcuts') as
| Record< | Record<
string, string,
@ -187,12 +189,12 @@ const migrations = {
} }
} }
}, },
'>=1.11.0'(store: Conf<Record<string, unknown>>) { '>=1.11.0'(store: IStore) {
if (store.get('options.resumeOnStart') === undefined) { if (store.get('options.resumeOnStart') === undefined) {
store.set('options.resumeOnStart', true); store.set('options.resumeOnStart', true);
} }
}, },
'>=1.7.0'(store: Conf<Record<string, unknown>>) { '>=1.7.0'(store: IStore) {
const enabledPlugins = store.get('plugins') as string[]; const enabledPlugins = store.get('plugins') as string[];
if (!Array.isArray(enabledPlugins)) { if (!Array.isArray(enabledPlugins)) {
console.warn('Plugins are not in array format, cannot migrate'); console.warn('Plugins are not in array format, cannot migrate');
@ -233,4 +235,4 @@ export default new Store({
}, },
clearInvalidConfig: false, clearInvalidConfig: false,
migrations, migrations,
}); }) as Store & IStore;

View File

@ -64,29 +64,29 @@ declare module 'custom-electron-prompt' {
export type PromptOptions<T extends string> = T extends 'input' export type PromptOptions<T extends string> = T extends 'input'
? InputPromptOptions ? InputPromptOptions
: T extends 'select' : T extends 'select'
? SelectPromptOptions ? SelectPromptOptions
: T extends 'counter' : T extends 'counter'
? CounterPromptOptions ? CounterPromptOptions
: T extends 'keybind' : T extends 'keybind'
? KeybindPromptOptions ? KeybindPromptOptions
: T extends 'multiInput' : T extends 'multiInput'
? MultiInputPromptOptions ? MultiInputPromptOptions
: never; : never;
type PromptResult<T extends string> = T extends 'input' type PromptResult<T extends string> = T extends 'input'
? string ? string
: T extends 'select' : T extends 'select'
? string ? string
: T extends 'counter' : T extends 'counter'
? number ? number
: T extends 'keybind' : T extends 'keybind'
? { ? {
value: string; value: string;
accelerator: string; accelerator: string;
}[] }[]
: T extends 'multiInput' : T extends 'multiInput'
? string[] ? string[]
: never; : never;
const prompt: <T extends Type>( const prompt: <T extends Type>(
options?: PromptOptions<T> & { type: T }, options?: PromptOptions<T> & { type: T },

View File

@ -14,9 +14,9 @@
} }
}, },
"language": { "language": {
"code": "إنجليزي", "code": "ar",
"local-name": "الإنجليزي", "local-name": "العربية",
"name": "الإنجليزية" "name": "Arabic"
}, },
"main": { "main": {
"console": { "console": {
@ -194,7 +194,21 @@
} }
}, },
"tray": { "tray": {
"next": "التالي" "next": "التالي",
"previous": "السابق",
"quit": "خروج",
"restart": "إعادة تشغيل التطبيق",
"show": "عرض النافدة",
"tooltip": {
"default": "يوتيوب اغاني",
"with-song-info": "يوتيوب أغاني: {{الفنان}}-{{العنوان}}"
}
}
},
"plugins": {
"adblocker": {
"description": "حجب جميع الإعلانات والمسارات خارج الصندوق",
"name": "حاجب الإعلانات"
} }
} }
} }

813
src/i18n/resources/ca.json Normal file
View File

@ -0,0 +1,813 @@
{
"common": {
"console": {
"plugins": {
"execute-failed": "Ha fallat l'execució de l'extensió {{pluginName}}::{{contextName}}",
"executed-at-ms": "L'extensió {{pluginName}}::{{contextName}} s'ha executat als {{ms}}ms",
"initialize-failed": "Ha fallat la inicialització de l'extensió «{{pluginName}}»",
"load-all": "Carregant totes les extensions",
"load-failed": "Error al carregar l'extensió «{{pluginName}}»",
"loaded": "L'extensió «{{pluginName}}» s'ha carregat",
"unload-failed": "Error al deshabilitar l'extensió «{{pluginName}}»",
"unloaded": "Extensió «{{pluginName}}» deshabilitada"
}
}
},
"language": {
"code": "ca",
"local-name": "Català",
"name": "Catalan"
},
"main": {
"console": {
"did-finish-load": {
"dev-tools": "Càrrega finalitzada. S'han obert les DevTools"
},
"i18n": {
"loaded": "i18n carregat"
},
"second-instance": {
"receive-command": "Comanda rebuda a través del protocol: «{{command}}»"
},
"theme": {
"css-file-not-found": "L'arxiu CSS «{{cssFile}}» no existeix, s'ha ignorat"
},
"unresponsive": {
"details": "Error sense resposta!\n{{error}}"
},
"when-ready": {
"clearing-cache-after-20s": "Netejant la memòria cau de l'aplicació"
},
"window": {
"tried-to-render-offscreen": "La finestra s'ha intentat mostrar fora de la pantalla, windowSize={{windowSize}}, displaySize={{displaySize}}, position={{position}}"
}
},
"dialog": {
"hide-menu-enabled": {
"detail": "El menú es troba amagat, premi «Alt» per mostrar-lo (o «Escapament» si utilitza el menú integrat In-App)",
"message": "S'ha habilitat l'amagament del menú",
"title": "Amagament del menú habilitat"
},
"need-to-restart": {
"buttons": {
"later": "Més tard",
"restart-now": "Reinicia ara"
},
"detail": "L'extensió «{{pluginName}}» requereix reiniciar l'aplicació per fer tenir efecte",
"message": "\"{{pluginName}}\" necessita reiniciar-se",
"title": "Es requereix reiniciar"
},
"unresponsive": {
"buttons": {
"quit": "Marxar",
"relaunch": "Rellançar",
"wait": "Espera"
},
"detail": "Ho sentim per les molèsties! si us plau, tria què fer:",
"message": "L'aplicació ha deixat de respondre",
"title": "La finestra ha deixat de respondre"
},
"update-available": {
"buttons": {
"disable": "Deshabilita les actualitzacions",
"download": "Descarrega",
"ok": "D'acord"
},
"detail": "Hi ha una nova versió disponible i pot ser descarregada a {{downloadLink}}",
"message": "Hi ha una nova versió disponible",
"title": "Actualització disponible"
}
},
"menu": {
"about": "Quant a",
"navigation": {
"label": "Navegació",
"submenu": {
"copy-current-url": "Copia l'URL actual",
"go-back": "Ves enrere",
"go-forward": "Ves endavant",
"quit": "Surt",
"restart": "Reinicia l'aplicació"
}
},
"options": {
"label": "Opcions",
"submenu": {
"advanced-options": {
"label": "Opcions avançades",
"submenu": {
"auto-reset-app-cache": "Reinicialitza la memòria cau de l'aplicació quan es reiniciï",
"disable-hardware-acceleration": "Deshabilita l'acceleració per hardware",
"edit-config-json": "Edita el config.json",
"override-user-agent": "Sobreescriu l'agent d'usuari (User-Agent)",
"restart-on-config-changes": "Reinicia quan es canviï la configuració",
"set-proxy": {
"label": "Definir servidor intermediari (proxy)",
"prompt": {
"label": "Introduir l'adreça del servidor intermediari: (deixar en blanc per deshabilitar)",
"placeholder": "Exemple: SOCKS5://127.0.0.1:9999",
"title": "Definir servidor intermediari (proxy)"
}
},
"toggle-dev-tools": "Commuta les DevTools"
}
},
"always-on-top": "Mostra sempre per sobre",
"auto-update": "Actualitza automàticament",
"hide-menu": {
"dialog": {
"message": "El menú s'amagarà la següent vegada que s'iniciï l'aplicació, prem «Alt» per mostrar-lo (o accent obert « ` » si utilitza el menú integrat In-App)",
"title": "Amagament del menú habilitat"
},
"label": "Amaga el menú"
},
"language": {
"dialog": {
"message": "L'idioma es canviarà un cop es reiniciï",
"title": "Idioma canviat"
},
"label": "Idioma",
"submenu": {
"to-help-translate": "Vols ajudar a traduir? Clica aquí"
}
},
"resume-on-start": "Reprèn l'última cançó quan s'inicia l'aplicació",
"single-instance-lock": "Bloqueja en una única instància",
"start-at-login": "Obre a l'iniciar sessió",
"starting-page": {
"label": "Pàgina d'inici",
"unset": "Sense establir"
},
"tray": {
"label": "Safata d'icones",
"submenu": {
"disabled": "Deshabilitat",
"enabled-and-hide-app": "Mostra la icona i amaga l'aplicació",
"enabled-and-show-app": "Mostra la icona i mostra l'aplicació",
"play-pause-on-click": "Reprodueix / pausa en clicar"
}
},
"visual-tweaks": {
"label": "Opcions visuals",
"submenu": {
"like-buttons": {
"default": "Per defecte",
"force-show": "Força que es mostri",
"hide": "Amaga",
"label": "Botons de «m'agrada»"
},
"remove-upgrade-button": "Elimina el botó «Actualitza a Music Premium»",
"theme": {
"dialog": {
"button": {
"cancel": "Cancel·la",
"remove": "Elimina"
},
"remove-theme": "De debó vols eliminar el tema personalitzat?",
"remove-theme-message": "Això eliminarà el tema personalitzat"
},
"label": "Tema",
"submenu": {
"import-css-file": "Importa un arxiu CSS personalitzat",
"no-theme": "Cap tema"
}
}
}
}
}
},
"plugins": {
"enabled": "Habilitat",
"label": "Extensions",
"new": "NOU"
},
"view": {
"label": "Veure",
"submenu": {
"force-reload": "Força la recàrrega",
"reload": "Recarrega",
"reset-zoom": "Mida real",
"toggle-fullscreen": "Commuta la pantalla completa",
"zoom-in": "Apropa el zoom",
"zoom-out": "Allunya el zoom"
}
}
},
"tray": {
"next": "Següent",
"play-pause": "Reprodueix/Pausa",
"previous": "Anterior",
"quit": "Tanca",
"restart": "Reinicia l'aplicació",
"show": "Mostra la finestra",
"tooltip": {
"default": "YouTube Music",
"with-song-info": "YouTube Music: {{artist}} - {{title}}"
}
}
},
"plugins": {
"ad-speedup": {
"description": "Si es reprodueix un anunci, silencia l'àudio i el reprodueix a la velocitat 16x",
"name": "Accelera els anuncis"
},
"adblocker": {
"description": "Bloqueja tots els anuncis i el seguiment",
"menu": {
"blocker": "Bloquejador"
},
"name": "Bloquejador d'anuncis"
},
"album-actions": {
"description": "Afegeix botons de «no m'agrada / retirar el no m'agrada» i «m'agrada / retirar el m'agrada» per aplicar-ho a totes les cançons en una llista de reproducció o àlbum",
"name": "Accions a l'àlbum"
},
"album-color-theme": {
"description": "Aplica un tema dinàmic i efectes visuals basats en la paleta de colors de l'àlbum",
"menu": {
"color-mix-ratio": {
"label": "Proporció de la barreja de colors",
"submenu": {
"percent": "{{ratio}}%"
}
}
},
"name": "Tema de color de l'àlbum"
},
"ambient-mode": {
"description": "Aplica un efecte d'il·luminació que projecta colors difusos del vídeo al fons de la pantalla",
"menu": {
"blur-amount": {
"label": "Quantitat de desenfocament",
"submenu": {
"pixels": "{{blurAmount}} píxels"
}
},
"buffer": {
"label": "Buffer",
"submenu": {
"buffer": "{{buffer}}"
}
},
"opacity": {
"label": "Opacitat",
"submenu": {
"percent": "{{opacity}}%"
}
},
"quality": {
"label": "Qualitat",
"submenu": {
"pixels": "{{quality}} píxels"
}
},
"size": {
"label": "Mida",
"submenu": {
"percent": "{{size}}%"
}
},
"smoothness-transition": {
"label": "Transició suau",
"submenu": {
"during": "Durant {{interpolationTime}} s"
}
},
"use-fullscreen": {
"label": "Utilitza en pantalla completa"
}
},
"name": "Mode ambient"
},
"api-server": {
"description": "Afegeix un servidor API per controlar el reproductor",
"dialog": {
"request": {
"buttons": {
"allow": "Permet",
"deny": "Denegar"
},
"message": "Permetre que {{ID}} ({{origin}}) accedeixi a l'API?",
"title": "Petició d'autorització API"
}
},
"menu": {
"auth-strategy": {
"label": "Estratègia d'autorització",
"submenu": {
"auth-at-first": {
"label": "Autoritza a la primera petició"
},
"none": {
"label": "Sense autorització"
}
}
},
"hostname": {
"label": "Nom del host"
},
"port": {
"label": "Port"
}
},
"name": "Servidor API [Beta]",
"prompt": {
"hostname": {
"label": "Introdueix el nom del host (per exemple 0.0.0.0) pel servidor API:",
"title": "Nom del host"
},
"port": {
"label": "Introdueix el port pel servidor API:",
"title": "Port"
}
}
},
"audio-compressor": {
"description": "Aplica compressió a l'àudio (baixa el volum de les parts més sorolloses de la senyal d'àudio i puja el volum de les parts més fluixes)",
"name": "Compressió d'àudio"
},
"blur-nav-bar": {
"description": "Desenfoca i aplica transparència a la barra de navegació",
"name": "Desenfoca la barra de navegació"
},
"bypass-age-restrictions": {
"description": "Esquiva la verificació d'edat de YouTube",
"name": "Esquiva les restriccions d'edat"
},
"captions-selector": {
"description": "Selector de subtítols per les pistes d'àudio de YouTube Music",
"menu": {
"autoload": "Selecciona automàticament l'últim subtítol emprat",
"disable-captions": "Sense subtítols per defecte"
},
"name": "Selector de subtítols",
"prompt": {
"selector": {
"label": "Idioma actual dels subtítols: {{language}}",
"none": "Cap",
"title": "Selecciona l'idioma dels subtítols"
}
},
"templates": {
"title": "Obra el selector de subtítols"
}
},
"compact-sidebar": {
"description": "Sempre mostrar la barra lateral en mode compacte",
"name": "Barra lateral compacta"
},
"crossfade": {
"description": "Transició creuada (crossfade) entre cançons",
"menu": {
"advanced": "Avançat"
},
"name": "Transició creuada [Beta]",
"prompt": {
"options": {
"multi-input": {
"fade-in-duration": "Durada de la transició d'entrada (ms)",
"fade-out-duration": "Durada de la transició de sortida (ms)",
"fade-scaling": {
"label": "Escala de la transició",
"linear": "Linear",
"logarithmic": "Logarítmica"
},
"seconds-before-end": "Transiciona N segons abans del final"
},
"title": "Opcions de transició creuada"
}
}
},
"disable-autoplay": {
"description": "Fa que la cançó comenci en mode «pausat»",
"menu": {
"apply-once": "Tan sols s'aplica a l'inici"
},
"name": "Deshabilita la reproducció automàtica"
},
"discord": {
"backend": {
"already-connected": "S'ha intentat connectar amb una connexió activa",
"connected": "Connectat a Discord",
"disconnected": "Desconnectat de Discord"
},
"description": "Mostra als teus amics allò que escoltes a l'estat d'activitat",
"menu": {
"auto-reconnect": "Reconnecta automàticament",
"clear-activity": "Esborra l'activitat",
"clear-activity-after-timeout": "Esborra l'activitat al cap d'un temps",
"connected": "Connectat",
"disconnected": "Desconnectat",
"hide-duration-left": "Amaga la durada restant",
"hide-github-button": "Amaga el botó de l'enllaç a GitHub",
"play-on-youtube-music": "Reprodueix a YouTube Music",
"set-inactivity-timeout": "Estableix temps d'espera d'inactivitat"
},
"name": "Estat d'activitat de Discord",
"prompt": {
"set-inactivity-timeout": {
"label": "Introdueix el temps d'espera d'inactivitat en segons:",
"title": "Estableix el temps d'espera d'inactivitat"
}
}
},
"downloader": {
"backend": {
"dialog": {
"error": {
"buttons": {
"ok": "D'acord"
},
"message": "Caram! Ho sentim, ha fallat la descàrrega…",
"title": "Error a la descàrrega!"
},
"start-download-playlist": {
"buttons": {
"ok": "D'acord"
},
"detail": "({{playlistSize}} cançons)",
"message": "Descarregant llista de reproducció {{playlistTitle}}",
"title": "Descàrrega començada"
}
},
"feedback": {
"conversion-progress": "Conversió: {{percent}}%",
"converting": "Convertint…",
"done": "Fet: {{filePath}}",
"download-info": "Descarregant {{artist}} - {{title}} [{{videoId}}",
"download-progress": "Descàrrega: {{percent}}%",
"downloading": "Descarregant…",
"downloading-counter": "Descarregant {{current}}/{{total}}…",
"downloading-playlist": "Descarregant la llista de reproducció «{{playlistTitle}}» - {{playlistSize}} cançons ({{playlistId}})",
"error-while-downloading": "Error al descarregar «{{author}} - {{title}}»: {{error}}",
"folder-already-exists": "La carpeta {{playlistFolder}} ja existeix",
"getting-playlist-info": "Obtenint la informació de la llista de reproducció…",
"loading": "Carregant…",
"playlist-has-only-one-song": "La llista de reproducció té un sol element, descarregant-lo directament",
"playlist-id-not-found": "No s'ha trobat cap ID de llista de reproducció",
"playlist-is-empty": "La llista de reproducció és buida",
"playlist-is-mix-or-private": "Error obtenint la informació de la llista de reproducció: assegura't que no és una llista de reproducció privada o de «Mixos per a tu»\n\n{{error}}",
"preparing-file": "Preparant arxiu…",
"saving": "Desant…",
"trying-to-get-playlist-id": "Intentant obtenir l'ID de la llista de reproducció: {{playlistId}}",
"video-id-not-found": "Vídeo no trobat",
"writing-id3": "Escrivint les etiquetes ID3…"
}
},
"description": "Descarrega el MP3 / àudio d'origen directament des de la interfície",
"menu": {
"choose-download-folder": "Tria la carpeta de descàrrega",
"download-finish-settings": {
"label": "Descarrega en finalitzar",
"prompt": {
"last-percent": "Desprès del x percent",
"last-seconds": "Últims x segons",
"title": "Configura quan descarregar"
},
"submenu": {
"advanced": "Avançat",
"enabled": "Habilitat",
"mode": "Mode de temps",
"percent": "Percentatge",
"seconds": "Segons"
}
},
"download-playlist": "Descarrega la llista de reproducció",
"presets": "Configuracions predefinides",
"skip-existing": "Omet els arxius existents"
},
"name": "Descàrregues",
"renderer": {
"can-not-update-progress": "No es pot actualitzar el progrés"
},
"templates": {
"button": "Descarrega"
}
},
"exponential-volume": {
"description": "Fa que el control lliscant del volum sigui exponencial per que sigui més fàcil seleccionar volums més baixos.",
"name": "Volum exponencial"
},
"in-app-menu": {
"description": "Fa que la barra de menú superior tingui un elegant aspecte fosc o basat en el color de l'àlbum",
"menu": {
"hide-dom-window-controls": "Amaga els controls de la finestra del DOM"
},
"name": "Menú integrat In-App"
},
"lumiastream": {
"description": "Afegeix suport pel Lumia Stream",
"name": "Lumia Stream [Beta]"
},
"lyrics-genius": {
"description": "Afegeix suport per la lletra de la majoria de cançons",
"menu": {
"romanized-lyrics": "Lletra romanitzada"
},
"name": "Lletres de Genius",
"renderer": {
"fetched-lyrics": "S'ha buscat la lletra a Genius"
}
},
"music-together": {
"description": "Comparteix una llista de reproducció amb els demés. Quan l'amfitrió reprodueix una cançó, la resta també sentiran la mateixa",
"dialog": {
"enter-host": "Introdueix l'ID de l'amfitrió"
},
"internal": {
"save": "Desa",
"track-source": "Origen de la pista",
"unknown-user": "Usuari desconegut"
},
"menu": {
"click-to-copy-id": "Copia l'ID d'amfitrió",
"close": "Tanca el Music Together",
"connected-users": "Usuaris connectats",
"disconnect": "Desconnecta el Music Together",
"empty-user": "No hi ha usuaris connectats",
"host": "Amfitrió de Music Together",
"join": "Uneix-te a Music Together",
"permission": {
"all": "Permet que els convidats controlin la llista de reproducció i el reproductor",
"host-only": "Tan sols l'amfitrió pot controlar la llista de reproducció i el reproductor",
"playlist": "Permet que els convidats controlin la llista de reproducció"
},
"set-permission": "Canvia els permisos de control",
"status": {
"disconnected": "Desconnectat",
"guest": "Connectat com a convidat",
"host": "Connectat com amfitrió"
}
},
"name": "Music Together [Beta]",
"toast": {
"add-song-failed": "Error al afegir la cançó",
"closed": "Music Together tancat",
"disconnected": "Music Together desconnectat",
"host-failed": "No s'ha pogut començar el Music Together",
"id-copied": "L'ID d'amfitrió s'ha copiat al porta-retalls",
"id-copy-failed": "Error al copiar l'ID d'amfitrió al porta-retalls",
"join-failed": "Error al unir-se al Music Together",
"joined": "T'has unit al Music Together",
"permission-changed": "Els permisos de Music Together han canviat a «{{permission}}»",
"remove-song-failed": "Error al eliminar la cançó",
"user-connected": "{{name}} s'ha unit al Music Together",
"user-disconnected": "{{name}} s'ha desconnectat del Music Together"
}
},
"navigation": {
"description": "Fletxes de navegació Següent / Enrere integrades directament a la interfície, com al teu navegador preferit",
"name": "Navegació"
},
"no-google-login": {
"description": "Elimina els botons d'inici de sessió de Google de la interfície",
"name": "Amaga l'inici de sessió de Google"
},
"notifications": {
"description": "Mostra una notificació quan una cançó es comença a reproduir (les notificacions interactives estan disponibles a Windows)",
"menu": {
"interactive": "Notificacions interactives",
"interactive-settings": {
"label": "Configuració interactiva",
"submenu": {
"hide-button-text": "Amaga text del botó",
"refresh-on-play-pause": "Recarrega al Reproduir/Pausar",
"tray-controls": "Obra/Tanca en clicar a la safata"
}
},
"priority": "Prioritat de les notificacions",
"toast-style": "Estil dels missatges emergents",
"unpause-notification": "Mostra notificació en reprendre la reproducció"
},
"name": "Notificacions"
},
"picture-in-picture": {
"description": "Permet commutar el mode d'imatge en imatge (PiP)",
"menu": {
"always-on-top": "Mostra sempre a sobre",
"hotkey": {
"label": "Drecera del teclat",
"prompt": {
"keybind-options": {
"hotkey": "Drecera del teclat"
},
"label": "Tria una drecera per commutar el mode d'imatge en imatge (PiP)",
"title": "Drecera del mode imatge en imatge (PiP)"
}
},
"save-window-position": "Desa la posició de la finestra",
"save-window-size": "Desa la mida de la finestra",
"use-native-pip": "Utilitza l'imatge en imatge (PiP) nativa del navegador"
},
"name": "Imatge en imatge (PiP)",
"templates": {
"button": "Imatge en imatge (PiP)"
}
},
"playback-speed": {
"description": "Escolta-ho ràpid, escolta-ho lent! Afegeix un control lliscant per canviar la velocitat de la cançó",
"name": "Velocitat de la reproducció",
"templates": {
"button": "Velocitat"
}
},
"precise-volume": {
"description": "Controla el volum de manera precisa a través de la rodeta del ratolí / dreceres del teclat, amb una interfície personalitzada i passos de volum personalitzats",
"menu": {
"arrows-shortcuts": "Controls locals de tecles de fletxa",
"custom-volume-steps": "Estableix passos de volum personalitzats",
"global-shortcuts": "Dreceres de teclat globals"
},
"name": "Volum precís",
"prompt": {
"global-shortcuts": {
"keybind-options": {
"decrease": "Baixa el volum",
"increase": "Puja el volum"
},
"label": "Tria les dreceres globals de volum:",
"title": "Dreceres globals de volum"
},
"volume-steps": {
"label": "Tria els passos d'augment o disminució del volum",
"title": "Passos de volum"
}
}
},
"quality-changer": {
"backend": {
"dialog": {
"quality-changer": {
"detail": "Qualitat actual: {{quality}}",
"message": "Tria la qualitat del vídeo:",
"title": "Tria la qualitat del vídeo"
}
}
},
"description": "Permet canviar la qualitat del vídeo amb un botó que s'hi mostra a sobre",
"name": "Botó de qualitat del vídeo"
},
"scrobbler": {
"description": "Afegeix suport per scrobbling (Last.fm, ListenBrainz, etc.)",
"dialog": {
"lastfm": {
"auth-failed": {
"message": "Error al autenticar amb Last.fm\nAmaga la finestra emergent fins el següent reinici.",
"title": "Error d'autenticació"
}
}
},
"menu": {
"lastfm": {
"api-settings": "Configuració de l'API de Last.fm"
},
"listenbrainz": {
"token": "Introduir token d'usuari de ListenBrainz"
},
"scrobble-other-media": "Scrobble amb altres mitjans"
},
"name": "Scrobbler",
"prompt": {
"lastfm": {
"api-key": "Clau d'API de Last.fm",
"api-secret": "Clau secreta de l'API de Last.fm"
},
"listenbrainz": {
"token": {
"label": "Introdueix el teu token de ListenBrainz:",
"title": "Token de ListenBrainz"
}
}
}
},
"shortcuts": {
"description": "Permet l'ús de dreceres globals del teclat per la reproducció (reproduir/pausar/següent/anterior) i desactivar l'OSD dels mitjans en sobreescriure les tecles de control multimèdia, habilita el Ctrl/CMD + F per buscar, habilita el suport MPRIS a Linux per tecles de control multimèdia, i dreceres de teclat personalitzades per usuaris avançats",
"menu": {
"override-media-keys": "Sobreescriu les tecles de control multimèdia",
"set-keybinds": "Estableix controls globals de les cançons"
},
"name": "Dreceres i MPRIS",
"prompt": {
"keybind": {
"keybind-options": {
"next": "Següent",
"play-pause": "Reproduir / Pausar",
"previous": "Anterior"
},
"label": "Tria combinacions de tecles per controlar les cançons:",
"title": "Dreceres globals"
}
}
},
"skip-disliked-songs": {
"description": "Salta les cançons amb «no m'agrada»",
"name": "Salta les cançons que no t'agraden"
},
"skip-silences": {
"description": "Omet automàticament les seccions amb silenci a les cançons",
"name": "Omet els silencis"
},
"sponsorblock": {
"description": "Omet automàticament els segments dels vídeos que no son música, com la intro o el final",
"name": "SponsorBlock"
},
"synced-lyrics": {
"description": "Proporciona lletres sincronitzades amb les cançons, a través de proveïdors com LRClib.",
"errors": {
"fetch": "⚠️ - Se ha produït un error en descarregar la lletra. Si us plau, intenta-ho més tard.",
"not-found": "⚠️ - No s'ha trobat la lletra per aquesta cançó."
},
"menu": {
"default-text-string": {
"label": "Caràcter per defecte entre lletres",
"tooltip": "Tria el caràcter per defecte que es mostrarà a l'espai entre les lletres"
},
"line-effect": {
"label": "Efecte de la línia",
"submenu": {
"focus": {
"label": "Focus",
"tooltip": "Mostra tan sols la línia actual en blanc"
},
"offset": {
"label": "Desplaçament",
"tooltip": "Desplaçament a la dreta de la línia actual"
},
"scale": {
"label": "Escala",
"tooltip": "Redimensiona la línia actual"
}
},
"tooltip": "Tria l'efecte a aplicar a la línia actual"
},
"precise-timing": {
"label": "Fes que les lletres es sincronitzin a la perfecció",
"tooltip": "Calcula al mil·lisegon l'aparició de la següent línia (pot tenir un petit impacte en el rendiment)"
},
"show-lyrics-even-if-inexact": {
"label": "Mostra la lletra tot i que sigui inexacta",
"tooltip": "Si no es troba la cançó, el plugin torna a intentar obtenir la lletra amb una cerca diferent.\nEl resultat d'aquesta segona cerca podria no ser exacte."
},
"show-time-codes": {
"label": "Mostra els codis de temps",
"tooltip": "Mostra els codis de temps al costat de la lletra"
}
},
"name": "Lletres sincronitzades",
"refetch-btn": {
"fetching": "Obtenint...",
"normal": "Tornar a obtenir la lletra"
},
"warnings": {
"duration-mismatch": "⚠️ - La lletra podria no estar ben sincronitzada, la durada no és coincident.",
"inexact": "⚠️ - La lletra d'aquesta cançó podria no ser exacta",
"instrumental": "⚠️ - Aquesta cançó és instrumental"
}
},
"taskbar-mediacontrol": {
"description": "Controla la reproducció des de la barra de tasques del Windows",
"name": "Control multimèdia a la barra de tasques"
},
"touchbar": {
"description": "Afegeix un giny a la Touch Bar per usuaris de macOS",
"name": "TouchBar"
},
"tuna-obs": {
"description": "Integració amb l'extensió «Tuna» del OBS",
"name": "Tuna OBS"
},
"video-toggle": {
"description": "Afegeix un botó per commutar entre el mode de vídeo o de cançó. Opcionalment, es pot eliminar la pestanya de vídeo per complet",
"menu": {
"align": {
"label": "Alineament",
"submenu": {
"left": "Esquerra",
"middle": "Mig",
"right": "Dreta"
}
},
"force-hide": "Força amagar la pestanya de vídeo",
"mode": {
"label": "Mode",
"submenu": {
"custom": "Commutador personalitzat",
"disabled": "Deshabilitat",
"native": "Commutador nadiu"
}
}
},
"name": "Botó de vídeo",
"templates": {
"button": "Cançó"
}
},
"visualizer": {
"description": "Afegeix un visualitzador al reproductor",
"menu": {
"visualizer-type": "Tipus de visualitzador"
},
"name": "Visualitzador"
}
}
}

View File

@ -207,6 +207,10 @@
} }
}, },
"plugins": { "plugins": {
"ad-speedup": {
"description": "Pokud se přehraje reklama tak ztlumí zvuk a nastaví rychlost přehrávání na 16x",
"name": "Zrychlovač Reklam"
},
"adblocker": { "adblocker": {
"description": "Blokuje všechny reklamy a sledování ihned od začátku", "description": "Blokuje všechny reklamy a sledování ihned od začátku",
"menu": { "menu": {
@ -410,6 +414,11 @@
"description": "Stahuje MP3 / source audio přímo z rozhraní", "description": "Stahuje MP3 / source audio přímo z rozhraní",
"menu": { "menu": {
"choose-download-folder": "Vybrat složku pro stahování", "choose-download-folder": "Vybrat složku pro stahování",
"download-finish-settings": {
"submenu": {
"advanced": "Pokoročile"
}
},
"download-playlist": "Stáhnout seznam písniček", "download-playlist": "Stáhnout seznam písniček",
"presets": "Předvolby", "presets": "Předvolby",
"skip-existing": "Přeskočit existující soubory" "skip-existing": "Přeskočit existující soubory"

View File

@ -207,6 +207,10 @@
} }
}, },
"plugins": { "plugins": {
"ad-speedup": {
"description": "Wenn eine Werbung spielt, stummt es das Audio und setzt die Wiedergabegeschwindigkeit auf 16x",
"name": "Werbungsbeschleunigung"
},
"adblocker": { "adblocker": {
"description": "Blockiere jegliche Werbung und Tracker", "description": "Blockiere jegliche Werbung und Tracker",
"menu": { "menu": {
@ -410,24 +414,24 @@
"description": "Lädt MP3-/Original-Audio direkt von der Schnittstelle herunter", "description": "Lädt MP3-/Original-Audio direkt von der Schnittstelle herunter",
"menu": { "menu": {
"choose-download-folder": "Downloadordner wählen", "choose-download-folder": "Downloadordner wählen",
"download-finish-settings": {
"label": "Song am Ende runterladen",
"prompt": {
"last-percent": "Nach x Prozent",
"last-seconds": "Letzten x Sekunden",
"title": "Konfiguriere wann runtergeladen werden soll"
},
"submenu": {
"advanced": "Erweitert",
"enabled": "Aktiviert",
"mode": "Zeitmodus",
"percent": "Prozent",
"seconds": "Sekunden"
}
},
"download-playlist": "Wiedergabeliste herunterladen", "download-playlist": "Wiedergabeliste herunterladen",
"presets": "Voreinstellungen", "presets": "Voreinstellungen",
"skip-existing": "Vorhandene Dateien überspringen", "skip-existing": "Vorhandene Dateien überspringen"
"download-finish-settings": {
"label": "Song am Ende runterladen",
"submenu": {
"enabled": "Aktiviert",
"mode": "Zeitmodus",
"seconds": "Sekunden",
"percent": "Prozent",
"advanced": "Erweitert"
},
"prompt": {
"title": "Konfiguriere wann runtergeladen werden soll",
"last-seconds": "Letzten x Sekunden",
"last-percent": "Nach x Prozent"
}
}
}, },
"name": "Downloader", "name": "Downloader",
"renderer": { "renderer": {
@ -664,6 +668,54 @@
"description": "Überspringt automatisch nicht-musikalische Teile wie Intro/Outro oder Teile von Musikvideos, in denen der Song nicht gespielt wird", "description": "Überspringt automatisch nicht-musikalische Teile wie Intro/Outro oder Teile von Musikvideos, in denen der Song nicht gespielt wird",
"name": "SponsorBlock" "name": "SponsorBlock"
}, },
"synced-lyrics": {
"description": "Bietet synchronisierte Liedtexte zu Songs, verwendet Anbieter wie LRClib.",
"errors": {
"fetch": "⚠️ - Beim Abrufen des Liedtexts ist ein Fehler aufgetreten. Bitte versuchen Sie es später nochmal.",
"not-found": "⚠️ - Kein Text für diesen Song gefunden."
},
"menu": {
"default-text-string": {
"label": "Standardzeichen zwischen Texten",
"tooltip": "Standardzeichen für die Lücke zwischen Songtexten auswählen"
},
"line-effect": {
"label": "Zeileneffekt",
"submenu": {
"focus": {
"label": "Fokussieren",
"tooltip": "Nur aktive Zeile weiß darstellen"
},
"offset": {
"label": "Versatz"
},
"scale": {
"label": "Skalieren",
"tooltip": "Aktuelle Zeile skalieren"
}
},
"tooltip": "Effekt für aktive Zeile auswählen"
},
"precise-timing": {
"label": "Den Songtext perfekt synchronisieren",
"tooltip": "Auf die Millisekunde genau berechnen, wann die nächste Zeile angezeigt werden soll (Kann Einfluss auf die Leistung haben)"
},
"show-time-codes": {
"label": "Zeitkodierungen anzeigen",
"tooltip": "Zeitkodierungen neben Songtext anzeigen"
}
},
"name": "Synchronisierte Texte",
"refetch-btn": {
"fetching": "Hole Songtext...",
"normal": "Songtext neu holen"
},
"warnings": {
"duration-mismatch": "⚠️ - Es kann sein, dass die Synchronization nicht stimmt, da die Songdauer nicht übereinstimmt.",
"inexact": "⚠️ - Es ist Möglich, dass der Songtext für diesen Song nicht übereinstimmt.",
"instrumental": "⚠️ - Das ist ein instrumentales Lied"
}
},
"taskbar-mediacontrol": { "taskbar-mediacontrol": {
"description": "Wiedergabe aus der Windows Taskleiste kontrollieren", "description": "Wiedergabe aus der Windows Taskleiste kontrollieren",
"name": "Mediensteuerung in der Taskleiste" "name": "Mediensteuerung in der Taskleiste"

View File

@ -2,7 +2,7 @@
"common": { "common": {
"console": { "console": {
"plugins": { "plugins": {
"execute-failed": "Αποτυχία εκτέλεσης προσθέτου {{pluginName}}::{{contextName}}", "execute-failed": "Απέτυχε η εκτέλεση του πρόσθετου {{pluginName}}::{{contextName}}",
"executed-at-ms": "Το πρόσθετο {{pluginName}}::{{contextName}} εκτελέστηκε σε {{ms}}ms", "executed-at-ms": "Το πρόσθετο {{pluginName}}::{{contextName}} εκτελέστηκε σε {{ms}}ms",
"initialize-failed": "Απέτυχε η αρχικοποίηση του πρόσθετου \"{{pluginName}}\"", "initialize-failed": "Απέτυχε η αρχικοποίηση του πρόσθετου \"{{pluginName}}\"",
"load-all": "Φόρτωση όλων των πρόσθετων", "load-all": "Φόρτωση όλων των πρόσθετων",
@ -36,7 +36,7 @@
"details": "Σφάλμα ανταπόκρισης!\n{{error}}" "details": "Σφάλμα ανταπόκρισης!\n{{error}}"
}, },
"when-ready": { "when-ready": {
"clearing-cache-after-20s": "Εκκαθάριση της cache της εφαρμογής" "clearing-cache-after-20s": "Εκκαθάριση μνήμης cache εφαρμογής"
}, },
"window": { "window": {
"tried-to-render-offscreen": "Το παράθυρο προσπάθησε να απεικονίσει εκτός οθόνης, windowSize={{windowSize}}, displaySize={{displaySize}}, position={{position}}" "tried-to-render-offscreen": "Το παράθυρο προσπάθησε να απεικονίσει εκτός οθόνης, windowSize={{windowSize}}, displaySize={{displaySize}}, position={{position}}"
@ -45,23 +45,23 @@
"dialog": { "dialog": {
"hide-menu-enabled": { "hide-menu-enabled": {
"detail": "Το μενού είναι κρυμμένο, χρησιμοποιήστε το 'Alt' για να το εμφανίσετε (ή το 'Escape' αν χρησιμοποιείτε το μενού εφαρμογής)", "detail": "Το μενού είναι κρυμμένο, χρησιμοποιήστε το 'Alt' για να το εμφανίσετε (ή το 'Escape' αν χρησιμοποιείτε το μενού εφαρμογής)",
"message": "Απόκρυψη μενού είναι ενεργοποιημένο", "message": "Η απόκρυψη μενού είναι ενεργοποιημένη",
"title": "Ενεργοποιήθηκε η Απόκρυψη του Μενού" "title": "Η απόκρυψη μενού ενεργοποιήθηκε"
}, },
"need-to-restart": { "need-to-restart": {
"buttons": { "buttons": {
"later": "Αργότερα", "later": "Αργότερα",
"restart-now": "Επανεκκίνηση Τώρα" "restart-now": "Επανεκκίνηση τώρα"
}, },
"detail": "Το πρόσθετο \"{{pluginName}}\" απαιτεί επανεκκίνηση για να ισχύσει", "detail": "Το πρόσθετο \"{{pluginName}}\" απαιτεί επανεκκίνηση για να ισχύσει",
"message": "Το \"{{pluginName}}\" χρειάζεται επανεκκίνηση", "message": "Το \"{{pluginName}}\" χρειάζεται επανεκκίνηση",
"title": "Απαιτείται Επανεκκίνηση" "title": "Απαιτείται επανεκκίνηση"
}, },
"unresponsive": { "unresponsive": {
"buttons": { "buttons": {
"quit": "Έξοδος", "quit": "Έξοδος",
"relaunch": "Επανεκκίνηση", "relaunch": "Επανεκκίνηση",
"wait": "Περίμενε" "wait": "Περιμένετε"
}, },
"detail": "Λυπούμαστε για την ταλαιπωρία! Παρακαλούμε επιλέξτε τι να κάνετε:", "detail": "Λυπούμαστε για την ταλαιπωρία! Παρακαλούμε επιλέξτε τι να κάνετε:",
"message": "Η εφαρμογή δεν ανταποκρίνεται", "message": "Η εφαρμογή δεν ανταποκρίνεται",
@ -69,13 +69,13 @@
}, },
"update-available": { "update-available": {
"buttons": { "buttons": {
"disable": "Απενεργοποίηση Ενημερώσεων", "disable": "Απενεργοποίηση ενημερώσεων",
"download": "Λήψη", "download": "Λήψη",
"ok": "Εντάξει" "ok": "OK"
}, },
"detail": "Μια νέα έκδοση είναι διαθέσιμη και μπορεί να ληφθεί από τον σύνδεσμο {{downloadLink}}", "detail": "Μια νέα έκδοση είναι διαθέσιμη και μπορεί να ληφθεί από τον σύνδεσμο {{downloadLink}}",
"message": "Μια νέα έκδοση είναι διαθέσιμη", "message": "Μια νέα έκδοση είναι διαθέσιμη",
"title": "Υπάρχει Διαθέσιμη Ενημέρωση" "title": "Υπάρχει διαθέσιμη ενημέρωση"
} }
}, },
"menu": { "menu": {
@ -87,7 +87,7 @@
"go-back": "Πήγαινε πίσω", "go-back": "Πήγαινε πίσω",
"go-forward": "Πήγαινε μπροστά", "go-forward": "Πήγαινε μπροστά",
"quit": "Έξοδος", "quit": "Έξοδος",
"restart": "Επανεκκίνηση Εφαρμογής" "restart": "Επανεκκίνηση εφαρμογής"
} }
}, },
"options": { "options": {
@ -106,7 +106,7 @@
"prompt": { "prompt": {
"label": "Εισαγωγή διεύθυνσης διακομιστή μεσολάβησης (proxy): (αφήστε κενό για απενεργοποίηση)", "label": "Εισαγωγή διεύθυνσης διακομιστή μεσολάβησης (proxy): (αφήστε κενό για απενεργοποίηση)",
"placeholder": "Παράδειγμα: SOCKS5://127.0.0.1:9999", "placeholder": "Παράδειγμα: SOCKS5://127.0.0.1:9999",
"title": "Ορισμός διακομιστή μεσολάβησης (proxy)" "title": "Ορισμός μεσολάβησης"
} }
}, },
"toggle-dev-tools": "Εναλλαγή DevTools" "toggle-dev-tools": "Εναλλαγή DevTools"
@ -135,8 +135,8 @@
"single-instance-lock": "Κλείδωμα Μοναδικής Εκδοχής", "single-instance-lock": "Κλείδωμα Μοναδικής Εκδοχής",
"start-at-login": "Έναρξη κατά την σύνδεση", "start-at-login": "Έναρξη κατά την σύνδεση",
"starting-page": { "starting-page": {
"label": "Σελίδα Έναρξης", "label": "Σελίδα έναρξης",
"unset": "Κατάργηση Ορισμού" "unset": "Κατάργηση ορισμού"
}, },
"tray": { "tray": {
"label": "Δίσκος", "label": "Δίσκος",
@ -148,13 +148,27 @@
} }
}, },
"visual-tweaks": { "visual-tweaks": {
"label": "Τροποποιήσεις Εμφάνισης",
"submenu": { "submenu": {
"like-buttons": { "like-buttons": {
"default": "Default" "default": "Default",
"force-show": "Αναγκαστική Εμφάνιση",
"hide": "Απόκρυψη",
"label": "Μου αρέσει"
}, },
"remove-upgrade-button": "Αφαίρεση κουμπιού αναβάθμισης",
"theme": { "theme": {
"label": "Theme", "dialog": {
"button": {
"cancel": "Άκυρο",
"remove": "Αφαίρεση"
},
"remove-theme": "Είστε βέβαιοι ότι θέλετε να αφαιρέσετε το προσαρμοσμένο θέμα;",
"remove-theme-message": "Αυτό θα αφαιρέσει το προσαρμοσμένο θέμα"
},
"label": "Θέμα",
"submenu": { "submenu": {
"import-css-file": "Εισαγωγή προσαρμοσμένου αρχείου CSS",
"no-theme": "No theme" "no-theme": "No theme"
} }
} }
@ -163,29 +177,68 @@
} }
}, },
"plugins": { "plugins": {
"label": "Plugins" "enabled": "Ενεργοποιημένο",
"label": "Πρόσθετα",
"new": "ΝΕΟ"
}, },
"view": { "view": {
"label": "View" "label": "Προβολή",
"submenu": {
"force-reload": "Αναγκαστική Eπαναφόρτωση",
"reload": "Επαναφόρτωση",
"reset-zoom": "Πραγματικό μέγεθος",
"toggle-fullscreen": "Εναλλαγή Πλήρους Οθόνης",
"zoom-in": "Μεγέθυνση",
"zoom-out": "Σμίκρυνση"
}
}
},
"tray": {
"next": "Επόμενο",
"play-pause": "Αναπαραγωγή/Παύση",
"previous": "Προηγούμενο",
"quit": "Έξοδος",
"restart": "Επανεκκίνηση εφαρμογής",
"show": "Εμφάνιση παραθύρου",
"tooltip": {
"default": "YouTube Music",
"with-song-info": "YouTube Music: {{artist}} - {{title}}"
} }
} }
}, },
"plugins": { "plugins": {
"ad-speedup": {
"description": "Εαν παίξει διαφήμιση κάνει σίγαση του ήχου και θέτει την ταχύτητα αναπαραγωγής στο 16x",
"name": "Γρήγορη Προώθηση Διαφημίσεων"
},
"adblocker": { "adblocker": {
"description": "Αποκλεισμός όλων των διαφημίσεων και tracker", "description": "Αποκλεισμός όλων των διαφημίσεων και tracker",
"menu": { "menu": {
"blocker": "Μέθοδος αποκλεισμού" "blocker": "Μέθοδος αποκλεισμού"
}, },
"name": "Adblocker" "name": "Μπλοκάρισμα Διαφημίσεων"
},
"album-actions": {
"description": "Προσθέτει κουμπιά Like/Unlike και Dislike/Undislike που δρουν συνολικά σε όλα τα κομμάτια μιας playlist ή ενός άλμπουμ",
"name": "Ενέργειες σε Άλμπουμ"
}, },
"album-color-theme": { "album-color-theme": {
"description": "Εφαρμόζει ένα δυναμικό θέμα και εφέ με βάση τη χρωματική παλέτα του άλμπουμ", "description": "Εφαρμόζει ένα δυναμικό θέμα και εφέ με βάση τη χρωματική παλέτα του άλμπουμ",
"menu": {
"color-mix-ratio": {
"label": "Αναλογία μίξης χρώματος",
"submenu": {
"percent": "{{ratio}}%"
}
}
},
"name": "Album Color Theme" "name": "Album Color Theme"
}, },
"ambient-mode": { "ambient-mode": {
"description": "Εφαρμόζει ένα εφέ φωτισμού ρίχνοντας απαλά χρώματα από το βίντεο, στο φόντο της οθόνης σας.", "description": "Εφαρμόζει ένα εφέ φωτισμού ρίχνοντας απαλά χρώματα από το βίντεο, στο φόντο της οθόνης σας.",
"menu": { "menu": {
"blur-amount": { "blur-amount": {
"label": "Ένταση θαμπώματος",
"submenu": { "submenu": {
"pixels": "{{blurAmount}} pixels" "pixels": "{{blurAmount}} pixels"
} }
@ -197,25 +250,30 @@
} }
}, },
"opacity": { "opacity": {
"label": "Ποσότητα αδιαφάνειας", "label": "Αδιαφάνεια",
"submenu": { "submenu": {
"percent": "{{opacity}}%" "percent": "{{opacity}}%"
} }
}, },
"quality": { "quality": {
"label": "Ποιότητα",
"submenu": { "submenu": {
"pixels": "{{quality}} pixels" "pixels": "{{quality}} pixels"
} }
}, },
"size": { "size": {
"label": "Μέγεθος",
"submenu": { "submenu": {
"percent": "{{size}}%" "percent": "{{size}}%"
} }
}, },
"smoothness-transition": { "smoothness-transition": {
"submenu": { "submenu": {
"during": "Σε {{interpolationTime}} δεύτερα" "during": "Σε {{interpolationTime}} δευτερόλεπτα"
} }
},
"use-fullscreen": {
"label": "Χρήση πλήρους οθόνης"
} }
} }
}, },
@ -257,11 +315,14 @@
"description": "Κάνει τα τραγούδια να είναι αυτόματα σε παύση", "description": "Κάνει τα τραγούδια να είναι αυτόματα σε παύση",
"menu": { "menu": {
"apply-once": "Εφαρμόζεται μόνο στο πρώτο τραγούδι" "apply-once": "Εφαρμόζεται μόνο στο πρώτο τραγούδι"
} },
"name": "Απενεργοποίηση αυτόματης αναπαραγωγής"
}, },
"discord": { "discord": {
"description": "Δείξτε στους φίλους σας τι ακούτε με το Rich Presence", "description": "Δείξτε στους φίλους σας τι ακούτε με το Rich Presence",
"menu": { "menu": {
"auto-reconnect": "Αυτόματη επανασύνδεση",
"clear-activity": "Εκκαθάριση δραστηριότητας",
"hide-duration-left": "Απόκρυψη της διάρκειας που απομένει", "hide-duration-left": "Απόκρυψη της διάρκειας που απομένει",
"hide-github-button": "Απόκρυψη του συνδέσμου προς GitHub", "hide-github-button": "Απόκρυψη του συνδέσμου προς GitHub",
"set-inactivity-timeout": "Ορισμός χρονικού ορίου αδράνειας" "set-inactivity-timeout": "Ορισμός χρονικού ορίου αδράνειας"
@ -280,34 +341,113 @@
"buttons": { "buttons": {
"ok": "OK" "ok": "OK"
}, },
"message": "Λήψη λίστας αναπαραγωγής {{playlistTitle}}", "detail": "{{playlistSize}} τραγούδια)",
"title": "Λήψη ξεκίνησε" "message": "Λήψη της λίστας αναπαραγωγής {{playlistTitle}}",
"title": "Η λήψη ξεκίνησε"
} }
}, },
"feedback": { "feedback": {
"conversion-progress": "Μετατροπή: {{percent}}%", "conversion-progress": "Μετατροπή: {{percent}}%",
"download-progress": "Download: {{percent}}%", "converting": "Μετατροπή…",
"preparing-file": "Προετοιμασία αρχείου…" "download-info": "Λήψη του {{artist}} - {{title}} [{{videoId}}",
"download-progress": "Λήψη: {{percent}}%",
"downloading": "Λήψη…",
"downloading-counter": "Λήψη {{current}}/{{total}}…",
"downloading-playlist": "Λήψη της λίστας αναπαραγωγής \"{{playlistTitle}}\" - {{playlistSize}} τραγούδια ({{playlistId}})",
"folder-already-exists": "Ο φάκελος {{playlistFolder}} υπάρχει ήδη",
"loading": "Φόρτωση…",
"playlist-is-empty": "Η λίστα αναπραγωγής είναι άδεια",
"preparing-file": "Προετοιμασία αρχείου…",
"saving": "Αποθήκευση…",
"video-id-not-found": "Το βίντεο δεν βρέθηκε"
} }
}, },
"menu": {
"download-finish-settings": {
"prompt": {
"last-seconds": "Τελευταία x δευτερόλεπτα"
},
"submenu": {
"percent": "Ποσοστό",
"seconds": "Δευτερόλεπτα"
}
},
"download-playlist": "Λήψη λίστας αναπαραγωγής",
"skip-existing": "Παράλειψη υπάρχοντων αρχείων"
},
"templates": { "templates": {
"button": "Download" "button": "Λήψη"
}
},
"music-together": {
"internal": {
"save": "Αποθήκευση",
"unknown-user": "Άγνωστος χρήστης"
},
"menu": {
"connected-users": "Συνδεδεμένοι χρήστες"
},
"toast": {
"add-song-failed": "Απέτυχε η προσθήκη τραγουδιού",
"remove-song-failed": "Απέτυχε η αφαίρεση τραγουδιού"
} }
}, },
"navigation": { "navigation": {
"name": "Navigation" "name": "Πλοήγηση"
}, },
"no-google-login": { "no-google-login": {
"name": "No Google Login" "name": "No Google Login"
}, },
"notifications": { "notifications": {
"name": "Notifications" "name": "Ειδοποιήσεις"
},
"picture-in-picture": {
"menu": {
"always-on-top": "Πάντα σε πρώτο πλάνο",
"hotkey": {
"label": "Πλήκτρο πρόσβασης",
"prompt": {
"keybind-options": {
"hotkey": "Πλήκτρο πρόσβασης"
}
}
},
"save-window-position": "Αποθήκευση θέσης παραθύρου",
"save-window-size": "Αποθήκευση μεγέθους παραθύρου"
}
},
"playback-speed": {
"name": "Ταχύτητα αναπαραγωγής",
"templates": {
"button": "Ταχύτητα"
}
},
"precise-volume": {
"prompt": {
"global-shortcuts": {
"keybind-options": {
"decrease": "Μείωση έντασης",
"increase": "Αύξηση έντασης"
}
}
}
},
"quality-changer": {
"backend": {
"dialog": {
"quality-changer": {
"detail": "Τρέχουσα ποιότητα: {{quality}}"
}
}
}
}, },
"shortcuts": { "shortcuts": {
"prompt": { "prompt": {
"keybind": { "keybind": {
"keybind-options": { "keybind-options": {
"next": "Next" "next": "Επόμενο",
"play-pause": "Αναπαραγωγή / Παύση",
"previous": "Προηγούμενο"
} }
} }
} }

View File

@ -158,18 +158,18 @@
}, },
"remove-upgrade-button": "Remove upgrade button", "remove-upgrade-button": "Remove upgrade button",
"theme": { "theme": {
"dialog": {
"button": {
"cancel": "Cancel",
"remove": "Remove"
},
"remove-theme": "Are you sure you want to remove the custom theme?",
"remove-theme-message": "This will remove the custom theme"
},
"label": "Theme", "label": "Theme",
"submenu": { "submenu": {
"import-css-file": "Import custom CSS file", "import-css-file": "Import custom CSS file",
"no-theme": "No theme" "no-theme": "No theme"
},
"dialog": {
"remove-theme": "Are you sure you want to remove the custom theme?",
"remove-theme-message": "This will remove the custom theme",
"button": {
"cancel": "Cancel",
"remove": "Remove"
}
} }
} }
} }
@ -207,6 +207,10 @@
} }
}, },
"plugins": { "plugins": {
"ad-speedup": {
"description": "If an ad play it mutes the audio and sets playback speed to 16x",
"name": "Ad Speedup"
},
"adblocker": { "adblocker": {
"description": "Block all ads and tracking out of the box", "description": "Block all ads and tracking out of the box",
"menu": { "menu": {
@ -220,7 +224,6 @@
}, },
"album-color-theme": { "album-color-theme": {
"description": "Applies a dynamic theme and visual effects based on the album color palette", "description": "Applies a dynamic theme and visual effects based on the album color palette",
"name": "Album Color Theme",
"menu": { "menu": {
"color-mix-ratio": { "color-mix-ratio": {
"label": "Color mix ratio", "label": "Color mix ratio",
@ -228,7 +231,8 @@
"percent": "{{ratio}}%" "percent": "{{ratio}}%"
} }
} }
} },
"name": "Album Color Theme"
}, },
"ambient-mode": { "ambient-mode": {
"description": "Applies a lighting effect by casting gentle colors from the video, into your screens background", "description": "Applies a lighting effect by casting gentle colors from the video, into your screens background",
@ -275,6 +279,49 @@
}, },
"name": "Ambient Mode" "name": "Ambient Mode"
}, },
"api-server": {
"description": "Adds an API server to control the player",
"dialog": {
"request": {
"buttons": {
"allow": "Allow",
"deny": "Deny"
},
"message": "Allow {{ID}} ({{origin}}) to access the API?",
"title": "API authorization request"
}
},
"menu": {
"auth-strategy": {
"label": "Authorization strategy",
"submenu": {
"auth-at-first": {
"label": "Authorize at first request"
},
"none": {
"label": "No authorization"
}
}
},
"hostname": {
"label": "Hostname"
},
"port": {
"label": "Port"
}
},
"name": "API Server [Beta]",
"prompt": {
"hostname": {
"label": "Enter the hostname (like 0.0.0.0) for the API server:",
"title": "Hostname"
},
"port": {
"label": "Enter the port for the API server:",
"title": "Port"
}
}
},
"audio-compressor": { "audio-compressor": {
"description": "Apply compression to audio (lowers the volume of the loudest parts of the signal and raises the volume of the softest parts)", "description": "Apply compression to audio (lowers the volume of the loudest parts of the signal and raises the volume of the softest parts)",
"name": "Audio Compressor" "name": "Audio Compressor"
@ -410,24 +457,24 @@
"description": "Downloads MP3 / source audio directly from the interface", "description": "Downloads MP3 / source audio directly from the interface",
"menu": { "menu": {
"choose-download-folder": "Choose download folder", "choose-download-folder": "Choose download folder",
"download-finish-settings": {
"label": "Download on finish",
"prompt": {
"last-percent": "After x percent",
"last-seconds": "Last x seconds",
"title": "Configure when to download"
},
"submenu": {
"advanced": "Advanced",
"enabled": "Enabled",
"mode": "Time mode",
"percent": "Percent",
"seconds": "Seconds"
}
},
"download-playlist": "Download playlist", "download-playlist": "Download playlist",
"presets": "Presets", "presets": "Presets",
"skip-existing": "Skip existing files", "skip-existing": "Skip existing files"
"download-finish-settings": {
"label": "Download on finish",
"submenu": {
"enabled": "Enabled",
"mode": "Time mode",
"seconds": "Seconds",
"percent": "Percent",
"advanced": "Advanced"
},
"prompt": {
"title": "Configure when to download",
"last-seconds": "Last x seconds",
"last-percent": "After x percent"
}
}
}, },
"name": "Downloader", "name": "Downloader",
"renderer": { "renderer": {
@ -605,19 +652,19 @@
"dialog": { "dialog": {
"lastfm": { "lastfm": {
"auth-failed": { "auth-failed": {
"title": "Authentication Failed", "message": "Failed to authenticate with Last.fm\nHide the popup until the next restart.",
"message": "Failed to authenticate with Last.fm\nHide the popup until the next restart." "title": "Authentication Failed"
} }
} }
}, },
"menu": { "menu": {
"scrobble-other-media": "Scrobble other media",
"lastfm": { "lastfm": {
"api-settings": "Last.fm API Settings" "api-settings": "Last.fm API Settings"
}, },
"listenbrainz": { "listenbrainz": {
"token": "Enter ListenBrainz user token" "token": "Enter ListenBrainz user token"
} },
"scrobble-other-media": "Scrobble other media"
}, },
"name": "Scrobbler", "name": "Scrobbler",
"prompt": { "prompt": {
@ -664,6 +711,59 @@
"description": "Automatically Skips non-music parts like intro/outro or parts of music videos where the song isn't playing", "description": "Automatically Skips non-music parts like intro/outro or parts of music videos where the song isn't playing",
"name": "SponsorBlock" "name": "SponsorBlock"
}, },
"synced-lyrics": {
"description": "Provides synced lyrics to songs, using providers like LRClib.",
"errors": {
"fetch": "⚠️ - An error occurred while fetching the lyrics. Please try again later.",
"not-found": "⚠️ - No lyrics found for this song."
},
"menu": {
"default-text-string": {
"label": "Default character between lyrics",
"tooltip": "Choose the default character to use for the gap between lyrics"
},
"line-effect": {
"label": "Line effect",
"submenu": {
"focus": {
"label": "Focus",
"tooltip": "Make only the current line white"
},
"offset": {
"label": "Offset",
"tooltip": "Offset on the right the current line"
},
"scale": {
"label": "Scale",
"tooltip": "Scale the current line"
}
},
"tooltip": "Choose the effect to apply to the current line"
},
"precise-timing": {
"label": "Make the lyrics perfectly synced",
"tooltip": "Calculate to the milisecond the display of the next line (can have a small impact on performance)"
},
"show-lyrics-even-if-inexact": {
"label": "Show lyrics even if inexact",
"tooltip": "If the song is not found, the plugin tries again with a different search query.\nThe result from the second attempt may not be exact."
},
"show-time-codes": {
"label": "Show time codes",
"tooltip": "Show the time codes next to the lyrics"
}
},
"name": "Synced Lyrics",
"refetch-btn": {
"fetching": "Fetching...",
"normal": "Refetch lyrics"
},
"warnings": {
"duration-mismatch": "⚠️ - The lyrics may be out of sync due to a duration mismatch.",
"inexact": "⚠️ - The lyrics for this song may not be exact",
"instrumental": "⚠️ - This is an instrumental song"
}
},
"taskbar-mediacontrol": { "taskbar-mediacontrol": {
"description": "Control playback from your Windows taskbar", "description": "Control playback from your Windows taskbar",
"name": "Taskbar Media Control" "name": "Taskbar Media Control"

View File

@ -207,6 +207,10 @@
} }
}, },
"plugins": { "plugins": {
"ad-speedup": {
"description": "Si se reproduce un anuncio, silencia el audio y fija la velocidad de reproducción en 16x",
"name": "Aumento de la velocidad de anuncios"
},
"adblocker": { "adblocker": {
"description": "Bloquear todos los anuncios y el rastreo", "description": "Bloquear todos los anuncios y el rastreo",
"menu": { "menu": {
@ -275,6 +279,49 @@
}, },
"name": "Modo ambiente" "name": "Modo ambiente"
}, },
"api-server": {
"description": "Añade un servidor API para controlar el reproductor",
"dialog": {
"request": {
"buttons": {
"allow": "Permitir",
"deny": "Denegar"
},
"message": "¿Permitir {{ID}} ({{origin}}) acceder a la API?",
"title": "Petición de autorización API"
}
},
"menu": {
"auth-strategy": {
"label": "Estrategia de autorización",
"submenu": {
"auth-at-first": {
"label": "Autorizar la primera solicitud"
},
"none": {
"label": "Sin autorización"
}
}
},
"hostname": {
"label": "Nombre del host"
},
"port": {
"label": "Puerto"
}
},
"name": "Servidor API [Beta]",
"prompt": {
"hostname": {
"label": "Introduzca el nombre de host (como 0.0.0.0) para el servidor API:",
"title": "Nombre de host"
},
"port": {
"label": "Introduzca el puerto para el servidor API:",
"title": "Puerto"
}
}
},
"audio-compressor": { "audio-compressor": {
"description": "Aplicar compresión al audio (reduce la diferencia entre las partes más fuertes y más suaves de una pista para que tenga un nivel más consistente)", "description": "Aplicar compresión al audio (reduce la diferencia entre las partes más fuertes y más suaves de una pista para que tenga un nivel más consistente)",
"name": "Compresor de audio" "name": "Compresor de audio"
@ -664,6 +711,59 @@
"description": "Salta automáticamente las partes no musicales como la introducción/final o secciones de videos musicales donde la canción no está sonando", "description": "Salta automáticamente las partes no musicales como la introducción/final o secciones de videos musicales donde la canción no está sonando",
"name": "SponsorBlock" "name": "SponsorBlock"
}, },
"synced-lyrics": {
"description": "Proporciona letras de canciones sincronizadas, utilizando proveedores como LRClib.",
"errors": {
"fetch": "⚠️ - Se ha producido un error al descargar la letra. Por favor, vuelve a intentarlo más tarde.",
"not-found": "⚠️ - No se ha encontrado ninguna letra para esta canción."
},
"menu": {
"default-text-string": {
"label": "Carácter predeterminado entre letras",
"tooltip": "Elige el carácter predeterminado que se utilizará para el espacio entre letras"
},
"line-effect": {
"label": "Efecto de la línea",
"submenu": {
"focus": {
"label": "Enfoque",
"tooltip": "Mostrar solo la línea actual en blanco"
},
"offset": {
"label": "Desplazamiento",
"tooltip": "Desplazamiento a la derecha de la línea actual"
},
"scale": {
"label": "Escala",
"tooltip": "Escala de la línea actual"
}
},
"tooltip": "Elige el efecto que deseas aplicar a la línea actual"
},
"precise-timing": {
"label": "Haz que la letra esté perfectamente sincronizada",
"tooltip": "Calcular al milisegundo la visualización de la línea siguiente (puede tener un pequeño impacto en el rendimiento)"
},
"show-lyrics-even-if-inexact": {
"label": "Mostrar la letra aunque sea inexacta",
"tooltip": "Si no se encuentra la canción, el complemento vuelve a intentarlo con una búsqueda diferente.\nEl resultado del segundo intento puede no ser exacto."
},
"show-time-codes": {
"label": "Visualización del código de tiempo",
"tooltip": "Mostrar los códigos de tiempo junto a la letra"
}
},
"name": "Letras sincronizadas",
"refetch-btn": {
"fetching": "Recuperando...",
"normal": "Volver a buscar letras"
},
"warnings": {
"duration-mismatch": "⚠️ - La letra puede estar desincronizada debido a un desajuste en la duración.",
"inexact": "⚠️ - La letra de esta canción puede no ser exacta",
"instrumental": "⚠️ - Se trata de una canción instrumental"
}
},
"taskbar-mediacontrol": { "taskbar-mediacontrol": {
"description": "Controla la reproducción desde la barra de tareas de Windows", "description": "Controla la reproducción desde la barra de tareas de Windows",
"name": "Control de medios de la barra de tareas" "name": "Control de medios de la barra de tareas"

182
src/i18n/resources/et.json Normal file
View File

@ -0,0 +1,182 @@
{
"common": {
"console": {
"plugins": {
"execute-failed": "{{pluginName}}::{{contextName}} lisamooduli käivitamine ei õnnestunud",
"executed-at-ms": "{{pluginName}}::{{contextName}} lisamoodul käivitus {{ms}} millisekundiga",
"initialize-failed": "„{{pluginName}}“ lisamooduli töö alustamine ei õnnestunud",
"load-all": "Laadime kõiki lisamooduleid",
"load-failed": "„{{pluginName}}“ lisamooduli laadimine ei õnnestunud",
"loaded": "„{{pluginName}}“ lisamoodul on laaditud",
"unload-failed": "„{{pluginName}}“ lisamooduli mälust eemaldamine ei õnnestunud",
"unloaded": "„{{pluginName}}“ lisamoodul on mälust eemaldatud"
}
}
},
"language": {
"code": "et",
"local-name": "Eesti",
"name": "Estonian"
},
"main": {
"console": {
"did-finish-load": {
"dev-tools": "Laadimine lõppes, arendaja tarvikud on avatud"
},
"i18n": {
"loaded": "i18n on laaditud"
},
"second-instance": {
"receive-command": "„{{command}}“ käsk on vastu võetud"
},
"theme": {
"css-file-not-found": "CSS faili „{{cssFile}}“ pole olemas, seega eirame eelistust"
}
},
"dialog": {
"hide-menu-enabled": {
"detail": "Menüü on peidetud ja „Alt“ klahviga saad ta nähtavaks (rakenduse-siseses menüüs „Esc“ klahviga)",
"message": "Menüü peitmine on sisselülitatud",
"title": "Menüü peitmine on sisselülitatud"
},
"need-to-restart": {
"buttons": {
"later": "Hiljem",
"restart-now": "Taaskäivita kohe"
},
"detail": "„{{pluginName}}“ lisamooduli sisselülitamine eeldab rakenduse taaskäivitamist",
"message": "„{{pluginName}}“ lisamoodul eeldab rakenduse taaskäivitamist",
"title": "Palun käivita rakendus uuesti"
},
"unresponsive": {
"buttons": {
"quit": "Välju",
"relaunch": "Käivita uuesti",
"wait": "Oota"
}
}
},
"menu": {
"navigation": {
"label": "Liikumine",
"submenu": {
"copy-current-url": "Kopeeri esitamisel oleva pala URL",
"go-back": "Mine tagasi",
"go-forward": "Mine edasi",
"quit": "Välju",
"restart": "Käivita rakendus uuesti"
}
},
"plugins": {
"label": "Lisamoodulid",
"new": "UUS"
},
"view": {
"submenu": {
"zoom-in": "Suumi sisse",
"zoom-out": "Suumi välja"
}
}
},
"tray": {
"next": "Edasi",
"play-pause": "Esita/Peata esitus",
"previous": "Eelmine",
"quit": "Välju",
"restart": "Käivita rakendus uuesti",
"show": "Näita akent",
"tooltip": {
"default": "YouTube Music",
"with-song-info": "YouTube Music: {{artist}} - {{title}}"
}
}
},
"plugins": {
"ad-speedup": {
"description": "Reklaami esitamisel summutatakse heli ja keritakse edasi 16-kordse kiirusega",
"name": "Reklaamikiirendaja"
},
"adblocker": {
"description": "Blokeeri kõik reklaamid ja jälitajad",
"menu": {
"blocker": "Blokeerijad"
},
"name": "Reklaamiblokeerija"
},
"ambient-mode": {
"menu": {
"opacity": {
"submenu": {
"percent": "{{opacity}}%"
}
},
"quality": {
"label": "Kvaliteet",
"submenu": {
"pixels": "{{quality}} pikslit"
}
}
}
},
"blur-nav-bar": {
"description": "Muudab navigatsiooniriba läbipaistavaks ja hägusaks",
"name": "Hägus navigatsiooniriba"
},
"lyrics-genius": {
"description": "Lisa enamustele lugudele laulusõnad",
"menu": {
"romanized-lyrics": "Latiniseeritud laulusõnad"
},
"name": "Lyrics Genius",
"renderer": {
"fetched-lyrics": "Leidsime Geeniuse jaoks ühed laulusõnad"
}
},
"navigation": {
"name": "Liikumine"
},
"scrobbler": {
"description": "Lisa kraasimise tugi (last.fm, Listenbrainz, jne)",
"dialog": {
"lastfm": {
"auth-failed": {
"message": "Last.fm'i autentimine ei õnnestunud\nPeida hüpikaken järgmise taaskäivituseni.",
"title": "Autentimine ei õnnestunud"
}
}
},
"menu": {
"lastfm": {
"api-settings": "Last.fm API seadistused"
},
"listenbrainz": {
"token": "Sisesta ListenBrainz'i kasutaja tunnusluba"
},
"scrobble-other-media": "Kraasi muud meediat"
},
"name": "Kraasija",
"prompt": {
"lastfm": {
"api-key": "Last.fm API võti",
"api-secret": "Last.fm API saladus"
},
"listenbrainz": {
"token": {
"label": "Sisesta oma ListenBrainz'i tunnusluba:",
"title": "ListenBrainz'i tunnusluba"
}
}
}
},
"synced-lyrics": {
"menu": {
"show-lyrics-even-if-inexact": {
"tooltip": "Kui lugu ei leidu, siis lisamoodul üritab uut otsingut teistsuguse päringuga.\nTeise katse puhul tulemused ei pruugi olla väga täpsed."
}
}
},
"tuna-obs": {
"description": "Lõimimine OBSi Tuna lisamooduliga"
}
}
}

397
src/i18n/resources/fa.json Normal file
View File

@ -0,0 +1,397 @@
{
"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": "fa",
"local-name": "فارسی",
"name": "Persian"
},
"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": {
"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": {
"dialog": {
"button": {
"cancel": "لغو",
"remove": "حذف"
},
"remove-theme": "آیا مطمئن هستید که می‌خواهید تم سفارشی را حذف کنید؟",
"remove-theme-message": "این کار تم سفارشی را حذف خواهد کرد"
},
"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": "نمایش پنجره",
"tooltip": {
"default": "یوتیوب موسیقی",
"with-song-info": "یوتیوب موسیقی: {{artist}} - {{title}}"
}
}
},
"plugins": {
"ad-speedup": {
"description": "اگر تبلیغ پخش شود، صدا را بی‌صدا کرده و سرعت پخش را به 16 برابر افزایش می‌دهد",
"name": "سرعت‌دهی به تبلیغ"
},
"adblocker": {
"description": "مسدود کردن تمامی تبلیغات و ردیابی‌ها از ابتدا",
"menu": {
"blocker": "مسدودکننده"
},
"name": "مسدودکننده تبلیغات"
},
"album-actions": {
"description": "افزودن دکمه‌های \"برگرفتن ناپسند\"، \"ناپسند\"، \"پسند\"، و \"حذف پسند\" برای اعمال آنها روی همه آهنگ‌ها در یک فهرست پخش یا آلبوم",
"name": "عملیات آلبوم"
},
"album-color-theme": {
"description": "اعمال یک تم پویا و جلوه‌های بصری بر اساس پالت رنگ آلبوم",
"menu": {
"color-mix-ratio": {
"label": "نسبت ترکیب رنگ"
}
},
"name": "تم رنگ آلبوم"
},
"ambient-mode": {
"description": "اعمال یک اثر نوری با پخش رنگ‌های ملایم از ویدئو به پس‌زمینه صفحه نمایش شما",
"menu": {
"blur-amount": {
"label": "میزان تاری",
"submenu": {
"pixels": "{{blurAmount}} پیکسل"
}
},
"buffer": {
"label": "بافر"
},
"opacity": {
"label": "شفافیت"
},
"quality": {
"label": "کیفیت",
"submenu": {
"pixels": "{{quality}} پیکسل"
}
},
"size": {
"label": "اندازه"
},
"smoothness-transition": {
"label": "انتقال نرمی",
"submenu": {
"during": "در طول {{interpolationTime}} ثانیه"
}
},
"use-fullscreen": {
"label": "استفاده از تمام‌صفحه"
}
},
"name": "حالت محیطی"
},
"audio-compressor": {
"description": "اعمال فشرده‌سازی به صدا (کاهش حجم بلندترین بخش‌های سیگنال و افزایش حجم بخش‌های نرم‌تر)",
"name": "فشرده‌ساز صدا"
},
"blur-nav-bar": {
"description": "شفاف و محو کردن نوار ناوبری",
"name": "محو کردن نوار ناوبری"
},
"bypass-age-restrictions": {
"description": "دور زدن تأیید سن یوتیوب",
"name": "دور زدن محدودیت‌های سنی"
},
"captions-selector": {
"description": "انتخاب زیرنویس برای آهنگ‌های یوتیوب موسیقی",
"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": "مدت زمان خروج تدریجی (میلی‌ثانیه)",
"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": "پخش در یوتیوب موسیقی",
"set-inactivity-timeout": "تنظیم تایم‌اوت عدم فعالیت"
},
"name": "Rich Presence در Discord",
"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": "شناسه فهرست پخش یافت نشد"
}
}
}
}
}

View File

@ -69,7 +69,7 @@
}, },
"update-available": { "update-available": {
"buttons": { "buttons": {
"disable": "Päivityksen pois päältä", "disable": "Poista päivitykset käytöstä",
"download": "Lataa", "download": "Lataa",
"ok": "Selvä" "ok": "Selvä"
}, },
@ -215,8 +215,8 @@
"name": "Mainos estäjä" "name": "Mainos estäjä"
}, },
"album-actions": { "album-actions": {
"description": "Alapeukuta musiikki/videota, jotta voimme tarjota sinulle parhaimmat MIX:it", "description": "Lisää tykkäysnappulat, joilla voit lisätä tai poistaa tykkäyksiä kerralla kaikille soittolistan tai albumin kappaleille",
"name": "Albumin toiminnot" "name": "Albumin Toiminnot"
}, },
"album-color-theme": { "album-color-theme": {
"description": "Käyttää dynaamista teemaa ja visuaalisia tehosteita albumin väripaletin perusteella", "description": "Käyttää dynaamista teemaa ja visuaalisia tehosteita albumin väripaletin perusteella",
@ -228,7 +228,7 @@
} }
} }
}, },
"name": "Albumin värin teema" "name": "Albumin Värinen Teema"
}, },
"ambient-mode": { "ambient-mode": {
"description": "Antaa valaistustehosteen heittämällä videosta lempeitä värejä näytön taustalle", "description": "Antaa valaistustehosteen heittämällä videosta lempeitä värejä näytön taustalle",
@ -236,11 +236,11 @@
"blur-amount": { "blur-amount": {
"label": "Sumennuksen voimakkuus", "label": "Sumennuksen voimakkuus",
"submenu": { "submenu": {
"pixels": "{{blurAmount}}pikseliä" "pixels": "{{blurAmount}} pikseliä"
} }
}, },
"buffer": { "buffer": {
"label": "Puskuroi", "label": "Puskurointi",
"submenu": { "submenu": {
"buffer": "{{buffer}}" "buffer": "{{buffer}}"
} }
@ -254,7 +254,7 @@
"quality": { "quality": {
"label": "Laatu", "label": "Laatu",
"submenu": { "submenu": {
"pixels": "{{quality}}pikseliä" "pixels": "{{quality}} pikseliä"
} }
}, },
"size": { "size": {
@ -264,7 +264,10 @@
} }
}, },
"smoothness-transition": { "smoothness-transition": {
"label": "sujuvuus siirtymässä" "label": "Siirtymän sujuvuus",
"submenu": {
"during": "Kesto {{interpolationTime}} s"
}
}, },
"use-fullscreen": { "use-fullscreen": {
"label": "Käytetään koko näytön tilaa" "label": "Käytetään koko näytön tilaa"

View File

@ -201,13 +201,22 @@
"restart": "I-restart ang App", "restart": "I-restart ang App",
"show": "Ipakita ang window", "show": "Ipakita ang window",
"tooltip": { "tooltip": {
"default": "YouTube Music",
"with-song-info": "YouTube Music: {{artist}} - {{title}}" "with-song-info": "YouTube Music: {{artist}} - {{title}}"
} }
} }
}, },
"plugins": { "plugins": {
"ad-speedup": {
"description": "Pag mag-play ng ad, I-mute ang audio at i-set ang bilis ng playback ng 16x",
"name": "Pagbilis ng Ad"
},
"adblocker": { "adblocker": {
"description": "I-block ang lahat ng ad at tracking" "description": "I-block ang lahat ng ad at tracking",
"menu": {
"blocker": "Blocker"
},
"name": "Pag-block ng Ad"
}, },
"album-actions": { "album-actions": {
"description": "Idadagdag ang Undislike, Dislike, Like, at Unlike na button para ilapat ito sa lahat ng kanta sa isang playlist o album", "description": "Idadagdag ang Undislike, Dislike, Like, at Unlike na button para ilapat ito sa lahat ng kanta sa isang playlist o album",
@ -217,7 +226,10 @@
"description": "Naglalapat ng dynamic na tema at visual effect batay sa color palette ng album", "description": "Naglalapat ng dynamic na tema at visual effect batay sa color palette ng album",
"menu": { "menu": {
"color-mix-ratio": { "color-mix-ratio": {
"label": "Ratio ng paghahalo ng kulay" "label": "Ratio ng paghahalo ng kulay",
"submenu": {
"percent": "{{ratio}}%"
}
} }
}, },
"name": "Tema ng Kulay ng Album" "name": "Tema ng Kulay ng Album"
@ -256,6 +268,7 @@
} }
}, },
"smoothness-transition": { "smoothness-transition": {
"label": "Ayos ng Transisyon",
"submenu": { "submenu": {
"during": "Habang {{interpolationTime}} s" "during": "Habang {{interpolationTime}} s"
} }
@ -263,6 +276,50 @@
"use-fullscreen": { "use-fullscreen": {
"label": "Gumamit ng fullscreen" "label": "Gumamit ng fullscreen"
} }
},
"name": "Ambient Mode"
},
"api-server": {
"description": "Nagdadagdag ng API Server upang kontrolin ang player",
"dialog": {
"request": {
"buttons": {
"allow": "Payagan",
"deny": "Tanggihan"
},
"message": "Payagan ang {{ID}} ({{origin}}) upang ma-access ang API?",
"title": "Awtorisasyon ng API request"
}
},
"menu": {
"auth-strategy": {
"label": "Estratehiya ng awtorisasyon",
"submenu": {
"auth-at-first": {
"label": "Mag-autorisa sa unang request"
},
"none": {
"label": "Walang awtorisasyon"
}
}
},
"hostname": {
"label": "Hostname"
},
"port": {
"label": "Port"
}
},
"name": "API Server [Beta]",
"prompt": {
"hostname": {
"label": "Itala ang hostname (tulad ng 0.0.0.0) para sa API server:",
"title": "Hostname"
},
"port": {
"label": "Itala ang port para sa API server:",
"title": "Port"
}
} }
}, },
"audio-compressor": { "audio-compressor": {
@ -296,7 +353,8 @@
} }
}, },
"compact-sidebar": { "compact-sidebar": {
"description": "Laging i-set ang sidebar sa compact mode" "description": "Laging i-set ang sidebar sa compact mode",
"name": "Pinaliit na Sidebar"
}, },
"crossfade": { "crossfade": {
"description": "I-crossfade kada kanta", "description": "I-crossfade kada kanta",
@ -386,12 +444,28 @@
"description": "Dina-download ang mga MP3 / source audio direkta mula sa interface", "description": "Dina-download ang mga MP3 / source audio direkta mula sa interface",
"menu": { "menu": {
"choose-download-folder": "Pumili ng download folder", "choose-download-folder": "Pumili ng download folder",
"download-finish-settings": {
"label": "Kung natapos ang download",
"prompt": {
"last-percent": "Tapos ng x na porsyento",
"last-seconds": "Huling x na segundo",
"title": "I-configure kung kailan magda-download"
},
"submenu": {
"enabled": "Napagana na",
"percent": "Porsyento",
"seconds": "Segundo"
}
},
"download-playlist": "Dina-download ang playlist", "download-playlist": "Dina-download ang playlist",
"presets": "Mga preset", "presets": "Mga preset",
"skip-existing": "Laktawan ang mga kasalukuyang file" "skip-existing": "Laktawan ang mga kasalukuyang file"
}, },
"renderer": { "renderer": {
"can-not-update-progress": "Hindi ma-update ang progress" "can-not-update-progress": "Hindi ma-update ang progress"
},
"templates": {
"button": "Mag download"
} }
}, },
"exponential-volume": { "exponential-volume": {
@ -404,7 +478,8 @@
} }
}, },
"lumiastream": { "lumiastream": {
"description": "Nabibigay suporta sa Lumia Stream" "description": "Nabibigay suporta sa Lumia Stream",
"name": "Lumia Stream [Beta]"
}, },
"lyrics-genius": { "lyrics-genius": {
"description": "Nagdaragdag ng suporta sa lyrics para sa karamihan ng kanta", "description": "Nagdaragdag ng suporta sa lyrics para sa karamihan ng kanta",
@ -427,6 +502,7 @@
"connected-users": "Nakakonektang (mga) User", "connected-users": "Nakakonektang (mga) User",
"disconnect": "Mag-diskonekta sa Music Together", "disconnect": "Mag-diskonekta sa Music Together",
"empty-user": "Walang naka-konektang user", "empty-user": "Walang naka-konektang user",
"host": "Host ng Music Together",
"join": "Sumali sa Music Together", "join": "Sumali sa Music Together",
"permission": { "permission": {
"all": "Payagan ang mga guest na kontrolin ang playlist at player", "all": "Payagan ang mga guest na kontrolin ang playlist at player",
@ -460,20 +536,26 @@
"name": "Nabigasyon" "name": "Nabigasyon"
}, },
"no-google-login": { "no-google-login": {
"description": "Tanggalin ang mga Google login na button at mga link mula sa interface" "description": "Tanggalin ang mga Google login na button at mga link mula sa interface",
"name": "Walang Google na Login"
}, },
"notifications": { "notifications": {
"description": "Magpakita ng notification kapag nagsimulang tumugtog ang kanta (magagamit ang mga interactive na notification sa Windows)", "description": "Magpakita ng notification kapag nagsimulang tumugtog ang kanta (magagamit ang mga interactive na notification sa Windows)",
"menu": { "menu": {
"interactive": "Interactive na Notification",
"interactive-settings": { "interactive-settings": {
"label": "Mga Interactive na Setting",
"submenu": { "submenu": {
"hide-button-text": "Itago ang button na texto", "hide-button-text": "Itago ang button na texto",
"refresh-on-play-pause": "I-refresh sa Pag-play/Pag-pause", "refresh-on-play-pause": "I-refresh sa Pag-play/Pag-pause",
"tray-controls": "Buksan/Isara sa pag-click sa tray" "tray-controls": "Buksan/Isara sa pag-click sa tray"
} }
}, },
"priority": "Prioridad ng Notification",
"toast-style": "Estilo ng toast",
"unpause-notification": "Ipakita ang notification sa pag-unpause" "unpause-notification": "Ipakita ang notification sa pag-unpause"
} },
"name": "Mga Abiso"
}, },
"picture-in-picture": { "picture-in-picture": {
"description": "Payagan ang pag-palit ng app sa picture-in-picture mode", "description": "Payagan ang pag-palit ng app sa picture-in-picture mode",
@ -486,12 +568,13 @@
} }
}, },
"save-window-position": "I-save ang posisyon ng window", "save-window-position": "I-save ang posisyon ng window",
"save-window-size": "I-save ang laki ng windo", "save-window-size": "I-save ang laki ng window",
"use-native-pip": "Gamitin ang browser native na PiP" "use-native-pip": "Gamitin ang browser native na PiP"
} }
}, },
"playback-speed": { "playback-speed": {
"description": "Makinig na mabilisan, makinig na mabagalan! Nagdaragdag ito ng slider upang makontrol ang bilis ng kanta", "description": "Makinig na mabilisan, makinig na mabagalan! Nagdaragdag ito ng slider upang makontrol ang bilis ng kanta",
"name": "Bilis ng Playback",
"templates": { "templates": {
"button": "Bilis" "button": "Bilis"
} }
@ -553,7 +636,8 @@
}, },
"listenbrainz": { "listenbrainz": {
"token": { "token": {
"label": "Ilagay ang ListenBrainz user token:" "label": "Ilagay ang ListenBrainz user token:",
"title": "Token ng ListenBrainz"
} }
} }
} }
@ -561,6 +645,7 @@
"shortcuts": { "shortcuts": {
"description": "Nagbibigay-daan sa pagtatakda ng mga global hotkey para sa playback (play/pause/susunod/nakaraan) at pag-off ng media OSD sa pamamagitan ng pag-override sa mga media key, pag-on sa Ctrl/CMD + F para maghanap, pag-on sa suporta ng Linux MPRIS para sa mga media key, at mga custom na hotkey para sa mga advanced na user", "description": "Nagbibigay-daan sa pagtatakda ng mga global hotkey para sa playback (play/pause/susunod/nakaraan) at pag-off ng media OSD sa pamamagitan ng pag-override sa mga media key, pag-on sa Ctrl/CMD + F para maghanap, pag-on sa suporta ng Linux MPRIS para sa mga media key, at mga custom na hotkey para sa mga advanced na user",
"menu": { "menu": {
"override-media-keys": "I-override ang mga Media Key",
"set-keybinds": "I-set ang Global Song Control" "set-keybinds": "I-set ang Global Song Control"
}, },
"name": "Mga shortcut (at MPRIS)", "name": "Mga shortcut (at MPRIS)",
@ -568,6 +653,7 @@
"keybind": { "keybind": {
"keybind-options": { "keybind-options": {
"next": "Susunod", "next": "Susunod",
"play-pause": "Mag-play / Mag-pause",
"previous": "Nakaraan" "previous": "Nakaraan"
}, },
"label": "Pumili ng Global na Keybind para sa Songs Control:" "label": "Pumili ng Global na Keybind para sa Songs Control:"
@ -575,14 +661,65 @@
} }
}, },
"skip-disliked-songs": { "skip-disliked-songs": {
"description": "Laktawan ang na-dislike na kanta" "description": "Laktawan ang na-dislike na kanta",
"name": "I-skip ang mga Na-dislike na Kanta"
}, },
"skip-silences": { "skip-silences": {
"description": "Automatikong laktawan ang mga tahimik na mga seksyon sa kanta" "description": "Automatikong laktawan ang mga tahimik na mga seksyon sa kanta",
"name": "I-skip ang mga Katahimikan"
}, },
"sponsorblock": { "sponsorblock": {
"description": "Automatikong Laktawan ang di part ng kanta tulad ng intro/outro o part ng mga music video na ang kanta ay di nagple-play" "description": "Automatikong Laktawan ang di part ng kanta tulad ng intro/outro o part ng mga music video na ang kanta ay di nagple-play"
}, },
"synced-lyrics": {
"description": "Nagbibigay ng naka-sync na lyrics sa mga kanta, gamit ang mga provider tulad ng LRClib.",
"errors": {
"fetch": "⚠️ - Nagkaroon ng error habang kinukuha ang lyrics. Subukang muli mamaya.",
"not-found": "⚠️ - Walang nakitang lyrics para sa kantang ito."
},
"menu": {
"default-text-string": {
"label": "Default na character sa pagitan ng lyrics",
"tooltip": "Pumili ng default na character na gagamitin sa pagitan ng lyrics"
},
"line-effect": {
"label": "Effect ng Linya",
"submenu": {
"focus": {
"tooltip": "Gawing puti lamang ang kasalukuyang linya"
},
"offset": {
"tooltip": "I-offset sa kanan ang kasalukuyang linya"
},
"scale": {
"tooltip": "I-scale ang kasalukuyang linya"
}
},
"tooltip": "Pumili ng effect na ilalapat sa kasalukuyang linya"
},
"precise-timing": {
"label": "Gawing perpektong naka-sync ang lyrics",
"tooltip": "Kalkulahin sa millisecond ang pagpapakita ng susunod na linya (maaaring magkaroon ng maliit na epekto sa performance)"
},
"show-lyrics-even-if-inexact": {
"label": "Ipakita ang lyrics kahit di-eksakto",
"tooltip": "Kung hindi matagpuan ang kanta, susubukan muli ng plugin gamit ang ibang query sa paghahanap.\nAng resulta mula sa pangalawang pagsubok ay maaaring hindi eksakto."
},
"show-time-codes": {
"label": "Ipakita ang mga time code",
"tooltip": "Ipakita ang mga time code kasunod sa lyrics"
}
},
"refetch-btn": {
"fetching": "Nag-fe-fetch...",
"normal": "I-fetch muli ang lyrics"
},
"warnings": {
"duration-mismatch": "⚠️ - Maaaring hindi naka-sync ang lyrics dahil sa hindi pagkakatugma ng duration.",
"inexact": "⚠️ - Maaaring hindi eksakto ang lyrics para sa kantang ito",
"instrumental": "⚠️ - Ito ay isang instrumental na kanta"
}
},
"taskbar-mediacontrol": { "taskbar-mediacontrol": {
"description": "Kontrolin ang pag-play mula sa iyong taskbar ng Windows" "description": "Kontrolin ang pag-play mula sa iyong taskbar ng Windows"
}, },
@ -614,7 +751,10 @@
} }
}, },
"visualizer": { "visualizer": {
"description": "Idaragdag ng visualizer sa player" "description": "Idaragdag ng visualizer sa player",
"menu": {
"visualizer-type": "Uri ng Visualizer"
}
} }
} }
} }

View File

@ -105,7 +105,7 @@
"label": "Définir un proxy", "label": "Définir un proxy",
"prompt": { "prompt": {
"label": "Entrez l'adresse proxy : (laissez vide pour désactiver)", "label": "Entrez l'adresse proxy : (laissez vide pour désactiver)",
"placeholder": "Exemple: SOCKS5://127.0.0.1:9999", "placeholder": "Exemple: SOCKS5://127.0.0.1:9999",
"title": "Définir un proxy" "title": "Définir un proxy"
} }
}, },
@ -179,7 +179,7 @@
"plugins": { "plugins": {
"enabled": "Activé", "enabled": "Activé",
"label": "Extensions", "label": "Extensions",
"new": "NOUVELLE" "new": "NOUVEAU"
}, },
"view": { "view": {
"label": "Vue", "label": "Vue",
@ -207,6 +207,10 @@
} }
}, },
"plugins": { "plugins": {
"ad-speedup": {
"description": "Si une publicité apparaît, le son est coupé et la vitesse de lecture est réglée sur 16x",
"name": "Accélérer les publicités"
},
"adblocker": { "adblocker": {
"description": "Bloquer toutes les annonces et le suivi par défaut", "description": "Bloquer toutes les annonces et le suivi par défaut",
"menu": { "menu": {
@ -410,6 +414,21 @@
"description": "Télécharge les fichiers MP3/source audio directement depuis l'interface", "description": "Télécharge les fichiers MP3/source audio directement depuis l'interface",
"menu": { "menu": {
"choose-download-folder": "Choisissez le dossier de téléchargement", "choose-download-folder": "Choisissez le dossier de téléchargement",
"download-finish-settings": {
"label": "Télécharger une fois terminé",
"prompt": {
"last-percent": "Après x pour cent",
"last-seconds": "Dernières x secondes",
"title": "Configurer quand télécharger"
},
"submenu": {
"advanced": "Avancé",
"enabled": "Activé",
"mode": "Mode de temps",
"percent": "Pourcent",
"seconds": "Secondes"
}
},
"download-playlist": "Télécharger la liste de lecture", "download-playlist": "Télécharger la liste de lecture",
"presets": "Préconfigurations", "presets": "Préconfigurations",
"skip-existing": "Passer les fichiers existants" "skip-existing": "Passer les fichiers existants"
@ -649,6 +668,59 @@
"description": "Saute automatiquement les parties non musicales comme l'intro/outro ou les parties de clips vidéo où la chanson n'est pas lue", "description": "Saute automatiquement les parties non musicales comme l'intro/outro ou les parties de clips vidéo où la chanson n'est pas lue",
"name": "SponsorBlock" "name": "SponsorBlock"
}, },
"synced-lyrics": {
"description": "Ajoute des paroles synchronisées aux chansons, grâce à LRClib par exemple.",
"errors": {
"fetch": "⚠️ - Une erreur s'est produite en allant chercher les paroles. Merci de réessayer plus tard.",
"not-found": "⚠️ - Aucune paroles trouvées pour cette musique."
},
"menu": {
"default-text-string": {
"label": "Caractère par défaut entre les paroles",
"tooltip": "Choisi le caractère par défaut à utiliser pour l'espace entre les paroles"
},
"line-effect": {
"label": "Effet de ligne",
"submenu": {
"focus": {
"label": "Focus",
"tooltip": "Rend seulement la ligne actuelle blanche"
},
"offset": {
"label": "Décalage",
"tooltip": "Décale sur la droite la ligne actuelle"
},
"scale": {
"label": "Grossissement",
"tooltip": "Agrandis la ligne actuelle"
}
},
"tooltip": "Choisi l'effet à appliquer sur la ligne actuelle"
},
"precise-timing": {
"label": "Rend les paroles parfaitement synchronisées",
"tooltip": "Calcul à la milliseconde près l'affichage de la ligne suivante (peut avoir un faible impact sur les performances)"
},
"show-lyrics-even-if-inexact": {
"label": "Afficher les paroles même si inexactes",
"tooltip": "Si la musique n'est pas trouvé, le plugin essaye à nouveau avec une différence requête.\nLe résultat du deuxième essais peut ne pas être exacte."
},
"show-time-codes": {
"label": "Afficher les timecodes",
"tooltip": "Affiche à côté de chaque paroles son timecode"
}
},
"name": "Paroles Synchronisées",
"refetch-btn": {
"fetching": "Chargement...",
"normal": "Rafraîchir les paroles"
},
"warnings": {
"duration-mismatch": "⚠️ - Les paroles peuvent ne pas être synchronisées à cause d'une différence de durée.",
"inexact": "⚠️ - Les paroles de cette chanson peuvent ne pas être exactes",
"instrumental": "⚠️ - Cette musique n'a pas de paroles"
}
},
"taskbar-mediacontrol": { "taskbar-mediacontrol": {
"description": "Contrôlez la lecture depuis votre barre des tâches Windows", "description": "Contrôlez la lecture depuis votre barre des tâches Windows",
"name": "Contrôle multimédia de la barre des tâches" "name": "Contrôle multimédia de la barre des tâches"

View File

@ -2,7 +2,14 @@
"common": { "common": {
"console": { "console": {
"plugins": { "plugins": {
"execute-failed": "נכשל ביצוע תוסף {{pluginName}}::{{contextName}}" "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}} הורד"
} }
} }
}, },
@ -10,5 +17,43 @@
"code": "he", "code": "he",
"local-name": "עברית", "local-name": "עברית",
"name": "Hebrew" "name": "Hebrew"
},
"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}}, גודל מסך={{displaySize}}, מיקום={{position}}"
}
},
"dialog": {
"hide-menu-enabled": {
"detail": "התפריט מוחבא, השתמש \"Alt\" על להציג אותו (או \"Esacpe\" אם משתמשים בתפריט בתוך האפליקציה)",
"message": "הסתרת התפריט מופעלת",
"title": "הסתרת התפריט הופעלה"
},
"need-to-restart": {
"buttons": {
"later": "אחר כך",
"restart-now": "מתחיל את התוכנה מחדש עכשיו"
}
}
}
} }
} }

View File

@ -7,8 +7,8 @@
"initialize-failed": "Nem sikerült inicializálni a \"{{pluginName}}\" plugint", "initialize-failed": "Nem sikerült inicializálni a \"{{pluginName}}\" plugint",
"load-all": "Összes bővítmény betöltése", "load-all": "Összes bővítmény betöltése",
"load-failed": "Nem sikerült betölteni a \"{{pluginName}}\" plugint", "load-failed": "Nem sikerült betölteni a \"{{pluginName}}\" plugint",
"loaded": "\"{{pluginName}}\" nevű plugin betöltve", "loaded": "\"{{pluginName}}\" plugin betöltve",
"unload-failed": "Nem sikerült a \"{{pluginName}}\" bővítményt letölteni", "unload-failed": "Nem sikerült a \"{{pluginName}}\" bővítményt kikapcsolni",
"unloaded": "A \"{{pluginName}}\" bővítmény kikapcsolva" "unloaded": "A \"{{pluginName}}\" bővítmény kikapcsolva"
} }
} }
@ -207,6 +207,10 @@
} }
}, },
"plugins": { "plugins": {
"ad-speedup": {
"description": "Ha reklám szól, elnémítja a hangot és a lejátszási sebességet 16x-ra állítja",
"name": "Gyorsítás hozzáadása"
},
"adblocker": { "adblocker": {
"description": "Alapértelmezés szerint blokkolja az összes hirdetést és nyomkövetést", "description": "Alapértelmezés szerint blokkolja az összes hirdetést és nyomkövetést",
"menu": { "menu": {
@ -368,6 +372,9 @@
"backend": { "backend": {
"dialog": { "dialog": {
"error": { "error": {
"buttons": {
"ok": "Rendben"
},
"message": "Hoppá! Elnézést, a letöltés sikertelen volt…", "message": "Hoppá! Elnézést, a letöltés sikertelen volt…",
"title": "A letöltés során hiba történt!" "title": "A letöltés során hiba történt!"
}, },
@ -407,7 +414,22 @@
"description": "MP3 / forrás hanganyag letöltése közvetlenül az interfészről", "description": "MP3 / forrás hanganyag letöltése közvetlenül az interfészről",
"menu": { "menu": {
"choose-download-folder": "Letöltési mappa kiválasztása", "choose-download-folder": "Letöltési mappa kiválasztása",
"download-finish-settings": {
"label": "Letöltés befejezéskor",
"prompt": {
"last-percent": "x százalék után",
"last-seconds": "Utolsó x másodperc"
},
"submenu": {
"advanced": "Speciális",
"enabled": "Engedélyezve",
"mode": "Időmód",
"percent": "Százalék",
"seconds": "Másodpercek"
}
},
"download-playlist": "Lejátszási lista letöltése", "download-playlist": "Lejátszási lista letöltése",
"presets": "Sablonok",
"skip-existing": "Meglévő fájlok kihagyása" "skip-existing": "Meglévő fájlok kihagyása"
}, },
"name": "Letöltő", "name": "Letöltő",
@ -489,6 +511,7 @@
} }
}, },
"navigation": { "navigation": {
"description": "Következő/Vissza navigációs nyilak közvetlenül az interfészbe integrálva, mint a kedvenc böngésződben",
"name": "Navigáció" "name": "Navigáció"
}, },
"no-google-login": { "no-google-login": {
@ -502,10 +525,14 @@
"interactive-settings": { "interactive-settings": {
"label": "Interaktív beállítások", "label": "Interaktív beállítások",
"submenu": { "submenu": {
"hide-button-text": "Gombok szövegének elrejtése" "hide-button-text": "Gombok szövegének elrejtése",
"refresh-on-play-pause": "Frissítés lejátszás/szünet megnyomásakor",
"tray-controls": "Megnyitás/Bezárás tálca ikonra kattintva"
} }
}, },
"priority": "Értesítési prioritás" "priority": "Értesítési prioritás",
"toast-style": "Értesítés stílusa",
"unpause-notification": "Értesítés megjelenítése a lejátszás folytatásakor"
}, },
"name": "Értesítések" "name": "Értesítések"
}, },
@ -542,7 +569,9 @@
"precise-volume": { "precise-volume": {
"description": "A hangerő precíz szabályozása egérgörgővel/gyorsbillentyűkkel, egy egyedi HUD és testreszabható hangerő csuszka segítségével", "description": "A hangerő precíz szabályozása egérgörgővel/gyorsbillentyűkkel, egy egyedi HUD és testreszabható hangerő csuszka segítségével",
"menu": { "menu": {
"global-shortcuts": "Globális gyorsbillentyűk" "arrows-shortcuts": "Helyi nyíl-billentyűkkel való vezérlés",
"custom-volume-steps": "Egyedi hangerőléptetés beállítása",
"global-shortcuts": "Globális Gyorsbillentyűk"
}, },
"name": "Precíz hangerő", "name": "Precíz hangerő",
"prompt": { "prompt": {
@ -553,6 +582,10 @@
}, },
"label": "Válaszd ki a globális hangerő gyorsbillentyűket:", "label": "Válaszd ki a globális hangerő gyorsbillentyűket:",
"title": "Globális hangerő gyorsbillentyűk" "title": "Globális hangerő gyorsbillentyűk"
},
"volume-steps": {
"label": "Hangerő növelés/csökkentés léptékének kiválasztása",
"title": "Hangerő lépték"
} }
} }
}, },
@ -566,7 +599,8 @@
} }
} }
}, },
"description": "Lehetővé teszi a videó minőségének megváltoztatását egy gombbal a videó fedvényen" "description": "Lehetővé teszi a videó minőségének megváltoztatását egy gombbal a videó fedvényen",
"name": "Videóminőség modosító"
}, },
"scrobbler": { "scrobbler": {
"description": "Scrobbling támogatás hozzáadása (pl. last.fm, ListenBrainz)", "description": "Scrobbling támogatás hozzáadása (pl. last.fm, ListenBrainz)",
@ -613,6 +647,7 @@
"play-pause": "Lejátszás / Szünet", "play-pause": "Lejátszás / Szünet",
"previous": "Előző" "previous": "Előző"
}, },
"label": "Globális billentyűparancsok választása a dalok vezérléséhez:",
"title": "Globális gyorsbillentyűk" "title": "Globális gyorsbillentyűk"
} }
} }
@ -629,8 +664,38 @@
"description": "Automatikusan kihagyja a nem zenés részeket, mint például az intro/outro vagy a zenei videók olyan részeit, ahol a zene nem szól", "description": "Automatikusan kihagyja a nem zenés részeket, mint például az intro/outro vagy a zenei videók olyan részeit, ahol a zene nem szól",
"name": "SzponzorBlokk" "name": "SzponzorBlokk"
}, },
"synced-lyrics": {
"description": "Szinkronizált dalszövegeket biztosít dalokhoz, LRClib-hez hasonló szolgáltatókat használva.",
"errors": {
"fetch": "⚠️ - Hiba történt a dalszövegek lekérése közben. Kérlek, próbáld újra később.",
"not-found": "⚠️ - Nem található dalszöveg ehhez a zenéhez."
},
"menu": {
"line-effect": {
"submenu": {
"scale": {
"label": "Mérték"
}
}
},
"precise-timing": {
"label": "Dalszöveg tökéletes szinkronizálása"
}
},
"name": "Szinkronizált dalszövegek",
"refetch-btn": {
"fetching": "Lekérés folyamatban...",
"normal": "Dalszöveg újra lekérése"
},
"warnings": {
"duration-mismatch": "⚠️ - A dalszövegek időzítése eltérhet a zene hossza miatt.",
"inexact": "⚠️ - Ennek a zenének a dalszövege pontatlan lehet",
"instrumental": "⚠️ - Ez egy hangszerekkel játszott zene"
}
},
"taskbar-mediacontrol": { "taskbar-mediacontrol": {
"description": "Lejátszás vezérlése a Windows tálcáról" "description": "Lejátszás vezérlése a Windows tálcáról",
"name": "Médiavezérlés a tálcán"
}, },
"touchbar": { "touchbar": {
"description": "macOS felhasználók számára hozzáad egy widgetet a TouchBar-hoz", "description": "macOS felhasználók számára hozzáad egy widgetet a TouchBar-hoz",
@ -667,6 +732,7 @@
} }
}, },
"visualizer": { "visualizer": {
"description": "Vizualizációt ad a lejátszóhoz",
"menu": { "menu": {
"visualizer-type": "Vizualizáció típus" "visualizer-type": "Vizualizáció típus"
}, },

View File

@ -202,11 +202,15 @@
"show": "Tampilkan jendela", "show": "Tampilkan jendela",
"tooltip": { "tooltip": {
"default": "YouTube Musik", "default": "YouTube Musik",
"with-song-info": "YouTube Music: {{artist}} - {{title}}" "with-song-info": "YouTube Musik: {{artist}} - {{title}}"
} }
} }
}, },
"plugins": { "plugins": {
"ad-speedup": {
"description": "Jika iklan diputar, audio akan dimatikan dan kecepatan pemutaran akan diatur ke 16x",
"name": "Percepatan Iklan"
},
"adblocker": { "adblocker": {
"description": "Blokir semua iklan dan pelacakan di luar kotak", "description": "Blokir semua iklan dan pelacakan di luar kotak",
"menu": { "menu": {
@ -275,6 +279,49 @@
}, },
"name": "Mode ambient" "name": "Mode ambient"
}, },
"api-server": {
"description": "Menambahkan server API untuk mengontrol pemutar",
"dialog": {
"request": {
"buttons": {
"allow": "Izinkan",
"deny": "Menolak"
},
"message": "Izinkan {{ID}} ({{origin}}) untuk mengakses API?",
"title": "Permintaan otorisasi API"
}
},
"menu": {
"auth-strategy": {
"label": "Strategi otorisasi",
"submenu": {
"auth-at-first": {
"label": "Otorisasi pada permintaan pertama"
},
"none": {
"label": "Tidak ada otorisasi"
}
}
},
"hostname": {
"label": "Nama host"
},
"port": {
"label": "Port"
}
},
"name": "API Server [Beta]",
"prompt": {
"hostname": {
"label": "Masukkan nama host (seperti 0.0.0.0) untuk server API:",
"title": "Nama host"
},
"port": {
"label": "Masukkan port untuk server API:",
"title": "Port"
}
}
},
"audio-compressor": { "audio-compressor": {
"description": "Menerapkan kompresi pada audio (mengurangi volume pada bagian paling keras dari sinyal dan meningkatkan volume pada bagian paling lembut)", "description": "Menerapkan kompresi pada audio (mengurangi volume pada bagian paling keras dari sinyal dan meningkatkan volume pada bagian paling lembut)",
"name": "Kompresi suara" "name": "Kompresi suara"
@ -410,6 +457,21 @@
"description": "Unduh MP3 / sumber suara secara langsung via antarmuka", "description": "Unduh MP3 / sumber suara secara langsung via antarmuka",
"menu": { "menu": {
"choose-download-folder": "Pilih folder unduhan", "choose-download-folder": "Pilih folder unduhan",
"download-finish-settings": {
"label": "Unduh setelah selesai",
"prompt": {
"last-percent": "x persen terakhir",
"last-seconds": "x detik terakhir",
"title": "Konfigurasikan kapan akan mengunduh"
},
"submenu": {
"advanced": "Lanjutan",
"enabled": "Diaktifkan",
"mode": "Mode waktu",
"percent": "Persen",
"seconds": "Detik"
}
},
"download-playlist": "Unduh daftar putar", "download-playlist": "Unduh daftar putar",
"presets": "Prasetel", "presets": "Prasetel",
"skip-existing": "Lewati berkas yang sudah ada" "skip-existing": "Lewati berkas yang sudah ada"
@ -649,6 +711,59 @@
"description": "Otomatis Melewati bagian yang bukan musik seperti intro/outro atau bagian dari video musik di mana lagu tidak dimainkan", "description": "Otomatis Melewati bagian yang bukan musik seperti intro/outro atau bagian dari video musik di mana lagu tidak dimainkan",
"name": "SponsorBlock" "name": "SponsorBlock"
}, },
"synced-lyrics": {
"description": "Menyediakan lirik lagu yang disinkronkan, menggunakan penyedia seperti LRClib.",
"errors": {
"fetch": "⚠️ - Terjadi kesalahan saat mengambil lirik. Coba lagi nanti.",
"not-found": "⚠️ - Tidak ada lirik yang ditemukan untuk lagu ini."
},
"menu": {
"default-text-string": {
"label": "Karakter default antara lirik",
"tooltip": "Pilih karakter default yang akan digunakan untuk celah antar lirik"
},
"line-effect": {
"label": "Efek garis",
"submenu": {
"focus": {
"label": "Fokus",
"tooltip": "Jadikan hanya baris saat ini berwarna putih"
},
"offset": {
"label": "Offset",
"tooltip": "Mengimbangi garis saat ini di sebelah kanan"
},
"scale": {
"label": "Skala",
"tooltip": "Skala garis saat ini"
}
},
"tooltip": "Pilih efek yang akan diterapkan ke baris saat ini"
},
"precise-timing": {
"label": "Buat liriknya tersinkronisasi dengan sempurna",
"tooltip": "Hitung hingga milidetik tampilan baris berikutnya (dapat berdampak kecil pada kinerja)"
},
"show-lyrics-even-if-inexact": {
"label": "Tampilkan lirik meskipun tidak tepat",
"tooltip": "Jika lagu tidak ditemukan, plugin akan mencoba lagi dengan kueri pencarian yang berbeda.\nHasil dari percobaan kedua mungkin tidak tepat."
},
"show-time-codes": {
"label": "Tampilkan kode waktu",
"tooltip": "Tampilkan kode waktu di samping lirik"
}
},
"name": "Lirik yang Disinkronkan",
"refetch-btn": {
"fetching": "Mengambil...",
"normal": "Ambil ulang lirik"
},
"warnings": {
"duration-mismatch": "⚠️ - Liriknya mungkin tidak sinkron karena ketidakcocokan durasi.",
"inexact": "⚠️ - Lirik lagu ini mungkin tidak tepat",
"instrumental": "⚠️ - Ini adalah lagu instrumental"
}
},
"taskbar-mediacontrol": { "taskbar-mediacontrol": {
"description": "Kendalikan pemutaran dari bilah alat Windows", "description": "Kendalikan pemutaran dari bilah alat Windows",
"name": "Pengendali Media di Bilah Alat" "name": "Pengendali Media di Bilah Alat"

View File

@ -158,6 +158,14 @@
}, },
"remove-upgrade-button": "Fjarlægja uppgræðartakkan", "remove-upgrade-button": "Fjarlægja uppgræðartakkan",
"theme": { "theme": {
"dialog": {
"button": {
"cancel": "Hætta við",
"remove": "Fjarlægja"
},
"remove-theme": "Ertu viss um að þú viljir fjarlægja þetta sérsniðna þema?",
"remove-theme-message": "Þetta mun fjarlægja sérsniðna þema"
},
"label": "Þema", "label": "Þema",
"submenu": { "submenu": {
"import-css-file": "Flytja inn sérsniðna CSS skrá", "import-css-file": "Flytja inn sérsniðna CSS skrá",
@ -199,6 +207,10 @@
} }
}, },
"plugins": { "plugins": {
"ad-speedup": {
"description": "Ef auglýsing spilar slökknar hún á hljóðinu og stillir spilunarhraðann á 16x",
"name": "Auglýsingahraða"
},
"adblocker": { "adblocker": {
"description": "Lokaðu fyrir allar auglýsingar og rakningar úr kassanum", "description": "Lokaðu fyrir allar auglýsingar og rakningar úr kassanum",
"menu": { "menu": {
@ -267,6 +279,49 @@
}, },
"name": "Umhverfishamur" "name": "Umhverfishamur"
}, },
"api-server": {
"description": "Bætir API netþjóni til að stjórna spilaranum",
"dialog": {
"request": {
"buttons": {
"allow": "Leyfa",
"deny": "Óleyfa"
},
"message": "Leyfa {{ID}} ({{origin}}) að aðganga API-ið?",
"title": "API heimildarbeiðni"
}
},
"menu": {
"auth-strategy": {
"label": "Heimildarstefna",
"submenu": {
"auth-at-first": {
"label": "Heimila á fyrst beiðni"
},
"none": {
"label": "Nei heimild"
}
}
},
"hostname": {
"label": "Hýsitölvunafn"
},
"port": {
"label": "Tengi"
}
},
"name": "API-Netþjónn [Beta]",
"prompt": {
"hostname": {
"label": "Sláðu inn hýsitölvunafnið (eins og 0.0.0.0) fyrir API-netþjónninn:",
"title": "Hýsitölvunafn"
},
"port": {
"label": "Sláðu inn tengið fyrir API-netþjónninn:",
"title": "Tengi"
}
}
},
"audio-compressor": { "audio-compressor": {
"description": "Notaðu þjöppun á hljóð (lækkar hljóðstyrk háværustu hluta merkis og hækkar hljóðstyrk í mýkstu hlutunum)", "description": "Notaðu þjöppun á hljóð (lækkar hljóðstyrk háværustu hluta merkis og hækkar hljóðstyrk í mýkstu hlutunum)",
"name": "Hljóðþjöppu" "name": "Hljóðþjöppu"
@ -402,6 +457,21 @@
"description": "Niðurhalar MP3 / upprunahljóði beint úr viðmótinu", "description": "Niðurhalar MP3 / upprunahljóði beint úr viðmótinu",
"menu": { "menu": {
"choose-download-folder": "Veldu niðurhalsmöppu", "choose-download-folder": "Veldu niðurhalsmöppu",
"download-finish-settings": {
"label": "Sækja þegar lokið",
"prompt": {
"last-percent": "Eftir x sekúndur",
"last-seconds": "Síðustu x sekúndur",
"title": "Stilla hvenær á að hlaða niður"
},
"submenu": {
"advanced": "Ítarlegri",
"enabled": "Virkt",
"mode": "Tímastilling",
"percent": "Hlutfall",
"seconds": "Sekúndur"
}
},
"download-playlist": "Sækja spilunarlista", "download-playlist": "Sækja spilunarlista",
"presets": "Forstillingar", "presets": "Forstillingar",
"skip-existing": "Slepptu núverandi skrám" "skip-existing": "Slepptu núverandi skrám"
@ -641,6 +711,37 @@
"description": "Sleppur sjálfkrafa hlutum sem ekki eru tónlist, eins og inngangur/lok eða hlutar af tónlistarmyndböndum þar sem lag er ekki að spila", "description": "Sleppur sjálfkrafa hlutum sem ekki eru tónlist, eins og inngangur/lok eða hlutar af tónlistarmyndböndum þar sem lag er ekki að spila",
"name": "Styrktarblokk" "name": "Styrktarblokk"
}, },
"synced-lyrics": {
"description": "Veitir samstillta texta við lög, með því að nota veitur eins og LRClib.",
"errors": {
"not-found": "⚠️ - Enginn texti fannst við þetta lag."
},
"menu": {
"line-effect": {
"label": "Línuafleiðing",
"submenu": {
"focus": {
"label": "Brennidepill",
"tooltip": "Gerðu aðeins núverandi línu hvíta"
},
"offset": {
"label": "Fararbyrjun"
},
"scale": {
"label": "Skali",
"tooltip": "Skala núverandi línu"
}
}
},
"show-time-codes": {
"label": "Sýna tímikóðar"
}
},
"name": "Samstilltur texti",
"warnings": {
"instrumental": "⚠️ - Þetta er hljóðfærilegt lag"
}
},
"taskbar-mediacontrol": { "taskbar-mediacontrol": {
"description": "Stjórnaðu spilun frá Windows verkefnastikunni þinni", "description": "Stjórnaðu spilun frá Windows verkefnastikunni þinni",
"name": "Miðlunarstýringarverkefnastikunnar" "name": "Miðlunarstýringarverkefnastikunnar"

View File

@ -207,6 +207,10 @@
} }
}, },
"plugins": { "plugins": {
"ad-speedup": {
"description": "Se viene riprodotto un annuncio, l'audio viene disattivato e viene impostata la velocità di riproduzione su 16x",
"name": "Accelerazione ad"
},
"adblocker": { "adblocker": {
"description": "Blocca tutti gli annunci e i tracker", "description": "Blocca tutti gli annunci e i tracker",
"menu": { "menu": {
@ -410,6 +414,21 @@
"description": "Download MP3 / sorgenti audio direttamente dall'interfaccia", "description": "Download MP3 / sorgenti audio direttamente dall'interfaccia",
"menu": { "menu": {
"choose-download-folder": "Scegli cartella download", "choose-download-folder": "Scegli cartella download",
"download-finish-settings": {
"label": "Scarica al termine",
"prompt": {
"last-percent": "Dopo x percento",
"last-seconds": "Ultimi x secondi",
"title": "Configura quando scaricare"
},
"submenu": {
"advanced": "Avanzato",
"enabled": "Abilitato",
"mode": "Modalità tempo",
"percent": "Percentuale",
"seconds": "Secondi"
}
},
"download-playlist": "Scarica la playlist", "download-playlist": "Scarica la playlist",
"presets": "Preimpostazioni", "presets": "Preimpostazioni",
"skip-existing": "Salta i file esistenti" "skip-existing": "Salta i file esistenti"
@ -649,6 +668,59 @@
"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", "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": "Blocco sponsor" "name": "Blocco sponsor"
}, },
"synced-lyrics": {
"description": "Fornisce testi sincronizzati alle canzoni, utilizzando provider come LRClib.",
"errors": {
"fetch": "⚠️ - Si è verificato un errore nel recuperare il testo. Per favore riprova più tardi.",
"not-found": "⚠️ - Nessun testo trovato per questa canzone."
},
"menu": {
"default-text-string": {
"label": "Carattere predefinito tra i testi",
"tooltip": "Scegliere il carattere predefinito da utilizzare per l'intervallo tra i testi"
},
"line-effect": {
"label": "Effetto linea",
"submenu": {
"focus": {
"label": "Focus",
"tooltip": "Rendi bianca solo la riga corrente"
},
"offset": {
"label": "Offset",
"tooltip": "Offset a destra della riga corrente"
},
"scale": {
"label": "Ingrandimento",
"tooltip": "Ingrandisci la linea corrente"
}
},
"tooltip": "Scegli l'effetto da applicare alla linea corrente"
},
"precise-timing": {
"label": "Rendi i testi perfettamente sincronizzati",
"tooltip": "Calcola al millisecondo la visualizzazione della riga successiva (può avere un piccolo impatto sulle prestazioni)"
},
"show-lyrics-even-if-inexact": {
"label": "Mostra le lyric anche se incorrette",
"tooltip": "Se il brano non viene trovato, il plugin riprova con un'altra query di ricerca.\nIl risultato del secondo tentativo potrebbe non essere esatto."
},
"show-time-codes": {
"label": "Mostra time code",
"tooltip": "Mostra i codici temporali accanto ai testi"
}
},
"name": "Testi sincronizzati",
"refetch-btn": {
"fetching": "Sto recuperando...",
"normal": "Recupera i testi"
},
"warnings": {
"duration-mismatch": "⚠️ - I testi potrebbero non essere sincronizzati a causa di una mancata corrispondenza della durata.",
"inexact": "⚠️ - Il testo di questa canzone potrebbe essere inesatto",
"instrumental": "⚠️ - Questo è un brano strumentale"
}
},
"taskbar-mediacontrol": { "taskbar-mediacontrol": {
"description": "Controlla riproduzione dalla taskbar di Windows", "description": "Controlla riproduzione dalla taskbar di Windows",
"name": "Controlli multimediali sulla taskbar" "name": "Controlli multimediali sulla taskbar"

View File

@ -65,7 +65,7 @@
}, },
"detail": "ご不便をおかけして申し訳ございません! 何をするか選んでください:", "detail": "ご不便をおかけして申し訳ございません! 何をするか選んでください:",
"message": "アプリケーションは応答していません", "message": "アプリケーションは応答していません",
"title": "ウィンドウが応答しません" "title": "ウィンドウが応答していません"
}, },
"update-available": { "update-available": {
"buttons": { "buttons": {
@ -207,6 +207,10 @@
} }
}, },
"plugins": { "plugins": {
"ad-speedup": {
"description": "広告が再生されると、自動的にミュートされ、再生速度が16倍に設定されます",
"name": "広告のスピードを上げる"
},
"adblocker": { "adblocker": {
"description": "すべての広告とトラッカーをブロックj", "description": "すべての広告とトラッカーをブロックj",
"menu": { "menu": {
@ -410,6 +414,21 @@
"description": "UIから直にMP3・ソースオーディオをダウンロードします", "description": "UIから直にMP3・ソースオーディオをダウンロードします",
"menu": { "menu": {
"choose-download-folder": "ダウンロードフォルダ", "choose-download-folder": "ダウンロードフォルダ",
"download-finish-settings": {
"label": "完了時にダウンロード",
"prompt": {
"last-percent": "x パーセント後",
"last-seconds": "最後の x 秒",
"title": "保存するタイミング"
},
"submenu": {
"advanced": "高度な設定",
"enabled": "有効",
"mode": "時間モード",
"percent": "パーセント",
"seconds": "秒"
}
},
"download-playlist": "プレイリストをダウンロード", "download-playlist": "プレイリストをダウンロード",
"presets": "プリセット", "presets": "プリセット",
"skip-existing": "存在するファイルをスキップ" "skip-existing": "存在するファイルをスキップ"
@ -649,6 +668,59 @@
"description": "イントロ/アウトロなどの音楽以外の部分や、曲が再生されていないミュージック ビデオの部分を自動的にスキップします", "description": "イントロ/アウトロなどの音楽以外の部分や、曲が再生されていないミュージック ビデオの部分を自動的にスキップします",
"name": "SponsorBlock" "name": "SponsorBlock"
}, },
"synced-lyrics": {
"description": "LRClibのようなプロバイダを使って、楽曲に同期した歌詞を使用する。",
"errors": {
"fetch": "⚠️ - 歌詞の取得中にエラーが発生しました。 後でもう一度お試しください。",
"not-found": "⚠️ - この曲の歌詞は見つかりませんでした。"
},
"menu": {
"default-text-string": {
"label": "デフォルトの歌詞間の文字",
"tooltip": "歌詞と歌詞の間に使用するデフォルトの文字を選択してください"
},
"line-effect": {
"label": "歌詞表示のエフェクト",
"submenu": {
"focus": {
"label": "フォーカス",
"tooltip": "現在の行だけを白くする"
},
"offset": {
"label": "オフセット",
"tooltip": "オフセットを現在の行の右側にする"
},
"scale": {
"label": "サイズ",
"tooltip": "現在の行のサイズ変更をする"
}
},
"tooltip": "現在の行に適用するエフェクトを選択"
},
"precise-timing": {
"label": "歌詞を完璧に同期させる",
"tooltip": "次の行の表示をミリ秒単位で計算する(パフォーマンスに若干の影響を与える可能性があります)"
},
"show-lyrics-even-if-inexact": {
"label": "歌詞が不正確でも表示する",
"tooltip": "曲が見つからなかった場合、プラグインは別の検索クエリで再試行します。\nただし、再試行の結果は正確でない可能性があります。"
},
"show-time-codes": {
"label": "タイムコードを表示",
"tooltip": "歌詞の横にタイムコードを表示"
}
},
"name": "歌詞を同期",
"refetch-btn": {
"fetching": "取得中...",
"normal": "歌詞を再取得"
},
"warnings": {
"duration-mismatch": "⚠️ - タイミングが合わないため、歌詞が同期されていない可能性があります。",
"inexact": "⚠️ - この曲の歌詞は正確ではないかもしれません",
"instrumental": "⚠️ - これは演奏のみの曲です"
}
},
"taskbar-mediacontrol": { "taskbar-mediacontrol": {
"description": "Windowsタスクバーから再生をコントロール", "description": "Windowsタスクバーから再生をコントロール",
"name": "タスクバーメディアコントロール" "name": "タスクバーメディアコントロール"

View File

@ -207,6 +207,10 @@
} }
}, },
"plugins": { "plugins": {
"ad-speedup": {
"description": "광고가 재생될 때, 오디오가 음소거되고 재생 속도가 16배로 설정됩니다",
"name": "광고 배속"
},
"adblocker": { "adblocker": {
"description": "모든 광고와 트래커를 즉시 차단합니다", "description": "모든 광고와 트래커를 즉시 차단합니다",
"menu": { "menu": {
@ -275,6 +279,49 @@
}, },
"name": "앰비언트 모드" "name": "앰비언트 모드"
}, },
"api-server": {
"description": "플레이어를 제어하기 위한 API 서버를 추가합니다",
"dialog": {
"request": {
"buttons": {
"allow": "허용",
"deny": "거부"
},
"message": "{{ID}} ({{origin}})이(가) API에 액세스하도록 허용하시겠습니까?",
"title": "API 권한 요청"
}
},
"menu": {
"auth-strategy": {
"label": "인증 정책",
"submenu": {
"auth-at-first": {
"label": "첫 번째 요청 시 인증"
},
"none": {
"label": "인증 없음"
}
}
},
"hostname": {
"label": "호스트 명"
},
"port": {
"label": "포트"
}
},
"name": "API 서버 [베타]",
"prompt": {
"hostname": {
"label": "API 서버가 사용할 호스트 명(예: 0.0.0.0)을 입력하세요:",
"title": "호스트 명"
},
"port": {
"label": "API 서버가 사용할 포트를 입력하세요:",
"title": "포트"
}
}
},
"audio-compressor": { "audio-compressor": {
"description": "오디오에 컴프레서를 적용합니다 (신호에서 가장 시끄러운 부분의 음량을 낮추고 가장 조용한 부분의 음량을 높임)", "description": "오디오에 컴프레서를 적용합니다 (신호에서 가장 시끄러운 부분의 음량을 낮추고 가장 조용한 부분의 음량을 높임)",
"name": "오디오 컴프레서" "name": "오디오 컴프레서"
@ -664,6 +711,59 @@
"description": "인트로/아웃트로와 같은 음악이 아닌 부분이나, 노래가 재생되지 않는 뮤직 비디오의 일부를 자동으로 건너뜁니다", "description": "인트로/아웃트로와 같은 음악이 아닌 부분이나, 노래가 재생되지 않는 뮤직 비디오의 일부를 자동으로 건너뜁니다",
"name": "SponsorBlock" "name": "SponsorBlock"
}, },
"synced-lyrics": {
"description": "LRClib등의 가사 제공자에서 싱크 가사를 불러옵니다.",
"errors": {
"fetch": "⚠️ - 가사를 불러오는 동안 오류가 발생했습니다. 나중에 다시 시도해 주세요.",
"not-found": "⚠️ - 이 노래의 가사를 찾을 수 없습니다."
},
"menu": {
"default-text-string": {
"label": "가사 사이에 표시할 문자",
"tooltip": "가사 사이의 빈 공간에 사용할 문자를 선택합니다"
},
"line-effect": {
"label": "줄 표시 효과",
"submenu": {
"focus": {
"label": "포커스",
"tooltip": "현재 줄만 하얀색으로 표시"
},
"offset": {
"label": "오프셋",
"tooltip": "현재 줄의 오른쪽에 오프셋 적용"
},
"scale": {
"label": "스케일",
"tooltip": "현재 줄에 스케일 적용"
}
},
"tooltip": "현재 줄에 적용할 효과를 선택합니다"
},
"precise-timing": {
"label": "가사를 최대한 정교하게 동기화",
"tooltip": "다음 줄의 표시를 밀리초 단위로 계산합니다 (성능에 약간의 영향을 미칠 수 있음)"
},
"show-lyrics-even-if-inexact": {
"label": "가사가 정확하지 않더라도 표시",
"tooltip": "노래를 찾을 수 없는 경우, 플러그인이 다른 검색어로 다시 검색합니다.\n두번째 검색 결과는 정확하지 않을 수 있습니다."
},
"show-time-codes": {
"label": "시간 코드 표시",
"tooltip": "가사 옆에 시간 코드 표시"
}
},
"name": "싱크 가사",
"refetch-btn": {
"fetching": "가져오는 중...",
"normal": "가사 다시 가져오기"
},
"warnings": {
"duration-mismatch": "⚠️ - 곡 길이 불일치로 인해 가사가 일치하지 않을 수 있습니다.",
"inexact": "⚠️ - 이 노래의 가사는 정확하지 않을 수 있습니다",
"instrumental": "⚠️ - 연주곡입니다"
}
},
"taskbar-mediacontrol": { "taskbar-mediacontrol": {
"description": "Windows 작업 표시줄에서 재생을 제어하세요", "description": "Windows 작업 표시줄에서 재생을 제어하세요",
"name": "작업표시줄 미디어 컨트롤" "name": "작업표시줄 미디어 컨트롤"

View File

@ -5,6 +5,7 @@
"execute-failed": "Pelaksaan plugin gagal {{pluginName}}::{{contextName}}", "execute-failed": "Pelaksaan plugin gagal {{pluginName}}::{{contextName}}",
"executed-at-ms": "Plugin {{pluginName}}::{{contextName}} dilaksanakan pada {{ms}}ms", "executed-at-ms": "Plugin {{pluginName}}::{{contextName}} dilaksanakan pada {{ms}}ms",
"initialize-failed": "Gagal untuk memulakan plugin \"{{pluginName}}\"", "initialize-failed": "Gagal untuk memulakan plugin \"{{pluginName}}\"",
"load-all": "Memuatkan semua plugin",
"loaded": "Plugin \"{{pluginName}}\" dimuatkan", "loaded": "Plugin \"{{pluginName}}\" dimuatkan",
"unload-failed": "Gagal untuk memunggah plugin \"{{pluginName}}\"", "unload-failed": "Gagal untuk memunggah plugin \"{{pluginName}}\"",
"unloaded": "Plugin \"{{pluginName}}\" dipunggahkan" "unloaded": "Plugin \"{{pluginName}}\" dipunggahkan"
@ -39,15 +40,22 @@
"detail": "Menu telah disembunyikan, guna 'Alt' untuk menunjukkannya (atau 'Escape' jika menggunakan In-App Menu)" "detail": "Menu telah disembunyikan, guna 'Alt' untuk menunjukkannya (atau 'Escape' jika menggunakan In-App Menu)"
}, },
"need-to-restart": { "need-to-restart": {
"buttons": {
"later": "Nanti",
"restart-now": "Restart Sekarang"
},
"message": "\"{{pluginName}}\" perlu dimulakan semula", "message": "\"{{pluginName}}\" perlu dimulakan semula",
"title": "Mulakan Semula Diperlukan" "title": "Mulakan Semula Diperlukan"
}, },
"unresponsive": { "unresponsive": {
"buttons": { "buttons": {
"quit": "Berhenti",
"relaunch": "Lancar Semula", "relaunch": "Lancar Semula",
"wait": "Tunggu" "wait": "Tunggu"
}, },
"detail": "Kami memohon maaf atas kesulitan! sila pilih apa yang perlu dilakukan:" "detail": "Kami memohon maaf atas kesulitan! sila pilih apa yang perlu dilakukan:",
"message": "Aplikasi Tidak Responsif",
"title": "Window Tidak Responsif"
}, },
"update-available": { "update-available": {
"buttons": { "buttons": {
@ -58,16 +66,137 @@
} }
}, },
"menu": { "menu": {
"about": "Mengenai",
"navigation": { "navigation": {
"label": "Navigasi", "label": "Navigasi",
"submenu": { "submenu": {
"copy-current-url": "Salin URL semasa", "copy-current-url": "Salin URL semasa",
"go-back": "Pulang", "go-back": "Belakang",
"go-forward": "Depan",
"quit": "Keluar" "quit": "Keluar"
} }
}, },
"options": { "options": {
"label": "Pilihan" "label": "Tetapan",
"submenu": {
"advanced-options": {
"label": "Tetapan Lanjutan",
"submenu": {
"set-proxy": {
"prompt": {
"placeholder": "Contoh: SOCKS5://127.0.0.1:9999",
"title": "Set proksi"
}
}
}
},
"always-on-top": "Sentiasa di atas",
"auto-update": "Kemas Kini Automatik",
"hide-menu": {
"dialog": {
"message": "Menu akan disembunyikan pada pelancaran seterusnya, gunakan [Alt] untuk menunjukkannya (atau backtick [`] jika menggunakan dalam aplikasi-menu)"
}
},
"language": {
"dialog": {
"message": "Bahasa akan ditukar selepas dimulakan semula",
"title": "Bahasa Berubah"
},
"label": "Bahasa",
"submenu": {
"to-help-translate": "Ingin membantu menterjemah? Klik di sini"
}
},
"resume-on-start": "Mulakan semula lagu terakhir apabila aplikasi dimulakan",
"start-at-login": "Mulakan semasa log masuk",
"starting-page": {
"label": "Halaman Permulaan"
},
"tray": {
"submenu": {
"play-pause-on-click": "Main / Hentikan pada klik"
}
},
"visual-tweaks": {
"label": "Pembaikan Visual",
"submenu": {
"like-buttons": {
"default": "Lalai",
"hide": "Sembunyi"
},
"theme": {
"dialog": {
"button": {
"cancel": "Batalkan",
"remove": "Padam"
}
},
"label": "Tema"
}
}
}
}
}
},
"tray": {
"next": "Seterusnya",
"play-pause": "Main / Jeda",
"previous": "Sebelumnya",
"quit": "Keluar",
"restart": "Mulakan Semula Aplikasi"
}
},
"plugins": {
"ambient-mode": {
"menu": {
"quality": {
"label": "Kualiti"
},
"size": {
"label": "Saiz"
}
}
},
"captions-selector": {
"prompt": {
"selector": {
"title": "Pilih bahasa kapsyen"
}
}
},
"synced-lyrics": {
"menu": {
"show-lyrics-even-if-inexact": {
"label": "Tunjukkan lirik walaupun tidak tepat",
"tooltip": "Jika lagu tidak ditemui, plugin cuba lagi dengan pertanyaan carian yang berbeza. \nHasil dari percubaan kedua mungkin tidak tepat."
},
"show-time-codes": {
"tooltip": "Tunjukkan kod masa di sebelah lirik"
}
}
},
"taskbar-mediacontrol": {
"description": "Kawalan main balik dari bar tugas Windows anda",
"name": "Kawalan Media Bar Tugas"
},
"video-toggle": {
"menu": {
"align": {
"submenu": {
"left": "Kiri",
"middle": "Tengah",
"right": "Kanan"
}
},
"force-hide": "Alih Keluar Tab Video",
"mode": {
"submenu": {
"disabled": "Tidak Aktif"
}
}
},
"templates": {
"button": "Lagu"
} }
} }
} }

View File

@ -158,6 +158,14 @@
}, },
"remove-upgrade-button": "Upgrade-knop verwijderen", "remove-upgrade-button": "Upgrade-knop verwijderen",
"theme": { "theme": {
"dialog": {
"button": {
"cancel": "Annuleren",
"remove": "Verwijderen"
},
"remove-theme": "Weet je zeker dat je het aangepaste thema wilt verwijderen?",
"remove-theme-message": "Dit verwijderd het aangepaste thema"
},
"label": "Thema", "label": "Thema",
"submenu": { "submenu": {
"import-css-file": "Aangepast CSS-bestand importeren", "import-css-file": "Aangepast CSS-bestand importeren",
@ -199,6 +207,10 @@
} }
}, },
"plugins": { "plugins": {
"ad-speedup": {
"description": "Wanneer een advertentie afspeelt, dempt het geluid en versnelt de playback naar 16x",
"name": "Snellere advertenties"
},
"adblocker": { "adblocker": {
"description": "Blokkeer alle advertenties en tracking vanuit de doos", "description": "Blokkeer alle advertenties en tracking vanuit de doos",
"menu": { "menu": {
@ -402,6 +414,21 @@
"description": "Download MP3 / bron-audio rechtstreeks vanuit de interface", "description": "Download MP3 / bron-audio rechtstreeks vanuit de interface",
"menu": { "menu": {
"choose-download-folder": "Kies de downloadmap", "choose-download-folder": "Kies de downloadmap",
"download-finish-settings": {
"label": "Downloaden bij voltooiing",
"prompt": {
"last-percent": "Na x procent",
"last-seconds": "Laatste x seconden",
"title": "Configureren wanneer te downloaden"
},
"submenu": {
"advanced": "Geavanceerd",
"enabled": "Ingeschakeld",
"mode": "Tijd-modus",
"percent": "Procent",
"seconds": "Seconden"
}
},
"download-playlist": "Afspeellijst downloaden", "download-playlist": "Afspeellijst downloaden",
"presets": "Voorinstellingen", "presets": "Voorinstellingen",
"skip-existing": "Bestaande bestanden overslaan" "skip-existing": "Bestaande bestanden overslaan"
@ -523,8 +550,20 @@
"save-window-size": "Sla schermgrootte op" "save-window-size": "Sla schermgrootte op"
} }
}, },
"video-toggle": {
"menu": {
"mode": {
"submenu": {
"disabled": "Uitgeschakeld"
}
}
}
},
"visualizer": { "visualizer": {
"description": "Voeg een visuele equalizer toe", "description": "Voeg een visuele equalizer toe",
"menu": {
"visualizer-type": "Type visualisator"
},
"name": "Visualisator" "name": "Visualisator"
} }
} }

View File

@ -207,6 +207,10 @@
} }
}, },
"plugins": { "plugins": {
"ad-speedup": {
"description": "Wycisza reklamę i przyśpiesza do 16x",
"name": "Przyśpieszacz reklam"
},
"adblocker": { "adblocker": {
"description": "Blokuj wszystkie reklamy i śledzenie", "description": "Blokuj wszystkie reklamy i śledzenie",
"menu": { "menu": {
@ -410,6 +414,21 @@
"description": "Pobiera MP3/ źródło audio bezpośrednio z interfejsu", "description": "Pobiera MP3/ źródło audio bezpośrednio z interfejsu",
"menu": { "menu": {
"choose-download-folder": "Wybierz folder pobierania", "choose-download-folder": "Wybierz folder pobierania",
"download-finish-settings": {
"label": "Pobierz po zakończeniu",
"prompt": {
"last-percent": "Po x procentach",
"last-seconds": "Ostatnie x sekund",
"title": "Konfiguruj, kiedy pobierać"
},
"submenu": {
"advanced": "Zaawansowane",
"enabled": "Włączone",
"mode": "Tryb czasowy",
"percent": "Procenty",
"seconds": "Sekundy"
}
},
"download-playlist": "Pobierz playlistę", "download-playlist": "Pobierz playlistę",
"presets": "Predefiniowane ustawienia", "presets": "Predefiniowane ustawienia",
"skip-existing": "Pomiń istniejące pliki" "skip-existing": "Pomiń istniejące pliki"
@ -649,6 +668,59 @@
"description": "Automatycznie pomija fragmenty niebędące muzyką, takie jak wstęp/zakończenie lub fragmenty teledysków, w których utwór nie jest odtwarzany", "description": "Automatycznie pomija fragmenty niebędące muzyką, takie jak wstęp/zakończenie lub fragmenty teledysków, w których utwór nie jest odtwarzany",
"name": "Pomiń nieistotne fragmenty" "name": "Pomiń nieistotne fragmenty"
}, },
"synced-lyrics": {
"description": "Dodaje zsynchronizowane napisy do utworów używając między innymi LRClib.",
"errors": {
"fetch": "⚠️ - Wystąpił błąd podczas pobierania tekstu utworu. Spróbuj ponownie później.",
"not-found": "⚠️ - Nie znaleziono napisów dla tego utworu."
},
"menu": {
"default-text-string": {
"label": "Standardowy znak luki",
"tooltip": "Wybierz domyślny znak, który ma być wyświetlany jako pauza między słowami"
},
"line-effect": {
"label": "Efekty linijki",
"submenu": {
"focus": {
"label": "Fokus",
"tooltip": "Spraw, aby tylko obecna linijka była biała"
},
"offset": {
"label": "Przesunięcie",
"tooltip": "Przesuń w prawo obecną linijkę"
},
"scale": {
"label": "Skala",
"tooltip": "Zmień skalę aktualnej linijki"
}
},
"tooltip": "Wybierz efekt, by zastosować go do aktualnej linijki"
},
"precise-timing": {
"label": "Zsynchronizuj tekst utworu do perfekcji",
"tooltip": "Wylicz czas wyświetlania następnej linijki co do milisekundy (może mieć mały wpływ na wydajność systemu)"
},
"show-lyrics-even-if-inexact": {
"label": "Pokaż teksty, mimo niezgodności",
"tooltip": "Jeżeli nie znaleziono tekstu piosenki z bazy danych, wtyczka spróbuje ponownie przez wyszukanie przybliżonej frazy.\nNależy jednak pamiętać, że następne próby mogą nie być trafne co do oryginału."
},
"show-time-codes": {
"label": "Pokaż znaczniki czasu",
"tooltip": "Pokaż znaczniki czasu obok linijek"
}
},
"name": "Napisy zsynchronizowane",
"refetch-btn": {
"fetching": "Pobieranie napisów...",
"normal": "Odśwież napisy"
},
"warnings": {
"duration-mismatch": "⚠️ - Napisy mogą nie być zsynchronizowane z powodu różnicy w czasie trwania utworu.",
"inexact": "⚠️ - Tekst utworu może się różnić od oryginału",
"instrumental": "⚠️ - To jest utwór instrumentalny"
}
},
"taskbar-mediacontrol": { "taskbar-mediacontrol": {
"description": "Steruj odtwarzaniem z paska zadań systemu Windows", "description": "Steruj odtwarzaniem z paska zadań systemu Windows",
"name": "Kontroler odtwarzania z paska zadań" "name": "Kontroler odtwarzania z paska zadań"

View File

@ -0,0 +1,813 @@
{
"common": {
"console": {
"plugins": {
"execute-failed": "Falha ao executar plugin {{pluginName}}::{{contextName}}",
"executed-at-ms": "Plugin {{pluginName}}::{{contextName}} executado em {{ms}} ms",
"initialize-failed": "Falha ao inicializar o plugin \"{{pluginName}}\"",
"load-all": "Carregando todos os plugins",
"load-failed": "Falha ao carregar o plugin \"{{pluginName}}\"",
"loaded": "Plugin \"{{pluginName}}\" carregado",
"unload-failed": "Falha ao descarregar o plugin \"{{pluginName}}\"",
"unloaded": "Plugin \"{{pluginName}}\" descarregado"
}
}
},
"language": {
"code": "pt-BR",
"local-name": "Português (Brasil)",
"name": "Portuguese (Brazil)"
},
"main": {
"console": {
"did-finish-load": {
"dev-tools": "Carregamento concluído. DevTools aberto"
},
"i18n": {
"loaded": "i18n carregado"
},
"second-instance": {
"receive-command": "Comando recebido pelo protocolo: \"{{command}}\""
},
"theme": {
"css-file-not-found": "Arquivo CSS \"{{cssFile}}\" não existe, ignorando"
},
"unresponsive": {
"details": "Erro de falta de resposta!\n{{error}}"
},
"when-ready": {
"clearing-cache-after-20s": "Limpando cache do aplicativo"
},
"window": {
"tried-to-render-offscreen": "A janela tentou renderizar fora da tela, tamanho da janela={{windowSize}}, tamanho da tela={{displaySize}}, posição={{position}}"
}
},
"dialog": {
"hide-menu-enabled": {
"detail": "O menu está oculto, use 'Alt' para mostrá-lo (ou 'Esc' ao usar o menu dentro 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}}\" requer uma reinicialização para entrar em vigor",
"message": "\"{{pluginName}}\" precisa reiniciar",
"title": "Necessário reiniciar"
},
"unresponsive": {
"buttons": {
"quit": "Fechar",
"relaunch": "Reiniciar",
"wait": "Aguardar"
},
"detail": "Lamentamos o inconveniente! Por favor, escolha o que fazer:",
"message": "O aplicativo não está respondendo",
"title": "Janela não responde"
},
"update-available": {
"buttons": {
"disable": "Desativar atualizações",
"download": "Baixar",
"ok": "OK"
},
"detail": "Uma versão mais recente está disponível em {{downloadLink}}",
"message": "Nova versão disponível",
"title": "Atualização disponível"
}
},
"menu": {
"about": "Sobre",
"navigation": {
"label": "Navegação",
"submenu": {
"copy-current-url": "Copiar URL atual",
"go-back": "Voltar",
"go-forward": "Avançar",
"quit": "Sair",
"restart": "Reiniciar aplicativo"
}
},
"options": {
"label": "Opções",
"submenu": {
"advanced-options": {
"label": "Opções avançadas",
"submenu": {
"auto-reset-app-cache": "Limpar cache ao iniciar aplicativo",
"disable-hardware-acceleration": "Desativar aceleração de hardware",
"edit-config-json": "Editar config.json",
"override-user-agent": "Substituir User-Agent",
"restart-on-config-changes": "Reiniciar ao alterar configurações",
"set-proxy": {
"label": "Definir proxy",
"prompt": {
"label": "Digite o endereço do proxy: (deixe em branco para desativar)",
"placeholder": "Exemplo: SOCKS5://127.0.0.1:9999",
"title": "Definir proxy"
}
},
"toggle-dev-tools": "Alternar DevTools"
}
},
"always-on-top": "Sempre no topo",
"auto-update": "Atualização automática",
"hide-menu": {
"dialog": {
"message": "O menu ficará oculto na próxima inicialização, use [Alt] para exibi-lo (ou a tecla de crase [`] se estiver usando o menu do aplicativo)",
"title": "Ocultar menu ativado"
},
"label": "Ocultar menu"
},
"language": {
"dialog": {
"message": "O idioma será alterado depois de reiniciar",
"title": "Idioma alterado"
},
"label": "Idioma",
"submenu": {
"to-help-translate": "Quer ajudar a traduzir? Clique aqui"
}
},
"resume-on-start": "Continuar última música ao iniciar o aplicativo",
"single-instance-lock": "Bloqueio de instância única",
"start-at-login": "Iniciar com o sistema",
"starting-page": {
"label": "Página inicial",
"unset": "Limpar"
},
"tray": {
"label": "Área de Notificação",
"submenu": {
"disabled": "Desativado",
"enabled-and-hide-app": "Ativado e aplicativo oculto",
"enabled-and-show-app": "Ativado e mostrar aplicativo",
"play-pause-on-click": "Reproduzir/Pausar ao clicar"
}
},
"visual-tweaks": {
"label": "Ajustes visuais",
"submenu": {
"like-buttons": {
"default": "Padrão",
"force-show": "Forçar exibir",
"hide": "Ocultar",
"label": "Botões de 'Curtir'"
},
"remove-upgrade-button": "Remover botão de atualização",
"theme": {
"dialog": {
"button": {
"cancel": "Cancelar",
"remove": "Remover"
},
"remove-theme": "Deseja realmente remover o tema personalizado?",
"remove-theme-message": "Isto removerá o tema personalizado"
},
"label": "Tema",
"submenu": {
"import-css-file": "Importar arquivo CSS personalizado",
"no-theme": "Sem tema"
}
}
}
}
}
},
"plugins": {
"enabled": "Ativado",
"label": "Plugins",
"new": "NOVO"
},
"view": {
"label": "Visualização",
"submenu": {
"force-reload": "Forçar recarregar",
"reload": "Recarregar",
"reset-zoom": "Tamanho atual",
"toggle-fullscreen": "Alternar tela cheia",
"zoom-in": "Ampliar",
"zoom-out": "Reduzir"
}
}
},
"tray": {
"next": "Próximo",
"play-pause": "Reproduzir/Pausar",
"previous": "Anterior",
"quit": "Sair",
"restart": "Reiniciar aplicativo",
"show": "Mostrar janela",
"tooltip": {
"default": "YouTube Music",
"with-song-info": "YouTube Music: {{artist}} - {{title}}"
}
}
},
"plugins": {
"ad-speedup": {
"description": "Se um anúncio for reproduzido, ele silencia o áudio e define a velocidade de reprodução para 16x",
"name": "Acelerador de Anúncios"
},
"adblocker": {
"description": "Bloqueio de todos os anúncios e rastreamentos imediatamente",
"menu": {
"blocker": "Bloqueador"
},
"name": "Bloqueador de Anúncios"
},
"album-actions": {
"description": "Adiciona botões Remover Não curtir, Não curtir, Curtir e Remover Curtir para aplicar em todas as músicas em uma lista ou álbum",
"name": "Ações do álbum"
},
"album-color-theme": {
"description": "Aplica um tema dinâmico e efeitos visuais com base na paleta de cores do álbum",
"menu": {
"color-mix-ratio": {
"label": "Proporção de mistura de cores",
"submenu": {
"percent": "{{ratio}}%"
}
}
},
"name": "Tema da cor do álbum"
},
"ambient-mode": {
"description": "Aplica um efeito de iluminação projetando cores suaves do vídeo no fundo da tela",
"menu": {
"blur-amount": {
"label": "Quantidade de desfoque",
"submenu": {
"pixels": "{{blurAmount}} pixels"
}
},
"buffer": {
"label": "Buffer",
"submenu": {
"buffer": "{{buffer}}"
}
},
"opacity": {
"label": "Opacidade",
"submenu": {
"percent": "{{opacity}}%"
}
},
"quality": {
"label": "Qualidade",
"submenu": {
"pixels": "{{quality}} pixels"
}
},
"size": {
"label": "Tamanho",
"submenu": {
"percent": "{{size}}%"
}
},
"smoothness-transition": {
"label": "Transição suave",
"submenu": {
"during": "Durante {{interpolationTime}} s"
}
},
"use-fullscreen": {
"label": "Usando tela cheia"
}
},
"name": "Modo ambiente"
},
"api-server": {
"description": "Adiciona um servidor API para controlar o player",
"dialog": {
"request": {
"buttons": {
"allow": "Permitir",
"deny": "Negar"
},
"message": "Permitir que {{ID}} {{origin}} acesse o API?",
"title": "Pedido de autorização API"
}
},
"menu": {
"auth-strategy": {
"label": "Estratégia de autorização",
"submenu": {
"auth-at-first": {
"label": "Autorizar na primeira solicitação"
},
"none": {
"label": "Não autorizar"
}
}
},
"hostname": {
"label": "Nome do anfitrião"
},
"port": {
"label": "Porta"
}
},
"name": "Servidor API [Beta]",
"prompt": {
"hostname": {
"label": "Entre o nome do host (como 0.0.0.0) para o servidor API:",
"title": "Nome do anfitrião"
},
"port": {
"label": "Entre a porta do servidor API:",
"title": "Porta"
}
}
},
"audio-compressor": {
"description": "Aplicar compressão ao áudio (reduz o volume das partes mais altas e aumenta o volume das partes mais baixas)",
"name": "Compressor de áudio"
},
"blur-nav-bar": {
"description": "Torna a barra de navegação transparente e desfocada",
"name": "Desfocar barra de navegação"
},
"bypass-age-restrictions": {
"description": "Pular a verificação de idade do YouTube",
"name": "Ignorar restrições de idade"
},
"captions-selector": {
"description": "Seletor de legendas 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 atual da legenda: {{language}}",
"none": "Nenhum",
"title": "Selecionar idioma da legenda"
}
},
"templates": {
"title": "Abrir seletor de legendas"
}
},
"compact-sidebar": {
"description": "Sempre definir a barra lateral no modo compacto",
"name": "Barra lateral compacta"
},
"crossfade": {
"description": "Crossfade entre músicas",
"menu": {
"advanced": "Avançado"
},
"name": "Crossfade [Beta]",
"prompt": {
"options": {
"multi-input": {
"fade-in-duration": "Duração do fade (ms)",
"fade-out-duration": "Duração do fade out (ms)",
"fade-scaling": {
"label": "Escala do fade",
"linear": "Linear",
"logarithmic": "Logarítmico"
},
"seconds-before-end": "Crossfade N segundos antes do fim"
},
"title": "Opções de crossfade"
}
}
},
"disable-autoplay": {
"description": "Faz a música começar no modo \"pausado\"",
"menu": {
"apply-once": "Aplicar somente ao iniciar"
},
"name": "Desativar reprodução automática"
},
"discord": {
"backend": {
"already-connected": "Tentativa de conectar-se com conexão ativa",
"connected": "Conectado no 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 atividades",
"clear-activity-after-timeout": "Limpar atividades após tempo limite",
"connected": "Conectado",
"disconnected": "Desconectado",
"hide-duration-left": "Ocultar duração restante",
"hide-github-button": "Ocultar botão do GitHub",
"play-on-youtube-music": "Reproduzir no YouTube Music",
"set-inactivity-timeout": "Definir tempo limite de inatividade"
},
"name": "Rich Presence do Discord",
"prompt": {
"set-inactivity-timeout": {
"label": "Digite o tempo de inatividade em segundos:",
"title": "Definir tempo limite de inatividade"
}
}
},
"downloader": {
"backend": {
"dialog": {
"error": {
"buttons": {
"ok": "OK"
},
"message": "Ah! 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": "Convertendo: {{percent}}%",
"converting": "Convertendo…",
"done": "Concluído: {{filePath}}",
"download-info": "Baixando {{artist}} - {{title}} [{{videoId}}",
"download-progress": "Download: {{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 playlist…",
"loading": "Carregando…",
"playlist-has-only-one-song": "Playlist possui apenas um item, baixando diretamente",
"playlist-id-not-found": "Nenhum playlist ID encontrado",
"playlist-is-empty": "Playlist está vazia",
"playlist-is-mix-or-private": "Erro ao obter informações da playlist: verifique se não é uma playlist privada ou “”Mixada para você”\n\n{{error}}",
"preparing-file": "Preparando arquivo…",
"saving": "Salvando…",
"trying-to-get-playlist-id": "Tentando obter playlist ID: {{playlistId}}",
"video-id-not-found": "Vídeo não encontrado",
"writing-id3": "Salvando tags ID3…"
}
},
"description": "Faça download do MP3 / fonte de áudio diretamente da interface",
"menu": {
"choose-download-folder": "Escolha a pasta de download",
"download-finish-settings": {
"label": "Baixar ao finalizar",
"prompt": {
"last-percent": "Após x %",
"last-seconds": "Últimos x segundos",
"title": "Configurar quando baixar"
},
"submenu": {
"advanced": "Avançado",
"enabled": "Ativado",
"mode": "Modo de tempo",
"percent": "Porcento",
"seconds": "Segundos"
}
},
"download-playlist": "Baixar playlist",
"presets": "Predefinições",
"skip-existing": "Pular 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 para que seja mais fácil selecionar volumes mais baixos.",
"name": "Volume Exponencial"
},
"in-app-menu": {
"description": "Dá às barras de menu uma aparência elegante, escura ou com a cor do álbum",
"menu": {
"hide-dom-window-controls": "Ocultar controles da janela DOM"
},
"name": "Menu no aplicativo"
},
"lumiastream": {
"description": "Adiciona suporte ao 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": "Letras buscadas por Genius"
}
},
"music-together": {
"description": "Compartilhe uma playlist com outras pessoas. Quando o anfitrião toca uma música, todos os outros ouvirão",
"dialog": {
"enter-host": "Insira o ID do host"
},
"internal": {
"save": "Salvar",
"track-source": "Fonte da Faixa",
"unknown-user": "Usuário Desconhecido"
},
"menu": {
"click-to-copy-id": "Copiar ID do host",
"close": "Fechar Music Together",
"connected-users": "Usuários Conectados",
"disconnect": "Desconectar Music Together",
"empty-user": "Nenhum usuário conectado",
"host": "Anfitrião do Music Together",
"join": "Entrar no Music Together",
"permission": {
"all": "Permitir que os convidados controlem a lista de reprodução e o player",
"host-only": "Somente o host pode controlar a lista de reprodução e o player",
"playlist": "Permitir que os convidados controlem a lista de reprodução"
},
"set-permission": "Mudar Permissões de Controle",
"status": {
"disconnected": "Desconectado",
"guest": "Conectado como convidado",
"host": "Conectado como Anfitrião"
}
},
"name": "Music Together [Beta]",
"toast": {
"add-song-failed": "Falha ao adicionar música",
"closed": "Music Together fechado",
"disconnected": "Music Together desconectado",
"host-failed": "Falha ao hospedar o Music Together",
"id-copied": "ID do anfitrião copiado para a área de transferência",
"id-copy-failed": "Falha ao copiar o ID do anfitrião para a área de transferência",
"join-failed": "Falha ao ingressar no Music Together",
"joined": "Entrou no Music Together",
"permission-changed": "A permissão do Music Together foi alterada para \"{{permission}}\"",
"remove-song-failed": "Falha ao remover música",
"user-connected": "{{name}} juntou-se ao Music Together",
"user-disconnected": "{{name}} saiu do Music Together"
}
},
"navigation": {
"description": "Setas de navegação para avançar/retornar diretamente integradas na interface, como no seu navegador favorito",
"name": "Navegação"
},
"no-google-login": {
"description": "Remova 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 ao clicar na área de notificação"
}
},
"priority": "Prioridade da notificação",
"toast-style": "Estilo de alerta",
"unpause-notification": "Mostrar notificação ao despausar"
},
"name": "Notificações"
},
"picture-in-picture": {
"description": "Permite alternar 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 entre picture-in-picture",
"title": "Atalho do picture-in-picture"
}
},
"save-window-position": "Salvar posição da janela",
"save-window-size": "Salvar tamanho da janela",
"use-native-pip": "Usar 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 de teclas de seta locais",
"custom-volume-steps": "Definir etapas de volume personalizadas",
"global-shortcuts": "Teclas de atalho globais"
},
"name": "Volume preciso",
"prompt": {
"global-shortcuts": {
"keybind-options": {
"decrease": "Diminuir volume",
"increase": "Aumentar volume"
},
"label": "Selecione as teclas de atalho global do volume:",
"title": "Teclas de atalho global de volume"
},
"volume-steps": {
"label": "Escolha as etapas de aumento/diminuição do volume",
"title": "Fases de volume"
}
}
},
"quality-changer": {
"backend": {
"dialog": {
"quality-changer": {
"detail": "Qualidade atual: {{quality}}",
"message": "Escolher qualidade do vídeo:",
"title": "Escolher qualidade do vídeo"
}
}
},
"description": "Permite alterar a qualidade do vídeo com um botão na sobreposição de vídeo",
"name": "Alterador de qualidade do vídeo"
},
"scrobbler": {
"description": "Adicionar suporte para scrobbling (last.fm, Listenbrainz, etc.)",
"dialog": {
"lastfm": {
"auth-failed": {
"message": "Falha ao autenticar com Last.fm\nOcultar o pop-up até a próxima reinicialização.",
"title": "Falha na autenticação"
}
}
},
"menu": {
"lastfm": {
"api-settings": "Configurações da API do Last.fm"
},
"listenbrainz": {
"token": "Insira o token de usuário ListenBrainz"
},
"scrobble-other-media": "Scrobble outras mídias"
},
"name": "Scrobbler",
"prompt": {
"lastfm": {
"api-key": "Chave de API do Last.fm",
"api-secret": "Chave secreta da API do Last.fm"
},
"listenbrainz": {
"token": {
"label": "Insira seu token de usuário do ListenBrainz:",
"title": "ListenBrainz token"
}
}
}
},
"shortcuts": {
"description": "Permite definir teclas de atalho globais para reprodução (reproduzir/pausar/próximo/anterior) e desativar 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 chaves de multimé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 de teclado global"
}
}
},
"skip-disliked-songs": {
"description": "Ignora músicas marcadas com \"não gostei\"",
"name": "Pular músicas marcadas com \"não gostei\""
},
"skip-silences": {
"description": "Pular automaticamente seções de silêncio em músicas",
"name": "Pular silêncios"
},
"sponsorblock": {
"description": "Pula automaticamente partes não musicais, como introdução/finalização ou partes de videoclipes onde a música não está tocando",
"name": "SponsorBlock [Bloquear patrocínios]"
},
"synced-lyrics": {
"description": "Fornece letras sincronizadas para músicas, usando provedores como LRClib.",
"errors": {
"fetch": "⚠️ - Ocorreu um erro ao buscar a letra. Tente novamente mais tarde.",
"not-found": "⚠️ - Nenhuma letra encontrada para esta música."
},
"menu": {
"default-text-string": {
"label": "Caractere padrão entre letras",
"tooltip": "Escolha o caractere padrão a ser usado para o intervalo entre as letras"
},
"line-effect": {
"label": "Efeito de linha",
"submenu": {
"focus": {
"label": "Foco",
"tooltip": "Deixe apenas a linha atual branca"
},
"offset": {
"label": "Deslocar",
"tooltip": "Deslocamento à direita da linha atual"
},
"scale": {
"label": "Aumentar",
"tooltip": "Aumentar a linha atual"
}
},
"tooltip": "Escolha o efeito a ser aplicado à linha atual"
},
"precise-timing": {
"label": "Deixa as letras perfeitamente sincronizadas",
"tooltip": "Calcular até o milissegundo a exibição da próxima linha (pode ter um pequeno impacto no desempenho)"
},
"show-lyrics-even-if-inexact": {
"label": "Mostrar letras mesmo que não sejam exatas",
"tooltip": "Se a música não for encontrada, o plugin tenta novamente com uma consulta de pesquisa diferente.\nO resultado da segunda tentativa pode não ser exato."
},
"show-time-codes": {
"label": "Mostrar códigos de tempo",
"tooltip": "Mostrar os códigos de tempo ao lado das letras"
}
},
"name": "Letras sincronizadas",
"refetch-btn": {
"fetching": "Buscando...",
"normal": "Buscar letras novamente"
},
"warnings": {
"duration-mismatch": "⚠️ - A letra pode estar dessincronizada devido a uma incompatibilidade de duração.",
"inexact": "⚠️ - A letra desta música pode não ser exata",
"instrumental": "⚠️ - Esta é uma música instrumental"
}
},
"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": "TouchBar"
},
"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 é possível remover opcionalmente toda a aba de vídeo",
"menu": {
"align": {
"label": "Alinhamento",
"submenu": {
"left": "Esquerda",
"middle": "Meio",
"right": "Direita"
}
},
"force-hide": "Forçar remoção da aba de vídeo",
"mode": {
"label": "Modo",
"submenu": {
"custom": "Alternância personalizada",
"disabled": "Desativado",
"native": "Alternância nativa"
}
}
},
"name": "Alternar vídeo",
"templates": {
"button": "Música"
}
},
"visualizer": {
"description": "Adiciona um visualizador ao player",
"menu": {
"visualizer-type": "Tipo de visualizador"
},
"name": "Visualizador"
}
}
}

View File

@ -207,6 +207,10 @@
} }
}, },
"plugins": { "plugins": {
"ad-speedup": {
"description": "Se um anúncio for reproduzido, ele será silenciado o áudio e será definido a velocidade de reprodução para 16x",
"name": "Acelerar os anúncios"
},
"adblocker": { "adblocker": {
"description": "Bloquear todos os anúncios e rastreamento automaticamente", "description": "Bloquear todos os anúncios e rastreamento automaticamente",
"menu": { "menu": {
@ -222,9 +226,9 @@
"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",
"menu": { "menu": {
"color-mix-ratio": { "color-mix-ratio": {
"label": "Rácio de mistura das cores", "label": "Relação de mistura de cores",
"submenu": { "submenu": {
"percent": "Proporção" "percent": "{{ratio}}%"
} }
} }
}, },
@ -410,6 +414,21 @@
"description": "Baixa MP3 / fonte de áudio diretamente da interface", "description": "Baixa MP3 / fonte de áudio diretamente da interface",
"menu": { "menu": {
"choose-download-folder": "Escolha a pasta de download", "choose-download-folder": "Escolha a pasta de download",
"download-finish-settings": {
"label": "Baixar ao terminar",
"prompt": {
"last-percent": "Depois de x por cento",
"last-seconds": "Últimos x segundos",
"title": "Configurar quando baixar"
},
"submenu": {
"advanced": "Avançado",
"enabled": "Ativado",
"mode": "Modo de tempo",
"percent": "Porcentagem",
"seconds": "Segundos"
}
},
"download-playlist": "Baixar lista de reprodução", "download-playlist": "Baixar lista de reprodução",
"presets": "Predefinições", "presets": "Predefinições",
"skip-existing": "Ignorar arquivos existentes" "skip-existing": "Ignorar arquivos existentes"
@ -482,15 +501,15 @@
"add-song-failed": "Falha ao adicionar canção", "add-song-failed": "Falha ao adicionar canção",
"closed": "Música Juntos encerrado", "closed": "Música Juntos encerrado",
"disconnected": "Música Juntos foi desconectado", "disconnected": "Música Juntos foi desconectado",
"host-failed": "Falha ao hospedar o Música Juntos", "host-failed": "Falha ao hospedar o Music Together",
"id-copied": "ID de anfitrião copiado para a área de transferência", "id-copied": "ID de anfitrião copiado para a área de transferência",
"id-copy-failed": "Falha ao copiar o ID de anfitrião para a área de transferência", "id-copy-failed": "Falha ao copiar o ID de anfitrião para a área de transferência",
"join-failed": "Falha ao entrar em Música Juntos", "join-failed": "Falha ao entrar em Music Together",
"joined": "Entrou em Música Juntos", "joined": "Entrou em Music Together",
"permission-changed": "A permissão do Música Juntos foi alterada para \"{{permission}}\"", "permission-changed": "A permissão do Music Together foi alterada para \"{{permission}}\"",
"remove-song-failed": "Falha ao remover música", "remove-song-failed": "Falha ao remover música",
"user-connected": "{{name}} entrou em Música Juntos", "user-connected": "{{name}} entrou em Music Together",
"user-disconnected": "{{name}} saiu do Música Juntos" "user-disconnected": "{{name}} saiu do Music Together"
} }
}, },
"navigation": { "navigation": {
@ -649,6 +668,59 @@
"description": "Ignora automaticamente partes não musicais, como introdução/final ou partes de videoclipes onde a música não está tocando", "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)" "name": "SponsorBlock (bloqueador de patrocínios)"
}, },
"synced-lyrics": {
"description": "Fornece letras sincronizadas de músicas, utilizando fornecedores como o LRClib.",
"errors": {
"fetch": "⚠️ - Ocorreu um erro ao obter as letras da música. Por favor, tenta novamente mais tarde.",
"not-found": "⚠️ - Não foram encontradas letras para esta música."
},
"menu": {
"default-text-string": {
"label": "Caractere padrão entre as letras",
"tooltip": "Escolha o caractere padrão para usar no espaço entre as letras"
},
"line-effect": {
"label": "Efeito de linha",
"submenu": {
"focus": {
"label": "Foco",
"tooltip": "Deixe apenas a linha atual branca"
},
"offset": {
"label": "Deslocamento",
"tooltip": "Desloque a linha atual para a direita"
},
"scale": {
"label": "Escala",
"tooltip": "Escalar a linha atual"
}
},
"tooltip": "Escolha o efeito a ser aplicado à linha atual"
},
"precise-timing": {
"label": "Sincronize perfeitamente as letras",
"tooltip": "Calcule até o milissegundo a exibição da próxima linha (pode ter um pequeno impacto no desempenho)"
},
"show-lyrics-even-if-inexact": {
"label": "Mostrar letras mesmo que imprecisas",
"tooltip": "Se a música não for encontrada, o plugin tenta novamente com uma consulta de pesquisa diferente.\nO resultado da segunda tentativa pode não ser exato."
},
"show-time-codes": {
"label": "Mostrar códigos de tempo",
"tooltip": "Mostrar os códigos de tempo ao lado das letras"
}
},
"name": "Letras Sincronizadas",
"refetch-btn": {
"fetching": "Buscando...",
"normal": "Buscar as letras novamente"
},
"warnings": {
"duration-mismatch": "⚠️ - As letras da música pode estar dessincronizada devido a um erro de duração.",
"inexact": "⚠️ - As letras desta música podem não ser exactas.",
"instrumental": "⚠️ - Esta é uma música instrumental."
}
},
"taskbar-mediacontrol": { "taskbar-mediacontrol": {
"description": "Controle a reprodução na barra de tarefas do Windows", "description": "Controle a reprodução na barra de tarefas do Windows",
"name": "Controle de mídia da barra de tarefas" "name": "Controle de mídia da barra de tarefas"

View File

@ -207,6 +207,10 @@
} }
}, },
"plugins": { "plugins": {
"ad-speedup": {
"description": "Если воспроизводится реклама, аудио заглушается и скорость воспроизведения устанавливается на 16х",
"name": "Ускоренная перемотка"
},
"adblocker": { "adblocker": {
"description": "Блокируйте всю рекламу и трекинг сразу после установки", "description": "Блокируйте всю рекламу и трекинг сразу после установки",
"menu": { "menu": {
@ -275,9 +279,52 @@
}, },
"name": "Режим Ambient" "name": "Режим Ambient"
}, },
"api-server": {
"description": "Добавляет API сервер для контроля за плеером",
"dialog": {
"request": {
"buttons": {
"allow": "Разрешить",
"deny": "Отказать"
},
"message": "Разрешить {{ID}} ({{origin}}) доступ к API?",
"title": "Запрос на авторизацию в API"
}
},
"menu": {
"auth-strategy": {
"label": "Способ авторизации",
"submenu": {
"auth-at-first": {
"label": "Авторизация при первом запросе"
},
"none": {
"label": "Без авторизации"
}
}
},
"hostname": {
"label": "Имя хоста"
},
"port": {
"label": "Порт"
}
},
"name": "API Сервер [БЕТА]",
"prompt": {
"hostname": {
"label": "Введите имя хоста (на подобии 0.0.0.0) для API сервера:",
"title": "Имя хоста"
},
"port": {
"label": "Введите порт для API сервера:",
"title": "Порт"
}
}
},
"audio-compressor": { "audio-compressor": {
"description": "Применяет компрессию к аудио (уменьшает громкость самых громких частей сигнала и повышает громкость самых тихих частей)", "description": "Применяет компрессию к аудио (уменьшает громкость самых громких частей сигнала и повышает громкость самых тихих частей)",
"name": "Аудио компрессор" "name": "Нормализация аудио"
}, },
"blur-nav-bar": { "blur-nav-bar": {
"description": "Делает панель навигации прозрачной и размытой", "description": "Делает панель навигации прозрачной и размытой",
@ -410,6 +457,21 @@
"description": "Скачивать MP3 / исходное аудио напрямую из интерфейса", "description": "Скачивать MP3 / исходное аудио напрямую из интерфейса",
"menu": { "menu": {
"choose-download-folder": "Выберите папку для загрузок", "choose-download-folder": "Выберите папку для загрузок",
"download-finish-settings": {
"label": "Скачать по завершении",
"prompt": {
"last-percent": "После х процентов",
"last-seconds": "Осталось x сек",
"title": "Условия скачивания"
},
"submenu": {
"advanced": "Расширенные настройки",
"enabled": "Включено",
"mode": "Врмеменной режим",
"percent": "Проценты",
"seconds": "Секунды"
}
},
"download-playlist": "Скачать плейлист", "download-playlist": "Скачать плейлист",
"presets": "Пресеты", "presets": "Пресеты",
"skip-existing": "Пропускать уже существующие файлы" "skip-existing": "Пропускать уже существующие файлы"
@ -649,6 +711,58 @@
"description": "Автоматически пропускает не музыкальные фрагменты, например интро/аутро или фрагменты музыкальных клипов, в которых песня не звучит (тишина)", "description": "Автоматически пропускает не музыкальные фрагменты, например интро/аутро или фрагменты музыкальных клипов, в которых песня не звучит (тишина)",
"name": "SponsorBlock" "name": "SponsorBlock"
}, },
"synced-lyrics": {
"description": "Предоставляет синхронизированные слова для песен из таких источников, как LRClib.",
"errors": {
"fetch": "⚠️ - Возникла ошибка во время получения слов. Повторите попытку позже.",
"not-found": "⚠️ - Для этой песни не найдено слов."
},
"menu": {
"default-text-string": {
"label": "Стандартный символ между словами",
"tooltip": "Выберите стандартный символ для заполнения пространства между словами"
},
"line-effect": {
"label": "Эффект строки",
"submenu": {
"focus": {
"label": "Фокусировка",
"tooltip": "Делает только текущую строку белой"
},
"offset": {
"label": "Сдвиг",
"tooltip": "Сдвигает текущую строку вправо"
},
"scale": {
"label": "Увеличение",
"tooltip": "Увеличивает текущую строку"
}
},
"tooltip": "Выберите эффект применяемый к текущей строке"
},
"precise-timing": {
"label": "Идеально синхронизировать слова",
"tooltip": "До миллисекунды рассчитывает отображение следующей строки(может оказать небольшое влияние на производительность)"
},
"show-lyrics-even-if-inexact": {
"label": "Показывать слова, даже если неточные",
"tooltip": "Если песня не найдена, плагин попытается снова с другим поисковым запросом.\nСо второй попытки результат может быть неточным."
},
"show-time-codes": {
"label": "Показывать временные метки",
"tooltip": "Показывает временные метки рядом со словами"
}
},
"refetch-btn": {
"fetching": "Сбор данных...",
"normal": "Обновить слова"
},
"warnings": {
"duration-mismatch": "⚠️ - Слова могут быть неточно синхронизированы из-за несовпадения длины трека.",
"inexact": "⚠️ - Слова для этой песни могут быть неточными",
"instrumental": "⚠️ - Это инструментальная музыка"
}
},
"taskbar-mediacontrol": { "taskbar-mediacontrol": {
"description": "Управляйте воспроизведением с панели задач Windows", "description": "Управляйте воспроизведением с панели задач Windows",
"name": "Управление мультимедиа на панели задач" "name": "Управление мультимедиа на панели задач"
@ -692,7 +806,7 @@
"menu": { "menu": {
"visualizer-type": "Вид визуализации" "visualizer-type": "Вид визуализации"
}, },
"name": "Визуалайзер" "name": "Визуализатор"
} }
} }
} }

View File

@ -7,11 +7,32 @@
"initialize-failed": "\"{{pluginName}}\" ප්ලගිනය ආරම්භ කිරීමට අසමත් විය", "initialize-failed": "\"{{pluginName}}\" ප්ලගිනය ආරම්භ කිරීමට අසමත් විය",
"load-all": "සියලුම ප්ලගින පූරණය කරමින්", "load-all": "සියලුම ප්ලගින පූරණය කරමින්",
"load-failed": "\"{{pluginName}}\" ප්ලගිනය පූරණය කිරීමට අසමත් විය", "load-failed": "\"{{pluginName}}\" ප්ලගිනය පූරණය කිරීමට අසමත් විය",
"loaded": "ප්ලගිනය \"{{pluginName}}\" පූරණය කරන ලදී" "loaded": "ප්ලගිනය \"{{pluginName}}\" පූරණය කරන ලදී",
"unload-failed": "ප්ලගින් \"{{pluginName}}\" ගලවන්න අසාර්ථක වුන",
"unloaded": "ප්ලගින් \"{{pluginName}}\" ගැලෙව්වා"
} }
} }
}, },
"language": {
"code": "si",
"local-name": "සිංහල",
"name": "Sinhala"
},
"main": { "main": {
"console": {
"did-finish-load": {
"dev-tools": "පූරණය සම්පුර්නි. ඩෙව්ටූල්ස් ඇරිලා"
},
"i18n": {
"loaded": "i18n පූරණය කර ඇත"
},
"second-instance": {
"receive-command": "ප්‍රෝටෝකාල් හරහා විධානය ලැබුණි: \"{{command}}\""
},
"theme": {
"css-file-not-found": "සීඑස්එස් ගොනුව \"{{cssFile}}\" නොපවතී, නොසලකා හැරීම"
}
},
"dialog": { "dialog": {
"need-to-restart": { "need-to-restart": {
"title": "නැවත ආරම්භ කිරීම අවශ්‍යයි" "title": "නැවත ආරම්භ කිරීම අවශ්‍යයි"

View File

@ -207,6 +207,10 @@
} }
}, },
"plugins": { "plugins": {
"ad-speedup": {
"description": "Bir reklam oynatılırsa sesi kapatır ve oynatma hızını 16x olarak ayarlar",
"name": "Hızlandırma"
},
"adblocker": { "adblocker": {
"description": "Tüm reklamları ve izleyicileri engelle", "description": "Tüm reklamları ve izleyicileri engelle",
"menu": { "menu": {
@ -410,6 +414,21 @@
"description": "MP3 / kaynak sesini doğrudan arayüzden indir", "description": "MP3 / kaynak sesini doğrudan arayüzden indir",
"menu": { "menu": {
"choose-download-folder": "İndirme klasörünü seç", "choose-download-folder": "İndirme klasörünü seç",
"download-finish-settings": {
"label": "Bittiğinde indir",
"prompt": {
"last-percent": "Yüzde x'ten sonra",
"last-seconds": "Son x saniyede",
"title": "Ne zaman indirileceğini ayarla"
},
"submenu": {
"advanced": "Gelişmiş",
"enabled": "Etkin",
"mode": "Zaman türü",
"percent": "Yüzde",
"seconds": "Saniye"
}
},
"download-playlist": "Oynatma listesini indir", "download-playlist": "Oynatma listesini indir",
"presets": "Hazır Ayarlar", "presets": "Hazır Ayarlar",
"skip-existing": "Mevcut dosyaları atla" "skip-existing": "Mevcut dosyaları atla"
@ -649,6 +668,59 @@
"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", "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" "name": "SponsorBlock"
}, },
"synced-lyrics": {
"description": "LRClib gibi sağlayıcıları kullanarak şarkılara senkronize şarkı sözleri sağlar.",
"errors": {
"fetch": "⚠️ - Şarkı sözleri getirilirken bir hata oluştu. Lütfen daha sonra tekrar deneyin.",
"not-found": "⚠️ - Bu şarkı için şarkı sözü bulunamadı."
},
"menu": {
"default-text-string": {
"label": "Şarkı sözleri arasında varsayılan karakter",
"tooltip": "Şarkı sözleri arasındaki boşluk için kullanılacak varsayılan karakteri seçin"
},
"line-effect": {
"label": "Çizgi etkisi",
"submenu": {
"focus": {
"label": "odak",
"tooltip": "Yalnızca geçerli satırı beyaz yapın"
},
"offset": {
"label": "telafi etmek,ofset",
"tooltip": "Geçerli satırın sağındaki ofset"
},
"scale": {
"label": "ölçek",
"tooltip": "Geçerli satırı ölçeklendirir"
}
},
"tooltip": "Geçerli satıra uygulanacak efekti seçin"
},
"precise-timing": {
"label": "Şarkı sözlerini mükemmel şekilde senkronize edin",
"tooltip": "Bir sonraki satırın görüntülenmesini milisaniyesine kadar hesaplayın (performans üzerinde küçük bir etkisi olabilir)"
},
"show-lyrics-even-if-inexact": {
"label": "Kesin olmasa bile şarkı sözlerini gösterin",
"tooltip": "Şarkı bulunamazsa, eklenti farklı bir arama sorgusuyla tekrar dener. \nİkinci denemenin sonucu tam olmayabilir."
},
"show-time-codes": {
"label": "Zaman kodlarını göster",
"tooltip": "Şarkı sözlerinin yanında zaman kodlarını gösterin"
}
},
"name": "Senkronize Şarkı Sözleri",
"refetch-btn": {
"fetching": "Getiriliyor...",
"normal": "Refetch şarkı sözleri"
},
"warnings": {
"duration-mismatch": "⚠️ - Süre uyuşmazlığı nedeniyle şarkı sözleri senkronize olmayabilir.",
"inexact": "⚠️ - Bu şarkının sözleri tam olmayabilir",
"instrumental": "⚠️ - Bu enstrümantal bir şarkıdır"
}
},
"taskbar-mediacontrol": { "taskbar-mediacontrol": {
"description": "Windows görev çubuğu üzerinden oynatmayı kontrol edebilmenize olanak sağlar", "description": "Windows görev çubuğu üzerinden oynatmayı kontrol edebilmenize olanak sağlar",
"name": "Görev Çubuğu Medya Kontrolü" "name": "Görev Çubuğu Medya Kontrolü"

View File

@ -207,6 +207,10 @@
} }
}, },
"plugins": { "plugins": {
"ad-speedup": {
"description": "При програванні реклами звук вимикається і встановлюється швидкість відтворення 16х",
"name": "Прискорення реклами"
},
"adblocker": { "adblocker": {
"description": "Блокувати всю рекламу та відстеження з коробки", "description": "Блокувати всю рекламу та відстеження з коробки",
"menu": { "menu": {
@ -275,6 +279,49 @@
}, },
"name": "Режим навколишнього середовища" "name": "Режим навколишнього середовища"
}, },
"api-server": {
"description": "Додає API сервер для контролю плеєра",
"dialog": {
"request": {
"buttons": {
"allow": "Дозволити",
"deny": "Відмінити"
},
"message": "Дозволити {{ID}} ({{origin}}) доступ до API?",
"title": "Запит авторизації до API"
}
},
"menu": {
"auth-strategy": {
"label": "Стратегія авторизації",
"submenu": {
"auth-at-first": {
"label": "Авторизувати при першому запиті"
},
"none": {
"label": "Немає авторизації"
}
}
},
"hostname": {
"label": "Назва серверу"
},
"port": {
"label": "Порт"
}
},
"name": "API сервер [Бета]",
"prompt": {
"hostname": {
"label": "Введіть ім'я хоста (наприклад 0.0.0.0) для API серверу:",
"title": "Ім'я хоста"
},
"port": {
"label": "Введіть порт API серверу:",
"title": "Порт"
}
}
},
"audio-compressor": { "audio-compressor": {
"description": "Застосувати стиснення аудіо (зменшити гучність найгучніших фрагментів сигналу та збільшити гучність тихих фрагментів)", "description": "Застосувати стиснення аудіо (зменшити гучність найгучніших фрагментів сигналу та збільшити гучність тихих фрагментів)",
"name": "Аудіокомпресор" "name": "Аудіокомпресор"
@ -410,6 +457,21 @@
"description": "Завантажує MP3 / джерело аудіо безпосередньо з інтерфейсу", "description": "Завантажує MP3 / джерело аудіо безпосередньо з інтерфейсу",
"menu": { "menu": {
"choose-download-folder": "Оберіть папку для завантаження", "choose-download-folder": "Оберіть папку для завантаження",
"download-finish-settings": {
"label": "Скачати по завершенню",
"prompt": {
"last-percent": "Після Х відсотків",
"last-seconds": "Останні Х секунд",
"title": "Налаштувати коли завантажувати"
},
"submenu": {
"advanced": "Розширені",
"enabled": "Увімкнено",
"mode": "Режим часу",
"percent": "Відсоток",
"seconds": "Секунди"
}
},
"download-playlist": "Завантажити плейлист", "download-playlist": "Завантажити плейлист",
"presets": "Попередні налаштування", "presets": "Попередні налаштування",
"skip-existing": "Пропустити наявні файли" "skip-existing": "Пропустити наявні файли"
@ -649,6 +711,59 @@
"description": "Автоматично пропускати немузичні частини, такі як вступ/закінчення або частини музичних відеороликів, де не відтворюється музика", "description": "Автоматично пропускати немузичні частини, такі як вступ/закінчення або частини музичних відеороликів, де не відтворюється музика",
"name": "SponsorBlock" "name": "SponsorBlock"
}, },
"synced-lyrics": {
"description": "Додає синхронізовані тексти до пісень використовуючи провайдери, такі як LRClib.",
"errors": {
"fetch": "⚠️ - При завантаженні тексту сталась помилка. Спробуйте ще раз пізніше.",
"not-found": "⚠️ - До цієї пісні текст не знайдено."
},
"menu": {
"default-text-string": {
"label": "Символ за замовчуванням між текстами пісень",
"tooltip": "Виберіть символ за замовчуванням, який буде використовуватися для проміжку між текстами пісень"
},
"line-effect": {
"label": "Лінійний ефект",
"submenu": {
"focus": {
"label": "Зосереджитись",
"tooltip": "Зробити білим лише поточний рядок"
},
"offset": {
"label": "Офсет",
"tooltip": "Офсет з права від нинішньої лінії"
},
"scale": {
"label": "Масштабувати",
"tooltip": "Масштабуваты поточну лінію"
}
},
"tooltip": "Виберіть ефект, який потрібно застосувати до поточної лінії"
},
"precise-timing": {
"label": "Зробити текст пісні ідеально синхронізованим",
"tooltip": "Обчисли до мілісекунд відображення наступного рядка (може мати невеликий вплив на продуктивність)"
},
"show-lyrics-even-if-inexact": {
"label": "Показувати текст пісні, навіть якщо він неточний",
"tooltip": "Якщо пісня не знайдена, плагін повторює спробу з іншим пошуковим запитом.\nРезультат з другої спроби може бути не точним."
},
"show-time-codes": {
"label": "Показувати часові марки",
"tooltip": "Показує часові маркы поруч із текстом пісні"
}
},
"name": "Синхронізовані тексти",
"refetch-btn": {
"fetching": "Завантаження...",
"normal": "Перезавантажити текст"
},
"warnings": {
"duration-mismatch": "⚠️ - Тексти цієї пісні можуть бути не синхронізовані через не співпадіння довжини пісні.",
"inexact": "⚠️ - Текст цієї пісні може не співпадати",
"instrumental": "⚠️ - Це інструментал"
}
},
"taskbar-mediacontrol": { "taskbar-mediacontrol": {
"description": "Керування відтворенням з панелі завдань Windows", "description": "Керування відтворенням з панелі завдань Windows",
"name": "Керування медіа на панелі завдань" "name": "Керування медіа на панелі завдань"

View File

@ -2,7 +2,7 @@
"common": { "common": {
"console": { "console": {
"plugins": { "plugins": {
"execute-failed": "Lỗi khi bắt đầu phần mở rộng {{pluginName}}::{{contextName}}", "execute-failed": "Lỗi thực thi plugin {{pluginName}}::{{contextName}}",
"executed-at-ms": "Phần mở rộng {{pluginName}}::{{contextName}} đã bắt đầu trong {{ms}}ms", "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}}\"", "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-all": "Đang tải tất cả phần mở rộng",
@ -207,6 +207,10 @@
} }
}, },
"plugins": { "plugins": {
"ad-speedup": {
"description": "Nếu một quảng cáo được phát thì sẽ bị tắt tiếng và tăng tốc độ phát lên 16x",
"name": "Tăng tốc quảng cáo"
},
"adblocker": { "adblocker": {
"description": "Chặn toàn bộ quảng cáo và trình theo dõi", "description": "Chặn toàn bộ quảng cáo và trình theo dõi",
"menu": { "menu": {
@ -410,6 +414,21 @@
"description": "Tải xuống MP3 / âm thanh nguồn trực tiếp từ giao diện", "description": "Tải xuống MP3 / âm thanh nguồn trực tiếp từ giao diện",
"menu": { "menu": {
"choose-download-folder": "Chọn thư mục tải xuống", "choose-download-folder": "Chọn thư mục tải xuống",
"download-finish-settings": {
"label": "Tải xuống khi hoàn tất",
"prompt": {
"last-percent": "Sau x phần trăm",
"last-seconds": "x giây cuối",
"title": "Định cấu hình thời điểm tải xuống"
},
"submenu": {
"advanced": "Nâng cao",
"enabled": "Đã kích hoạt",
"mode": "Chế độ thời gian",
"percent": "Phần trăm",
"seconds": "Giây"
}
},
"download-playlist": "Tải danh sách phát", "download-playlist": "Tải danh sách phát",
"presets": "Cài đặt sẵn", "presets": "Cài đặt sẵn",
"skip-existing": "Bỏ qua các tập tin hiện có" "skip-existing": "Bỏ qua các tập tin hiện có"
@ -649,6 +668,55 @@
"description": "Tự động bỏ qua các phần không phải âm nhạc như phần giới thiệu/kết thúc hoặc các phần của video nhạc mà bài hát không được phát", "description": "Tự động bỏ qua các phần không phải âm nhạc như phần giới thiệu/kết thúc hoặc các phần của video nhạc mà bài hát không được phát",
"name": "SponsorBlock" "name": "SponsorBlock"
}, },
"synced-lyrics": {
"description": "Cung cấp lời bài hát được đồng bộ hoá với các bài hát, sử dụng những nhà cung cấp như LRClib.",
"errors": {
"fetch": "⚠️ - Đã xảy ra lỗi khi tìm nạp lời bài hát, Vui lòng thử lại sau.",
"not-found": "⚠️ - Không tìm thấy lời cho bài hát này."
},
"menu": {
"default-text-string": {
"label": "Kí tự mặc định giữa các lời bài hát",
"tooltip": "Chọn kí tự mặc định cho khoảng trống giữa các lời bài hát"
},
"line-effect": {
"label": "Kiểu đường thẳng",
"submenu": {
"focus": {
"label": "Tập trung",
"tooltip": "Chỉ làm cho dòng hiện tại có màu trắng"
},
"scale": {
"label": "Tỉ lệ",
"tooltip": "Áp dụng tỉ lệ cho dòng hiện tại"
}
},
"tooltip": "Chọn kiểu để áp dụng cho dòng hiện tại"
},
"precise-timing": {
"label": "Làm cho lời bài hát được đồng bộ hoàn hảo",
"tooltip": "Tính toán chính xác đến mili giây thời gian hiển thị dòng tiếp theo (có thể có tác động nhỏ đến hiệu suất)"
},
"show-lyrics-even-if-inexact": {
"label": "Hiển thị lời bài hát ngay cả khi không chính xác",
"tooltip": "Nếu không tìm thấy bài hát, plugin sẽ thử lại bằng truy vấn tìm kiếm khác.\nKết quả từ lần thử thứ hai có thể không chính xác."
},
"show-time-codes": {
"label": "Hiện mốc thời gian",
"tooltip": "Hiện mốc thời gian bên cạnh lời bài hát"
}
},
"name": "Lời bài hát được đồng bộ hoá",
"refetch-btn": {
"fetching": "Đang tìm nạp...",
"normal": "Tải lại lời bài hát"
},
"warnings": {
"duration-mismatch": "⚠️ - Lời bài hát có thể không đồng bộ do thời lượng không khớp.",
"inexact": "⚠️ - Lời bài hát này có thể không chính xác",
"instrumental": "⚠️ - Đây là một bài hát trình diễn bằng nhạc khí"
}
},
"taskbar-mediacontrol": { "taskbar-mediacontrol": {
"description": "Kiểm soát phát lại từ thanh tác vụ Windows của bạn", "description": "Kiểm soát phát lại từ thanh tác vụ Windows của bạn",
"name": "Kiểm soát phương tiện trên thanh tác vụ" "name": "Kiểm soát phương tiện trên thanh tác vụ"

View File

@ -207,6 +207,10 @@
} }
}, },
"plugins": { "plugins": {
"ad-speedup": {
"description": "使用静音以及 16 倍速播放跳过广告片段",
"name": "广告加速跳过"
},
"adblocker": { "adblocker": {
"description": "屏蔽所有广告与跟踪器", "description": "屏蔽所有广告与跟踪器",
"menu": { "menu": {
@ -410,6 +414,21 @@
"description": "在界面内直接下载 MP3 / 源音频", "description": "在界面内直接下载 MP3 / 源音频",
"menu": { "menu": {
"choose-download-folder": "选择下载文件夹", "choose-download-folder": "选择下载文件夹",
"download-finish-settings": {
"label": "边播边下",
"prompt": {
"last-percent": "播放超过指定百分比时开始下载",
"last-seconds": "歌曲剩余指定秒数时开始下载",
"title": "配置在何时开始下载"
},
"submenu": {
"advanced": "高级",
"enabled": "已启用",
"mode": "激活时机",
"percent": "按播放百分比",
"seconds": "按播放秒数"
}
},
"download-playlist": "下载播放列表", "download-playlist": "下载播放列表",
"presets": "预设", "presets": "预设",
"skip-existing": "跳过已存在的文件" "skip-existing": "跳过已存在的文件"
@ -649,6 +668,59 @@
"description": "自动跳过非音乐部分,如 MV 的介绍/结语以及歌曲未开始的部分", "description": "自动跳过非音乐部分,如 MV 的介绍/结语以及歌曲未开始的部分",
"name": "SponsorBlock" "name": "SponsorBlock"
}, },
"synced-lyrics": {
"description": "透过 LRClib 等服务提供滚动歌词显示。",
"errors": {
"fetch": "⚠️ - 获取歌词时发生错误。请稍后再试。",
"not-found": "⚠️ - 未找到此歌曲的歌词。"
},
"menu": {
"default-text-string": {
"label": "默认的歌词行间字符",
"tooltip": "选择在歌词间隙期间默认显示的字符"
},
"line-effect": {
"label": "歌词行特效",
"submenu": {
"focus": {
"label": "高亮",
"tooltip": "仅将当前歌词行显示为白色"
},
"offset": {
"label": "偏移",
"tooltip": "将当前歌词行向右偏移"
},
"scale": {
"label": "放大",
"tooltip": "放大当前歌词行"
}
},
"tooltip": "选择当前歌词行应用的特效"
},
"precise-timing": {
"label": "让滚动歌词完全同步",
"tooltip": "以毫秒精度估算下句歌词的显示时间(可能对性能有小幅影响)"
},
"show-lyrics-even-if-inexact": {
"label": "即使时值不精确依然显示歌词",
"tooltip": "若首次搜索未找到该歌曲的歌词,插件将尝试用不同的查询方式重新获取。\n重试查询的结果可能不精确。"
},
"show-time-codes": {
"label": "显示时值",
"tooltip": "在歌词旁显示时值"
}
},
"name": "滚动歌词",
"refetch-btn": {
"fetching": "正在获取…",
"normal": "重新获取歌词"
},
"warnings": {
"duration-mismatch": "⚠️ - 由于持续时间不对应,滚动歌词可能不同步。",
"inexact": "⚠️ - 此曲目的歌词可能不准确",
"instrumental": "⚠️ - 此曲目为纯音乐"
}
},
"taskbar-mediacontrol": { "taskbar-mediacontrol": {
"description": "从 Windows 任务栏控制音乐回放", "description": "从 Windows 任务栏控制音乐回放",
"name": "任务栏媒体控件" "name": "任务栏媒体控件"

View File

@ -96,11 +96,11 @@
"advanced-options": { "advanced-options": {
"label": "進階選項", "label": "進階選項",
"submenu": { "submenu": {
"auto-reset-app-cache": "當程式啟動時重設應用程式快取", "auto-reset-app-cache": "啟動時重設應用快取",
"disable-hardware-acceleration": "關閉硬體加速", "disable-hardware-acceleration": "關閉硬體加速",
"edit-config-json": "編輯 config.json", "edit-config-json": "編輯 config.json",
"override-user-agent": "覆寫使用者代理", "override-user-agent": "覆寫使用者代理",
"restart-on-config-changes": "設定檔更動時自動重啟應用程式", "restart-on-config-changes": "設定變更時自動重啟應用",
"set-proxy": { "set-proxy": {
"label": "設定代理伺服器", "label": "設定代理伺服器",
"prompt": { "prompt": {
@ -123,7 +123,7 @@
}, },
"language": { "language": {
"dialog": { "dialog": {
"message": "語言會在下一次重啟應用程式時變更", "message": "語言會在重啟應用變更",
"title": "語言已變更" "title": "語言已變更"
}, },
"label": "語言", "label": "語言",
@ -131,7 +131,7 @@
"to-help-translate": "想協助翻譯?按一下這裡" "to-help-translate": "想協助翻譯?按一下這裡"
} }
}, },
"resume-on-start": "應用啟時繼續上次播放的歌曲", "resume-on-start": "應用啟時繼續播放上次的歌曲",
"single-instance-lock": "單實例模式", "single-instance-lock": "單實例模式",
"start-at-login": "開機時啟動", "start-at-login": "開機時啟動",
"starting-page": { "starting-page": {
@ -142,8 +142,8 @@
"label": "系統匣", "label": "系統匣",
"submenu": { "submenu": {
"disabled": "已停用", "disabled": "已停用",
"enabled-and-hide-app": "啟用並最小化應用程式", "enabled-and-hide-app": "啟用並最小化應用",
"enabled-and-show-app": "啟用但持續顯示應用程式", "enabled-and-show-app": "啟用顯示應用",
"play-pause-on-click": "點擊時播放/暫停" "play-pause-on-click": "點擊時播放/暫停"
} }
}, },
@ -207,6 +207,10 @@
} }
}, },
"plugins": { "plugins": {
"ad-speedup": {
"description": "使用 16 倍速播放及靜音來跳過廣告片段",
"name": "加速略過"
},
"adblocker": { "adblocker": {
"description": "阻擋所有廣告", "description": "阻擋所有廣告",
"menu": { "menu": {
@ -219,7 +223,7 @@
"name": "進階專輯操作" "name": "進階專輯操作"
}, },
"album-color-theme": { "album-color-theme": {
"description": "依歌曲色調自動更改應用程式主題", "description": "根據專輯封面色調更改應用程式主題顏色",
"menu": { "menu": {
"color-mix-ratio": { "color-mix-ratio": {
"label": "顏色混合程度", "label": "顏色混合程度",
@ -275,6 +279,49 @@
}, },
"name": "微光效果" "name": "微光效果"
}, },
"api-server": {
"description": "新增伺服器以使用 API 控制播放器",
"dialog": {
"request": {
"buttons": {
"allow": "允許",
"deny": "拒絕"
},
"message": "允許 {{ID}} ({{origin}}) 訪問 API 嗎?",
"title": "API 驗證請求"
}
},
"menu": {
"auth-strategy": {
"label": "驗證策略",
"submenu": {
"auth-at-first": {
"label": "首次請求時驗證"
},
"none": {
"label": "不要驗證"
}
}
},
"hostname": {
"label": "主機名稱"
},
"port": {
"label": "接口"
}
},
"name": "API 伺服器 [Beta]",
"prompt": {
"hostname": {
"label": "輸入 API 伺服器的主機名稱 例 (0.0.0.0)",
"title": "主機名稱"
},
"port": {
"label": "輸入 API 伺服器接口:",
"title": "接口"
}
}
},
"audio-compressor": { "audio-compressor": {
"description": "使用音效壓縮 (大聲部份的音量降低, 柔和部份的音量提高)", "description": "使用音效壓縮 (大聲部份的音量降低, 柔和部份的音量提高)",
"name": "音效壓縮器" "name": "音效壓縮器"
@ -296,7 +343,7 @@
"name": "標題選擇器", "name": "標題選擇器",
"prompt": { "prompt": {
"selector": { "selector": {
"label": "目前標題語言: {{language}}", "label": "目前標題語言{{language}}",
"none": "無", "none": "無",
"title": "選擇標題語言" "title": "選擇標題語言"
} }
@ -341,10 +388,10 @@
"discord": { "discord": {
"backend": { "backend": {
"already-connected": "已嘗試可用連接", "already-connected": "已嘗試可用連接",
"connected": "已連接至Discord", "connected": "已連接至 Discord",
"disconnected": "與Discord斷開連接" "disconnected": "與 Discord 斷開連接"
}, },
"description": "使用Discord狀態與你的好友分享你正在收聽的音樂", "description": "使用 Discord 狀態與你的好友分享你正在收聽的音樂",
"menu": { "menu": {
"auto-reconnect": "自動重新連接", "auto-reconnect": "自動重新連接",
"clear-activity": "清除狀態", "clear-activity": "清除狀態",
@ -352,14 +399,14 @@
"connected": "已連接", "connected": "已連接",
"disconnected": "已斷開連接", "disconnected": "已斷開連接",
"hide-duration-left": "隱藏音樂剩餘時間狀態", "hide-duration-left": "隱藏音樂剩餘時間狀態",
"hide-github-button": "隱藏Github頁面按鈕", "hide-github-button": "隱藏 Github 頁面按鈕",
"play-on-youtube-music": "顯示Play on YouTube Music按鈕", "play-on-youtube-music": "顯示 Play on YouTube Music 按鈕",
"set-inactivity-timeout": "設定閒置狀態時長" "set-inactivity-timeout": "設定閒置狀態時長"
}, },
"name": "Discord狀態", "name": "Discord 狀態",
"prompt": { "prompt": {
"set-inactivity-timeout": { "set-inactivity-timeout": {
"label": "設定多少秒後清除狀態:", "label": "設定多少秒後清除狀態",
"title": "設定閒置狀態時長" "title": "設定閒置狀態時長"
} }
} }
@ -384,11 +431,11 @@
} }
}, },
"feedback": { "feedback": {
"conversion-progress": "轉檔進度: {{percent}}%", "conversion-progress": "轉檔進度{{percent}}%",
"converting": "轉檔中…", "converting": "轉檔中…",
"done": "完成下載: {{filePath}}", "done": "完成下載{{filePath}}",
"download-info": "正在下載 {{artist}} - {{title}} [{{videoId}}", "download-info": "正在下載 {{artist}} - {{title}} [{{videoId}}",
"download-progress": "下載進度: {{percent}}%", "download-progress": "下載進度{{percent}}%",
"downloading": "下載中…", "downloading": "下載中…",
"downloading-counter": "正在下載第 {{current}}/{{total}}…", "downloading-counter": "正在下載第 {{current}}/{{total}}…",
"downloading-playlist": "正在下載播放清單 \"{{playlistTitle}}\" - 共 {{playlistSize}} 首歌 ({{playlistId}})", "downloading-playlist": "正在下載播放清單 \"{{playlistTitle}}\" - 共 {{playlistSize}} 首歌 ({{playlistId}})",
@ -399,17 +446,32 @@
"playlist-has-only-one-song": "播放清單內只有一首歌曲, 將直接下載", "playlist-has-only-one-song": "播放清單內只有一首歌曲, 將直接下載",
"playlist-id-not-found": "沒有找到播放清單 ID", "playlist-id-not-found": "沒有找到播放清單 ID",
"playlist-is-empty": "播放清單是空的", "playlist-is-empty": "播放清單是空的",
"playlist-is-mix-or-private": "獲取播放清單資訊時發生錯誤: 請確認非私人播放清單或是\"為你推薦的合輯\"\n\n{{error}}", "playlist-is-mix-or-private": "獲取播放清單資訊時發生錯誤請確認非私人播放清單或是\"為你推薦的合輯\"\n\n{{error}}",
"preparing-file": "正在準備檔案…", "preparing-file": "正在準備檔案…",
"saving": "儲存中…", "saving": "儲存中…",
"trying-to-get-playlist-id": "正在嘗試獲取播放清單 ID: {{playlistId}}", "trying-to-get-playlist-id": "正在嘗試獲取播放清單 ID{{playlistId}}",
"video-id-not-found": "未能找到該影片", "video-id-not-found": "未能找到該影片",
"writing-id3": "正在寫入 ID3 標籤…" "writing-id3": "正在寫入 ID3 標籤…"
} }
}, },
"description": "應用程式內下載 MP3原始音檔", "description": "開啟應用程式內下載 MP3原始音檔功能",
"menu": { "menu": {
"choose-download-folder": "選擇下載位置", "choose-download-folder": "選擇下載位置",
"download-finish-settings": {
"label": "智慧下載",
"prompt": {
"last-percent": "歌曲剩餘多少 % 時下載",
"last-seconds": "歌曲剩餘多少秒時下載",
"title": "智慧下載進階設定"
},
"submenu": {
"advanced": "進階",
"enabled": "啟用",
"mode": "判斷方式",
"percent": "百分比",
"seconds": "秒數"
}
},
"download-playlist": "下載播放清單", "download-playlist": "下載播放清單",
"presets": "預設格式", "presets": "預設格式",
"skip-existing": "跳過已存在的檔案" "skip-existing": "跳過已存在的檔案"
@ -494,12 +556,12 @@
} }
}, },
"navigation": { "navigation": {
"description": "將上一頁/下一頁按鈕新增至應用程式上方, 就像你最熟悉的瀏覽器", "description": "允許應用程式上方顯示上一頁/下一頁按鈕",
"name": "導覽列" "name": "導覽列"
}, },
"no-google-login": { "no-google-login": {
"description": "移除Google登入按鈕及連結", "description": "移除 Google 登入按鈕及連結",
"name": "停用Google登入" "name": "停用 Google 登入"
}, },
"notifications": { "notifications": {
"description": "在歌曲播放時發送一個系統通知 (可互動通知僅限Windows)", "description": "在歌曲播放時發送一個系統通知 (可互動通知僅限Windows)",
@ -563,7 +625,7 @@
"decrease": "降低音量", "decrease": "降低音量",
"increase": "增加音量" "increase": "增加音量"
}, },
"label": "選擇全域音量控制快捷鍵:", "label": "選擇全域音量控制快捷鍵",
"title": "全域音量控制快捷鍵" "title": "全域音量控制快捷鍵"
}, },
"volume-steps": { "volume-steps": {
@ -576,8 +638,8 @@
"backend": { "backend": {
"dialog": { "dialog": {
"quality-changer": { "quality-changer": {
"detail": "目前畫質: {{quality}}", "detail": "目前畫質{{quality}}",
"message": "選擇影片畫質:", "message": "選擇影片畫質",
"title": "選擇影片畫質" "title": "選擇影片畫質"
} }
} }
@ -632,7 +694,7 @@
"play-pause": "播放/暫停", "play-pause": "播放/暫停",
"previous": "上一首" "previous": "上一首"
}, },
"label": "選擇全域音樂控制快捷鍵:", "label": "選擇全域音樂控制快捷鍵",
"title": "全域快捷鍵" "title": "全域快捷鍵"
} }
} }
@ -649,8 +711,61 @@
"description": "自動跳過贊助片段", "description": "自動跳過贊助片段",
"name": "贊助阻擋" "name": "贊助阻擋"
}, },
"synced-lyrics": {
"description": "使用 LRClib 等管道提供歌詞同步顯示。",
"errors": {
"fetch": "⚠️擷取歌詞時發生錯誤。請稍後再試。",
"not-found": "⚠️未找到該首歌曲的歌詞。"
},
"menu": {
"default-text-string": {
"label": "預設歌詞中間隔的符號",
"tooltip": "選擇歌詞中間隔要使用的符號"
},
"line-effect": {
"label": "歌詞顯示效果",
"submenu": {
"focus": {
"label": "高亮",
"tooltip": "高亮當前的歌詞"
},
"offset": {
"label": "凸行",
"tooltip": "凸行當前的歌詞"
},
"scale": {
"label": "放大",
"tooltip": "放大當前的歌詞"
}
},
"tooltip": "選擇要使用的歌詞顯示效果"
},
"precise-timing": {
"label": "使歌詞完美同步",
"tooltip": "更精確的計算下一行歌詞的顯示(將會降低些許效能)"
},
"show-lyrics-even-if-inexact": {
"label": "即使不精確依然強制顯示歌詞",
"tooltip": "當找不到符合該歌曲的歌詞時,該功能會嘗試不同的搜尋方式。\n使用不同的搜尋方式會導致不精確的結果。"
},
"show-time-codes": {
"label": "顯示時間線",
"tooltip": "在歌詞旁顯示時間線"
}
},
"name": "歌詞同步",
"refetch-btn": {
"fetching": "擷取中...",
"normal": "重新擷取歌詞"
},
"warnings": {
"duration-mismatch": "⚠️歌詞可能會出現不同步的情況。",
"inexact": "⚠️該歌曲的歌詞可能並不精確",
"instrumental": "⚠️該首歌曲並無人聲"
}
},
"taskbar-mediacontrol": { "taskbar-mediacontrol": {
"description": "透過工作列應用程式圖式控制媒體播放", "description": "允許工作列應用程式預覽介面顯示媒體控制相關按鈕",
"name": "工作列媒體控制" "name": "工作列媒體控制"
}, },
"touchbar": { "touchbar": {

View File

@ -11,6 +11,8 @@ import {
shell, shell,
dialog, dialog,
ipcMain, ipcMain,
protocol,
type BrowserWindowConstructorOptions,
} from 'electron'; } from 'electron';
import enhanceWebRequest, { import enhanceWebRequest, {
BetterSession, BetterSession,
@ -82,6 +84,34 @@ if (!gotTheLock) {
app.exit(); app.exit();
} }
protocol.registerSchemesAsPrivileged([
{
scheme: 'http',
privileges: {
standard: true,
bypassCSP: true,
allowServiceWorkers: true,
supportFetchAPI: true,
corsEnabled: true,
stream: true,
codeCache: true,
},
},
{
scheme: 'https',
privileges: {
standard: true,
bypassCSP: true,
allowServiceWorkers: true,
supportFetchAPI: true,
corsEnabled: true,
stream: true,
codeCache: true,
},
},
{ scheme: 'mailto', privileges: { standard: true } },
]);
// Ozone platform hint: Required for Wayland support // Ozone platform hint: Required for Wayland support
app.commandLine.appendSwitch('ozone-platform-hint', 'auto'); app.commandLine.appendSwitch('ozone-platform-hint', 'auto');
// SharedArrayBuffer: Required for downloader (@ffmpeg/core-mt) // SharedArrayBuffer: Required for downloader (@ffmpeg/core-mt)
@ -100,9 +130,18 @@ if (config.get('options.disableHardwareAcceleration')) {
app.disableHardwareAcceleration(); app.disableHardwareAcceleration();
} }
if (is.linux() && config.plugins.isEnabled('shortcuts')) { if (is.linux()) {
const disabledFeatures = [
// Workaround for issue #2248
'UseMultiPlaneFormatForSoftwareVideo',
];
// Stops chromium from launching its own MPRIS service // Stops chromium from launching its own MPRIS service
app.commandLine.appendSwitch('disable-features', 'MediaSessionService'); if (config.plugins.isEnabled('shortcuts')) {
disabledFeatures.push('MediaSessionService');
}
app.commandLine.appendSwitch('disable-features', disabledFeatures.join());
} }
if (config.get('options.proxy')) { if (config.get('options.proxy')) {
@ -277,6 +316,23 @@ async function createMainWindow() {
height: 32, height: 32,
}; };
const decorations: Partial<BrowserWindowConstructorOptions> = {
frame: !is.macOS() && !useInlineMenu,
titleBarOverlay: defaultTitleBarOverlayOptions,
titleBarStyle: useInlineMenu
? 'hidden'
: is.macOS()
? 'hiddenInset'
: 'default',
autoHideMenuBar: config.get('options.hideMenu'),
};
// Note: on linux, for some weird reason, having these extra properties with 'frame: false' does not work
if (is.linux() && useInlineMenu) {
delete decorations.titleBarOverlay;
delete decorations.titleBarStyle;
}
const win = new BrowserWindow({ const win = new BrowserWindow({
icon, icon,
width: windowSize.width, width: windowSize.width,
@ -294,14 +350,7 @@ async function createMainWindow() {
sandbox: false, sandbox: false,
}), }),
}, },
frame: !is.macOS() && !useInlineMenu, ...decorations,
titleBarOverlay: defaultTitleBarOverlayOptions,
titleBarStyle: useInlineMenu
? 'hidden'
: is.macOS()
? 'hiddenInset'
: 'default',
autoHideMenuBar: config.get('options.hideMenu'),
}); });
initHook(win); initHook(win);
initTheme(win); initTheme(win);
@ -312,28 +361,31 @@ async function createMainWindow() {
const { x: windowX, y: windowY } = windowPosition; const { x: windowX, y: windowY } = windowPosition;
const winSize = win.getSize(); const winSize = win.getSize();
const display = screen.getDisplayNearestPoint(windowPosition); const display = screen.getDisplayNearestPoint(windowPosition);
const scaleFactor = is.windows() ? display.scaleFactor: 1; const primaryDisplay = screen.getPrimaryDisplay();
const scaledWidth = Math.floor(windowSize.width / scaleFactor); const scaleFactor = is.windows()
const scaledHeight = Math.floor(windowSize.height / scaleFactor); ? primaryDisplay.scaleFactor / display.scaleFactor
: 1;
const scaledWidth = Math.floor(windowSize.width * scaleFactor);
const scaledHeight = Math.floor(windowSize.height * scaleFactor);
const scaledX = windowX; const scaledX = windowX;
const scaledY = windowY; const scaledY = windowY;
if ( if (
scaledX + scaledWidth < display.bounds.x - 8 || scaledX + scaledWidth / 2 < display.bounds.x - 8 || // Left
scaledX - scaledWidth > display.bounds.x + display.bounds.width || scaledX + scaledWidth / 2 > display.bounds.x + display.bounds.width || // Right
scaledY < display.bounds.y - 8 || scaledY < display.bounds.y - 8 || // Top
scaledY > display.bounds.y + display.bounds.height scaledY + scaledHeight / 2 > display.bounds.y + display.bounds.height // Bottom
) { ) {
// Window is offscreen // Window is offscreen
if (is.dev()) { if (is.dev()) {
console.warn( console.warn(
LoggerPrefix, LoggerPrefix,
t('main.console.window.tried-to-render-offscreen', { t('main.console.window.tried-to-render-offscreen', {
winSize: String(winSize), windowSize: String(winSize),
displaySize: String(display.bounds), displaySize: JSON.stringify(display.bounds),
windowPosition: String(windowPosition), position: JSON.stringify(windowPosition),
}), }),
); );
} }
@ -421,7 +473,7 @@ async function createMainWindow() {
...defaultTitleBarOverlayOptions, ...defaultTitleBarOverlayOptions,
height: Math.floor( height: Math.floor(
defaultTitleBarOverlayOptions.height! * defaultTitleBarOverlayOptions.height! *
win.webContents.getZoomFactor(), win.webContents.getZoomFactor(),
), ),
}); });
} }
@ -434,7 +486,7 @@ async function createMainWindow() {
event.preventDefault(); event.preventDefault();
win.webContents.loadURL( win.webContents.loadURL(
'https://accounts.google.com/ServiceLogin?ltmpl=music&service=youtube&continue=https%3A%2F%2Fwww.youtube.com%2Fsignin%3Faction_handle_signin%3Dtrue%26next%3Dhttps%253A%252F%252Fmusic.youtube.com%252F' 'https://accounts.google.com/ServiceLogin?ltmpl=music&service=youtube&continue=https%3A%2F%2Fwww.youtube.com%2Fsignin%3Faction_handle_signin%3Dtrue%26next%3Dhttps%253A%252F%252Fmusic.youtube.com%252F',
); );
} }
}); });
@ -458,8 +510,8 @@ app.once('browser-window-created', (_event, win) => {
const updatedUserAgent = is.macOS() const updatedUserAgent = is.macOS()
? userAgents.mac ? userAgents.mac
: is.windows() : is.windows()
? userAgents.windows ? userAgents.windows
: userAgents.linux; : userAgents.linux;
win.webContents.userAgent = updatedUserAgent; win.webContents.userAgent = updatedUserAgent;
app.userAgentFallback = updatedUserAgent; app.userAgentFallback = updatedUserAgent;
@ -508,7 +560,11 @@ app.once('browser-window-created', (_event, win) => {
console.log(log); console.log(log);
} }
if (errorCode !== -3) { if (
errorCode !== -3 &&
// Workaround for #2435
!new URL(validatedURL).hostname.includes('doubleclick.net')
) {
// -3 is a false positive // -3 is a false positive
win.webContents.send('log', log); win.webContents.send('log', log);
win.webContents.loadFile(ErrorHtmlAsset); win.webContents.loadFile(ErrorHtmlAsset);
@ -597,6 +653,7 @@ app.whenReady().then(async () => {
shortcutDetails.target !== appLocation || shortcutDetails.target !== appLocation ||
shortcutDetails.appUserModelId !== appID shortcutDetails.appUserModelId !== appID
) { ) {
// eslint-disable-next-line @typescript-eslint/only-throw-error
throw 'needUpdate'; throw 'needUpdate';
} }
} catch (error) { } catch (error) {
@ -620,7 +677,9 @@ app.whenReady().then(async () => {
// In dev mode, get string from process.env.VITE_DEV_SERVER_URL, else use fs.readFileSync // In dev mode, get string from process.env.VITE_DEV_SERVER_URL, else use fs.readFileSync
if (is.dev() && process.env.ELECTRON_RENDERER_URL) { if (is.dev() && process.env.ELECTRON_RENDERER_URL) {
// HACK: to make vite work with electron renderer (supports hot reload) // HACK: to make vite work with electron renderer (supports hot reload)
event.returnValue = [null, ` event.returnValue = [
null,
`
console.log('${LoggerPrefix}', 'Loading vite from dev server'); console.log('${LoggerPrefix}', 'Loading vite from dev server');
(async () => { (async () => {
await new Promise((resolve) => { await new Promise((resolve) => {
@ -641,7 +700,8 @@ app.whenReady().then(async () => {
document.body.appendChild(rendererScript); document.body.appendChild(rendererScript);
})(); })();
0 0
`]; `,
];
} else { } else {
const rendererPath = path.join(__dirname, '..', 'renderer'); const rendererPath = path.join(__dirname, '..', 'renderer');
const indexHTML = parse( const indexHTML = parse(
@ -653,7 +713,10 @@ app.whenReady().then(async () => {
scriptSrc.getAttribute('src')!, scriptSrc.getAttribute('src')!,
); );
const scriptString = fs.readFileSync(scriptPath, 'utf-8'); const scriptString = fs.readFileSync(scriptPath, 'utf-8');
event.returnValue = [url.pathToFileURL(scriptPath).toString(), scriptString + ';0']; event.returnValue = [
url.pathToFileURL(scriptPath).toString(),
scriptString + ';0',
];
} }
}); });

View File

@ -34,11 +34,12 @@ const createContext = (
win.webContents.send(event, ...args); win.webContents.send(event, ...args);
}, },
handle: (event: string, listener: CallableFunction) => { handle: (event: string, listener: CallableFunction) => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-return // eslint-disable-next-line @typescript-eslint/no-unsafe-return,@typescript-eslint/no-unsafe-call
ipcMain.handle(event, (_, ...args: unknown[]) => listener(...args)); ipcMain.handle(event, (_, ...args: unknown[]) => listener(...args));
}, },
on: (event: string, listener: CallableFunction) => { on: (event: string, listener: CallableFunction) => {
ipcMain.on(event, (_, ...args: unknown[]) => { ipcMain.on(event, (_, ...args: unknown[]) => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
listener(...args); listener(...args);
}); });
}, },
@ -75,11 +76,11 @@ export const forceUnloadMainPlugin = async (
); );
return; return;
} else { } else {
console.log( const message = t('common.console.plugins.unload-failed', {
LoggerPrefix, pluginName: id,
t('common.console.plugins.unload-failed', { pluginName: id }), });
); console.log(LoggerPrefix, message);
return Promise.reject(); return Promise.reject(new Error(message));
} }
} catch (err) { } catch (err) {
console.error( console.error(
@ -87,7 +88,7 @@ export const forceUnloadMainPlugin = async (
t('common.console.plugins.unload-failed', { pluginName: id }), t('common.console.plugins.unload-failed', { pluginName: id }),
); );
console.trace(err); console.trace(err);
return Promise.reject(err); return Promise.reject(err as Error);
} }
}; };
@ -111,11 +112,11 @@ export const forceLoadMainPlugin = async (
) { ) {
loadedPluginMap[id] = plugin; loadedPluginMap[id] = plugin;
} else { } else {
console.log( const message = t('common.console.plugins.load-failed', {
LoggerPrefix, pluginName: id,
t('common.console.plugins.load-failed', { pluginName: id }), });
); console.log(LoggerPrefix, message);
return Promise.reject(); return Promise.reject(new Error(message));
} }
} catch (err) { } catch (err) {
console.error( console.error(
@ -123,7 +124,7 @@ export const forceLoadMainPlugin = async (
t('common.console.plugins.initialize-failed', { pluginName: id }), t('common.console.plugins.initialize-failed', { pluginName: id }),
); );
console.trace(err); console.trace(err);
return Promise.reject(err); return Promise.reject(err as Error);
} }
}; };

View File

@ -18,7 +18,8 @@ const loadedPluginMap: Record<
export const createContext = <Config extends PluginConfig>( export const createContext = <Config extends PluginConfig>(
id: string, id: string,
): RendererContext<Config> => ({ ): RendererContext<Config> => ({
getConfig: async () => window.ipcRenderer.invoke('ytmd:get-config', id), getConfig: async () =>
window.ipcRenderer.invoke('ytmd:get-config', id) as Promise<Config>,
setConfig: async (newConfig) => { setConfig: async (newConfig) => {
await window.ipcRenderer.invoke('ytmd:set-config', id, newConfig); await window.ipcRenderer.invoke('ytmd:set-config', id, newConfig);
}, },
@ -30,6 +31,7 @@ export const createContext = <Config extends PluginConfig>(
window.ipcRenderer.invoke(event, ...args), window.ipcRenderer.invoke(event, ...args),
on: (event: string, listener: CallableFunction) => { on: (event: string, listener: CallableFunction) => {
window.ipcRenderer.on(event, (_, ...args: unknown[]) => { window.ipcRenderer.on(event, (_, ...args: unknown[]) => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
listener(...args); listener(...args);
}); });
}, },

View File

@ -1,5 +1,13 @@
import is from 'electron-is'; import is from 'electron-is';
import { app, BrowserWindow, clipboard, dialog, Menu, MenuItem, shell, } from 'electron'; import {
app,
BrowserWindow,
clipboard,
dialog,
Menu,
MenuItem,
shell,
} from 'electron';
import prompt from 'custom-electron-prompt'; import prompt from 'custom-electron-prompt';
import { satisfies } from 'semver'; import { satisfies } from 'semver';
@ -68,12 +76,21 @@ export const mainMenuTemplate = async (
const plugin = allPlugins[id]; const plugin = allPlugins[id];
const pluginLabel = plugin?.name?.() ?? id; const pluginLabel = plugin?.name?.() ?? id;
const pluginDescription = plugin?.description?.() ?? undefined; const pluginDescription = plugin?.description?.() ?? undefined;
const isNew = plugin?.addedVersion ? satisfies(packageJson.version, plugin.addedVersion) : false; const isNew = plugin?.addedVersion
? satisfies(packageJson.version, plugin.addedVersion)
: false;
if (!config.plugins.isEnabled(id)) { if (!config.plugins.isEnabled(id)) {
return [ return [
id, id,
pluginEnabledMenu(id, pluginLabel, pluginDescription, isNew, true, innerRefreshMenu), pluginEnabledMenu(
id,
pluginLabel,
pluginDescription,
isNew,
true,
innerRefreshMenu,
),
] as const; ] as const;
} }
@ -115,9 +132,18 @@ export const mainMenuTemplate = async (
const plugin = allPlugins[id]; const plugin = allPlugins[id];
const pluginLabel = plugin?.name?.() ?? id; const pluginLabel = plugin?.name?.() ?? id;
const pluginDescription = plugin?.description?.() ?? undefined; const pluginDescription = plugin?.description?.() ?? undefined;
const isNew = plugin?.addedVersion ? satisfies(packageJson.version, plugin.addedVersion) : false; const isNew = plugin?.addedVersion
? satisfies(packageJson.version, plugin.addedVersion)
: false;
return pluginEnabledMenu(id, pluginLabel, pluginDescription, isNew, true, innerRefreshMenu); return pluginEnabledMenu(
id,
pluginLabel,
pluginDescription,
isNew,
true,
innerRefreshMenu,
);
}); });
const availableLanguages = Object.keys(languageResources); const availableLanguages = Object.keys(languageResources);
@ -229,12 +255,12 @@ export const mainMenuTemplate = async (
submenu: [ submenu: [
...((config.get('options.themes')?.length ?? 0) === 0 ...((config.get('options.themes')?.length ?? 0) === 0
? [ ? [
{ {
label: t( label: t(
'main.menu.options.submenu.visual-tweaks.submenu.theme.submenu.no-theme', 'main.menu.options.submenu.visual-tweaks.submenu.theme.submenu.no-theme',
), ),
} },
] ]
: []), : []),
...(config.get('options.themes')?.map((theme: string) => ({ ...(config.get('options.themes')?.map((theme: string) => ({
type: 'normal' as const, type: 'normal' as const,
@ -251,16 +277,25 @@ export const mainMenuTemplate = async (
{ theme }, { theme },
), ),
buttons: [ buttons: [
t('main.menu.options.submenu.visual-tweaks.submenu.theme.dialog.button.cancel'), t(
t('main.menu.options.submenu.visual-tweaks.submenu.theme.dialog.button.remove'), 'main.menu.options.submenu.visual-tweaks.submenu.theme.dialog.button.cancel',
),
t(
'main.menu.options.submenu.visual-tweaks.submenu.theme.dialog.button.remove',
),
], ],
}); });
if (response === 1) { if (response === 1) {
config.set('options.themes', config.get('options.themes')?.filter((t) => t !== theme) ?? []); config.set(
'options.themes',
config
.get('options.themes')
?.filter((t) => t !== theme) ?? [],
);
innerRefreshMenu(); innerRefreshMenu();
} }
} },
})) ?? []), })) ?? []),
{ type: 'separator' }, { type: 'separator' },
{ {
@ -306,40 +341,40 @@ export const mainMenuTemplate = async (
}, },
...((is.windows() || is.linux() ...((is.windows() || is.linux()
? [ ? [
{ {
label: t('main.menu.options.submenu.hide-menu.label'), label: t('main.menu.options.submenu.hide-menu.label'),
type: 'checkbox', type: 'checkbox',
checked: config.get('options.hideMenu'), checked: config.get('options.hideMenu'),
click(item) { click(item) {
config.setMenuOption('options.hideMenu', item.checked); config.setMenuOption('options.hideMenu', item.checked);
if (item.checked && !config.get('options.hideMenuWarned')) { if (item.checked && !config.get('options.hideMenuWarned')) {
dialog.showMessageBox(win, { dialog.showMessageBox(win, {
type: 'info', type: 'info',
title: t( title: t(
'main.menu.options.submenu.hide-menu.dialog.title', 'main.menu.options.submenu.hide-menu.dialog.title',
), ),
message: t( message: t(
'main.menu.options.submenu.hide-menu.dialog.message', 'main.menu.options.submenu.hide-menu.dialog.message',
), ),
}); });
} }
},
}, },
}, ]
]
: []) satisfies Electron.MenuItemConstructorOptions[]), : []) satisfies Electron.MenuItemConstructorOptions[]),
...((is.windows() || is.macOS() ...((is.windows() || is.macOS()
? // Only works on Win/Mac ? // Only works on Win/Mac
// https://www.electronjs.org/docs/api/app#appsetloginitemsettingssettings-macos-windows // https://www.electronjs.org/docs/api/app#appsetloginitemsettingssettings-macos-windows
[ [
{ {
label: t('main.menu.options.submenu.start-at-login'), label: t('main.menu.options.submenu.start-at-login'),
type: 'checkbox', type: 'checkbox',
checked: config.get('options.startAtLogin'), checked: config.get('options.startAtLogin'),
click(item) { click(item) {
config.setMenuOption('options.startAtLogin', item.checked); config.setMenuOption('options.startAtLogin', item.checked);
},
}, },
}, ]
]
: []) satisfies Electron.MenuItemConstructorOptions[]), : []) satisfies Electron.MenuItemConstructorOptions[]),
{ {
label: t('main.menu.options.submenu.tray.label'), label: t('main.menu.options.submenu.tray.label'),
@ -493,25 +528,25 @@ export const mainMenuTemplate = async (
{ type: 'separator' }, { type: 'separator' },
is.macOS() is.macOS()
? { ? {
label: t( label: t(
'main.menu.options.submenu.advanced-options.submenu.toggle-dev-tools', 'main.menu.options.submenu.advanced-options.submenu.toggle-dev-tools',
), ),
// Cannot use "toggleDevTools" role in macOS // Cannot use "toggleDevTools" role in macOS
click() { click() {
const { webContents } = win; const { webContents } = win;
if (webContents.isDevToolsOpened()) { if (webContents.isDevToolsOpened()) {
webContents.closeDevTools(); webContents.closeDevTools();
} else { } else {
webContents.openDevTools(); webContents.openDevTools();
} }
}, },
} }
: { : {
label: t( label: t(
'main.menu.options.submenu.advanced-options.submenu.toggle-dev-tools', 'main.menu.options.submenu.advanced-options.submenu.toggle-dev-tools',
), ),
role: 'toggleDevTools', role: 'toggleDevTools',
}, },
{ {
label: t( label: t(
'main.menu.options.submenu.advanced-options.submenu.edit-config-json', 'main.menu.options.submenu.advanced-options.submenu.edit-config-json',

View File

@ -0,0 +1,58 @@
function skipAd(target: Element) {
const skipButton = target.querySelector<HTMLButtonElement>(
'button.ytp-ad-skip-button-modern',
);
if (skipButton) {
skipButton.click();
}
}
function speedUpAndMute(player: Element, isAdShowing: boolean) {
const video = player.querySelector<HTMLVideoElement>('video');
if (!video) return;
if (isAdShowing) {
video.playbackRate = 16;
video.muted = true;
} else if (!isAdShowing) {
video.playbackRate = 1;
video.muted = false;
}
}
export const loadAdSpeedup = () => {
const player = document.querySelector<HTMLVideoElement>('#movie_player');
if (!player) return;
new MutationObserver((mutations) => {
for (const mutation of mutations) {
if (
mutation.type === 'attributes' &&
mutation.attributeName === 'class'
) {
const target = mutation.target as HTMLElement;
const isAdShowing =
target.classList.contains('ad-showing') ||
target.classList.contains('ad-interrupting');
speedUpAndMute(target, isAdShowing);
}
if (
mutation.type === 'childList' &&
mutation.addedNodes.length &&
mutation.target instanceof HTMLElement
) {
skipAd(mutation.target);
}
}
}).observe(player, {
attributes: true,
childList: true,
subtree: true,
});
const isAdShowing =
player.classList.contains('ad-showing') ||
player.classList.contains('ad-interrupting');
speedUpAndMute(player, isAdShowing);
skipAd(player);
};

View File

@ -10,6 +10,7 @@ import {
import injectCliqzPreload from './injectors/inject-cliqz-preload'; import injectCliqzPreload from './injectors/inject-cliqz-preload';
import { inject, isInjected } from './injectors/inject'; import { inject, isInjected } from './injectors/inject';
import { loadAdSpeedup } from './adSpeedup';
import { t } from '@/i18n'; import { t } from '@/i18n';
@ -72,6 +73,14 @@ export default createPlugin({
}, },
]; ];
}, },
renderer: {
async onPlayerApiReady(_, { getConfig }) {
const config = await getConfig();
if (config.blocker === blockers.AdSpeedup) {
await loadAdSpeedup();
}
},
},
backend: { backend: {
mainWindow: null as BrowserWindow | null, mainWindow: null as BrowserWindow | null,
async start({ getConfig, window }) { async start({ getConfig, window }) {
@ -109,7 +118,19 @@ export default createPlugin({
}, },
}, },
preload: { preload: {
script: 'window.JSON.parse = window._proxyJsonParse; window._proxyJsonParse = undefined; window.Response.prototype.json = window._proxyResponseJson; window._proxyResponseJson = undefined; 0', // see #1478
script: `const _prunerFn = window._pruner;
window._pruner = undefined;
JSON.parse = new Proxy(JSON.parse, {
apply() {
return _prunerFn(Reflect.apply(...arguments));
},
});
Response.prototype.json = new Proxy(Response.prototype.json, {
apply() {
return Reflect.apply(...arguments).then((o) => _prunerFn(o));
},
}); 0`,
async start({ getConfig }) { async start({ getConfig }) {
const config = await getConfig(); const config = await getConfig();

View File

@ -37,19 +37,9 @@ export const inject = (contextBridge) => {
// //
return o; return o;
}; }
contextBridge.exposeInMainWorld('_proxyJsonParse', new Proxy(JSON.parse, { contextBridge.exposeInMainWorld('_pruner', pruner);
apply() {
return pruner(Reflect.apply(...arguments));
},
}));
contextBridge.exposeInMainWorld('_proxyResponseJson', new Proxy(Response.prototype.json, {
apply() {
return Reflect.apply(...arguments).then((o) => pruner(o));
},
}));
} }
const chains = [ const chains = [

View File

@ -1,4 +1,5 @@
export const blockers = { export const blockers = {
WithBlocklists: 'With blocklists', WithBlocklists: 'With blocklists',
InPlayer: 'In player', InPlayer: 'In player',
AdSpeedup: 'Ad speedup',
} as const; } as const;

View File

@ -1,6 +1,7 @@
import { t } from '@/i18n'; import { t } from '@/i18n';
import { createPlugin } from '@/utils'; import { createPlugin } from '@/utils';
import { ElementFromHtml } from '@/plugins/utils/renderer'; import { ElementFromHtml } from '@/plugins/utils/renderer';
import { waitForElement } from '@/utils/wait-for-element';
import undislikeHTML from './templates/undislike.html?raw'; import undislikeHTML from './templates/undislike.html?raw';
import dislikeHTML from './templates/dislike.html?raw'; import dislikeHTML from './templates/dislike.html?raw';
@ -16,7 +17,6 @@ export default createPlugin<
changeObserver?: MutationObserver; changeObserver?: MutationObserver;
waiting: boolean; waiting: boolean;
onPageChange(): void; onPageChange(): void;
waitForElem(selector: string): Promise<HTMLElement>;
loadFullList: (event: MouseEvent) => void; loadFullList: (event: MouseEvent) => void;
applyToList(id: string, loader: HTMLElement): void; applyToList(id: string, loader: HTMLElement): void;
start(): void; start(): void;
@ -50,7 +50,7 @@ export default createPlugin<
} else { } else {
this.waiting = true; this.waiting = true;
} }
const continuations = await this.waitForElem('#continuations'); const continuations = await waitForElement<HTMLElement>('#continuations');
this.waiting = false; this.waiting = false;
//Gets the for buttons //Gets the for buttons
const buttons: Array<HTMLElement> = [ const buttons: Array<HTMLElement> = [
@ -104,21 +104,28 @@ export default createPlugin<
buttons.splice(i, 1); buttons.splice(i, 1);
i--; i--;
} else { } else {
(buttons[i].children[0].children[0] as HTMLElement).style.setProperty( (
buttons[i].children[0].children[0] as HTMLElement
).style.setProperty(
'-webkit-mask-size', '-webkit-mask-size',
`100% ${100 - ((count / listsLength) * 100)}%`, `100% ${100 - (count / listsLength) * 100}%`,
); );
} }
i++; i++;
} }
} }
const menuParent = document.querySelector('#action-buttons')?.parentElement; const menuParent =
document.querySelector('#action-buttons')?.parentElement;
if (menuParent && !document.querySelector('.like-menu')) { if (menuParent && !document.querySelector('.like-menu')) {
const menu = document.createElement('div'); const menu = document.createElement('div');
menu.id = 'ytmd-album-action-buttons'; menu.id = 'ytmd-album-action-buttons';
menu.className = 'action-buttons style-scope ytmusic-responsive-header-renderer'; menu.className =
'action-buttons style-scope ytmusic-responsive-header-renderer';
menuParent.insertBefore(menu, menuParent.children[menuParent.children.length - 1]); menuParent.insertBefore(
menu,
menuParent.children[menuParent.children.length - 1],
);
for (const button of buttons) { for (const button of buttons) {
menu.appendChild(button); menu.appendChild(button);
button.addEventListener('click', this.loadFullList); button.addEventListener('click', this.loadFullList);
@ -183,16 +190,5 @@ export default createPlugin<
button.remove(); button.remove();
} }
}, },
waitForElem(selector: string) {
return new Promise((resolve) => {
const interval = setInterval(() => {
const elem = document.querySelector<HTMLElement>(selector);
if (!elem) return;
clearInterval(interval);
resolve(elem);
});
});
},
}, },
}); });

View File

@ -25,7 +25,12 @@ export default createPlugin<
sidebarSmall: HTMLElement | null; sidebarSmall: HTMLElement | null;
ytmusicAppLayout: HTMLElement | null; ytmusicAppLayout: HTMLElement | null;
getMixedColor(color: string, key: string, alpha?: number, ratioMultiply?: number): string; getMixedColor(
color: string,
key: string,
alpha?: number,
ratioMultiply?: number,
): string;
updateColor(): void; updateColor(): void;
}, },
{ {
@ -91,7 +96,10 @@ export default createPlugin<
this.ytmusicAppLayout = document.querySelector<HTMLElement>('#layout'); this.ytmusicAppLayout = document.querySelector<HTMLElement>('#layout');
const config = await getConfig(); const config = await getConfig();
document.documentElement.style.setProperty(RATIO_KEY, `${~~(config.ratio * 100)}%`); document.documentElement.style.setProperty(
RATIO_KEY,
`${~~(config.ratio * 100)}%`,
);
}, },
onPlayerApiReady(playerApi) { onPlayerApiReady(playerApi) {
const fastAverageColor = new FastAverageColor(); const fastAverageColor = new FastAverageColor();
@ -100,10 +108,12 @@ export default createPlugin<
if (event.detail.name !== 'dataloaded') return; if (event.detail.name !== 'dataloaded') return;
const playerResponse = playerApi.getPlayerResponse(); const playerResponse = playerApi.getPlayerResponse();
const thumbnail = playerResponse?.videoDetails?.thumbnail?.thumbnails?.at(0); const thumbnail =
playerResponse?.videoDetails?.thumbnail?.thumbnails?.at(0);
if (!thumbnail) return; if (!thumbnail) return;
const albumColor = await fastAverageColor.getColorAsync(thumbnail.url) const albumColor = await fastAverageColor
.getColorAsync(thumbnail.url)
.catch((err) => { .catch((err) => {
console.error(err); console.error(err);
return null; return null;
@ -120,8 +130,14 @@ export default createPlugin<
this.darkColor = this.darkColor?.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(
document.documentElement.style.setProperty(DARK_COLOR_KEY, `${~~this.darkColor.red()}, ${~~this.darkColor.green()}, ${~~this.darkColor.blue()}`); 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 { } else {
document.documentElement.style.setProperty(COLOR_KEY, '0, 0, 0'); document.documentElement.style.setProperty(COLOR_KEY, '0, 0, 0');
document.documentElement.style.setProperty(DARK_COLOR_KEY, '0, 0, 0'); document.documentElement.style.setProperty(DARK_COLOR_KEY, '0, 0, 0');
@ -131,7 +147,10 @@ export default createPlugin<
}); });
}, },
onConfigChange(config) { onConfigChange(config) {
document.documentElement.style.setProperty(RATIO_KEY, `${~~(config.ratio * 100)}%`); document.documentElement.style.setProperty(
RATIO_KEY,
`${~~(config.ratio * 100)}%`,
);
}, },
getMixedColor(color: string, key: string, alpha = 1, ratioMultiply) { getMixedColor(color: string, key: string, alpha = 1, ratioMultiply) {
const keyColor = `rgba(var(${key}), ${alpha})`; const keyColor = `rgba(var(${key}), ${alpha})`;
@ -181,11 +200,23 @@ export default createPlugin<
'--yt-spec-black-1-alpha-95': 'rgba(40,40,40,0.95)', '--yt-spec-black-1-alpha-95': 'rgba(40,40,40,0.95)',
}; };
Object.entries(variableMap).map(([variable, color]) => { Object.entries(variableMap).map(([variable, color]) => {
document.documentElement.style.setProperty(variable, this.getMixedColor(color, COLOR_KEY), 'important'); document.documentElement.style.setProperty(
variable,
this.getMixedColor(color, COLOR_KEY),
'important',
);
}); });
document.body.style.setProperty('background', this.getMixedColor('#030303', COLOR_KEY), 'important'); document.body.style.setProperty(
document.documentElement.style.setProperty('--ytmusic-background', this.getMixedColor('#030303', DARK_COLOR_KEY), 'important'); 'background',
this.getMixedColor('#030303', COLOR_KEY),
'important',
);
document.documentElement.style.setProperty(
'--ytmusic-background',
this.getMixedColor('#030303', DARK_COLOR_KEY),
'important',
);
}, },
}, },
}); });

View File

@ -4,6 +4,7 @@ import { t } from '@/i18n';
import { createPlugin } from '@/utils'; import { createPlugin } from '@/utils';
import { menu } from './menu'; import { menu } from './menu';
import { AmbientModePluginConfig } from './types'; import { AmbientModePluginConfig } from './types';
import { waitForElement } from '@/utils/wait-for-element';
const defaultConfig: AmbientModePluginConfig = { const defaultConfig: AmbientModePluginConfig = {
enabled: false, enabled: false,
@ -36,7 +37,7 @@ export default createPlugin({
unregister: null as (() => void) | null, unregister: null as (() => void) | null,
update: null as (() => void) | null, update: null as (() => void) | null,
interval: null as NodeJS.Timeout | null, interval: null as NodeJS.Timeout | null,
lastMediaType: null as "video" | "image" | null, lastMediaType: null as 'video' | 'image' | null,
lastVideoSource: null as string | null, lastVideoSource: null as string | null,
lastImageSource: null as string | null, lastImageSource: null as string | null,
@ -52,9 +53,16 @@ export default createPlugin({
const songImage = document.querySelector<HTMLImageElement>('#song-image'); const songImage = document.querySelector<HTMLImageElement>('#song-image');
const songVideo = document.querySelector<HTMLDivElement>('#song-video'); const songVideo = document.querySelector<HTMLDivElement>('#song-video');
const image = songImage?.querySelector<HTMLImageElement>('yt-img-shadow > img'); const image = songImage?.querySelector<HTMLImageElement>(
const video = songVideo?.querySelector<HTMLVideoElement>('.html5-video-container > video'); 'yt-img-shadow > img',
const videoWrapper = document.querySelector('#song-video > .player-wrapper'); );
const video = await waitForElement<HTMLVideoElement>(
'.html5-video-container > video',
);
const videoWrapper = document.querySelector(
'#song-video > .player-wrapper',
);
const injectBlurImage = () => { const injectBlurImage = () => {
if (!songImage || !image) return null; if (!songImage || !image) return null;
@ -93,7 +101,9 @@ export default createPlugin({
const blurCanvas = document.createElement('canvas'); const blurCanvas = document.createElement('canvas');
blurCanvas.classList.add('html5-blur-canvas'); blurCanvas.classList.add('html5-blur-canvas');
const context = blurCanvas.getContext('2d', { willReadFrequently: true }); const context = blurCanvas.getContext('2d', {
willReadFrequently: true,
});
/* effect */ /* effect */
let lastEffectWorkId: number | null = null; let lastEffectWorkId: number | null = null;
@ -107,14 +117,18 @@ export default createPlugin({
if (!context) return; if (!context) return;
const width = this.qualityRatio; const width = this.qualityRatio;
let height = Math.max(Math.floor((blurCanvas.height / blurCanvas.width) * width), 1,); let height = Math.max(
Math.floor((blurCanvas.height / blurCanvas.width) * width),
1,
);
if (!Number.isFinite(height)) height = width; if (!Number.isFinite(height)) height = width;
if (!height) return; if (!height) return;
context.globalAlpha = 1; context.globalAlpha = 1;
if (lastImageData) { if (lastImageData) {
const frameOffset = (1 / this.buffer) * (1000 / this.interpolationTime); const frameOffset =
context.globalAlpha = 1 - (frameOffset * 2); // because of alpha value must be < 1 (1 / this.buffer) * (1000 / this.interpolationTime);
context.globalAlpha = 1 - frameOffset * 2; // because of alpha value must be < 1
context.putImageData(lastImageData, 0, 0); context.putImageData(lastImageData, 0, 0);
context.globalAlpha = frameOffset; context.globalAlpha = frameOffset;
} }
@ -135,7 +149,9 @@ export default createPlugin({
if (newWidth === 0 || newHeight === 0) return; if (newWidth === 0 || newHeight === 0) return;
blurCanvas.width = this.qualityRatio; blurCanvas.width = this.qualityRatio;
blurCanvas.height = Math.floor((newHeight / newWidth) * this.qualityRatio); blurCanvas.height = Math.floor(
(newHeight / newWidth) * this.qualityRatio,
);
if (this.isFullscreen) blurCanvas.classList.add('fullscreen'); if (this.isFullscreen) blurCanvas.classList.add('fullscreen');
else blurCanvas.classList.remove('fullscreen'); else blurCanvas.classList.remove('fullscreen');
@ -149,7 +165,10 @@ export default createPlugin({
/* hooking */ /* hooking */
let canvasInterval: NodeJS.Timeout | null = null; let canvasInterval: NodeJS.Timeout | null = null;
canvasInterval = setInterval(onSync, Math.max(1, Math.ceil(1000 / this.buffer))); canvasInterval = setInterval(
onSync,
Math.max(1, Math.ceil(1000 / this.buffer)),
);
const onPause = () => { const onPause = () => {
if (canvasInterval) clearInterval(canvasInterval); if (canvasInterval) clearInterval(canvasInterval);
@ -157,7 +176,10 @@ export default createPlugin({
}; };
const onPlay = () => { const onPlay = () => {
if (canvasInterval) clearInterval(canvasInterval); if (canvasInterval) clearInterval(canvasInterval);
canvasInterval = setInterval(onSync, Math.max(1, Math.ceil(1000 / this.buffer))); canvasInterval = setInterval(
onSync,
Math.max(1, Math.ceil(1000 / this.buffer)),
);
}; };
songVideo.addEventListener('pause', onPause); songVideo.addEventListener('pause', onPause);
songVideo.addEventListener('play', onPlay); songVideo.addEventListener('play', onPlay);
@ -179,12 +201,12 @@ export default createPlugin({
const isVideoMode = () => { const isVideoMode = () => {
const songVideo = document.querySelector<HTMLDivElement>('#song-video'); const songVideo = document.querySelector<HTMLDivElement>('#song-video');
if (!songVideo) { if (!songVideo) {
this.lastMediaType = "image"; this.lastMediaType = 'image';
return false; return false;
} }
const isVideo = getComputedStyle(songVideo).display !== 'none'; const isVideo = getComputedStyle(songVideo).display !== 'none';
this.lastMediaType = isVideo ? "video" : "image"; this.lastMediaType = isVideo ? 'video' : 'image';
return isVideo; return isVideo;
}; };
@ -196,16 +218,25 @@ export default createPlugin({
if (isPageOpen) { if (isPageOpen) {
const isVideo = isVideoMode(); const isVideo = isVideoMode();
if (!force) { if (!force) {
if (this.lastMediaType === "video" && this.lastVideoSource === video?.src) return false; if (
if (this.lastMediaType === "image" && this.lastImageSource === image?.src) return false; this.lastMediaType === 'video' &&
this.lastVideoSource === video?.src
)
return false;
if (
this.lastMediaType === 'image' &&
this.lastImageSource === image?.src
)
return false;
} }
this.unregister?.(); this.unregister?.();
this.unregister = (isVideo ? injectBlurVideo() : injectBlurImage()) ?? null; this.unregister =
(isVideo ? injectBlurVideo() : injectBlurImage()) ?? null;
} else { } else {
this.unregister?.(); this.unregister?.();
this.unregister = null; this.unregister = null;
} }
} };
/* needed for switching between different views (e.g. miniplayer) */ /* needed for switching between different views (e.g. miniplayer) */
const observer = new MutationObserver((mutationsList) => { const observer = new MutationObserver((mutationsList) => {

View File

@ -1,14 +1,24 @@
import { t } from "@/i18n"; import { MenuItemConstructorOptions } from 'electron';
import { MenuContext } from "@/types/contexts";
import { MenuItemConstructorOptions } from "electron"; import { t } from '@/i18n';
import { AmbientModePluginConfig } from "./types"; import { MenuContext } from '@/types/contexts';
import { AmbientModePluginConfig } from './types';
export interface menuParameters { export interface menuParameters {
getConfig: () => AmbientModePluginConfig | Promise<AmbientModePluginConfig>; getConfig: () => AmbientModePluginConfig | Promise<AmbientModePluginConfig>;
setConfig: (conf: Partial<Omit<AmbientModePluginConfig, "enabled">>) => void | Promise<void>; setConfig: (
conf: Partial<Omit<AmbientModePluginConfig, 'enabled'>>,
) => void | Promise<void>;
} }
export const menu: (ctx: MenuContext<AmbientModePluginConfig>) => MenuItemConstructorOptions[] | Promise<MenuItemConstructorOptions[]> = async ({ getConfig, setConfig }: menuParameters) => { export const menu: (
ctx: MenuContext<AmbientModePluginConfig>,
) =>
| MenuItemConstructorOptions[]
| Promise<MenuItemConstructorOptions[]> = async ({
getConfig,
setConfig,
}: menuParameters) => {
const interpolationTimeList = [0, 500, 1000, 1500, 2000, 3000, 4000, 5000]; const interpolationTimeList = [0, 500, 1000, 1500, 2000, 3000, 4000, 5000];
const qualityList = [10, 25, 50, 100, 200, 500, 1000]; const qualityList = [10, 25, 50, 100, 200, 500, 1000];
const sizeList = [100, 110, 125, 150, 175, 200, 300]; const sizeList = [100, 110, 125, 150, 175, 200, 300];
@ -107,4 +117,4 @@ export const menu: (ctx: MenuContext<AmbientModePluginConfig>) => MenuItemConstr
}, },
}, },
]; ];
} };

View File

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

View File

@ -0,0 +1,121 @@
import { jwt } from 'hono/jwt';
import { OpenAPIHono as Hono } from '@hono/zod-openapi';
import { cors } from 'hono/cors';
import { swaggerUI } from '@hono/swagger-ui';
import { serve } from '@hono/node-server';
import registerCallback from '@/providers/song-info';
import { createBackend } from '@/utils';
import { JWTPayloadSchema } from './scheme';
import { registerAuth, registerControl } from './routes';
import { type APIServerConfig, AuthStrategy } from '../config';
import type { BackendType } from './types';
export const backend = createBackend<BackendType, APIServerConfig>({
async start(ctx) {
const config = await ctx.getConfig();
await this.init(ctx);
registerCallback((songInfo) => {
this.songInfo = songInfo;
});
this.run(config.hostname, config.port);
},
stop() {
this.end();
},
onConfigChange(config) {
if (
this.oldConfig?.hostname === config.hostname &&
this.oldConfig?.port === config.port
) {
this.oldConfig = config;
return;
}
this.end();
this.run(config.hostname, config.port);
this.oldConfig = config;
},
// Custom
async init(ctx) {
const config = await ctx.getConfig();
this.app = new Hono();
this.app.use('*', cors());
// middlewares
this.app.use('/api/*', async (ctx, next) => {
if (config.authStrategy !== AuthStrategy.NONE) {
return await jwt({
secret: config.secret,
})(ctx, next);
}
await next();
});
this.app.use('/api/*', async (ctx, next) => {
const result = await JWTPayloadSchema.spa(await ctx.get('jwtPayload'));
const isAuthorized =
config.authStrategy === AuthStrategy.NONE ||
(result.success && config.authorizedClients.includes(result.data.id));
if (!isAuthorized) {
ctx.status(401);
return ctx.body('Unauthorized');
}
return await next();
});
// routes
registerControl(this.app, ctx, () => this.songInfo);
registerAuth(this.app, ctx);
// swagger
this.app.openAPIRegistry.registerComponent(
'securitySchemes',
'bearerAuth',
{
type: 'http',
scheme: 'bearer',
bearerFormat: 'JWT',
},
);
this.app.doc('/doc', {
openapi: '3.1.0',
info: {
version: '1.0.0',
title: 'Youtube Music API Server',
},
security: [
{
bearerAuth: [],
},
],
});
this.app.get('/swagger', swaggerUI({ url: '/doc' }));
},
run(hostname, port) {
if (!this.app) return;
try {
this.server = serve({
fetch: this.app.fetch.bind(this.app),
port,
hostname,
});
} catch (err) {
console.error(err);
}
},
end() {
this.server?.close();
this.server = undefined;
},
});

View File

@ -0,0 +1,95 @@
import { createRoute, z } from '@hono/zod-openapi';
import { dialog } from 'electron';
import { sign } from 'hono/jwt';
import { getConnInfo } from '@hono/node-server/conninfo';
import { t } from '@/i18n';
import { type APIServerConfig, AuthStrategy } from '../../config';
import type { JWTPayload } from '../scheme';
import type { HonoApp } from '../types';
import type { BackendContext } from '@/types/contexts';
const routes = {
request: createRoute({
method: 'post',
path: '/auth/{id}',
summary: '',
description: '',
security: [],
request: {
params: z.object({
id: z.string(),
}),
},
responses: {
200: {
description: 'Success',
content: {
'application/json': {
schema: z.object({
accessToken: z.string(),
}),
},
},
},
403: {
description: 'Forbidden',
},
},
}),
};
export const register = (
app: HonoApp,
{ getConfig, setConfig }: BackendContext<APIServerConfig>,
) => {
app.openapi(routes.request, async (ctx) => {
const config = await getConfig();
const { id } = ctx.req.param();
if (config.authorizedClients.includes(id)) {
// SKIP CHECK
} else if (config.authStrategy === AuthStrategy.AUTH_AT_FIRST) {
const result = await dialog.showMessageBox({
title: t('plugins.api-server.dialog.request.title'),
message: t('plugins.api-server.dialog.request.message', {
origin: getConnInfo(ctx).remote.address,
ID: id,
}),
buttons: [
t('plugins.api-server.dialog.request.buttons.allow'),
t('plugins.api-server.dialog.request.buttons.deny'),
],
defaultId: 1,
cancelId: 1,
});
if (result.response === 1) {
ctx.status(403);
return ctx.body(null);
}
} else if (config.authStrategy === AuthStrategy.NONE) {
// SKIP CHECK
}
setConfig({
authorizedClients: [...config.authorizedClients, id],
});
const token = await sign(
{
id,
iat: ~~(Date.now() / 1000),
} satisfies JWTPayload,
config.secret,
);
ctx.status(200);
return ctx.json({
accessToken: token,
});
});
};

View File

@ -0,0 +1,446 @@
import { createRoute, z } from '@hono/zod-openapi';
import { ipcMain } from 'electron';
import getSongControls from '@/providers/song-controls';
import {
AuthHeadersSchema,
type ResponseSongInfo,
SongInfoSchema,
GoForwardScheme,
GoBackSchema,
SwitchRepeatSchema,
SetVolumeSchema,
SetFullscreenSchema,
} from '../scheme';
import type { SongInfo } from '@/providers/song-info';
import type { BackendContext } from '@/types/contexts';
import type { APIServerConfig } from '../../config';
import type { HonoApp } from '../types';
import type { QueueResponse } from '@/types/youtube-music-desktop-internal';
const API_VERSION = 'v1';
const routes = {
previous: createRoute({
method: 'post',
path: `/api/${API_VERSION}/previous`,
summary: 'play previous song',
description: 'Plays the previous song in the queue',
responses: {
204: {
description: 'Success',
},
},
}),
next: createRoute({
method: 'post',
path: `/api/${API_VERSION}/next`,
summary: 'play next song',
description: 'Plays the next song in the queue',
responses: {
204: {
description: 'Success',
},
},
}),
play: createRoute({
method: 'post',
path: `/api/${API_VERSION}/play`,
summary: 'Play',
description: 'Change the state of the player to play',
responses: {
204: {
description: 'Success',
},
},
}),
pause: createRoute({
method: 'post',
path: `/api/${API_VERSION}/pause`,
summary: 'Pause',
description: 'Change the state of the player to pause',
responses: {
204: {
description: 'Success',
},
},
}),
togglePlay: createRoute({
method: 'post',
path: `/api/${API_VERSION}/toggle-play`,
summary: 'Toggle play/pause',
description:
'Change the state of the player to play if paused, or pause if playing',
responses: {
204: {
description: 'Success',
},
},
}),
like: createRoute({
method: 'post',
path: `/api/${API_VERSION}/like`,
summary: 'like song',
description: 'Set the current song as liked',
responses: {
204: {
description: 'Success',
},
},
}),
dislike: createRoute({
method: 'post',
path: `/api/${API_VERSION}/dislike`,
summary: 'dislike song',
description: 'Set the current song as disliked',
responses: {
204: {
description: 'Success',
},
},
}),
goBack: createRoute({
method: 'post',
path: `/api/${API_VERSION}/go-back`,
summary: 'go back',
description: 'Move the current song back by a number of seconds',
request: {
headers: AuthHeadersSchema,
body: {
description: 'seconds to go back',
content: {
'application/json': {
schema: GoBackSchema,
},
},
},
},
responses: {
204: {
description: 'Success',
},
},
}),
goForward: createRoute({
method: 'post',
path: `/api/${API_VERSION}/go-forward`,
summary: 'go forward',
description: 'Move the current song forward by a number of seconds',
request: {
headers: AuthHeadersSchema,
body: {
description: 'seconds to go forward',
content: {
'application/json': {
schema: GoForwardScheme,
},
},
},
},
responses: {
204: {
description: 'Success',
},
},
}),
shuffle: createRoute({
method: 'post',
path: `/api/${API_VERSION}/shuffle`,
summary: 'shuffle',
description: 'Shuffle the queue',
responses: {
204: {
description: 'Success',
},
},
}),
switchRepeat: createRoute({
method: 'post',
path: `/api/${API_VERSION}/switch-repeat`,
summary: 'switch repeat',
description: 'Switch the repeat mode',
request: {
headers: AuthHeadersSchema,
body: {
description: 'number of times to click the repeat button',
content: {
'application/json': {
schema: SwitchRepeatSchema,
},
},
},
},
responses: {
204: {
description: 'Success',
},
},
}),
setVolume: createRoute({
method: 'post',
path: `/api/${API_VERSION}/volume`,
summary: 'set volume',
description: 'Set the volume of the player',
request: {
headers: AuthHeadersSchema,
body: {
description: 'volume to set',
content: {
'application/json': {
schema: SetVolumeSchema,
},
},
},
},
responses: {
204: {
description: 'Success',
},
},
}),
setFullscreen: createRoute({
method: 'post',
path: `/api/${API_VERSION}/fullscreen`,
summary: 'set fullscreen',
description: 'Set the fullscreen state of the player',
request: {
headers: AuthHeadersSchema,
body: {
description: 'fullscreen state',
content: {
'application/json': {
schema: SetFullscreenSchema,
},
},
},
},
responses: {
204: {
description: 'Success',
},
},
}),
toggleMute: createRoute({
method: 'post',
path: `/api/${API_VERSION}/toggle-mute`,
summary: 'toggle mute',
description: 'Toggle the mute state of the player',
responses: {
204: {
description: 'Success',
},
},
}),
getFullscreenState: createRoute({
method: 'get',
path: `/api/${API_VERSION}/fullscreen`,
summary: 'get fullscreen state',
description: 'Get the current fullscreen state',
responses: {
200: {
description: 'Success',
content: {
'application/json': {
schema: z.object({
state: z.boolean(),
}),
},
},
},
},
}),
queueInfo: createRoute({
method: 'get',
path: `/api/${API_VERSION}/queue-info`,
summary: 'get current queue info',
description: 'Get the current queue info',
responses: {
200: {
description: 'Success',
content: {
'application/json': {
schema: z.object({}),
},
},
},
204: {
description: 'No queue info',
},
},
}),
songInfo: createRoute({
method: 'get',
path: `/api/${API_VERSION}/song-info`,
summary: 'get current song info',
description: 'Get the current song info',
responses: {
200: {
description: 'Success',
content: {
'application/json': {
schema: SongInfoSchema,
},
},
},
204: {
description: 'No song info',
},
},
}),
};
export const register = (
app: HonoApp,
{ window }: BackendContext<APIServerConfig>,
songInfoGetter: () => SongInfo | undefined,
) => {
const controller = getSongControls(window);
app.openapi(routes.previous, (ctx) => {
controller.previous();
ctx.status(204);
return ctx.body(null);
});
app.openapi(routes.next, (ctx) => {
controller.next();
ctx.status(204);
return ctx.body(null);
});
app.openapi(routes.play, (ctx) => {
controller.play();
ctx.status(204);
return ctx.body(null);
});
app.openapi(routes.pause, (ctx) => {
controller.pause();
ctx.status(204);
return ctx.body(null);
});
app.openapi(routes.togglePlay, (ctx) => {
controller.playPause();
ctx.status(204);
return ctx.body(null);
});
app.openapi(routes.like, (ctx) => {
controller.like();
ctx.status(204);
return ctx.body(null);
});
app.openapi(routes.dislike, (ctx) => {
controller.dislike();
ctx.status(204);
return ctx.body(null);
});
app.openapi(routes.goBack, (ctx) => {
const { seconds } = ctx.req.valid('json');
controller.goBack(seconds);
ctx.status(204);
return ctx.body(null);
});
app.openapi(routes.goForward, (ctx) => {
const { seconds } = ctx.req.valid('json');
controller.goForward(seconds);
ctx.status(204);
return ctx.body(null);
});
app.openapi(routes.shuffle, (ctx) => {
controller.shuffle();
ctx.status(204);
return ctx.body(null);
});
app.openapi(routes.switchRepeat, (ctx) => {
const { iteration } = ctx.req.valid('json');
controller.switchRepeat(iteration);
ctx.status(204);
return ctx.body(null);
});
app.openapi(routes.setVolume, (ctx) => {
const { volume } = ctx.req.valid('json');
controller.setVolume(volume);
ctx.status(204);
return ctx.body(null);
});
app.openapi(routes.setFullscreen, (ctx) => {
const { state } = ctx.req.valid('json');
controller.setFullscreen(state);
ctx.status(204);
return ctx.body(null);
});
app.openapi(routes.toggleMute, (ctx) => {
controller.muteUnmute();
ctx.status(204);
return ctx.body(null);
});
app.openapi(routes.getFullscreenState, async (ctx) => {
const stateResponsePromise = new Promise<boolean>((resolve) => {
ipcMain.once(
'ytmd:set-fullscreen',
(_, isFullscreen: boolean | undefined) => {
return resolve(!!isFullscreen);
},
);
controller.requestFullscreenInformation();
});
const fullscreen = await stateResponsePromise;
ctx.status(200);
return ctx.json({ state: fullscreen });
});
app.openapi(routes.queueInfo, async (ctx) => {
const queueResponsePromise = new Promise<QueueResponse>((resolve) => {
ipcMain.once('ytmd:get-queue-response', (_, queue: QueueResponse) => {
return resolve(queue);
});
controller.requestQueueInformation();
});
const info = await queueResponsePromise;
if (!info) {
ctx.status(204);
return ctx.body(null);
}
ctx.status(200);
return ctx.json(info);
});
app.openapi(routes.songInfo, (ctx) => {
const info = songInfoGetter();
if (!info) {
ctx.status(204);
return ctx.body(null);
}
const body = { ...info };
delete body.image;
ctx.status(200);
return ctx.json(body satisfies ResponseSongInfo);
});
};

View File

@ -0,0 +1,2 @@
export { register as registerControl } from './control';
export { register as registerAuth } from './auth';

View File

@ -0,0 +1,13 @@
import { z } from '@hono/zod-openapi';
export const AuthHeadersSchema = z.object({
authorization: z.string().openapi({
example: 'Bearer token',
}),
});
export type JWTPayload = z.infer<typeof JWTPayloadSchema>;
export const JWTPayloadSchema = z.object({
id: z.string(),
iat: z.number(),
});

View File

@ -0,0 +1,5 @@
import { z } from '@hono/zod-openapi';
export const GoBackSchema = z.object({
seconds: z.number(),
});

View File

@ -0,0 +1,5 @@
import { z } from '@hono/zod-openapi';
export const GoForwardScheme = z.object({
seconds: z.number(),
});

View File

@ -0,0 +1,7 @@
export * from './auth';
export * from './song-info';
export * from './go-back';
export * from './go-forward';
export * from './switch-repeat';
export * from './set-volume';
export * from './set-fullscreen';

View File

@ -0,0 +1,5 @@
import { z } from '@hono/zod-openapi';
export const SetFullscreenSchema = z.object({
state: z.boolean(),
});

View File

@ -0,0 +1,5 @@
import { z } from '@hono/zod-openapi';
export const SetVolumeSchema = z.object({
volume: z.number(),
});

View File

@ -0,0 +1,26 @@
import { z } from '@hono/zod-openapi';
import { MediaType } from '@/providers/song-info';
export type ResponseSongInfo = z.infer<typeof SongInfoSchema>;
export const SongInfoSchema = z.object({
title: z.string(),
artist: z.string(),
views: z.number(),
uploadDate: z.string().optional(),
imageSrc: z.string().nullable().optional(),
isPaused: z.boolean().optional(),
songDuration: z.number(),
elapsedSeconds: z.number().optional(),
url: z.string().optional(),
album: z.string().nullable().optional(),
videoId: z.string(),
playlistId: z.string().optional(),
mediaType: z.enum([
MediaType.Audio,
MediaType.OriginalMusicVideo,
MediaType.UserGeneratedContent,
MediaType.PodcastEpisode,
MediaType.OtherVideo,
]),
});

View File

@ -0,0 +1,5 @@
import { z } from '@hono/zod-openapi';
export const SwitchRepeatSchema = z.object({
iteration: z.number(),
});

View File

@ -0,0 +1,18 @@
import { OpenAPIHono as Hono } from '@hono/zod-openapi';
import { serve } from '@hono/node-server';
import type { BackendContext } from '@/types/contexts';
import type { SongInfo } from '@/providers/song-info';
import type { APIServerConfig } from '../config';
export type HonoApp = Hono;
export type BackendType = {
app?: HonoApp;
server?: ReturnType<typeof serve>;
oldConfig?: APIServerConfig;
songInfo?: SongInfo;
init: (ctx: BackendContext<APIServerConfig>) => Promise<void>;
run: (hostname: string, port: number) => void;
end: () => void;
};

View File

@ -0,0 +1,24 @@
export enum AuthStrategy {
AUTH_AT_FIRST = 'AUTH_AT_FIRST',
NONE = 'NONE',
}
export interface APIServerConfig {
enabled: boolean;
hostname: string;
port: number;
authStrategy: AuthStrategy;
secret: string;
authorizedClients: string[];
}
export const defaultAPIServerConfig: APIServerConfig = {
enabled: true,
hostname: '0.0.0.0',
port: 26538,
authStrategy: AuthStrategy.AUTH_AT_FIRST,
secret: Date.now().toString(36),
authorizedClients: [],
};

View File

@ -0,0 +1,17 @@
import { createPlugin } from '@/utils';
import { t } from '@/i18n';
import { defaultAPIServerConfig } from './config';
import { onMenu } from './menu';
import { backend } from './backend';
export default createPlugin({
name: () => t('plugins.api-server.name'),
description: () => t('plugins.api-server.description'),
restartNeeded: false,
config: defaultAPIServerConfig,
addedVersion: '3.6.X',
menu: onMenu,
backend,
});

View File

@ -0,0 +1,97 @@
import prompt from 'custom-electron-prompt';
import { t } from '@/i18n';
import promptOptions from '@/providers/prompt-options';
import {
type APIServerConfig,
AuthStrategy,
defaultAPIServerConfig,
} from './config';
import type { MenuContext } from '@/types/contexts';
import type { MenuTemplate } from '@/menu';
export const onMenu = async ({
getConfig,
setConfig,
window,
}: MenuContext<APIServerConfig>): Promise<MenuTemplate> => {
const config = await getConfig();
return [
{
label: t('plugins.api-server.menu.hostname.label'),
type: 'normal',
async click() {
const config = await getConfig();
const newHostname =
(await prompt(
{
title: t('plugins.api-server.prompt.hostname.title'),
label: t('plugins.api-server.prompt.hostname.label'),
value: config.hostname,
type: 'input',
width: 380,
...promptOptions(),
},
window,
)) ??
config.hostname ??
defaultAPIServerConfig.hostname;
setConfig({ ...config, hostname: newHostname });
},
},
{
label: t('plugins.api-server.menu.port.label'),
type: 'normal',
async click() {
const config = await getConfig();
const newPort =
(await prompt(
{
title: t('plugins.api-server.prompt.port.title'),
label: t('plugins.api-server.prompt.port.label'),
value: config.port,
type: 'counter',
counterOptions: { minimum: 0, maximum: 65565 },
width: 380,
...promptOptions(),
},
window,
)) ??
config.port ??
defaultAPIServerConfig.port;
setConfig({ ...config, port: newPort });
},
},
{
label: t('plugins.api-server.menu.auth-strategy.label'),
type: 'submenu',
submenu: [
{
label: t(
'plugins.api-server.menu.auth-strategy.submenu.auth-at-first.label',
),
type: 'radio',
checked: config.authStrategy === AuthStrategy.AUTH_AT_FIRST,
click() {
setConfig({ ...config, authStrategy: AuthStrategy.AUTH_AT_FIRST });
},
},
{
label: t('plugins.api-server.menu.auth-strategy.submenu.none.label'),
type: 'radio',
checked: config.authStrategy === AuthStrategy.NONE,
click() {
setConfig({ ...config, authStrategy: AuthStrategy.NONE });
},
},
],
},
];
};

View File

@ -15,7 +15,10 @@ export default createPlugin({
this.styleSheet = new CSSStyleSheet(); this.styleSheet = new CSSStyleSheet();
await this.styleSheet.replace(style); await this.styleSheet.replace(style);
document.adoptedStyleSheets = [...document.adoptedStyleSheets, this.styleSheet]; document.adoptedStyleSheets = [
...document.adoptedStyleSheets,
this.styleSheet,
];
}, },
async stop() { async stop() {
await this.styleSheet?.replace(''); await this.styleSheet?.replace('');

View File

@ -34,7 +34,7 @@ export default createPlugin<
{ {
label: t('plugins.captions-selector.menu.autoload'), label: t('plugins.captions-selector.menu.autoload'),
type: 'checkbox', type: 'checkbox',
checked: config.autoload as boolean, checked: config.autoload,
click(item) { click(item) {
setConfig({ autoload: item.checked }); setConfig({ autoload: item.checked });
}, },
@ -42,7 +42,7 @@ export default createPlugin<
{ {
label: t('plugins.captions-selector.menu.disable-captions'), label: t('plugins.captions-selector.menu.disable-captions'),
type: 'checkbox', type: 'checkbox',
checked: config.disableCaptions as boolean, checked: config.disableCaptions,
click(item) { click(item) {
setConfig({ disableCaptions: item.checked }); setConfig({ disableCaptions: item.checked });
}, },

View File

@ -64,7 +64,7 @@ interface VolumeFade {
// Main class // Main class
export class VolumeFader { export class VolumeFader {
private readonly media: HTMLMediaElement; private readonly media: HTMLMediaElement;
private readonly logger: VolumeLogger | false; private readonly logger: VolumeLogger | null;
private scale: { private scale: {
internalToVolume: (level: number) => number; internalToVolume: (level: number) => number;
volumeToInternal: (level: number) => number; volumeToInternal: (level: number) => number;
@ -100,7 +100,7 @@ export class VolumeFader {
this.logger = options.logger; this.logger = options.logger;
} else { } else {
// Set log function explicitly to false // Set log function explicitly to false
this.logger = false; this.logger = null;
} }
// Linear volume fading? // Linear volume fading?
@ -112,7 +112,7 @@ export class VolumeFader {
}; };
// Log setting // Log setting
this.logger && this.logger('Using linear fading.'); this.logger?.('Using linear fading.');
} }
// No linear, but logarithmic fading… // No linear, but logarithmic fading…
else { else {
@ -152,9 +152,8 @@ export class VolumeFader {
}; };
// Log setting if not default // Log setting if not default
options.fadeScaling && if (options.fadeScaling)
this.logger && this.logger?.(
this.logger(
'Using logarithmic fading with ' + 'Using logarithmic fading with ' +
String(10 * dynamicRange) + String(10 * dynamicRange) +
' dB dynamic range.', ' dB dynamic range.',
@ -170,8 +169,7 @@ export class VolumeFader {
this.media.volume = options.initialVolume; this.media.volume = options.initialVolume;
// Log setting // Log setting
this.logger && this.logger?.('Set initial volume to ' + String(this.media.volume) + '.');
this.logger('Set initial volume to ' + String(this.media.volume) + '.');
} }
// Fade duration given? // Fade duration given?
@ -187,7 +185,7 @@ export class VolumeFader {
this.active = false; this.active = false;
// Initialization done // Initialization done
this.logger && this.logger('Initialized for', this.media); this.logger?.('Initialized for', this.media);
} }
/** /**
@ -236,8 +234,7 @@ export class VolumeFader {
this.fadeDuration = fadeDuration; this.fadeDuration = fadeDuration;
// Log setting // Log setting
this.logger && this.logger?.('Set fade duration to ' + String(fadeDuration) + ' ms.');
this.logger('Set fade duration to ' + String(fadeDuration) + ' ms.');
} else { } else {
// Abort and throw an exception // Abort and throw an exception
throw new TypeError('Positive number expected as fade duration!'); throw new TypeError('Positive number expected as fade duration!');
@ -279,7 +276,7 @@ export class VolumeFader {
this.start(); this.start();
// Log new fade // Log new fade
this.logger && this.logger('New fade started:', this.fade); this.logger?.('New fade started:', this.fade);
// Return instance for chaining // Return instance for chaining
return this; return this;
@ -313,7 +310,7 @@ export class VolumeFader {
// Compute current level on internal scale // Compute current level on internal scale
const level = const level =
(progress * (this.fade.volume.end - this.fade.volume.start)) + progress * (this.fade.volume.end - this.fade.volume.start) +
this.fade.volume.start; this.fade.volume.start;
// Map fade level to volume level and apply it to media element // Map fade level to volume level and apply it to media element
@ -323,8 +320,7 @@ export class VolumeFader {
window.requestAnimationFrame(this.updateVolume.bind(this)); window.requestAnimationFrame(this.updateVolume.bind(this));
} else { } else {
// Log end of fade // Log end of fade
this.logger && this.logger?.('Fade to ' + String(this.fade.volume.end) + ' complete.');
this.logger('Fade to ' + String(this.fade.volume.end) + ' complete.');
// Time is up, jump to target volume // Time is up, jump to target volume
this.media.volume = this.scale.internalToVolume(this.fade.volume.end); this.media.volume = this.scale.internalToVolume(this.fade.volume.end);
@ -333,7 +329,7 @@ export class VolumeFader {
this.active = false; this.active = false;
// Done, call back (if callable) // Done, call back (if callable)
typeof this.fade.callback === 'function' && this.fade.callback(); if (typeof this.fade.callback === 'function') this.fade.callback();
// Clear fade // Clear fade
this.fade = undefined; this.fade = undefined;
@ -382,7 +378,7 @@ export class VolumeFader {
input = Math.log10(input); input = Math.log10(input);
// Scale minus something × 10 dB to 0…1 (clipping at 0) // Scale minus something × 10 dB to 0…1 (clipping at 0)
return Math.max(1 + (input / dynamicRange), 0); return Math.max(1 + input / dynamicRange, 0);
} }
} }

View File

@ -191,7 +191,7 @@ export default createPlugin<
let waitForTransition: Promise<unknown>; let waitForTransition: Promise<unknown>;
const getStreamURL = async (videoID: string): Promise<string> => const getStreamURL = async (videoID: string): Promise<string> =>
this.ipc?.invoke('audio-url', videoID); this.ipc?.invoke('audio-url', videoID) as Promise<string>;
const getVideoIDFromURL = (url: string) => const getVideoIDFromURL = (url: string) =>
new URLSearchParams(url.split('?')?.at(-1)).get('v'); new URLSearchParams(url.split('?')?.at(-1)).get('v');

View File

@ -2,11 +2,12 @@ import { app, dialog, ipcMain } from 'electron';
import { Client as DiscordClient } from '@xhayper/discord-rpc'; import { Client as DiscordClient } from '@xhayper/discord-rpc';
import { dev } from 'electron-is'; import { dev } from 'electron-is';
import { ActivityType, GatewayActivityButton } from 'discord-api-types/v10';
import registerCallback, { type SongInfo } from '@/providers/song-info'; import registerCallback, { type SongInfo } from '@/providers/song-info';
import { createBackend, LoggerPrefix } from '@/utils'; import { createBackend, LoggerPrefix } from '@/utils';
import { t } from '@/i18n'; import { t } from '@/i18n';
import type { GatewayActivityButton } from 'discord-api-types/v10';
import type { SetActivity } from '@xhayper/discord-rpc/dist/structures/ClientUser'; import type { SetActivity } from '@xhayper/discord-rpc/dist/structures/ClientUser';
import type { DiscordPluginConfig } from './index'; import type { DiscordPluginConfig } from './index';
@ -180,6 +181,7 @@ export const backend = createBackend<
} }
const activityInfo: SetActivity = { const activityInfo: SetActivity = {
type: ActivityType.Listening,
details: songInfo.title, details: songInfo.title,
state: songInfo.artist, state: songInfo.artist,
largeImageKey: songInfo.imageSrc ?? '', largeImageKey: songInfo.imageSrc ?? '',
@ -200,15 +202,15 @@ export const backend = createBackend<
} }
} else if (!config.hideDurationLeft) { } else if (!config.hideDurationLeft) {
// Add the start and end time of the song // Add the start and end time of the song
const songStartTime = Date.now() - ((songInfo.elapsedSeconds ?? 0) * 1000); const songStartTime = Date.now() - (songInfo.elapsedSeconds ?? 0) * 1000;
activityInfo.startTimestamp = songStartTime; activityInfo.startTimestamp = songStartTime;
activityInfo.endTimestamp = songStartTime + (songInfo.songDuration * 1000); activityInfo.endTimestamp = songStartTime + songInfo.songDuration * 1000;
} }
info.rpc.user?.setActivity(activityInfo).catch(console.error); info.rpc.user?.setActivity(activityInfo).catch(console.error);
}, },
async start({ window: win, getConfig }) { async start(ctx) {
this.config = await getConfig(); this.config = await ctx.getConfig();
info.rpc.on('connected', () => { info.rpc.on('connected', () => {
if (dev()) { if (dev()) {
@ -237,10 +239,10 @@ export const backend = createBackend<
info.autoReconnect = this.config.autoReconnect; info.autoReconnect = this.config.autoReconnect;
window = win; window = ctx.window;
// If the page is ready, register the callback // If the page is ready, register the callback
win.once('ready-to-show', () => { ctx.window.once('ready-to-show', () => {
let lastSongInfo: SongInfo; let lastSongInfo: SongInfo;
registerCallback((songInfo) => { registerCallback((songInfo) => {
lastSongInfo = songInfo; lastSongInfo = songInfo;

View File

@ -172,6 +172,8 @@ function downloadSongOnFinishSetup({
let duration: number | undefined; let duration: number | undefined;
let time = 0; let time = 0;
const defaultDownloadFolder = app.getPath('downloads');
registerCallback((songInfo: SongInfo) => { registerCallback((songInfo: SongInfo) => {
if ( if (
!songInfo.isPaused && !songInfo.isPaused &&
@ -183,12 +185,22 @@ function downloadSongOnFinishSetup({
config.downloadOnFinish.mode === 'seconds' && config.downloadOnFinish.mode === 'seconds' &&
duration - time <= config.downloadOnFinish.seconds duration - time <= config.downloadOnFinish.seconds
) { ) {
downloadSong(currentUrl, config.downloadOnFinish.folder ?? config.downloadFolder); downloadSong(
currentUrl,
config.downloadOnFinish.folder ??
config.downloadFolder ??
defaultDownloadFolder,
);
} else if ( } else if (
config.downloadOnFinish.mode === 'percent' && config.downloadOnFinish.mode === 'percent' &&
time >= duration * (config.downloadOnFinish.percent / 100) time >= duration * (config.downloadOnFinish.percent / 100)
) { ) {
downloadSong(currentUrl, config.downloadOnFinish.folder ?? config.downloadFolder); downloadSong(
currentUrl,
config.downloadOnFinish.folder ??
config.downloadFolder ??
defaultDownloadFolder,
);
} }
} }
@ -261,12 +273,12 @@ async function downloadSongUnsafe(
let playabilityStatus = info.playability_status; let playabilityStatus = info.playability_status;
let bypassedResult = null; let bypassedResult = null;
if (playabilityStatus.status === 'LOGIN_REQUIRED') { if (playabilityStatus?.status === 'LOGIN_REQUIRED') {
// Try to bypass the age restriction // Try to bypass the age restriction
bypassedResult = await getAndroidTvInfo(id); bypassedResult = await getAndroidTvInfo(id);
playabilityStatus = bypassedResult.playability_status; playabilityStatus = bypassedResult.playability_status;
if (playabilityStatus.status === 'LOGIN_REQUIRED') { if (playabilityStatus?.status === 'LOGIN_REQUIRED') {
throw new Error( throw new Error(
`[${playabilityStatus.status}] ${playabilityStatus.reason}`, `[${playabilityStatus.status}] ${playabilityStatus.reason}`,
); );
@ -275,7 +287,7 @@ async function downloadSongUnsafe(
info = bypassedResult; info = bypassedResult;
} }
if (playabilityStatus.status === 'UNPLAYABLE') { if (playabilityStatus?.status === 'UNPLAYABLE') {
const errorScreen = const errorScreen =
playabilityStatus.error_screen as PlayerErrorMessage | null; playabilityStatus.error_screen as PlayerErrorMessage | null;
throw new Error( throw new Error(
@ -438,7 +450,7 @@ async function iterableStreamToProcessedUint8Array(
}), }),
ratio, ratio,
); );
increasePlaylistProgress(0.15 + (ratio * 0.85)); increasePlaylistProgress(0.15 + ratio * 0.85);
}); });
const safeVideoNameWithExtension = `${safeVideoName}.${extension}`; const safeVideoNameWithExtension = `${safeVideoName}.${extension}`;
@ -566,7 +578,13 @@ export async function downloadPlaylist(givenUrl?: string | URL) {
return; return;
} }
if (!playlist || !playlist.items || playlist.items.length === 0 || !playlist.header || !('title' in playlist.header)) { if (
!playlist ||
!playlist.items ||
playlist.items.length === 0 ||
!playlist.header ||
!('title' in playlist.header)
) {
sendError( sendError(
new Error(t('plugins.downloader.backend.feedback.playlist-is-empty')), new Error(t('plugins.downloader.backend.feedback.playlist-is-empty')),
); );
@ -660,7 +678,7 @@ export async function downloadPlaylist(givenUrl?: string | URL) {
const increaseProgress = (itemPercentage: number) => { const increaseProgress = (itemPercentage: number) => {
const currentProgress = (counter - 1) / (items.length ?? 1); const currentProgress = (counter - 1) / (items.length ?? 1);
const newProgress = currentProgress + (progressStep * itemPercentage); const newProgress = currentProgress + progressStep * itemPercentage;
win.setProgressBar(newProgress); win.setProgressBar(newProgress);
}; };

View File

@ -35,7 +35,10 @@ export const onMenu = async ({
click(item) { click(item) {
setConfig({ setConfig({
downloadOnFinish: { downloadOnFinish: {
...deepmerge(defaultConfig.downloadOnFinish, config.downloadOnFinish)!, ...deepmerge(
defaultConfig.downloadOnFinish,
config.downloadOnFinish,
)!,
enabled: item.checked, enabled: item.checked,
}, },
}); });
@ -49,14 +52,19 @@ export const onMenu = async ({
click() { click() {
const result = dialog.showOpenDialogSync({ const result = dialog.showOpenDialogSync({
properties: ['openDirectory', 'createDirectory'], properties: ['openDirectory', 'createDirectory'],
defaultPath: getFolder(config.downloadOnFinish?.folder ?? config.downloadFolder), defaultPath: getFolder(
config.downloadOnFinish?.folder ?? config.downloadFolder,
),
}); });
if (result) { if (result) {
setConfig({ setConfig({
downloadOnFinish: { downloadOnFinish: {
...deepmerge(defaultConfig.downloadOnFinish, config.downloadOnFinish)!, ...deepmerge(
defaultConfig.downloadOnFinish,
config.downloadOnFinish,
)!,
folder: result[0], folder: result[0],
} },
}); });
} }
}, },
@ -76,7 +84,10 @@ export const onMenu = async ({
click() { click() {
setConfig({ setConfig({
downloadOnFinish: { downloadOnFinish: {
...deepmerge(defaultConfig.downloadOnFinish, config.downloadOnFinish)!, ...deepmerge(
defaultConfig.downloadOnFinish,
config.downloadOnFinish,
)!,
mode: 'seconds', mode: 'seconds',
}, },
}); });
@ -91,7 +102,10 @@ export const onMenu = async ({
click() { click() {
setConfig({ setConfig({
downloadOnFinish: { downloadOnFinish: {
...deepmerge(defaultConfig.downloadOnFinish, config.downloadOnFinish)!, ...deepmerge(
defaultConfig.downloadOnFinish,
config.downloadOnFinish,
)!,
mode: 'percent', mode: 'percent',
}, },
}); });
@ -120,7 +134,9 @@ export const onMenu = async ({
min: '0', min: '0',
step: '1', step: '1',
}, },
value: config.downloadOnFinish?.seconds ?? defaultConfig.downloadOnFinish!.seconds, value:
config.downloadOnFinish?.seconds ??
defaultConfig.downloadOnFinish!.seconds,
}, },
{ {
label: t( label: t(
@ -133,7 +149,9 @@ export const onMenu = async ({
max: '100', max: '100',
step: '1', step: '1',
}, },
value: config.downloadOnFinish?.percent ?? defaultConfig.downloadOnFinish!.percent, value:
config.downloadOnFinish?.percent ??
defaultConfig.downloadOnFinish!.percent,
}, },
], ],
...promptOptions(), ...promptOptions(),
@ -147,7 +165,10 @@ export const onMenu = async ({
setConfig({ setConfig({
downloadOnFinish: { downloadOnFinish: {
...deepmerge(defaultConfig.downloadOnFinish, config.downloadOnFinish)!, ...deepmerge(
defaultConfig.downloadOnFinish,
config.downloadOnFinish,
)!,
seconds: Number(res[0]), seconds: Number(res[0]),
percent: Number(res[1]), percent: Number(res[1]),
}, },

View File

@ -39,7 +39,9 @@ const menuObserver = new MutationObserver(() => {
if (!menuUrl?.includes('watch?')) { if (!menuUrl?.includes('watch?')) {
menuUrl = undefined; menuUrl = undefined;
// check for podcast // check for podcast
for (const it of document.querySelectorAll('tp-yt-paper-listbox [tabindex="-1"] #navigation-endpoint')) { for (const it of document.querySelectorAll(
'tp-yt-paper-listbox [tabindex="-1"] #navigation-endpoint',
)) {
if (it.getAttribute('href')?.includes('podcast/')) { if (it.getAttribute('href')?.includes('podcast/')) {
menuUrl = it.getAttribute('href')!; menuUrl = it.getAttribute('href')!;
break; break;
@ -72,7 +74,9 @@ export const onRendererLoad = ({
?.getAttribute('href'); ?.getAttribute('href');
if (!videoUrl && songMenu) { if (!videoUrl && songMenu) {
for (const it of songMenu.querySelectorAll('ytmusic-menu-navigation-item-renderer[tabindex="-1"] #navigation-endpoint')) { for (const it of songMenu.querySelectorAll(
'ytmusic-menu-navigation-item-renderer[tabindex="-1"] #navigation-endpoint',
)) {
if (it.getAttribute('href')?.includes('podcast/')) { if (it.getAttribute('href')?.includes('podcast/')) {
videoUrl = it.getAttribute('href'); videoUrl = it.getAttribute('href');
break; break;
@ -86,7 +90,8 @@ export const onRendererLoad = ({
} }
if (videoUrl.startsWith('podcast/')) { if (videoUrl.startsWith('podcast/')) {
videoUrl = defaultConfig.url + '/watch?' + videoUrl.replace('podcast/', 'v='); videoUrl =
defaultConfig.url + '/watch?' + videoUrl.replace('podcast/', 'v=');
} }
if (videoUrl.includes('?playlist=')) { if (videoUrl.includes('?playlist=')) {
@ -102,7 +107,8 @@ export const onRendererLoad = ({
ipc.on('downloader-feedback', (feedback: string) => { ipc.on('downloader-feedback', (feedback: string) => {
if (progress) { if (progress) {
progress.innerHTML = feedback || t('plugins.downloader.templates.button'); const targetHtml = feedback || t('plugins.downloader.templates.button');
progress.innerHTML = window.trustedTypes?.defaultPolicy ? window.trustedTypes.defaultPolicy.createHTML(targetHtml) : targetHtml;
} else { } else {
console.warn( console.warn(
LoggerPrefix, LoggerPrefix,

View File

@ -4,24 +4,12 @@ export interface InAppMenuConfig {
} }
export const defaultInAppMenuConfig: InAppMenuConfig = { export const defaultInAppMenuConfig: InAppMenuConfig = {
enabled: enabled:
( ((typeof window !== 'undefined' &&
( !window.navigator?.userAgent?.toLowerCase().includes('mac')) ||
typeof window !== 'undefined' && (typeof global !== 'undefined' &&
!window.navigator?.userAgent?.toLowerCase().includes('mac') global.process?.platform !== 'darwin')) &&
) || ((typeof window !== 'undefined' &&
( !window.navigator?.userAgent?.toLowerCase().includes('linux')) ||
typeof global !== 'undefined' && (typeof global !== 'undefined' && global.process?.platform !== 'linux')),
global.process?.platform !== 'darwin'
)
) && (
(
typeof window !== 'undefined' &&
!window.navigator?.userAgent?.toLowerCase().includes('linux')
) ||
(
typeof global !== 'undefined' &&
global.process?.platform !== 'linux'
)
),
hideDOMWindowControls: false, hideDOMWindowControls: false,
}; };

View File

@ -1,6 +1,13 @@
import { register } from 'electron-localshortcut'; import { register } from 'electron-localshortcut';
import { BrowserWindow, Menu, MenuItem, ipcMain, nativeImage } from 'electron'; import {
BrowserWindow,
Menu,
MenuItem,
ipcMain,
nativeImage,
WebContents,
} from 'electron';
import type { BackendContext } from '@/types/contexts'; import type { BackendContext } from '@/types/contexts';
import type { InAppMenuConfig } from './constants'; import type { InAppMenuConfig } from './constants';
@ -50,11 +57,13 @@ export const onMainLoad = ({
ipcMain.handle('ytmd:menu-event', (event, commandId: number) => { ipcMain.handle('ytmd:menu-event', (event, commandId: number) => {
const target = getMenuItemById(commandId); const target = getMenuItemById(commandId);
if (target) if (target)
target.click( (
undefined, target.click as (
BrowserWindow.fromWebContents(event.sender), args0: unknown,
event.sender, args1: BrowserWindow | null,
); args3: WebContents,
) => void
)(undefined, BrowserWindow.fromWebContents(event.sender), event.sender);
}); });
handle('get-menu-by-id', (commandId: number) => { handle('get-menu-by-id', (commandId: number) => {

View File

@ -16,8 +16,9 @@ const isMacOS = navigator.userAgent.includes('Macintosh');
const isNotWindowsOrMacOS = const isNotWindowsOrMacOS =
!navigator.userAgent.includes('Windows') && !isMacOS; !navigator.userAgent.includes('Windows') && !isMacOS;
const [config, setConfig] = createSignal<InAppMenuConfig>(
const [config, setConfig] = createSignal<InAppMenuConfig>(defaultInAppMenuConfig); defaultInAppMenuConfig,
);
export const onRendererLoad = async ({ export const onRendererLoad = async ({
getConfig, getConfig,
ipc, ipc,
@ -29,14 +30,19 @@ export const onRendererLoad = async ({
stylesheet.replaceSync(scrollStyle); stylesheet.replaceSync(scrollStyle);
document.adoptedStyleSheets = [...document.adoptedStyleSheets, stylesheet]; document.adoptedStyleSheets = [...document.adoptedStyleSheets, stylesheet];
render(() => ( render(
<TitleBar () => (
ipc={ipc} <TitleBar
isMacOS={isMacOS} ipc={ipc}
enableController={isNotWindowsOrMacOS && !config().hideDOMWindowControls} isMacOS={isMacOS}
initialCollapsed={window.mainConfig.get('options.hideMenu')} enableController={
/> isNotWindowsOrMacOS && !config().hideDOMWindowControls
), document.body); }
initialCollapsed={window.mainConfig.get('options.hideMenu')}
/>
),
document.body,
);
}; };
export const onPlayerApiReady = () => { export const onPlayerApiReady = () => {

View File

@ -1,38 +1,40 @@
import { JSX } from 'solid-js'; import { JSX } from 'solid-js';
import { css } from 'solid-styled-components'; import { css } from 'solid-styled-components';
import { cache } from '@/providers/decorators'; import { cacheNoArgs } from '@/providers/decorators';
const iconButton = cache(() => css` const iconButton = cacheNoArgs(
-webkit-app-region: none; () => css`
-webkit-app-region: none;
background: transparent; background: transparent;
width: 24px; width: 24px;
height: 24px; height: 24px;
padding: 2px; padding: 2px;
border-radius: 2px; border-radius: 2px;
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
color: white; color: white;
outline: none; outline: none;
border: none; border: none;
transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1); transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1);
&:hover { &:hover {
background: rgba(255, 255, 255, 0.1); background: rgba(255, 255, 255, 0.1);
} }
&:active { &:active {
scale: 0.9; scale: 0.9;
} }
`); `,
);
type CollapseIconButtonProps = JSX.HTMLAttributes<HTMLButtonElement>; type CollapseIconButtonProps = JSX.HTMLAttributes<HTMLButtonElement>;
export const IconButton = (props: CollapseIconButtonProps) => { export const IconButton = (props: CollapseIconButtonProps) => {

View File

@ -1,33 +1,35 @@
import { JSX, splitProps } from 'solid-js'; import { JSX, splitProps } from 'solid-js';
import { css } from 'solid-styled-components'; import { css } from 'solid-styled-components';
import { cache } from '@/providers/decorators'; import { cacheNoArgs } from '@/providers/decorators';
const menuStyle = cache(() => css` const menuStyle = cacheNoArgs(
-webkit-app-region: none; () => css`
-webkit-app-region: none;
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
align-self: stretch; align-self: stretch;
padding: 2px 8px; padding: 2px 8px;
border-radius: 4px; border-radius: 4px;
cursor: pointer; cursor: pointer;
transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1); transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1);
&:hover { &:hover {
background-color: rgba(255, 255, 255, 0.1); background-color: rgba(255, 255, 255, 0.1);
} }
&:active { &:active {
scale: 0.9; scale: 0.9;
} }
&[data-selected="true"] { &[data-selected='true'] {
background-color: rgba(255, 255, 255, 0.2); background-color: rgba(255, 255, 255, 0.2);
} }
`); `,
);
export type MenuButtonProps = JSX.HTMLAttributes<HTMLLIElement> & { export type MenuButtonProps = JSX.HTMLAttributes<HTMLLIElement> & {
text?: string; text?: string;

View File

@ -2,59 +2,72 @@ import { createSignal, JSX, Show, splitProps } from 'solid-js';
import { mergeProps, Portal } from 'solid-js/web'; import { mergeProps, Portal } from 'solid-js/web';
import { css } from 'solid-styled-components'; import { css } from 'solid-styled-components';
import { Transition } from 'solid-transition-group'; import { Transition } from 'solid-transition-group';
import { autoUpdate, flip, offset, OffsetOptions, size } from '@floating-ui/dom'; import {
autoUpdate,
flip,
offset,
OffsetOptions,
size,
} from '@floating-ui/dom';
import { useFloating } from 'solid-floating-ui'; import { useFloating } from 'solid-floating-ui';
import { cache } from '@/providers/decorators'; import { cacheNoArgs } from '@/providers/decorators';
const panelStyle = cache(() => css` const panelStyle = cacheNoArgs(
position: fixed; () => css`
top: var(--offset-y, 0); position: fixed;
left: var(--offset-x, 0); top: var(--offset-y, 0);
left: var(--offset-x, 0);
max-width: var(--max-width, 100%); max-width: var(--max-width, 100%);
max-height: var(--max-height, 100%); max-height: var(--max-height, 100%);
z-index: 10000; z-index: 10000;
width: fit-content; width: fit-content;
height: fit-content; height: fit-content;
padding: 4px; padding: 4px;
box-sizing: border-box; box-sizing: border-box;
border-radius: 8px; border-radius: 8px;
overflow: auto; overflow: auto;
background-color: color-mix( background-color: color-mix(
in srgb, in srgb,
var(--titlebar-background-color, #030303) 50%, var(--titlebar-background-color, #030303) 50%,
rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1)
); );
backdrop-filter: blur(8px); backdrop-filter: blur(8px);
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.05), box-shadow:
0 2px 8px rgba(0, 0, 0, 0.2); 0 0 0 1px rgba(0, 0, 0, 0.05),
0 2px 8px rgba(0, 0, 0, 0.2);
transform-origin: var(--origin-x, 50%) var(--origin-y, 50%); transform-origin: var(--origin-x, 50%) var(--origin-y, 50%);
`); `,
);
const animationStyle = cache(() => ({ const animationStyle = cacheNoArgs(() => ({
enter: css` enter: css`
opacity: 0; opacity: 0;
transform: scale(0.9); transform: scale(0.9);
`, `,
enterActive: css` enterActive: css`
transition: opacity 0.225s cubic-bezier(0.33, 1, 0.68, 1), transform 0.225s cubic-bezier(0.33, 1, 0.68, 1); transition:
opacity 0.225s cubic-bezier(0.33, 1, 0.68, 1),
transform 0.225s cubic-bezier(0.33, 1, 0.68, 1);
`, `,
exitTo: css` exitTo: css`
opacity: 0; opacity: 0;
transform: scale(0.9); transform: scale(0.9);
`, `,
exitActive: css` exitActive: css`
transition: opacity 0.225s cubic-bezier(0.32, 0, 0.67, 0), transform 0.225s cubic-bezier(0.32, 0, 0.67, 0); transition:
opacity 0.225s cubic-bezier(0.32, 0, 0.67, 0),
transform 0.225s cubic-bezier(0.32, 0, 0.67, 0);
`, `,
})); }));
export type Placement = export type Placement =
'top' | 'top'
| 'bottom' | 'bottom'
| 'left' | 'left'
| 'right' | 'right'
@ -92,9 +105,15 @@ export const Panel = (props: PanelProps) => {
size({ size({
padding: 8, padding: 8,
apply({ elements, availableWidth, availableHeight }) { apply({ elements, availableWidth, availableHeight }) {
elements.floating.style.setProperty('--max-width', `${Math.max(200, availableWidth)}px`); elements.floating.style.setProperty(
elements.floating.style.setProperty('--max-height', `${Math.max(200, availableHeight)}px`); '--max-width',
} `${Math.max(200, availableWidth)}px`,
);
elements.floating.style.setProperty(
'--max-height',
`${Math.max(200, availableHeight)}px`,
);
},
}), }),
flip({ fallbackStrategy: 'initialPlacement' }), flip({ fallbackStrategy: 'initialPlacement' }),
], ],
@ -103,7 +122,10 @@ export const Panel = (props: PanelProps) => {
const originX = () => { const originX = () => {
if (position.placement.includes('left')) return '100%'; if (position.placement.includes('left')) return '100%';
if (position.placement.includes('right')) return '0'; if (position.placement.includes('right')) return '0';
if (position.placement.includes('top') || position.placement.includes('bottom')) { if (
position.placement.includes('top') ||
position.placement.includes('bottom')
) {
if (position.placement.includes('start')) return '0'; if (position.placement.includes('start')) return '0';
if (position.placement.includes('end')) return '100%'; if (position.placement.includes('end')) return '100%';
} }
@ -113,7 +135,10 @@ export const Panel = (props: PanelProps) => {
const originY = () => { const originY = () => {
if (position.placement.includes('top')) return '100%'; if (position.placement.includes('top')) return '100%';
if (position.placement.includes('bottom')) return '0'; if (position.placement.includes('bottom')) return '0';
if (position.placement.includes('left') || position.placement.includes('right')) { if (
position.placement.includes('left') ||
position.placement.includes('right')
) {
if (position.placement.includes('start')) return '0'; if (position.placement.includes('start')) return '0';
if (position.placement.includes('end')) return '100%'; if (position.placement.includes('end')) return '100%';
} }

View File

@ -8,117 +8,132 @@ import { useFloating } from 'solid-floating-ui';
import { autoUpdate, offset, size } from '@floating-ui/dom'; import { autoUpdate, offset, size } from '@floating-ui/dom';
import { Panel } from './Panel'; import { Panel } from './Panel';
import { cache } from '@/providers/decorators'; import { cacheNoArgs } from '@/providers/decorators';
const itemStyle = cache(() => css` const itemStyle = cacheNoArgs(
position: relative; () => css`
position: relative;
-webkit-app-region: none; -webkit-app-region: none;
min-height: 32px; min-height: 32px;
height: 32px; height: 32px;
display: grid; display: grid;
grid-template-columns: 32px 1fr auto minmax(32px, auto); grid-template-columns: 32px 1fr auto minmax(32px, auto);
justify-content: flex-start; justify-content: flex-start;
align-items: center; align-items: center;
border-radius: 4px; border-radius: 4px;
cursor: pointer; cursor: pointer;
box-sizing: border-box;
user-select: none;
-webkit-user-drag: none;
transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1);
&:hover {
background-color: rgba(255, 255, 255, 0.1);
}
&:active {
background-color: rgba(255, 255, 255, 0.2);
}
&[data-selected="true"] {
background-color: rgba(255, 255, 255, 0.2);
}
& * {
box-sizing: border-box; box-sizing: border-box;
} user-select: none;
`); -webkit-user-drag: none;
const itemIconStyle = cache(() => css` transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1);
height: 32px;
padding: 4px;
color: white;
`);
const itemLabelStyle = cache(() => css` &:hover {
font-size: 12px; background-color: rgba(255, 255, 255, 0.1);
color: white; }
`);
const itemChipStyle = cache(() => css` &:active {
display: flex; background-color: rgba(255, 255, 255, 0.2);
justify-content: center; }
align-items: center;
min-width: 16px; &[data-selected='true'] {
height: 16px; background-color: rgba(255, 255, 255, 0.2);
padding: 0 4px; }
margin-left: 8px;
border-radius: 4px; & * {
background-color: rgba(255, 255, 255, 0.2); box-sizing: border-box;
color: #f1f1f1; }
font-size: 10px; `,
font-weight: 500; );
line-height: 1;
`);
const toolTipStyle = cache(() => css` const itemIconStyle = cacheNoArgs(
min-width: 32px; () => css`
width: 100%; height: 32px;
height: 100%; padding: 4px;
color: white;
`,
);
padding: 4px; const itemLabelStyle = cacheNoArgs(
() => css`
font-size: 12px;
color: white;
`,
);
max-width: calc(var(--max-width, 100%) - 8px); const itemChipStyle = cacheNoArgs(
max-height: calc(var(--max-height, 100%) - 8px); () => css`
display: flex;
justify-content: center;
align-items: center;
border-radius: 4px; min-width: 16px;
background-color: rgba(25, 25, 25, 0.8); height: 16px;
color: #f1f1f1; padding: 0 4px;
font-size: 10px; margin-left: 8px;
`);
const popupStyle = cache(() => css` border-radius: 4px;
position: fixed; background-color: rgba(255, 255, 255, 0.2);
top: var(--offset-y, 0); color: #f1f1f1;
left: var(--offset-x, 0); font-size: 10px;
font-weight: 500;
line-height: 1;
`,
);
max-width: var(--max-width, 100%); const toolTipStyle = cacheNoArgs(
max-height: var(--max-height, 100%); () => css`
min-width: 32px;
width: 100%;
height: 100%;
z-index: 100000000; padding: 4px;
pointer-events: none;
`); max-width: calc(var(--max-width, 100%) - 8px);
max-height: calc(var(--max-height, 100%) - 8px);
const animationStyle = cache(() => ({ border-radius: 4px;
background-color: rgba(25, 25, 25, 0.8);
color: #f1f1f1;
font-size: 10px;
`,
);
const popupStyle = cacheNoArgs(
() => css`
position: fixed;
top: var(--offset-y, 0);
left: var(--offset-x, 0);
max-width: var(--max-width, 100%);
max-height: var(--max-height, 100%);
z-index: 100000000;
pointer-events: none;
`,
);
const animationStyle = cacheNoArgs(() => ({
enter: css` enter: css`
opacity: 0; opacity: 0;
transform: scale(0.9); transform: scale(0.9);
`, `,
enterActive: css` enterActive: css`
transition: opacity 0.225s cubic-bezier(0.33, 1, 0.68, 1), transform 0.225s cubic-bezier(0.33, 1, 0.68, 1); transition:
opacity 0.225s cubic-bezier(0.33, 1, 0.68, 1),
transform 0.225s cubic-bezier(0.33, 1, 0.68, 1);
`, `,
exitTo: css` exitTo: css`
opacity: 0; opacity: 0;
transform: scale(0.9); transform: scale(0.9);
`, `,
exitActive: css` exitActive: css`
transition: opacity 0.225s cubic-bezier(0.32, 0, 0.67, 0), transform 0.225s cubic-bezier(0.32, 0, 0.67, 0); transition:
opacity 0.225s cubic-bezier(0.32, 0, 0.67, 0),
transform 0.225s cubic-bezier(0.32, 0, 0.67, 0);
`, `,
})); }));
@ -160,7 +175,11 @@ type CheckboxPanelItemProps = BasePanelItemProps & {
checked: boolean; checked: boolean;
onChange?: (checked: boolean) => void; onChange?: (checked: boolean) => void;
}; };
export type PanelItemProps = NormalPanelItemProps | SubmenuItemProps | RadioPanelItemProps | CheckboxPanelItemProps; export type PanelItemProps =
| NormalPanelItemProps
| SubmenuItemProps
| RadioPanelItemProps
| CheckboxPanelItemProps;
export const PanelItem = (props: PanelItemProps) => { export const PanelItem = (props: PanelItemProps) => {
const [open, setOpen] = createSignal(false); const [open, setOpen] = createSignal(false);
const [toolTipOpen, setToolTipOpen] = createSignal(false); const [toolTipOpen, setToolTipOpen] = createSignal(false);
@ -176,17 +195,24 @@ export const PanelItem = (props: PanelItemProps) => {
offset({ mainAxis: 8 }), offset({ mainAxis: 8 }),
size({ size({
apply({ rects, elements }) { apply({ rects, elements }) {
elements.floating.style.setProperty('--max-width', `${rects.reference.width}px`); elements.floating.style.setProperty(
} '--max-width',
`${rects.reference.width}px`,
);
},
}), }),
], ],
}); });
const handleHover = (event: MouseEvent) => { const handleHover = (event: MouseEvent) => {
setToolTipOpen(true); setToolTipOpen(true);
event.target?.addEventListener('mouseleave', () => { event.target?.addEventListener(
setToolTipOpen(false); 'mouseleave',
}, { once: true }); () => {
setToolTipOpen(false);
},
{ once: true },
);
if (props.type === 'submenu') { if (props.type === 'submenu') {
const timer = setTimeout(() => { const timer = setTimeout(() => {
@ -200,36 +226,54 @@ export const PanelItem = (props: PanelItemProps) => {
}; };
document.addEventListener('mousemove', onMouseMove); document.addEventListener('mousemove', onMouseMove);
event.target?.addEventListener('mouseleave', () => { event.target?.addEventListener(
setTimeout(() => { 'mouseleave',
document.removeEventListener('mousemove', onMouseMove); () => {
const parents = getParents(document.elementFromPoint(mouseX, mouseY)); setTimeout(() => {
document.removeEventListener('mousemove', onMouseMove);
const parents = getParents(
document.elementFromPoint(mouseX, mouseY),
);
if (!parents.includes(child())) { if (!parents.includes(child())) {
setOpen(false); setOpen(false);
} else { } else {
const onOtherHover = (event: MouseEvent) => { const onOtherHover = (event: MouseEvent) => {
const parents = getParents(event.target as HTMLElement); const parents = getParents(event.target as HTMLElement);
const closestLevel = parents.find((it) => it?.dataset?.level)?.dataset.level ?? ''; const closestLevel =
const path = event.composedPath(); parents.find((it) => it?.dataset?.level)?.dataset.level ??
'';
const path = event.composedPath();
const isOtherItem = path.some((it) => it instanceof HTMLElement && it.classList.contains(itemStyle())); const isOtherItem = path.some(
const isChild = closestLevel.startsWith(props.level.join('/')); (it) =>
it instanceof HTMLElement &&
it.classList.contains(itemStyle()),
);
const isChild = closestLevel.startsWith(
props.level.join('/'),
);
if (isOtherItem && !isChild) { if (isOtherItem && !isChild) {
setOpen(false); setOpen(false);
document.removeEventListener('mousemove', onOtherHover); document.removeEventListener('mousemove', onOtherHover);
} }
}; };
document.addEventListener('mousemove', onOtherHover); document.addEventListener('mousemove', onOtherHover);
} }
}, 225); }, 225);
}, { once: true }); },
{ once: true },
);
}, 225); }, 225);
event.target?.addEventListener('mouseleave', () => { event.target?.addEventListener(
clearTimeout(timer); 'mouseleave',
}, { once: true }); () => {
clearTimeout(timer);
},
{ once: true },
);
} }
}; };
@ -244,7 +288,6 @@ export const PanelItem = (props: PanelItemProps) => {
} }
}; };
return ( return (
<li <li
ref={setAnchor} ref={setAnchor}
@ -253,45 +296,66 @@ export const PanelItem = (props: PanelItemProps) => {
onClick={handleClick} onClick={handleClick}
data-selected={open()} data-selected={open()}
> >
<Switch fallback={<div class={itemIconStyle()}/>}> <Switch fallback={<div class={itemIconStyle()} />}>
<Match when={props.type === 'checkbox' && props.checked}> <Match when={props.type === 'checkbox' && props.checked}>
<svg class={itemIconStyle()} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="1.5" <svg
stroke="currentColor" fill="none" class={itemIconStyle()}
stroke-linecap="round" stroke-linejoin="round"> xmlns="http://www.w3.org/2000/svg"
<path stroke="none" d="M0 0h24v24H0z" fill="none"/> viewBox="0 0 24 24"
<path d="M5 12l5 5l10 -10"/> stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M5 12l5 5l10 -10" />
</svg> </svg>
</Match> </Match>
<Match when={props.type === 'radio' && props.checked}> <Match when={props.type === 'radio' && props.checked}>
<svg class={itemIconStyle()} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" <svg
style={{ padding: '6px' }}> class={itemIconStyle()}
<path fill="currentColor" xmlns="http://www.w3.org/2000/svg"
d="M10,5 C7.2,5 5,7.2 5,10 C5,12.8 7.2,15 10,15 C12.8,15 15,12.8 15,10 C15,7.2 12.8,5 10,5 L10,5 Z M10,0 C4.5,0 0,4.5 0,10 C0,15.5 4.5,20 10,20 C15.5,20 20,15.5 20,10 C20,4.5 15.5,0 10,0 L10,0 Z M10,18 C5.6,18 2,14.4 2,10 C2,5.6 5.6,2 10,2 C14.4,2 18,5.6 18,10 C18,14.4 14.4,18 10,18 L10,18 Z"/> viewBox="0 0 20 20"
style={{ padding: '6px' }}
>
<path
fill="currentColor"
d="M10,5 C7.2,5 5,7.2 5,10 C5,12.8 7.2,15 10,15 C12.8,15 15,12.8 15,10 C15,7.2 12.8,5 10,5 L10,5 Z M10,0 C4.5,0 0,4.5 0,10 C0,15.5 4.5,20 10,20 C15.5,20 20,15.5 20,10 C20,4.5 15.5,0 10,0 L10,0 Z M10,18 C5.6,18 2,14.4 2,10 C2,5.6 5.6,2 10,2 C14.4,2 18,5.6 18,10 C18,14.4 14.4,18 10,18 L10,18 Z"
/>
</svg> </svg>
</Match> </Match>
<Match when={props.type === 'radio' && !props.checked}> <Match when={props.type === 'radio' && !props.checked}>
<svg class={itemIconStyle()} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" <svg
style={{ padding: '6px' }}> class={itemIconStyle()}
<path fill="currentColor" xmlns="http://www.w3.org/2000/svg"
d="M10,0 C4.5,0 0,4.5 0,10 C0,15.5 4.5,20 10,20 C15.5,20 20,15.5 20,10 C20,4.5 15.5,0 10,0 L10,0 Z M10,18 C5.6,18 2,14.4 2,10 C2,5.6 5.6,2 10,2 C14.4,2 18,5.6 18,10 C18,14.4 14.4,18 10,18 L10,18 Z"/> viewBox="0 0 20 20"
style={{ padding: '6px' }}
>
<path
fill="currentColor"
d="M10,0 C4.5,0 0,4.5 0,10 C0,15.5 4.5,20 10,20 C15.5,20 20,15.5 20,10 C20,4.5 15.5,0 10,0 L10,0 Z M10,18 C5.6,18 2,14.4 2,10 C2,5.6 5.6,2 10,2 C14.4,2 18,5.6 18,10 C18,14.4 14.4,18 10,18 L10,18 Z"
/>
</svg> </svg>
</Match> </Match>
</Switch> </Switch>
<span class={itemLabelStyle()}> <span class={itemLabelStyle()}>{props.name}</span>
{props.name} <Show when={props.chip} fallback={<div />}>
</span> <span class={itemChipStyle()}>{props.chip}</span>
<Show when={props.chip} fallback={<div/>}>
<span class={itemChipStyle()}>
{props.chip}
</span>
</Show> </Show>
<Show when={props.type === 'submenu'}> <Show when={props.type === 'submenu'}>
<svg class={itemIconStyle()} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="1.5" <svg
stroke="currentColor" class={itemIconStyle()}
fill="none" xmlns="http://www.w3.org/2000/svg"
stroke-linecap="round" stroke-linejoin="round"> viewBox="0 0 24 24"
<path stroke="none" d="M0 0h24v24H0z" fill="none"/> stroke-width="1.5"
<polyline points="9 6 15 12 9 18"/> stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<polyline points="9 6 15 12 9 18" />
</svg> </svg>
<Panel <Panel
ref={setChild} ref={setChild}
@ -322,9 +386,7 @@ export const PanelItem = (props: PanelItemProps) => {
exitActiveClass={animationStyle().exitActive} exitActiveClass={animationStyle().exitActive}
> >
<Show when={toolTipOpen()}> <Show when={toolTipOpen()}>
<div class={toolTipStyle()}> <div class={toolTipStyle()}>{props.toolTip}</div>
{props.toolTip}
</div>
</Show> </Show>
</Transition> </Transition>
</div> </div>

View File

@ -1,5 +1,15 @@
import { Menu, MenuItem } from 'electron'; import { Menu, MenuItem } from 'electron';
import { createEffect, createResource, createSignal, Index, Match, onCleanup, onMount, Show, Switch } from 'solid-js'; import {
createEffect,
createResource,
createSignal,
Index,
Match,
onCleanup,
onMount,
Show,
Switch,
} from 'solid-js';
import { css } from 'solid-styled-components'; import { css } from 'solid-styled-components';
import { TransitionGroup } from 'solid-transition-group'; import { TransitionGroup } from 'solid-transition-group';
@ -9,69 +19,79 @@ import { PanelItem } from './PanelItem';
import { IconButton } from './IconButton'; import { IconButton } from './IconButton';
import { WindowController } from './WindowController'; import { WindowController } from './WindowController';
import { cache } from '@/providers/decorators'; import { cacheNoArgs } from '@/providers/decorators';
import type { RendererContext } from '@/types/contexts'; import type { RendererContext } from '@/types/contexts';
import type { InAppMenuConfig } from '../constants'; import type { InAppMenuConfig } from '../constants';
const titleStyle = cache(() => css` const titleStyle = cacheNoArgs(
-webkit-app-region: drag; () => css`
box-sizing: border-box; -webkit-app-region: drag;
box-sizing: border-box;
position: fixed; position: fixed;
top: 0; top: 0;
z-index: 10000000; z-index: 10000000;
width: 100%; width: 100%;
height: var(--menu-bar-height, 32px); height: var(--menu-bar-height, 32px);
display: flex; display: flex;
flex-flow: row; flex-flow: row;
justify-content: flex-start; justify-content: flex-start;
align-items: center; align-items: center;
gap: 4px; gap: 4px;
color: #f1f1f1; color: #f1f1f1;
font-size: 12px; font-size: 12px;
padding: 4px 4px 4px var(--offset-left, 4px); padding: 4px 4px 4px var(--offset-left, 4px);
background-color: var(--titlebar-background-color, #030303); background-color: var(--titlebar-background-color, #030303);
user-select: none; user-select: none;
transition: opacity 200ms ease 0s, transition:
transform 300ms cubic-bezier(0.2, 0, 0.6, 1) 0s, opacity 200ms ease 0s,
background-color 300ms cubic-bezier(0.2, 0, 0.6, 1) 0s; transform 300ms cubic-bezier(0.2, 0, 0.6, 1) 0s,
background-color 300ms cubic-bezier(0.2, 0, 0.6, 1) 0s;
&[data-macos="true"] { &[data-macos='true'] {
padding: 4px 4px 4px 74px; padding: 4px 4px 4px 74px;
} }
ytmusic-app:has(ytmusic-player[player-ui-state=FULLSCREEN]) ~ &:not([data-show="true"]) { ytmusic-app:has(ytmusic-player[player-ui-state='FULLSCREEN'])
transform: translateY(calc(-1 * var(--menu-bar-height, 32px))); ~ &:not([data-show='true']) {
} transform: translateY(calc(-1 * var(--menu-bar-height, 32px)));
`); }
`,
);
const separatorStyle = cache(() => css` const separatorStyle = cacheNoArgs(
min-height: 1px; () => css`
height: 1px; min-height: 1px;
margin: 4px 0; height: 1px;
margin: 4px 0;
background-color: rgba(255, 255, 255, 0.2); background-color: rgba(255, 255, 255, 0.2);
`); `,
);
const animationStyle = cache(() => ({ const animationStyle = cacheNoArgs(() => ({
enter: css` enter: css`
opacity: 0; opacity: 0;
transform: translateX(-50%) scale(0.8); transform: translateX(-50%) scale(0.8);
`, `,
enterActive: css` enterActive: css`
transition: opacity 0.1s cubic-bezier(0.33, 1, 0.68, 1), transform 0.1s cubic-bezier(0.33, 1, 0.68, 1); transition:
opacity 0.1s cubic-bezier(0.33, 1, 0.68, 1),
transform 0.1s cubic-bezier(0.33, 1, 0.68, 1);
`, `,
exitTo: css` exitTo: css`
opacity: 0; opacity: 0;
transform: translateX(-50%) scale(0.8); transform: translateX(-50%) scale(0.8);
`, `,
exitActive: css` exitActive: css`
transition: opacity 0.1s cubic-bezier(0.32, 0, 0.67, 0), transform 0.1s cubic-bezier(0.32, 0, 0.67, 0); transition:
opacity 0.1s cubic-bezier(0.32, 0, 0.67, 0),
transform 0.1s cubic-bezier(0.32, 0, 0.67, 0);
`, `,
move: css` move: css`
transition: all 0.1s cubic-bezier(0.65, 0, 0.35, 1); transition: all 0.1s cubic-bezier(0.65, 0, 0.35, 1);
@ -89,7 +109,7 @@ export type PanelRendererProps = {
items: Electron.Menu['items']; items: Electron.Menu['items'];
level?: number[]; level?: number[];
onClick?: (commandId: number, radioGroup?: MenuItem[]) => void; onClick?: (commandId: number, radioGroup?: MenuItem[]) => void;
} };
const PanelRenderer = (props: PanelRendererProps) => { const PanelRenderer = (props: PanelRendererProps) => {
const radioGroup = () => props.items.filter((it) => it.type === 'radio'); const radioGroup = () => props.items.filter((it) => it.type === 'radio');
@ -114,12 +134,12 @@ const PanelRenderer = (props: PanelRendererProps) => {
name={subItem().label} name={subItem().label}
chip={subItem().sublabel} chip={subItem().sublabel}
toolTip={subItem().toolTip} toolTip={subItem().toolTip}
level={[...props.level ?? [], subItem().commandId]} level={[...(props.level ?? []), subItem().commandId]}
commandId={subItem().commandId} commandId={subItem().commandId}
> >
<PanelRenderer <PanelRenderer
items={subItem().submenu?.items ?? []} items={subItem().submenu?.items ?? []}
level={[...props.level ?? [], subItem().commandId]} level={[...(props.level ?? []), subItem().commandId]}
onClick={props.onClick} onClick={props.onClick}
/> />
</PanelItem> </PanelItem>
@ -143,11 +163,13 @@ const PanelRenderer = (props: PanelRendererProps) => {
chip={subItem().sublabel} chip={subItem().sublabel}
toolTip={subItem().toolTip} toolTip={subItem().toolTip}
commandId={subItem().commandId} commandId={subItem().commandId}
onChange={() => props.onClick?.(subItem().commandId, radioGroup())} onChange={() =>
props.onClick?.(subItem().commandId, radioGroup())
}
/> />
</Match> </Match>
<Match when={subItem().type === 'separator'}> <Match when={subItem().type === 'separator'}>
<hr class={separatorStyle()}/> <hr class={separatorStyle()} />
</Match> </Match>
</Switch> </Switch>
</Show> </Show>
@ -169,8 +191,13 @@ export const TitleBar = (props: TitleBarProps) => {
const [menu, setMenu] = createSignal<Menu | null>(null); const [menu, setMenu] = createSignal<Menu | null>(null);
const [mouseY, setMouseY] = createSignal(0); const [mouseY, setMouseY] = createSignal(0);
const [data, { refetch }] = createResource(async () => await props.ipc.invoke('get-menu') as Promise<Menu | null>); const [data, { refetch }] = createResource(
const [isMaximized, { refetch: refetchMaximize }] = createResource(async () => await props.ipc.invoke('window-is-maximized') as Promise<boolean>); async () => (await props.ipc.invoke('get-menu')) as Promise<Menu | null>,
);
const [isMaximized, { refetch: refetchMaximize }] = createResource(
async () =>
(await props.ipc.invoke('window-is-maximized')) as Promise<boolean>,
);
const handleToggleMaximize = async () => { const handleToggleMaximize = async () => {
if (isMaximized()) { if (isMaximized()) {
@ -194,10 +221,12 @@ export const TitleBar = (props: TitleBarProps) => {
)) as MenuItem | null; )) as MenuItem | null;
const newMenu = structuredClone(originalMenu); const newMenu = structuredClone(originalMenu);
const stack = [...newMenu?.items ?? []]; const stack = [...(newMenu?.items ?? [])];
let now: MenuItem | undefined = stack.pop(); let now: MenuItem | undefined = stack.pop();
while (now) { while (now) {
const index = now?.submenu?.items?.findIndex((it) => it.commandId === commandId) ?? -1; const index =
now?.submenu?.items?.findIndex((it) => it.commandId === commandId) ??
-1;
if (index >= 0) { if (index >= 0) {
if (menuItem) now?.submenu?.items?.splice(index, 1, menuItem); if (menuItem) now?.submenu?.items?.splice(index, 1, menuItem);
@ -213,13 +242,16 @@ export const TitleBar = (props: TitleBarProps) => {
return newMenu; return newMenu;
}; };
const handleItemClick = async (commandId: number, radioGroup?: MenuItem[]) => { const handleItemClick = async (
commandId: number,
radioGroup?: MenuItem[],
) => {
const menuData = menu(); const menuData = menu();
if (!menuData) return; if (!menuData) return;
if (Array.isArray(radioGroup)) { if (Array.isArray(radioGroup)) {
let newMenu = menuData; let newMenu = menuData;
for await (const item of radioGroup) { for (const item of radioGroup) {
newMenu = await refreshMenuItem(newMenu, item.commandId); newMenu = await refreshMenuItem(newMenu, item.commandId);
} }
@ -271,20 +303,16 @@ export const TitleBar = (props: TitleBarProps) => {
// tracking mouse position // tracking mouse position
window.addEventListener('mousemove', listener); window.addEventListener('mousemove', listener);
const ytmusicAppLayout = document.querySelector<HTMLElement>('#layout'); const ytmusicAppLayout = document.querySelector<HTMLElement>('#layout');
ytmusicAppLayout?.addEventListener("scroll",()=>{ ytmusicAppLayout?.addEventListener('scroll', () => {
const scrollValue = ytmusicAppLayout.scrollTop; const scrollValue = ytmusicAppLayout.scrollTop;
if (scrollValue > 20){ if (scrollValue > 20) {
ytmusicAppLayout.classList.add("content-scrolled"); ytmusicAppLayout.classList.add('content-scrolled');
} } else {
else{ ytmusicAppLayout.classList.remove('content-scrolled');
ytmusicAppLayout.classList.remove("content-scrolled"); }
} });
})
}); });
createEffect(() => { createEffect(() => {
if (!menu() && data()) { if (!menu() && data()) {
setMenu(data() ?? null); setMenu(data() ?? null);
@ -296,7 +324,12 @@ export const TitleBar = (props: TitleBarProps) => {
}); });
return ( return (
<nav data-ytmd-main-panel={true} class={titleStyle()} data-macos={props.isMacOS} data-show={mouseY() < 32}> <nav
data-ytmd-main-panel={true}
class={titleStyle()}
data-macos={props.isMacOS}
data-show={mouseY() < 32}
>
<IconButton <IconButton
onClick={() => setCollapsed(!collapsed())} onClick={() => setCollapsed(!collapsed())}
style={{ style={{
@ -311,15 +344,34 @@ export const TitleBar = (props: TitleBarProps) => {
</svg> </svg>
</IconButton> </IconButton>
<TransitionGroup <TransitionGroup
enterClass={ignoreTransition() ? animationStyle().fakeTarget : animationStyle().enter} enterClass={
enterActiveClass={ignoreTransition() ? animationStyle().fake : animationStyle().enterActive} ignoreTransition()
exitToClass={ignoreTransition() ? animationStyle().fakeTarget : animationStyle().exitTo} ? animationStyle().fakeTarget
exitActiveClass={ignoreTransition() ? animationStyle().fake : animationStyle().exitActive} : animationStyle().enter
}
enterActiveClass={
ignoreTransition()
? animationStyle().fake
: animationStyle().enterActive
}
exitToClass={
ignoreTransition()
? animationStyle().fakeTarget
: animationStyle().exitTo
}
exitActiveClass={
ignoreTransition()
? animationStyle().fake
: animationStyle().exitActive
}
onBeforeEnter={(element) => { onBeforeEnter={(element) => {
if (ignoreTransition()) return; if (ignoreTransition()) return;
const index = Number(element.getAttribute('data-index') ?? 0); const index = Number(element.getAttribute('data-index') ?? 0);
(element as HTMLElement).style.setProperty('transition-delay', `${(index * 0.025)}s`); (element as HTMLElement).style.setProperty(
'transition-delay',
`${index * 0.025}s`,
);
}} }}
onAfterEnter={(element) => { onAfterEnter={(element) => {
(element as HTMLElement).style.removeProperty('transition-delay'); (element as HTMLElement).style.removeProperty('transition-delay');
@ -329,13 +381,18 @@ export const TitleBar = (props: TitleBarProps) => {
const index = Number(element.getAttribute('data-index') ?? 0); const index = Number(element.getAttribute('data-index') ?? 0);
const length = Number(element.getAttribute('data-length') ?? 1); const length = Number(element.getAttribute('data-length') ?? 1);
(element as HTMLElement).style.setProperty('transition-delay', `${(length * 0.025) - (index * 0.025)}s`); (element as HTMLElement).style.setProperty(
'transition-delay',
`${length * 0.025 - index * 0.025}s`,
);
}} }}
> >
<Show when={!collapsed()}> <Show when={!collapsed()}>
<Index each={menu()?.items}> <Index each={menu()?.items}>
{(item, index) => { {(item, index) => {
const [anchor, setAnchor] = createSignal<HTMLElement | null>(null); const [anchor, setAnchor] = createSignal<HTMLElement | null>(
null,
);
const handleClick = () => { const handleClick = () => {
if (openTarget() === anchor()) { if (openTarget() === anchor()) {
@ -373,7 +430,7 @@ export const TitleBar = (props: TitleBarProps) => {
</Show> </Show>
</TransitionGroup> </TransitionGroup>
<Show when={props.enableController}> <Show when={props.enableController}>
<div style={{ flex: 1 }}/> <div style={{ flex: 1 }} />
<WindowController <WindowController
isMaximize={isMaximized()} isMaximize={isMaximized()}
onToggleMaximize={handleToggleMaximize} onToggleMaximize={handleToggleMaximize}
@ -384,4 +441,3 @@ export const TitleBar = (props: TitleBarProps) => {
</nav> </nav>
); );
}; };

View File

@ -2,21 +2,23 @@ import { css } from 'solid-styled-components';
import { Show } from 'solid-js'; import { Show } from 'solid-js';
import { IconButton } from './IconButton'; import { IconButton } from './IconButton';
import { cache } from '@/providers/decorators'; import { cacheNoArgs } from '@/providers/decorators';
const containerStyle = cache(() => css` const containerStyle = cacheNoArgs(
display: flex; () => css`
justify-content: flex-end; display: flex;
align-items: center; justify-content: flex-end;
align-items: center;
& > *:last-of-type { & > *:last-of-type {
border-top-right-radius: 4px; border-top-right-radius: 4px;
&:hover { &:hover {
background: rgba(255, 0, 0, 0.5); background: rgba(255, 0, 0, 0.5);
}
} }
} `,
`); );
export type WindowControllerProps = { export type WindowControllerProps = {
isMaximize?: boolean; isMaximize?: boolean;
@ -24,20 +26,35 @@ export type WindowControllerProps = {
onToggleMaximize?: () => void; onToggleMaximize?: () => void;
onMinimize?: () => void; onMinimize?: () => void;
onClose?: () => void; onClose?: () => void;
} };
export const WindowController = (props: WindowControllerProps) => { export const WindowController = (props: WindowControllerProps) => {
return ( return (
<div class={containerStyle()}> <div class={containerStyle()}>
<IconButton onClick={props.onMinimize}> <IconButton onClick={props.onMinimize}>
<svg width={16} height={16} fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> <svg
<path fill="currentColor" d="M3.755 12.5h16.492a.75.75 0 0 0 0-1.5H3.755a.75.75 0 0 0 0 1.5Z"/> width={16}
height={16}
fill="none"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill="currentColor"
d="M3.755 12.5h16.492a.75.75 0 0 0 0-1.5H3.755a.75.75 0 0 0 0 1.5Z"
/>
</svg> </svg>
</IconButton> </IconButton>
<IconButton onClick={props.onToggleMaximize}> <IconButton onClick={props.onToggleMaximize}>
<Show <Show
when={props.isMaximize} when={props.isMaximize}
fallback={ fallback={
<svg width={16} height={16} fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> <svg
width={16}
height={16}
fill="none"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path <path
fill="currentColor" fill="currentColor"
d="M6 3h12a3 3 0 0 1 3 3v12a3 3 0 0 1-3 3H6a3 3 0 0 1-3-3V6a3 3 0 0 1 3-3Zm0 2a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V6a1 1 0 0 0-1-1H6Z" d="M6 3h12a3 3 0 0 1 3 3v12a3 3 0 0 1-3 3H6a3 3 0 0 1-3-3V6a3 3 0 0 1 3-3Zm0 2a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V6a1 1 0 0 0-1-1H6Z"
@ -45,7 +62,13 @@ export const WindowController = (props: WindowControllerProps) => {
</svg> </svg>
} }
> >
<svg width={16} height={16} fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> <svg
width={16}
height={16}
fill="none"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path <path
fill="currentColor" fill="currentColor"
d="M7.518 5H6.009a3.25 3.25 0 0 1 3.24-3h8.001A4.75 4.75 0 0 1 22 6.75v8a3.25 3.25 0 0 1-3 3.24v-1.508a1.75 1.75 0 0 0 1.5-1.732v-8a3.25 3.25 0 0 0-3.25-3.25h-8A1.75 1.75 0 0 0 7.518 5ZM5.25 6A3.25 3.25 0 0 0 2 9.25v9.5A3.25 3.25 0 0 0 5.25 22h9.5A3.25 3.25 0 0 0 18 18.75v-9.5A3.25 3.25 0 0 0 14.75 6h-9.5ZM3.5 9.25c0-.966.784-1.75 1.75-1.75h9.5c.967 0 1.75.784 1.75 1.75v9.5a1.75 1.75 0 0 1-1.75 1.75h-9.5a1.75 1.75 0 0 1-1.75-1.75v-9.5Z" d="M7.518 5H6.009a3.25 3.25 0 0 1 3.24-3h8.001A4.75 4.75 0 0 1 22 6.75v8a3.25 3.25 0 0 1-3 3.24v-1.508a1.75 1.75 0 0 0 1.5-1.732v-8a3.25 3.25 0 0 0-3.25-3.25h-8A1.75 1.75 0 0 0 7.518 5ZM5.25 6A3.25 3.25 0 0 0 2 9.25v9.5A3.25 3.25 0 0 0 5.25 22h9.5A3.25 3.25 0 0 0 18 18.75v-9.5A3.25 3.25 0 0 0 14.75 6h-9.5ZM3.5 9.25c0-.966.784-1.75 1.75-1.75h9.5c.967 0 1.75.784 1.75 1.75v9.5a1.75 1.75 0 0 1-1.75 1.75h-9.5a1.75 1.75 0 0 1-1.75-1.75v-9.5Z"
@ -54,7 +77,13 @@ export const WindowController = (props: WindowControllerProps) => {
</Show> </Show>
</IconButton> </IconButton>
<IconButton onClick={props.onClose}> <IconButton onClick={props.onClose}>
<svg width={16} height={16} fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> <svg
width={16}
height={16}
fill="none"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path <path
fill="currentColor" fill="currentColor"
d="m4.21 4.387.083-.094a1 1 0 0 1 1.32-.083l.094.083L12 10.585l6.293-6.292a1 1 0 1 1 1.414 1.414L13.415 12l6.292 6.293a1 1 0 0 1 .083 1.32l-.083.094a1 1 0 0 1-1.32.083l-.094-.083L12 13.415l-6.293 6.292a1 1 0 0 1-1.414-1.414L10.585 12 4.293 5.707a1 1 0 0 1-.083-1.32l.083-.094-.083.094Z" d="m4.21 4.387.083-.094a1 1 0 0 1 1.32-.083l.094.083L12 10.585l6.293-6.292a1 1 0 1 1 1.414 1.414L13.415 12l6.292 6.293a1 1 0 0 1 .083 1.32l-.083.094a1 1 0 0 1-1.32.083l-.094-.083L12 13.415l-6.293 6.292a1 1 0 0 1-1.414-1.414L10.585 12 4.293 5.707a1 1 0 0 1-.083-1.32l.083-.094-.083.094Z"

View File

@ -10,7 +10,7 @@ export const onRendererLoad = ({
ipc: { invoke, on }, ipc: { invoke, on },
}: RendererContext<LyricsGeniusPluginConfig>) => { }: RendererContext<LyricsGeniusPluginConfig>) => {
const setLyrics = (lyricsContainer: Element, lyrics: string | null) => { const setLyrics = (lyricsContainer: Element, lyrics: string | null) => {
lyricsContainer.innerHTML = ` const targetHtml = `
<div id="contents" class="style-scope ytmusic-section-list-renderer description ytmusic-description-shelf-renderer genius-lyrics"> <div id="contents" class="style-scope ytmusic-section-list-renderer description ytmusic-description-shelf-renderer genius-lyrics">
${ ${
lyrics?.replaceAll(/\r\n|\r|\n/g, '<br/>') ?? lyrics?.replaceAll(/\r\n|\r|\n/g, '<br/>') ??
@ -20,6 +20,7 @@ export const onRendererLoad = ({
<yt-formatted-string class="footer style-scope ytmusic-description-shelf-renderer" style="align-self: baseline"> <yt-formatted-string class="footer style-scope ytmusic-description-shelf-renderer" style="align-self: baseline">
</yt-formatted-string> </yt-formatted-string>
`; `;
lyricsContainer.innerHTML = window.trustedTypes?.defaultPolicy ? window.trustedTypes.defaultPolicy.createHTML(targetHtml) : targetHtml;
if (lyrics) { if (lyrics) {
const footer = lyricsContainer.querySelector('.footer'); const footer = lyricsContainer.querySelector('.footer');
@ -77,6 +78,7 @@ export const onRendererLoad = ({
applyLyricsTabState(); applyLyricsTabState();
} }
}; };
const applyLyricsTabState = () => { const applyLyricsTabState = () => {
if (lyrics) { if (lyrics) {
tabs.lyrics.removeAttribute('disabled'); tabs.lyrics.removeAttribute('disabled');
@ -86,6 +88,7 @@ export const onRendererLoad = ({
tabs.lyrics.setAttribute('aria-disabled', ''); tabs.lyrics.setAttribute('aria-disabled', '');
} }
}; };
const lyricsTabHandler = () => { const lyricsTabHandler = () => {
const tabContainer = document.querySelector('ytmusic-tab-renderer'); const tabContainer = document.querySelector('ytmusic-tab-renderer');
if (!tabContainer) return; if (!tabContainer) return;

View File

@ -3,13 +3,15 @@ import { DataConnection, Peer } from 'peerjs';
import type { Permission, Profile, VideoData } from './types'; import type { Permission, Profile, VideoData } from './types';
export type ConnectionEventMap = { export type ConnectionEventMap = {
ADD_SONGS: { videoList: VideoData[], index?: number }; ADD_SONGS: { videoList: VideoData[]; index?: number };
REMOVE_SONG: { index: number }; REMOVE_SONG: { index: number };
MOVE_SONG: { fromIndex: number; toIndex: number }; MOVE_SONG: { fromIndex: number; toIndex: number };
IDENTIFY: { profile: Profile } | undefined; IDENTIFY: { profile: Profile } | undefined;
SYNC_PROFILE: { profiles: Record<string, Profile> } | undefined; SYNC_PROFILE: { profiles: Record<string, Profile> } | undefined;
SYNC_QUEUE: { videoList: VideoData[] } | undefined; SYNC_QUEUE: { videoList: VideoData[] } | undefined;
SYNC_PROGRESS: { progress?: number; state?: number; index?: number; } | undefined; SYNC_PROGRESS:
| { progress?: number; state?: number; index?: number }
| undefined;
PERMISSION: Permission | undefined; PERMISSION: Permission | undefined;
}; };
export type ConnectionEventUnion = { export type ConnectionEventUnion = {
@ -24,9 +26,12 @@ type PromiseUtil<T> = {
promise: Promise<T>; promise: Promise<T>;
resolve: (id: T) => void; resolve: (id: T) => void;
reject: (err: unknown) => void; reject: (err: unknown) => void;
} };
export type ConnectionListener = (event: ConnectionEventUnion, conn: DataConnection) => void; export type ConnectionListener = (
event: ConnectionEventUnion,
conn: DataConnection,
) => void;
export type ConnectionMode = 'host' | 'guest' | 'disconnected'; export type ConnectionMode = 'host' | 'guest' | 'disconnected';
export class Connection { export class Connection {
private peer: Peer; private peer: Peer;
@ -95,9 +100,12 @@ export class Connection {
return Object.values(this.connections); return Object.values(this.connections);
} }
public async broadcast<Event extends keyof ConnectionEventMap>(type: Event, payload: ConnectionEventMap[Event]) { public async broadcast<Event extends keyof ConnectionEventMap>(
type: Event,
payload: ConnectionEventMap[Event],
) {
await Promise.all( await Promise.all(
this.getConnections().map((conn) => conn.send({ type, payload })) this.getConnections().map((conn) => conn.send({ type, payload })),
); );
} }
@ -125,7 +133,13 @@ export class Connection {
this.connectionListeners.forEach((listener) => listener(conn)); this.connectionListeners.forEach((listener) => listener(conn));
conn.on('data', (data) => { conn.on('data', (data) => {
if (!data || typeof data !== 'object' || !('type' in data) || !('payload' in data) || !data.type) { if (
!data ||
typeof data !== 'object' ||
!('type' in data) ||
!('payload' in data) ||
!data.type
) {
console.warn('Music Together: Invalid data', data); console.warn('Music Together: Invalid data', data);
return; return;
} }

View File

@ -4,7 +4,7 @@ import itemHTML from './templates/item.html?raw';
import popupHTML from './templates/popup.html?raw'; import popupHTML from './templates/popup.html?raw';
type Placement = type Placement =
'top' | 'top'
| 'bottom' | 'bottom'
| 'right' | 'right'
| 'left' | 'left'
@ -15,32 +15,40 @@ type Placement =
| 'top-right' | 'top-right'
| 'bottom-left' | 'bottom-left'
| 'bottom-right'; | 'bottom-right';
type PopupItem = (ItemRendererProps & { type: 'item'; }) type PopupItem =
| { type: 'divider'; } | (ItemRendererProps & { type: 'item' })
| { type: 'custom'; element: HTMLElement; }; | { type: 'divider' }
| { type: 'custom'; element: HTMLElement };
type PopupProps = { type PopupProps = {
data: PopupItem[]; data: PopupItem[];
anchorAt?: Placement; anchorAt?: Placement;
popupAt?: Placement; popupAt?: Placement;
} };
export const Popup = (props: PopupProps) => { export const Popup = (props: PopupProps) => {
const popup = ElementFromHtml(popupHTML); const popup = ElementFromHtml(popupHTML);
const container = popup.querySelector<HTMLElement>('.music-together-popup-container')!; const container = popup.querySelector<HTMLElement>(
'.music-together-popup-container',
)!;
const items = props.data const items = props.data
.map((props) => { .map((props) => {
if (props.type === 'item') return { if (props.type === 'item')
type: 'item' as const, return {
...ItemRenderer(props), type: 'item' as const,
}; ...ItemRenderer(props),
if (props.type === 'divider') return { };
type: 'divider' as const, if (props.type === 'divider')
element: ElementFromHtml('<div class="music-together-divider horizontal"></div>'), return {
}; type: 'divider' as const,
if (props.type === 'custom') return { element: ElementFromHtml(
type: 'custom' as const, '<div class="music-together-divider horizontal"></div>',
element: props.element, ),
}; };
if (props.type === 'custom')
return {
type: 'custom' as const,
element: props.element,
};
return null; return null;
}) })
@ -80,7 +88,9 @@ export const Popup = (props: PopupProps) => {
setTimeout(() => { setTimeout(() => {
const onClose = (event: MouseEvent) => { const onClose = (event: MouseEvent) => {
const isPopupClick = event.composedPath().some((element) => element === popup); const isPopupClick = event
.composedPath()
.some((element) => element === popup);
if (!isPopupClick) { if (!isPopupClick) {
this.dismiss(); this.dismiss();
document.removeEventListener('click', onClose); document.removeEventListener('click', onClose);
@ -101,7 +111,7 @@ export const Popup = (props: PopupProps) => {
dismiss() { dismiss() {
popup.style.setProperty('opacity', '0'); popup.style.setProperty('opacity', '0');
popup.style.setProperty('pointer-events', 'none'); popup.style.setProperty('pointer-events', 'none');
} },
}; };
}; };
@ -133,6 +143,6 @@ export const ItemRenderer = (props: ItemRendererProps) => {
setText(text: string) { setText(text: string) {
textContainer.replaceChildren(text); textContainer.replaceChildren(text);
}, },
id: props.id id: props.id,
}; };
}; };

View File

@ -6,7 +6,12 @@ import { t } from '@/i18n';
import { createPlugin } from '@/utils'; import { createPlugin } from '@/utils';
import promptOptions from '@/providers/prompt-options'; import promptOptions from '@/providers/prompt-options';
import { getDefaultProfile, type Permission, type Profile, type VideoData } from './types'; import {
getDefaultProfile,
type Permission,
type Profile,
type VideoData,
} from './types';
import { Queue } from './queue'; import { Queue } from './queue';
import { Connection, type ConnectionEventUnion } from './connection'; import { Connection, type ConnectionEventUnion } from './connection';
import { createHostPopup } from './ui/host'; import { createHostPopup } from './ui/host';
@ -26,7 +31,7 @@ type RawAccountData = {
runs: { text: string }[]; runs: { text: string }[];
}; };
accountPhoto: { accountPhoto: {
thumbnails: { url: string; width: number; height: number; }[]; thumbnails: { url: string; width: number; height: number }[];
}; };
settingsEndpoint: unknown; settingsEndpoint: unknown;
manageAccountTitle: unknown; manageAccountTitle: unknown;
@ -59,7 +64,7 @@ export default createPlugin<
stateInterval?: number; stateInterval?: number;
updateNext: boolean; updateNext: boolean;
ignoreChange: boolean; ignoreChange: boolean;
rollbackInjector?: (() => void); rollbackInjector?: () => void;
me?: Omit<Profile, 'id'>; me?: Omit<Profile, 'id'>;
profiles: Record<string, Profile>; profiles: Record<string, Profile>;
permission: Permission; permission: Permission;
@ -79,16 +84,18 @@ export default createPlugin<
restartNeeded: false, restartNeeded: false,
addedVersion: '3.2.X', addedVersion: '3.2.X',
config: { config: {
enabled: false enabled: false,
}, },
stylesheets: [style], stylesheets: [style],
backend({ ipc }) { backend({ ipc }) {
ipc.handle('music-together:prompt', async (title: string, label: string) => prompt({ ipc.handle('music-together:prompt', async (title: string, label: string) =>
title, prompt({
label, title,
type: 'input', label,
...promptOptions() type: 'input',
})); ...promptOptions(),
}),
);
}, },
renderer: { renderer: {
updateNext: false, updateNext: false,
@ -112,15 +119,19 @@ export default createPlugin<
videoChangeListener(event: CustomEvent<VideoDataChanged>) { videoChangeListener(event: CustomEvent<VideoDataChanged>) {
if (event.detail.name === 'dataloaded' || this.updateNext) { if (event.detail.name === 'dataloaded' || this.updateNext) {
if (this.connection?.mode === 'host') { if (this.connection?.mode === 'host') {
const videoList: VideoData[] = this.queue?.flatItems.map((it) => ({ const videoList: VideoData[] =
videoId: it!.videoId, this.queue?.flatItems.map(
ownerId: this.connection!.id (it) =>
} satisfies VideoData)) ?? []; ({
videoId: it!.videoId,
ownerId: this.connection!.id,
}) satisfies VideoData,
) ?? [];
this.queue?.setVideoList(videoList, false); this.queue?.setVideoList(videoList, false);
this.queue?.syncQueueOwner(); this.queue?.syncQueueOwner();
this.connection.broadcast('SYNC_QUEUE', { this.connection.broadcast('SYNC_QUEUE', {
videoList videoList,
}); });
this.updateNext = event.detail.name === 'dataloaded'; this.updateNext = event.detail.name === 'dataloaded';
@ -138,7 +149,7 @@ export default createPlugin<
this.connection.broadcast('SYNC_PROGRESS', { this.connection.broadcast('SYNC_PROGRESS', {
// progress: this.playerApi?.getCurrentTime(), // progress: this.playerApi?.getCurrentTime(),
state: this.playerApi?.getPlayerState() state: this.playerApi?.getPlayerState(),
// index: this.queue?.selectedIndex ?? 0, // index: this.queue?.selectedIndex ?? 0,
}); });
}, },
@ -150,13 +161,17 @@ export default createPlugin<
if (!wait) return false; if (!wait) return false;
if (!this.me) this.me = getDefaultProfile(this.connection.id); if (!this.me) this.me = getDefaultProfile(this.connection.id);
const rawItems = this.queue?.flatItems?.map((it) => ({ const rawItems =
videoId: it!.videoId, this.queue?.flatItems?.map(
ownerId: this.connection!.id (it) =>
} satisfies VideoData)) ?? []; ({
videoId: it!.videoId,
ownerId: this.connection!.id,
}) satisfies VideoData,
) ?? [];
this.queue?.setOwner({ this.queue?.setOwner({
id: this.connection.id, id: this.connection.id,
...this.me ...this.me,
}); });
this.queue?.setVideoList(rawItems, false); this.queue?.setVideoList(rawItems, false);
this.queue?.syncQueueOwner(); this.queue?.syncQueueOwner();
@ -166,31 +181,41 @@ export default createPlugin<
this.profiles = {}; this.profiles = {};
this.connection.onConnections((connection) => { this.connection.onConnections((connection) => {
if (!connection) { if (!connection) {
this.api?.openToast(t('plugins.music-together.toast.disconnected')); this.api?.toastService?.show(
t('plugins.music-together.toast.disconnected'),
);
this.onStop(); this.onStop();
return; return;
} }
if (!connection.open) { if (!connection.open) {
this.api?.openToast(t('plugins.music-together.toast.user-disconnected', { this.api?.toastService?.show(
name: this.profiles[connection.peer]?.name t('plugins.music-together.toast.user-disconnected', {
})); name: this.profiles[connection.peer]?.name,
}),
);
this.putProfile(connection.peer, undefined); this.putProfile(connection.peer, undefined);
} }
}); });
this.putProfile(this.connection.id, { this.putProfile(this.connection.id, {
id: this.connection.id, id: this.connection.id,
...this.me ...this.me,
}); });
const listener = async (event: ConnectionEventUnion, conn?: DataConnection) => { const listener = async (
event: ConnectionEventUnion,
conn?: DataConnection,
) => {
this.ignoreChange = true; this.ignoreChange = true;
switch (event.type) { switch (event.type) {
case 'ADD_SONGS': { case 'ADD_SONGS': {
if (conn && this.permission === 'host-only') return; if (conn && this.permission === 'host-only') return;
await this.queue?.addVideos(event.payload.videoList, event.payload.index); await this.queue?.addVideos(
event.payload.videoList,
event.payload.index,
);
await this.connection?.broadcast('ADD_SONGS', event.payload); await this.connection?.broadcast('ADD_SONGS', event.payload);
break; break;
} }
@ -204,27 +229,38 @@ export default createPlugin<
case 'MOVE_SONG': { case 'MOVE_SONG': {
if (conn && this.permission === 'host-only') { if (conn && this.permission === 'host-only') {
await this.connection?.broadcast('SYNC_QUEUE', { await this.connection?.broadcast('SYNC_QUEUE', {
videoList: this.queue?.videoList ?? [] videoList: this.queue?.videoList ?? [],
}); });
break; break;
} }
this.queue?.moveItem(event.payload.fromIndex, event.payload.toIndex); this.queue?.moveItem(
event.payload.fromIndex,
event.payload.toIndex,
);
await this.connection?.broadcast('MOVE_SONG', event.payload); await this.connection?.broadcast('MOVE_SONG', event.payload);
break; break;
} }
case 'IDENTIFY': { case 'IDENTIFY': {
if (!event.payload || !conn) { if (!event.payload || !conn) {
console.warn('Music Together [Host]: Received "IDENTIFY" event without payload or connection'); console.warn(
'Music Together [Host]: Received "IDENTIFY" event without payload or connection',
);
break; break;
} }
this.api?.openToast(t('plugins.music-together.toast.user-connected', { name: event.payload.profile.name })); this.api?.toastService?.show(
t('plugins.music-together.toast.user-connected', {
name: event.payload.profile.name,
}),
);
this.putProfile(conn.peer, event.payload.profile); this.putProfile(conn.peer, event.payload.profile);
break; break;
} }
case 'SYNC_PROFILE': { case 'SYNC_PROFILE': {
await this.connection?.broadcast('SYNC_PROFILE', { profiles: this.profiles }); await this.connection?.broadcast('SYNC_PROFILE', {
profiles: this.profiles,
});
break; break;
} }
@ -237,7 +273,7 @@ export default createPlugin<
} }
case 'SYNC_QUEUE': { case 'SYNC_QUEUE': {
await this.connection?.broadcast('SYNC_QUEUE', { await this.connection?.broadcast('SYNC_QUEUE', {
videoList: this.queue?.videoList ?? [] videoList: this.queue?.videoList ?? [],
}); });
break; break;
} }
@ -251,7 +287,8 @@ export default createPlugin<
if (permissionLevel >= 2) { if (permissionLevel >= 2) {
if (typeof event.payload?.progress === 'number') { if (typeof event.payload?.progress === 'number') {
const currentTime = this.playerApi?.getCurrentTime() ?? 0; const currentTime = this.playerApi?.getCurrentTime() ?? 0;
if (Math.abs(event.payload.progress - currentTime) > 3) this.playerApi?.seekTo(event.payload.progress); if (Math.abs(event.payload.progress - currentTime) > 3)
this.playerApi?.seekTo(event.payload.progress);
} }
if (this.playerApi?.getPlayerState() !== event.payload?.state) { if (this.playerApi?.getPlayerState() !== event.payload?.state) {
if (event.payload?.state === 2) this.playerApi?.pauseVideo(); if (event.payload?.state === 2) this.playerApi?.pauseVideo();
@ -300,25 +337,32 @@ export default createPlugin<
this.profiles = {}; this.profiles = {};
const id = await this.showPrompt(t('plugins.music-together.name'), t('plugins.music-together.dialog.enter-host')); const id = await this.showPrompt(
t('plugins.music-together.name'),
t('plugins.music-together.dialog.enter-host'),
);
if (typeof id !== 'string') return false; if (typeof id !== 'string') return false;
const connection = await this.connection.connect(id).catch(() => false); const connection = await this.connection.connect(id).catch(() => false);
if (!connection) return false; if (!connection) return false;
this.connection.onConnections((connection) => { this.connection.onConnections((connection) => {
if (!connection?.open) { if (!connection?.open) {
this.api?.openToast(t('plugins.music-together.toast.disconnected')); this.api?.toastService?.show(
t('plugins.music-together.toast.disconnected'),
);
this.onStop(); this.onStop();
} }
}); });
let resolveIgnore: number | null = null; let resolveIgnore: number | null = null;
const listener = async (event: ConnectionEventUnion) => { const listener = async (event: ConnectionEventUnion) => {
this.ignoreChange = true; this.ignoreChange = true;
switch (event.type) { switch (event.type) {
case 'ADD_SONGS': { case 'ADD_SONGS': {
await this.queue?.addVideos(event.payload.videoList, event.payload.index); await this.queue?.addVideos(
event.payload.videoList,
event.payload.index,
);
break; break;
} }
case 'REMOVE_SONG': { case 'REMOVE_SONG': {
@ -326,11 +370,16 @@ export default createPlugin<
break; break;
} }
case 'MOVE_SONG': { case 'MOVE_SONG': {
this.queue?.moveItem(event.payload.fromIndex, event.payload.toIndex); this.queue?.moveItem(
event.payload.fromIndex,
event.payload.toIndex,
);
break; break;
} }
case 'IDENTIFY': { case 'IDENTIFY': {
console.warn('Music Together [Guest]: Received "IDENTIFY" event from guest'); console.warn(
'Music Together [Guest]: Received "IDENTIFY" event from guest',
);
break; break;
} }
case 'SYNC_QUEUE': { case 'SYNC_QUEUE': {
@ -341,7 +390,9 @@ export default createPlugin<
} }
case 'SYNC_PROFILE': { case 'SYNC_PROFILE': {
if (!event.payload) { if (!event.payload) {
console.warn('Music Together [Guest]: Received "SYNC_PROFILE" event without payload'); console.warn(
'Music Together [Guest]: Received "SYNC_PROFILE" event without payload',
);
break; break;
} }
@ -353,7 +404,8 @@ export default createPlugin<
case 'SYNC_PROGRESS': { case 'SYNC_PROGRESS': {
if (typeof event.payload?.progress === 'number') { if (typeof event.payload?.progress === 'number') {
const currentTime = this.playerApi?.getCurrentTime() ?? 0; const currentTime = this.playerApi?.getCurrentTime() ?? 0;
if (Math.abs(event.payload.progress - currentTime) > 3) this.playerApi?.seekTo(event.payload.progress); if (Math.abs(event.payload.progress - currentTime) > 3)
this.playerApi?.seekTo(event.payload.progress);
} }
if (this.playerApi?.getPlayerState() !== event.payload?.state) { if (this.playerApi?.getPlayerState() !== event.payload?.state) {
if (event.payload?.state === 2) this.playerApi?.pauseVideo(); if (event.payload?.state === 2) this.playerApi?.pauseVideo();
@ -370,7 +422,9 @@ export default createPlugin<
} }
case 'PERMISSION': { case 'PERMISSION': {
if (!event.payload) { if (!event.payload) {
console.warn('Music Together [Guest]: Received "PERMISSION" event without payload'); console.warn(
'Music Together [Guest]: Received "PERMISSION" event without payload',
);
break; break;
} }
@ -379,9 +433,15 @@ export default createPlugin<
this.popups.host.setPermission(this.permission); this.popups.host.setPermission(this.permission);
this.popups.setting.setPermission(this.permission); this.popups.setting.setPermission(this.permission);
const permissionLabel = t(`plugins.music-together.menu.permission.${this.permission}`); const permissionLabel = t(
`plugins.music-together.menu.permission.${this.permission}`,
);
this.api?.openToast(t('plugins.music-together.toast.permission-changed', { permission: permissionLabel })); this.api?.toastService?.show(
t('plugins.music-together.toast.permission-changed', {
permission: permissionLabel,
}),
);
break; break;
} }
default: { default: {
@ -415,8 +475,10 @@ export default createPlugin<
break; break;
} }
case 'SYNC_PROGRESS': { case 'SYNC_PROGRESS': {
if (this.permission === 'host-only') await this.connection?.broadcast('SYNC_QUEUE', undefined); if (this.permission === 'host-only')
else await this.connection?.broadcast('SYNC_PROGRESS', event.payload); await this.connection?.broadcast('SYNC_QUEUE', undefined);
else
await this.connection?.broadcast('SYNC_PROGRESS', event.payload);
break; break;
} }
} }
@ -431,12 +493,16 @@ export default createPlugin<
this.queue?.injection(); this.queue?.injection();
this.queue?.setOwner({ this.queue?.setOwner({
id: this.connection.id, id: this.connection.id,
...this.me ...this.me,
}); });
const progress = Array.from(document.querySelectorAll<HTMLElement & { const progress = Array.from(
_update: (...args: unknown[]) => void document.querySelectorAll<
}>('tp-yt-paper-progress')); HTMLElement & {
_update: (...args: unknown[]) => void;
}
>('tp-yt-paper-progress'),
);
const rollbackList = progress.map((progress) => { const rollbackList = progress.map((progress) => {
const original = progress._update; const original = progress._update;
progress._update = (...args) => { progress._update = (...args) => {
@ -444,10 +510,11 @@ export default createPlugin<
if (this.permission === 'all' && typeof now === 'number') { if (this.permission === 'all' && typeof now === 'number') {
const currentTime = this.playerApi?.getCurrentTime() ?? 0; const currentTime = this.playerApi?.getCurrentTime() ?? 0;
if (Math.abs(now - currentTime) > 3) this.connection?.broadcast('SYNC_PROGRESS', { if (Math.abs(now - currentTime) > 3)
progress: now, this.connection?.broadcast('SYNC_PROGRESS', {
state: this.playerApi?.getPlayerState() progress: now,
}); state: this.playerApi?.getPlayerState(),
});
} }
original.call(progress, ...args); original.call(progress, ...args);
@ -466,8 +533,8 @@ export default createPlugin<
id: this.connection.id, id: this.connection.id,
handleId: this.me.handleId, handleId: this.me.handleId,
name: this.me.name, name: this.me.name,
thumbnail: this.me.thumbnail thumbnail: this.me.thumbnail,
} },
}); });
this.connection.broadcast('SYNC_PROFILE', undefined); this.connection.broadcast('SYNC_PROFILE', undefined);
@ -525,14 +592,18 @@ export default createPlugin<
}, },
initMyProfile() { initMyProfile() {
const accountButton = document.querySelector<HTMLElement & { const accountButton = document.querySelector<
onButtonTap: () => void HTMLElement & {
}>('ytmusic-settings-button'); onButtonTap: () => void;
}
>('ytmusic-settings-button');
accountButton?.onButtonTap(); accountButton?.onButtonTap();
setTimeout(() => { setTimeout(() => {
accountButton?.onButtonTap(); accountButton?.onButtonTap();
const renderer = document.querySelector<HTMLElement & { data: unknown }>('ytd-active-account-header-renderer'); const renderer = document.querySelector<
HTMLElement & { data: unknown }
>('ytd-active-account-header-renderer');
if (!accountButton || !renderer) { if (!accountButton || !renderer) {
console.warn('Music Together: Cannot find account'); console.warn('Music Together: Cannot find account');
this.me = getDefaultProfile(this.connection?.id ?? ''); this.me = getDefaultProfile(this.connection?.id ?? '');
@ -543,7 +614,7 @@ export default createPlugin<
this.me = { this.me = {
handleId: accountData.channelHandle.runs[0].text, handleId: accountData.channelHandle.runs[0].text,
name: accountData.accountName.runs[0].text, name: accountData.accountName.runs[0].text,
thumbnail: accountData.accountPhoto.thumbnails[0].url thumbnail: accountData.accountPhoto.thumbnails[0].url,
}; };
if (this.me.thumbnail) { if (this.me.thumbnail) {
@ -557,14 +628,23 @@ export default createPlugin<
start({ ipc }) { start({ ipc }) {
this.ipc = ipc; this.ipc = ipc;
this.showPrompt = async (title: string, label: string) => ipc.invoke('music-together:prompt', title, label) as Promise<string>; this.showPrompt = async (title: string, label: string) =>
ipc.invoke('music-together:prompt', title, label) as Promise<string>;
this.api = document.querySelector<AppElement>('ytmusic-app'); this.api = document.querySelector<AppElement>('ytmusic-app');
/* setup */ /* setup */
document.querySelector('#right-content > ytmusic-settings-button')?.insertAdjacentHTML('beforebegin', settingHTML); document
const setting = document.querySelector<HTMLElement>('#music-together-setting-button'); .querySelector('#right-content > ytmusic-settings-button')
const icon = document.querySelector<SVGElement>('#music-together-setting-button > svg'); ?.insertAdjacentHTML('beforebegin', settingHTML);
const spinner = document.querySelector<HTMLElement>('#music-together-setting-button > tp-yt-paper-spinner-lite'); 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) { if (!setting || !icon || !spinner) {
console.warn('Music Together: Cannot inject html'); console.warn('Music Together: Cannot inject html');
console.log(setting, icon, spinner); console.log(setting, icon, spinner);
@ -574,7 +654,7 @@ export default createPlugin<
this.elements = { this.elements = {
setting, setting,
icon, icon,
spinner spinner,
}; };
this.stateInterval = window.setInterval(() => { this.stateInterval = window.setInterval(() => {
@ -584,7 +664,7 @@ export default createPlugin<
this.connection.broadcast('SYNC_PROGRESS', { this.connection.broadcast('SYNC_PROGRESS', {
progress: this.playerApi?.getCurrentTime(), progress: this.playerApi?.getCurrentTime(),
state: this.playerApi?.getPlayerState(), state: this.playerApi?.getPlayerState(),
index index,
}); });
}, 1000); }, 1000);
@ -593,18 +673,25 @@ export default createPlugin<
onItemClick: (id) => { onItemClick: (id) => {
if (id === 'music-together-close') { if (id === 'music-together-close') {
this.onStop(); this.onStop();
this.api?.openToast(t('plugins.music-together.toast.closed')); this.api?.toastService?.show(
t('plugins.music-together.toast.closed'),
);
hostPopup.dismiss(); hostPopup.dismiss();
} }
if (id === 'music-together-copy-id') { if (id === 'music-together-copy-id') {
navigator.clipboard.writeText(this.connection?.id ?? '') navigator.clipboard
.writeText(this.connection?.id ?? '')
.then(() => { .then(() => {
this.api?.openToast(t('plugins.music-together.toast.id-copied')); this.api?.toastService?.show(
t('plugins.music-together.toast.id-copied'),
);
hostPopup.dismiss(); hostPopup.dismiss();
}) })
.catch(() => { .catch(() => {
this.api?.openToast(t('plugins.music-together.toast.id-copy-failed')); this.api?.toastService?.show(
t('plugins.music-together.toast.id-copy-failed'),
);
hostPopup.dismiss(); hostPopup.dismiss();
}); });
} }
@ -612,30 +699,39 @@ export default createPlugin<
if (id === 'music-together-permission') { if (id === 'music-together-permission') {
if (this.permission === 'all') this.permission = 'host-only'; if (this.permission === 'all') this.permission = 'host-only';
else if (this.permission === 'playlist') this.permission = 'all'; else if (this.permission === 'playlist') this.permission = 'all';
else if (this.permission === 'host-only') this.permission = 'playlist'; else if (this.permission === 'host-only')
this.permission = 'playlist';
this.connection?.broadcast('PERMISSION', this.permission); this.connection?.broadcast('PERMISSION', this.permission);
hostPopup.setPermission(this.permission); hostPopup.setPermission(this.permission);
guestPopup.setPermission(this.permission); guestPopup.setPermission(this.permission);
settingPopup.setPermission(this.permission); settingPopup.setPermission(this.permission);
const permissionLabel = t(`plugins.music-together.menu.permission.${this.permission}`); const permissionLabel = t(
this.api?.openToast(t('plugins.music-together.toast.permission-changed', { permission: permissionLabel })); `plugins.music-together.menu.permission.${this.permission}`,
);
this.api?.toastService?.show(
t('plugins.music-together.toast.permission-changed', {
permission: permissionLabel,
}),
);
const item = hostPopup.items.find((it) => it?.element.id === id); const item = hostPopup.items.find((it) => it?.element.id === id);
if (item?.type === 'item') { if (item?.type === 'item') {
item.setText(t('plugins.music-together.menu.set-permission')); item.setText(t('plugins.music-together.menu.set-permission'));
} }
} }
} },
}); });
const guestPopup = createGuestPopup({ const guestPopup = createGuestPopup({
onItemClick: (id) => { onItemClick: (id) => {
if (id === 'music-together-disconnect') { if (id === 'music-together-disconnect') {
this.onStop(); this.onStop();
this.api?.openToast(t('plugins.music-together.toast.disconnected')); this.api?.toastService?.show(
t('plugins.music-together.toast.disconnected'),
);
guestPopup.dismiss(); guestPopup.dismiss();
} }
} },
}); });
const settingPopup = createSettingPopup({ const settingPopup = createSettingPopup({
onItemClick: async (id) => { onItemClick: async (id) => {
@ -646,16 +742,24 @@ export default createPlugin<
this.hideSpinner(); this.hideSpinner();
if (result) { if (result) {
navigator.clipboard.writeText(this.connection?.id ?? '') navigator.clipboard
.writeText(this.connection?.id ?? '')
.then(() => { .then(() => {
this.api?.openToast(t('plugins.music-together.toast.id-copied')); this.api?.toastService?.show(
t('plugins.music-together.toast.id-copied'),
);
hostPopup.showAtAnchor(setting); hostPopup.showAtAnchor(setting);
}).catch(() => { })
this.api?.openToast(t('plugins.music-together.toast.id-copy-failed')); .catch(() => {
this.api?.toastService?.show(
t('plugins.music-together.toast.id-copy-failed'),
);
hostPopup.showAtAnchor(setting); hostPopup.showAtAnchor(setting);
}); });
} else { } else {
this.api?.openToast(t('plugins.music-together.toast.host-failed')); this.api?.toastService?.show(
t('plugins.music-together.toast.host-failed'),
);
} }
} }
@ -666,18 +770,22 @@ export default createPlugin<
this.hideSpinner(); this.hideSpinner();
if (result) { if (result) {
this.api?.openToast(t('plugins.music-together.toast.joined')); this.api?.toastService?.show(
t('plugins.music-together.toast.joined'),
);
guestPopup.showAtAnchor(setting); guestPopup.showAtAnchor(setting);
} else { } else {
this.api?.openToast(t('plugins.music-together.toast.join-failed')); this.api?.toastService?.show(
t('plugins.music-together.toast.join-failed'),
);
} }
} }
} },
}); });
this.popups = { this.popups = {
host: hostPopup, host: hostPopup,
guest: guestPopup, guest: guestPopup,
setting: settingPopup setting: settingPopup,
}; };
setting.addEventListener('click', () => { setting.addEventListener('click', () => {
let popup = settingPopup; let popup = settingPopup;
@ -695,24 +803,38 @@ export default createPlugin<
this.queue = new Queue({ this.queue = new Queue({
owner: { owner: {
id: this.connection?.id ?? '', id: this.connection?.id ?? '',
...this.me! ...this.me!,
}, },
getProfile: (id) => this.profiles[id] getProfile: (id) => this.profiles[id],
}); });
this.playerApi = playerApi; this.playerApi = playerApi;
this.playerApi.addEventListener('onStateChange', this.videoStateChangeListener); this.playerApi.addEventListener(
'onStateChange',
this.videoStateChangeListener,
);
document.addEventListener('videodatachange', this.videoChangeListener); document.addEventListener('videodatachange', this.videoChangeListener);
}, },
stop() { stop() {
const dividers = Array.from(document.querySelectorAll('.music-together-divider')); const dividers = Array.from(
document.querySelectorAll('.music-together-divider'),
);
dividers.forEach((divider) => divider.remove()); dividers.forEach((divider) => divider.remove());
this.elements.setting?.remove(); this.elements.setting?.remove();
this.onStop(); this.onStop();
if (typeof this.stateInterval === 'number') clearInterval(this.stateInterval); if (typeof this.stateInterval === 'number')
if (this.playerApi) this.playerApi.removeEventListener('onStateChange', this.videoStateChangeListener); clearInterval(this.stateInterval);
if (this.videoChangeListener) document.removeEventListener('videodatachange', this.videoChangeListener); if (this.playerApi)
} this.playerApi.removeEventListener(
} 'onStateChange',
this.videoStateChangeListener,
);
if (this.videoChangeListener)
document.removeEventListener(
'videodatachange',
this.videoChangeListener,
);
},
},
}); });

View File

@ -1,11 +1,20 @@
import { SHA1Hash } from './sha1hash'; import { SHA1Hash } from './sha1hash';
export const extractToken = (cookie = document.cookie) => cookie.match(/SAPISID=([^;]+);/)?.[1] ?? cookie.match(/__Secure-3PAPISID=([^;]+);/)?.[1]; 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') => export const getHash = async (
(await SHA1Hash(`${millis} ${papisid} ${origin}`)).toLowerCase(); 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') => { export const getAuthorizationHeader = async (
papisid: string,
millis = Date.now(),
origin: string = 'https://music.youtube.com',
) => {
return `SAPISIDHASH ${millis}_${await getHash(papisid, millis, origin)}`; return `SAPISIDHASH ${millis}_${await getHash(papisid, millis, origin)}`;
}; };
@ -23,15 +32,17 @@ export const getClient = () => {
platform: 'DESKTOP', platform: 'DESKTOP',
timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone, timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
locationInfo: { locationInfo: {
locationPermissionAuthorizationStatus: 'LOCATION_PERMISSION_AUTHORIZATION_STATUS_UNSUPPORTED', locationPermissionAuthorizationStatus:
'LOCATION_PERMISSION_AUTHORIZATION_STATUS_UNSUPPORTED',
}, },
musicAppInfo: { musicAppInfo: {
pwaInstallabilityStatus: 'PWA_INSTALLABILITY_STATUS_UNKNOWN', pwaInstallabilityStatus: 'PWA_INSTALLABILITY_STATUS_UNKNOWN',
webDisplayMode: 'WEB_DISPLAY_MODE_BROWSER', webDisplayMode: 'WEB_DISPLAY_MODE_BROWSER',
storeDigitalGoodsApiSupportStatus: { storeDigitalGoodsApiSupportStatus: {
playStoreDigitalGoodsApiSupportStatus: 'DIGITAL_GOODS_API_SUPPORT_STATUS_UNSUPPORTED', playStoreDigitalGoodsApiSupportStatus:
'DIGITAL_GOODS_API_SUPPORT_STATUS_UNSUPPORTED',
}, },
}, },
utcOffsetMinutes: -1 * (new Date()).getTimezoneOffset(), utcOffsetMinutes: -1 * new Date().getTimezoneOffset(),
}; };
}; };

View File

@ -54,46 +54,46 @@ const getHeaderPayload = (() => {
title: { title: {
runs: [ runs: [
{ {
text: t('plugins.music-together.internal.track-source') text: t('plugins.music-together.internal.track-source'),
} },
] ],
}, },
subtitle: { subtitle: {
runs: [ runs: [
{ {
text: t('plugins.music-together.name') text: t('plugins.music-together.name'),
} },
] ],
}, },
buttons: [ buttons: [
{ {
chipCloudChipRenderer: { chipCloudChipRenderer: {
style: { style: {
styleType: 'STYLE_TRANSPARENT' styleType: 'STYLE_TRANSPARENT',
}, },
text: { text: {
runs: [ runs: [
{ {
text: t('plugins.music-together.internal.save') text: t('plugins.music-together.internal.save'),
} },
] ],
}, },
navigationEndpoint: { navigationEndpoint: {
saveQueueToPlaylistCommand: {} saveQueueToPlaylistCommand: {},
}, },
icon: { icon: {
iconType: 'ADD_TO_PLAYLIST' iconType: 'ADD_TO_PLAYLIST',
}, },
accessibilityData: { accessibilityData: {
accessibilityData: { accessibilityData: {
label: t('plugins.music-together.internal.save') label: t('plugins.music-together.internal.save'),
} },
}, },
isSelected: false, isSelected: false,
uniqueId: t('plugins.music-together.internal.save') uniqueId: t('plugins.music-together.internal.save'),
} },
} },
] ],
}; };
} }
@ -106,7 +106,7 @@ export type QueueOptions = {
owner?: Profile; owner?: Profile;
queue?: QueueElement; queue?: QueueElement;
getProfile: (id: string) => Profile | undefined; getProfile: (id: string) => Profile | undefined;
} };
export type QueueEventListener = (event: ConnectionEventUnion) => void; export type QueueEventListener = (event: ConnectionEventUnion) => void;
export class Queue { export class Queue {
@ -114,7 +114,7 @@ export class Queue {
private originalDispatch?: (obj: { private originalDispatch?: (obj: {
type: string; type: string;
payload?: { items?: QueueItem[] | undefined; }; payload?: { items?: QueueItem[] | undefined };
}) => void; }) => void;
private internalDispatch = false; private internalDispatch = false;
@ -126,7 +126,8 @@ export class Queue {
constructor(options: QueueOptions) { constructor(options: QueueOptions) {
this.getProfile = options.getProfile; this.getProfile = options.getProfile;
this.queue = options.queue ?? (document.querySelector<QueueElement>('#queue')!); this.queue =
options.queue ?? document.querySelector<QueueElement>('#queue')!;
this.owner = options.owner ?? null; this.owner = options.owner ?? null;
this._videoList = options.videoList ?? []; this._videoList = options.videoList ?? [];
} }
@ -139,7 +140,12 @@ export class Queue {
} }
get selectedIndex() { get selectedIndex() {
return mapQueueItem((it) => it?.selected, this.queue.queue.store.store.getState().queue.items).findIndex(Boolean) ?? 0; return (
mapQueueItem(
(it) => it?.selected,
this.queue.queue.store.store.getState().queue.items,
).findIndex(Boolean) ?? 0
);
} }
get rawItems() { get rawItems() {
@ -162,7 +168,9 @@ export class Queue {
} }
async addVideos(videos: VideoData[], index?: number) { async addVideos(videos: VideoData[], index?: number) {
const response = await getMusicQueueRenderer(videos.map((it) => it.videoId)); const response = await getMusicQueueRenderer(
videos.map((it) => it.videoId),
);
if (!response) return false; if (!response) return false;
const items = response.queueDatas.map((it) => it?.content).filter(Boolean); const items = response.queueDatas.map((it) => it?.content).filter(Boolean);
@ -173,12 +181,16 @@ export class Queue {
this.queue?.dispatch({ this.queue?.dispatch({
type: 'ADD_ITEMS', type: 'ADD_ITEMS',
payload: { payload: {
nextQueueItemId: this.queue.queue.store.store.getState().queue.nextQueueItemId, nextQueueItemId:
index: index ?? this.queue.queue.store.store.getState().queue.items.length ?? 0, this.queue.queue.store.store.getState().queue.nextQueueItemId,
index:
index ??
this.queue.queue.store.store.getState().queue.items.length ??
0,
items, items,
shuffleEnabled: false, shuffleEnabled: false,
shouldAssignIds: true shouldAssignIds: true,
} },
}); });
this.internalDispatch = false; this.internalDispatch = false;
setTimeout(() => { setTimeout(() => {
@ -194,7 +206,7 @@ export class Queue {
this._videoList.splice(index, 1); this._videoList.splice(index, 1);
this.queue?.dispatch({ this.queue?.dispatch({
type: 'REMOVE_ITEM', type: 'REMOVE_ITEM',
payload: index payload: index,
}); });
this.internalDispatch = false; this.internalDispatch = false;
setTimeout(() => { setTimeout(() => {
@ -207,7 +219,7 @@ export class Queue {
this.internalDispatch = true; this.internalDispatch = true;
this.queue?.dispatch({ this.queue?.dispatch({
type: 'SET_INDEX', type: 'SET_INDEX',
payload: index payload: index,
}); });
this.internalDispatch = false; this.internalDispatch = false;
} }
@ -220,8 +232,8 @@ export class Queue {
type: 'MOVE_ITEM', type: 'MOVE_ITEM',
payload: { payload: {
fromIndex, fromIndex,
toIndex toIndex,
} },
}); });
this.internalDispatch = false; this.internalDispatch = false;
setTimeout(() => { setTimeout(() => {
@ -234,7 +246,7 @@ export class Queue {
this.internalDispatch = true; this.internalDispatch = true;
this._videoList = []; this._videoList = [];
this.queue?.dispatch({ this.queue?.dispatch({
type: 'CLEAR' type: 'CLEAR',
}); });
this.internalDispatch = false; this.internalDispatch = false;
} }
@ -253,7 +265,8 @@ export class Queue {
return; return;
} }
if (this.originalDispatch) this.queue.queue.store.store.dispatch = this.originalDispatch; if (this.originalDispatch)
this.queue.queue.store.store.dispatch = this.originalDispatch;
} }
injection() { injection() {
@ -276,40 +289,54 @@ export class Queue {
if (event.type === 'ADD_ITEMS') { if (event.type === 'ADD_ITEMS') {
if (this.ignoreFlag) { if (this.ignoreFlag) {
this.ignoreFlag = false; this.ignoreFlag = false;
const videoList = mapQueueItem((it) => ({ const videoList = mapQueueItem(
videoId: it!.videoId, (it) =>
ownerId: this.owner!.id ({
} satisfies VideoData), event.payload!.items!); videoId: it!.videoId,
ownerId: this.owner!.id,
}) satisfies VideoData,
event.payload!.items!,
);
const index = this._videoList.length + videoList.length - 1; const index = this._videoList.length + videoList.length - 1;
if (videoList.length > 0) { if (videoList.length > 0) {
this.broadcast({ // play this.broadcast({
// play
type: 'ADD_SONGS', type: 'ADD_SONGS',
payload: { payload: {
videoList videoList,
}, },
after: [ after: [
{ {
type: 'SYNC_PROGRESS', type: 'SYNC_PROGRESS',
payload: { payload: {
index index,
} },
} },
] ],
}); });
} }
} else if ((event.payload as { } else if (
items: unknown[]; (
}).items.length === 1) { event.payload as {
this.broadcast({ // add playlist items: unknown[];
}
).items.length === 1
) {
this.broadcast({
// add playlist
type: 'ADD_SONGS', type: 'ADD_SONGS',
payload: { payload: {
// index: (event.payload as any).index, // index: (event.payload as any).index,
videoList: mapQueueItem((it) => ({ videoList: mapQueueItem(
videoId: it!.videoId, (it) =>
ownerId: this.owner!.id ({
} satisfies VideoData), event.payload!.items!) videoId: it!.videoId,
} ownerId: this.owner!.id,
}) satisfies VideoData,
event.payload!.items!,
),
},
}); });
} }
@ -320,13 +347,17 @@ export class Queue {
this.broadcast({ this.broadcast({
type: 'MOVE_SONG', type: 'MOVE_SONG',
payload: { payload: {
fromIndex: (event.payload as { fromIndex: (
fromIndex: number; event.payload as {
}).fromIndex, fromIndex: number;
toIndex: (event.payload as { }
toIndex: number; ).fromIndex,
}).toIndex toIndex: (
} event.payload as {
toIndex: number;
}
).toIndex,
},
}); });
return; return;
} }
@ -334,8 +365,8 @@ export class Queue {
this.broadcast({ this.broadcast({
type: 'REMOVE_SONG', type: 'REMOVE_SONG',
payload: { payload: {
index: event.payload as number index: event.payload as number,
} },
}); });
return; return;
} }
@ -343,8 +374,8 @@ export class Queue {
this.broadcast({ this.broadcast({
type: 'SYNC_PROGRESS', type: 'SYNC_PROGRESS',
payload: { payload: {
index: event.payload as number index: event.payload as number,
} },
}); });
return; return;
} }
@ -355,7 +386,10 @@ export class Queue {
event.payload = undefined; event.payload = undefined;
} }
if (event.type === 'SET_PLAYER_UI_STATE') { if (event.type === 'SET_PLAYER_UI_STATE') {
if (event.payload as string === 'INACTIVE' && this.videoList.length > 0) { if (
(event.payload as string) === 'INACTIVE' &&
this.videoList.length > 0
) {
return; return;
} }
} }
@ -370,7 +404,7 @@ export class Queue {
store: { store: {
...this.queue.queue.store, ...this.queue.queue.store,
dispatch: this.originalDispatch, dispatch: this.originalDispatch,
} },
}, },
}; };
this.originalDispatch?.call(fakeContext, event); this.originalDispatch?.call(fakeContext, event);
@ -384,20 +418,22 @@ export class Queue {
this.internalDispatch = true; this.internalDispatch = true;
this.queue.dispatch({ this.queue.dispatch({
type: 'HAS_SHOWN_AUTOPLAY', type: 'HAS_SHOWN_AUTOPLAY',
payload: false payload: false,
}); });
this.queue.dispatch({ this.queue.dispatch({
type: 'SET_HEADER', type: 'SET_HEADER',
payload: getHeaderPayload(), payload: getHeaderPayload(),
}); });
this.queue.dispatch({ this.queue.dispatch({
type: 'CLEAR_STEERING_CHIPS' type: 'CLEAR_STEERING_CHIPS',
}); });
this.internalDispatch = false; this.internalDispatch = false;
} }
async syncVideo() { async syncVideo() {
const response = await getMusicQueueRenderer(this._videoList.map((it) => it.videoId)); const response = await getMusicQueueRenderer(
this._videoList.map((it) => it.videoId),
);
if (!response) return false; if (!response) return false;
const items = response.queueDatas.map((it) => it.content); const items = response.queueDatas.map((it) => it.content);
@ -407,10 +443,11 @@ export class Queue {
type: 'UPDATE_ITEMS', type: 'UPDATE_ITEMS',
payload: { payload: {
items: items, items: items,
nextQueueItemId: this.queue.queue.store.store.getState().queue.nextQueueItemId, nextQueueItemId:
this.queue.queue.store.store.getState().queue.nextQueueItemId,
shouldAssignIds: true, shouldAssignIds: true,
currentIndex: -1 currentIndex: -1,
} },
}); });
this.internalDispatch = false; this.internalDispatch = false;
setTimeout(() => { setTimeout(() => {
@ -425,7 +462,9 @@ export class Queue {
const allQueue = document.querySelectorAll('#queue'); const allQueue = document.querySelectorAll('#queue');
allQueue.forEach((queue) => { allQueue.forEach((queue) => {
const list = Array.from(queue?.querySelectorAll<HTMLElement>('ytmusic-player-queue-item') ?? []); const list = Array.from(
queue?.querySelectorAll<HTMLElement>('ytmusic-player-queue-item') ?? [],
);
list.forEach((item, index: number | undefined) => { list.forEach((item, index: number | undefined) => {
if (typeof index !== 'number') return; if (typeof index !== 'number') return;
@ -433,14 +472,19 @@ export class Queue {
const id = this._videoList[index]?.ownerId; const id = this._videoList[index]?.ownerId;
const data = this.getProfile(id); const data = this.getProfile(id);
const profile = item.querySelector<HTMLImageElement>('.music-together-owner') ?? document.createElement('img'); const profile =
item.querySelector<HTMLImageElement>('.music-together-owner') ??
document.createElement('img');
profile.classList.add('music-together-owner'); profile.classList.add('music-together-owner');
profile.dataset.id = id; profile.dataset.id = id;
profile.dataset.index = index.toString(); profile.dataset.index = index.toString();
const name = item.querySelector<HTMLElement>('.music-together-name') ?? document.createElement('div'); const name =
item.querySelector<HTMLElement>('.music-together-name') ??
document.createElement('div');
name.classList.add('music-together-name'); name.classList.add('music-together-name');
name.textContent = data?.name ?? t('plugins.music-together.internal.unknown-user'); name.textContent =
data?.name ?? t('plugins.music-together.internal.unknown-user');
if (data) { if (data) {
profile.dataset.thumbnail = data.thumbnail ?? ''; profile.dataset.thumbnail = data.thumbnail ?? '';
@ -463,10 +507,14 @@ export class Queue {
const allQueue = document.querySelectorAll('#queue'); const allQueue = document.querySelectorAll('#queue');
allQueue.forEach((queue) => { allQueue.forEach((queue) => {
const list = Array.from(queue?.querySelectorAll<HTMLElement>('ytmusic-player-queue-item') ?? []); const list = Array.from(
queue?.querySelectorAll<HTMLElement>('ytmusic-player-queue-item') ?? [],
);
list.forEach((item) => { list.forEach((item) => {
const profile = item.querySelector<HTMLImageElement>('.music-together-owner'); const profile = item.querySelector<HTMLImageElement>(
'.music-together-owner',
);
const name = item.querySelector<HTMLElement>('.music-together-name'); const name = item.querySelector<HTMLElement>('.music-together-name');
profile?.remove(); profile?.remove();
name?.remove(); name?.remove();

View File

@ -8,7 +8,9 @@ type QueueRendererResponse = {
trackingParams: string; trackingParams: string;
}; };
export const getMusicQueueRenderer = async (videoIds: string[]): Promise<QueueRendererResponse | null> => { export const getMusicQueueRenderer = async (
videoIds: string[],
): Promise<QueueRendererResponse | null> => {
const token = extractToken(); const token = extractToken();
if (!token) return null; if (!token) return null;
@ -35,8 +37,8 @@ export const getMusicQueueRenderer = async (videoIds: string[]): Promise<QueueRe
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Origin': 'https://music.youtube.com', 'Origin': 'https://music.youtube.com',
'Authorization': await getAuthorizationHeader(token), 'Authorization': await getAuthorizationHeader(token),
} },
} },
); );
const text = await response.text(); const text = await response.text();

View File

@ -1,21 +1,26 @@
import { import {
ItemPlaylistPanelVideoRenderer, ItemPlaylistPanelVideoRenderer,
PlaylistPanelVideoWrapperRenderer, PlaylistPanelVideoWrapperRenderer,
QueueItem QueueItem,
} from '@/types/datahost-get-state'; } from '@/types/datahost-get-state';
export const mapQueueItem = <T>(map: (item?: ItemPlaylistPanelVideoRenderer) => T, array: QueueItem[]): T[] => array export const mapQueueItem = <T>(
.map((item) => { map: (item?: ItemPlaylistPanelVideoRenderer) => T,
if ('playlistPanelVideoWrapperRenderer' in item) { array: QueueItem[],
const keys = Object.keys(item.playlistPanelVideoWrapperRenderer!.primaryRenderer) as (keyof PlaylistPanelVideoWrapperRenderer['primaryRenderer'])[]; ): T[] =>
return item.playlistPanelVideoWrapperRenderer!.primaryRenderer[keys[0]]; array
} .map((item) => {
if ('playlistPanelVideoRenderer' in item) { if ('playlistPanelVideoWrapperRenderer' in item) {
return item.playlistPanelVideoRenderer; const keys = Object.keys(
} item.playlistPanelVideoWrapperRenderer!.primaryRenderer,
) as (keyof PlaylistPanelVideoWrapperRenderer['primaryRenderer'])[];
console.error('Music Together: Unknown item', item); return item.playlistPanelVideoWrapperRenderer!.primaryRenderer[keys[0]];
return undefined; }
}) if ('playlistPanelVideoRenderer' in item) {
.map(map); return item.playlistPanelVideoRenderer;
}
console.error('Music Together: Unknown item', item);
return undefined;
})
.map(map);

Some files were not shown because too many files have changed in this diff Show More