Compare commits

...

232 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
145 changed files with 8983 additions and 2725 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,139 @@
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) #### [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) - 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 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) - chore(deps): update typescript-eslint monorepo to v7.18.0 [`#2292`](https://github.com/th-ch/youtube-music/pull/2292)

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> [обратный апостроф], если используете плагин "Меню в приложении")

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.5.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": {
@ -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,11 +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.25.0" "@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",
"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": {
@ -159,11 +220,16 @@
"@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.8", "@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",
"@skyra/jaro-winkler": "^1.1.1", "@jimp/plugin-invert": "0.22.12",
"@skyra/jaro-winkler": "1.1.1",
"@xhayper/discord-rpc": "1.2.0", "@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",
@ -172,69 +238,76 @@
"conf": "13.0.1", "conf": "13.0.1",
"custom-electron-prompt": "1.5.8", "custom-electron-prompt": "1.5.8",
"dbus-next": "0.10.2", "dbus-next": "0.10.2",
"deepmerge-ts": "7.1.0", "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.3.2", "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.2", "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.3", "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.19", "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.3.0" "youtubei.js": "10.5.0",
"zod": "3.23.8"
}, },
"devDependencies": { "devDependencies": {
"@playwright/test": "1.45.3", "@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": "8.0.0", "@types/trusted-types": "2.0.7",
"@typescript-eslint/parser": "8.0.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.93", "discord-api-types": "0.37.102",
"electron": "31.3.1", "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.2.1", "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.3", "playwright": "1.48.0",
"rollup": "4.19.2", "rollup": "4.24.0",
"typescript": "5.5.4", "typescript": "5.6.3",
"typescript-eslint": "8.9.0",
"utf-8-validate": "6.0.4", "utf-8-validate": "6.0.4",
"vite": "5.3.5", "vite": "5.4.9",
"vite-plugin-inspect": "0.8.5", "vite-plugin-inspect": "0.8.7",
"vite-plugin-resolve": "2.5.2", "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)
})

3169
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

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": "حاجب الإعلانات"
} }
} }
} }

View File

@ -15,7 +15,7 @@
}, },
"language": { "language": {
"code": "ca", "code": "ca",
"local-name": "català", "local-name": "Català",
"name": "Catalan" "name": "Catalan"
}, },
"main": { "main": {
@ -84,9 +84,9 @@
"label": "Navegació", "label": "Navegació",
"submenu": { "submenu": {
"copy-current-url": "Copia l'URL actual", "copy-current-url": "Copia l'URL actual",
"go-back": "Anar enrere", "go-back": "Ves enrere",
"go-forward": "Anar endavant", "go-forward": "Ves endavant",
"quit": "Sortir", "quit": "Surt",
"restart": "Reinicia l'aplicació" "restart": "Reinicia l'aplicació"
} }
}, },
@ -102,11 +102,11 @@
"override-user-agent": "Sobreescriu l'agent d'usuari (User-Agent)", "override-user-agent": "Sobreescriu l'agent d'usuari (User-Agent)",
"restart-on-config-changes": "Reinicia quan es canviï la configuració", "restart-on-config-changes": "Reinicia quan es canviï la configuració",
"set-proxy": { "set-proxy": {
"label": "Definir proxy", "label": "Definir servidor intermediari (proxy)",
"prompt": { "prompt": {
"label": "Introduir l'adreça del proxy: (deixar en blanc per deshabilitar)", "label": "Introduir l'adreça del servidor intermediari: (deixar en blanc per deshabilitar)",
"placeholder": "Exemple: SOCKS5://127.0.0.1:9999", "placeholder": "Exemple: SOCKS5://127.0.0.1:9999",
"title": "Definir proxy" "title": "Definir servidor intermediari (proxy)"
} }
}, },
"toggle-dev-tools": "Commuta les DevTools" "toggle-dev-tools": "Commuta les DevTools"
@ -144,11 +144,11 @@
"disabled": "Deshabilitat", "disabled": "Deshabilitat",
"enabled-and-hide-app": "Mostra la icona i amaga l'aplicació", "enabled-and-hide-app": "Mostra la icona i amaga l'aplicació",
"enabled-and-show-app": "Mostra la icona i mostra l'aplicació", "enabled-and-show-app": "Mostra la icona i mostra l'aplicació",
"play-pause-on-click": "Reproduir/Pausar en clicar" "play-pause-on-click": "Reprodueix / pausa en clicar"
} }
}, },
"visual-tweaks": { "visual-tweaks": {
"label": "Configuració visual", "label": "Opcions visuals",
"submenu": { "submenu": {
"like-buttons": { "like-buttons": {
"default": "Per defecte", "default": "Per defecte",
@ -182,9 +182,9 @@
"new": "NOU" "new": "NOU"
}, },
"view": { "view": {
"label": "Mostra", "label": "Veure",
"submenu": { "submenu": {
"force-reload": "Força recàrrega", "force-reload": "Força la recàrrega",
"reload": "Recarrega", "reload": "Recarrega",
"reset-zoom": "Mida real", "reset-zoom": "Mida real",
"toggle-fullscreen": "Commuta la pantalla completa", "toggle-fullscreen": "Commuta la pantalla completa",
@ -220,7 +220,7 @@
}, },
"album-actions": { "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", "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 de l'àlbum" "name": "Accions a l'àlbum"
}, },
"album-color-theme": { "album-color-theme": {
"description": "Aplica un tema dinàmic i efectes visuals basats en la paleta de colors de l'àlbum", "description": "Aplica un tema dinàmic i efectes visuals basats en la paleta de colors de l'àlbum",
@ -279,9 +279,52 @@
}, },
"name": "Mode ambient" "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": { "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)", "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": "Compressor d'àudio" "name": "Compress d'àudio"
}, },
"blur-nav-bar": { "blur-nav-bar": {
"description": "Desenfoca i aplica transparència a la barra de navegació", "description": "Desenfoca i aplica transparència a la barra de navegació",
@ -518,7 +561,7 @@
}, },
"no-google-login": { "no-google-login": {
"description": "Elimina els botons d'inici de sessió de Google de la interfície", "description": "Elimina els botons d'inici de sessió de Google de la interfície",
"name": "Sense inici de sessió de Google" "name": "Amaga l'inici de sessió de Google"
}, },
"notifications": { "notifications": {
"description": "Mostra una notificació quan una cançó es comença a reproduir (les notificacions interactives estan disponibles a Windows)", "description": "Mostra una notificació quan una cançó es comença a reproduir (les notificacions interactives estan disponibles a Windows)",
@ -602,7 +645,7 @@
} }
}, },
"description": "Permet canviar la qualitat del vídeo amb un botó que s'hi mostra a sobre", "description": "Permet canviar la qualitat del vídeo amb un botó que s'hi mostra a sobre",
"name": "Canvia la qualitat del vídeo" "name": "Botó de qualitat del vídeo"
}, },
"scrobbler": { "scrobbler": {
"description": "Afegeix suport per scrobbling (Last.fm, ListenBrainz, etc.)", "description": "Afegeix suport per scrobbling (Last.fm, ListenBrainz, etc.)",
@ -657,8 +700,8 @@
} }
}, },
"skip-disliked-songs": { "skip-disliked-songs": {
"description": "Omet cançons amb «No m'agrada»", "description": "Salta les cançons amb «no m'agrada»",
"name": "Omet cançons amb «No m'agrad" "name": "Salta les cançons que no t'agraden"
}, },
"skip-silences": { "skip-silences": {
"description": "Omet automàticament les seccions amb silenci a les cançons", "description": "Omet automàticament les seccions amb silenci a les cançons",
@ -668,6 +711,59 @@
"description": "Omet automàticament els segments dels vídeos que no son música, com la intro o el final", "description": "Omet automàticament els segments dels vídeos que no son música, com la intro o el final",
"name": "SponsorBlock" "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": { "taskbar-mediacontrol": {
"description": "Controla la reproducció des de la barra de tasques del Windows", "description": "Controla la reproducció des de la barra de tasques del Windows",
"name": "Control multimèdia a la barra de tasques" "name": "Control multimèdia a la barra de tasques"
@ -701,7 +797,7 @@
} }
} }
}, },
"name": "Commutador de vídeo", "name": "Botó de vídeo",
"templates": { "templates": {
"button": "Cançó" "button": "Cançó"
} }

View File

@ -668,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": {
@ -214,17 +218,12 @@
}, },
"name": "Ad Blocker" "name": "Ad Blocker"
}, },
"ad-speedup": {
"name": "Ad Speedup",
"description": "If an ad play it mutes the audio and sets playback speed to 16x"
},
"album-actions": { "album-actions": {
"description": "Adds Undislike, Dislike, Like, and Unlike buttons to apply this to all songs in a playlist or album", "description": "Adds Undislike, Dislike, Like, and Unlike buttons to apply this to all songs in a playlist or album",
"name": "Album Actions" "name": "Album Actions"
}, },
"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",
@ -232,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",
@ -279,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"
@ -414,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-playlist": "Download playlist",
"presets": "Presets",
"skip-existing": "Skip existing files",
"download-finish-settings": { "download-finish-settings": {
"label": "Download on finish", "label": "Download on finish",
"prompt": {
"last-percent": "After x percent",
"last-seconds": "Last x seconds",
"title": "Configure when to download"
},
"submenu": { "submenu": {
"advanced": "Advanced",
"enabled": "Enabled", "enabled": "Enabled",
"mode": "Time mode", "mode": "Time mode",
"seconds": "Seconds",
"percent": "Percent", "percent": "Percent",
"advanced": "Advanced" "seconds": "Seconds"
},
"prompt": {
"title": "Configure when to download",
"last-seconds": "Last x seconds",
"last-percent": "After x percent"
} }
} },
"download-playlist": "Download playlist",
"presets": "Presets",
"skip-existing": "Skip existing files"
}, },
"name": "Downloader", "name": "Downloader",
"renderer": { "renderer": {
@ -609,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": {
@ -670,55 +713,55 @@
}, },
"synced-lyrics": { "synced-lyrics": {
"description": "Provides synced lyrics to songs, using providers like LRClib.", "description": "Provides synced lyrics to songs, using providers like LRClib.",
"name": "Synced Lyrics",
"errors": { "errors": {
"fetch": "⚠️ - An error occurred while fetching the lyrics. Please try again later.", "fetch": "⚠️ - An error occurred while fetching the lyrics. Please try again later.",
"not-found": "⚠️ - No lyrics found for this song." "not-found": "⚠️ - No lyrics found for this song."
}, },
"warnings": {
"instrumental": "⚠️ - This is an instrumental song",
"inexact": "⚠️ - The lyrics for this song may not be exact",
"duration-mismatch": "⚠️ - The lyrics may be out of sync due to a duration mismatch."
},
"refetch-btn": {
"normal": "Refetch lyrics",
"fetching": "Fetching..."
},
"menu": { "menu": {
"precise-timing": { "default-text-string": {
"label": "Make the lyrics perfectly synced", "label": "Default character between lyrics",
"tooltip": "Calculate to the milisecond the display of the next line (can have a small impact on performance)" "tooltip": "Choose the default character to use for the gap between lyrics"
}, },
"line-effect": { "line-effect": {
"label": "Line effect", "label": "Line effect",
"tooltip": "Choose the effect to apply to the current line",
"submenu": { "submenu": {
"scale": { "focus": {
"label": "Scale", "label": "Focus",
"tooltip": "Scale the current line" "tooltip": "Make only the current line white"
}, },
"offset": { "offset": {
"label": "Offset", "label": "Offset",
"tooltip": "Offset on the right the current line" "tooltip": "Offset on the right the current line"
}, },
"focus": { "scale": {
"label": "Focus", "label": "Scale",
"tooltip": "Make only the current line white" "tooltip": "Scale the current line"
} }
} },
"tooltip": "Choose the effect to apply to the current line"
}, },
"default-text-string": { "precise-timing": {
"label": "Default character between lyrics", "label": "Make the lyrics perfectly synced",
"tooltip": "Choose the default character to use for the gap between lyrics" "tooltip": "Calculate to the milisecond the display of the next line (can have a small impact on performance)"
},
"show-time-codes": {
"label": "Show time codes",
"tooltip": "Show the time codes next to the lyrics"
}, },
"show-lyrics-even-if-inexact": { "show-lyrics-even-if-inexact": {
"label": "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." "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": {

View File

@ -279,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"
@ -668,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

@ -201,16 +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": { "ad-speedup": {
"description": "Pag mag-play ng ad, I-mute ang audio at i-set ang bilis ng playback ng 16x" "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",
@ -220,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"
@ -259,6 +268,7 @@
} }
}, },
"smoothness-transition": { "smoothness-transition": {
"label": "Ayos ng Transisyon",
"submenu": { "submenu": {
"during": "Habang {{interpolationTime}} s" "during": "Habang {{interpolationTime}} s"
} }
@ -266,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": {
@ -299,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",
@ -392,6 +447,8 @@
"download-finish-settings": { "download-finish-settings": {
"label": "Kung natapos ang download", "label": "Kung natapos ang download",
"prompt": { "prompt": {
"last-percent": "Tapos ng x na porsyento",
"last-seconds": "Huling x na segundo",
"title": "I-configure kung kailan magda-download" "title": "I-configure kung kailan magda-download"
}, },
"submenu": { "submenu": {
@ -406,6 +463,9 @@
}, },
"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": {
@ -418,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",
@ -475,7 +536,8 @@
"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)",
@ -490,8 +552,10 @@
} }
}, },
"priority": "Prioridad ng Notification", "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",
@ -572,7 +636,8 @@
}, },
"listenbrainz": { "listenbrainz": {
"token": { "token": {
"label": "Ilagay ang ListenBrainz user token:" "label": "Ilagay ang ListenBrainz user token:",
"title": "Token ng ListenBrainz"
} }
} }
} }
@ -580,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)",
@ -587,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:"
@ -594,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"
}, },
@ -633,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

@ -668,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,13 +2,14 @@
"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": "טוען את כל התוספים",
"load-failed": "טעינת התוסף \"{{pluginName}}\" נכשלה", "load-failed": "לא ניתן לטעון את התוסף {{pluginName}}",
"loaded": "התוסף \"{{pluginName}}\" נטען", "loaded": "התוסף \"{{pluginName}}\" נטען",
"unload-failed": "הסרת התוסף \"{{pluginName}} נכשלה" "unload-failed": "הסרת התוסף \"{{pluginName}} נכשלה",
"unloaded": "תוסף {{pluginName}} הורד"
} }
} }
}, },
@ -25,14 +26,28 @@
"i18n": { "i18n": {
"loaded": "i18n נטען" "loaded": "i18n נטען"
}, },
"second-instance": {
"receive-command": "התקבלה פקודה מעבר פרוטוקל: {{command}}"
},
"theme": { "theme": {
"css-file-not-found": "קובץ ה-CSS \"{{cssFile}}\" לא קיים. מדלג" "css-file-not-found": "קובץ ה-CSS \"{{cssFile}}\" לא קיים. מדלג"
}, },
"unresponsive": {
"details": "שגיאה ללא תגובה\n{{error}}"
},
"when-ready": { "when-ready": {
"clearing-cache-after-20s": "מוחק קבצי מתמון" "clearing-cache-after-20s": "מוחק קבצי מתמון"
},
"window": {
"tried-to-render-offscreen": "ווינדוס ניסה להציג תוכן מחוץ למסך, גודל חלון={{windowSize}}, גודל מסך={{displaySize}}, מיקום={{position}}"
} }
}, },
"dialog": { "dialog": {
"hide-menu-enabled": {
"detail": "התפריט מוחבא, השתמש \"Alt\" על להציג אותו (או \"Esacpe\" אם משתמשים בתפריט בתוך האפליקציה)",
"message": "הסתרת התפריט מופעלת",
"title": "הסתרת התפריט הופעלה"
},
"need-to-restart": { "need-to-restart": {
"buttons": { "buttons": {
"later": "אחר כך", "later": "אחר כך",

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"
} }
} }
@ -372,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!"
}, },
@ -412,6 +415,7 @@
"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": { "download-finish-settings": {
"label": "Letöltés befejezéskor",
"prompt": { "prompt": {
"last-percent": "x százalék után", "last-percent": "x százalék után",
"last-seconds": "Utolsó x másodperc" "last-seconds": "Utolsó x másodperc"
@ -425,7 +429,7 @@
} }
}, },
"download-playlist": "Lejátszási lista letöltése", "download-playlist": "Lejátszási lista letöltése",
"presets": "Előbeállítások", "presets": "Sablonok",
"skip-existing": "Meglévő fájlok kihagyása" "skip-existing": "Meglévő fájlok kihagyása"
}, },
"name": "Letöltő", "name": "Letöltő",
@ -567,7 +571,7 @@
"menu": { "menu": {
"arrows-shortcuts": "Helyi nyíl-billentyűkkel való vezérlés", "arrows-shortcuts": "Helyi nyíl-billentyűkkel való vezérlés",
"custom-volume-steps": "Egyedi hangerőléptetés beállítása", "custom-volume-steps": "Egyedi hangerőléptetés beállítása",
"global-shortcuts": "Globális gyorsbillentyűk" "global-shortcuts": "Globális Gyorsbillentyűk"
}, },
"name": "Precíz hangerő", "name": "Precíz hangerő",
"prompt": { "prompt": {
@ -666,6 +670,18 @@
"fetch": "⚠️ - Hiba történt a dalszövegek lekérése közben. Kérlek, próbáld újra később.", "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." "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", "name": "Szinkronizált dalszövegek",
"refetch-btn": { "refetch-btn": {
"fetching": "Lekérés folyamatban...", "fetching": "Lekérés folyamatban...",

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

@ -674,6 +674,42 @@
"fetch": "⚠️ - Si è verificato un errore nel recuperare il testo. Per favore riprova più tardi.", "fetch": "⚠️ - Si è verificato un errore nel recuperare il testo. Per favore riprova più tardi.",
"not-found": "⚠️ - Nessun testo trovato per questa canzone." "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", "name": "Testi sincronizzati",
"refetch-btn": { "refetch-btn": {
"fetching": "Sto recuperando...", "fetching": "Sto recuperando...",

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

@ -279,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": "오디오 컴프레서"

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

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

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

@ -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": {
@ -664,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

@ -674,6 +674,38 @@
"fetch": "⚠️ - Đã xảy ra lỗi khi tìm nạp lời bài hát, Vui lòng thử lại sau.", "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." "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á", "name": "Lời bài hát được đồng bộ hoá",
"refetch-btn": { "refetch-btn": {
"fetching": "Đang tìm nạp...", "fetching": "Đang tìm nạp...",

View File

@ -207,6 +207,10 @@
} }
}, },
"plugins": { "plugins": {
"ad-speedup": {
"description": "使用静音以及 16 倍速播放跳过广告片段",
"name": "广告加速跳过"
},
"adblocker": { "adblocker": {
"description": "屏蔽所有广告与跟踪器", "description": "屏蔽所有广告与跟踪器",
"menu": { "menu": {
@ -664,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

@ -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 伺服器 [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,10 +446,10 @@
"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 標籤…"
} }
@ -513,8 +560,8 @@
"name": "導覽列" "name": "導覽列"
}, },
"no-google-login": { "no-google-login": {
"description": "移除Google登入按鈕及連結", "description": "移除 Google 登入按鈕及連結",
"name": "停用Google登入" "name": "停用 Google 登入"
}, },
"notifications": { "notifications": {
"description": "在歌曲播放時發送一個系統通知 (可互動通知僅限Windows)", "description": "在歌曲播放時發送一個系統通知 (可互動通知僅限Windows)",
@ -578,7 +625,7 @@
"decrease": "降低音量", "decrease": "降低音量",
"increase": "增加音量" "increase": "增加音量"
}, },
"label": "選擇全域音量控制快捷鍵:", "label": "選擇全域音量控制快捷鍵",
"title": "全域音量控制快捷鍵" "title": "全域音量控制快捷鍵"
}, },
"volume-steps": { "volume-steps": {
@ -591,8 +638,8 @@
"backend": { "backend": {
"dialog": { "dialog": {
"quality-changer": { "quality-changer": {
"detail": "目前畫質: {{quality}}", "detail": "目前畫質{{quality}}",
"message": "選擇影片畫質:", "message": "選擇影片畫質",
"title": "選擇影片畫質" "title": "選擇影片畫質"
} }
} }
@ -647,7 +694,7 @@
"play-pause": "播放/暫停", "play-pause": "播放/暫停",
"previous": "上一首" "previous": "上一首"
}, },
"label": "選擇全域音樂控制快捷鍵:", "label": "選擇全域音樂控制快捷鍵",
"title": "全域快捷鍵" "title": "全域快捷鍵"
} }
} }
@ -664,6 +711,59 @@
"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": "工作列媒體控制"

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)
@ -286,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,
@ -303,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);
@ -323,7 +363,9 @@ async function createMainWindow() {
const display = screen.getDisplayNearestPoint(windowPosition); const display = screen.getDisplayNearestPoint(windowPosition);
const primaryDisplay = screen.getPrimaryDisplay(); const primaryDisplay = screen.getPrimaryDisplay();
const scaleFactor = is.windows() ? primaryDisplay.scaleFactor / display.scaleFactor : 1; const scaleFactor = is.windows()
? primaryDisplay.scaleFactor / display.scaleFactor
: 1;
const scaledWidth = Math.floor(windowSize.width * scaleFactor); const scaledWidth = Math.floor(windowSize.width * scaleFactor);
const scaledHeight = Math.floor(windowSize.height * scaleFactor); const scaledHeight = Math.floor(windowSize.height * scaleFactor);
@ -331,10 +373,10 @@ async function createMainWindow() {
const scaledY = windowY; const scaledY = windowY;
if ( if (
scaledX + (scaledWidth / 2) < display.bounds.x - 8 || // Left scaledX + scaledWidth / 2 < display.bounds.x - 8 || // Left
scaledX + (scaledWidth / 2) > display.bounds.x + display.bounds.width || // Right scaledX + scaledWidth / 2 > display.bounds.x + display.bounds.width || // Right
scaledY < display.bounds.y - 8 || // Top scaledY < display.bounds.y - 8 || // Top
scaledY + (scaledHeight / 2) > display.bounds.y + display.bounds.height // Bottom scaledY + scaledHeight / 2 > display.bounds.y + display.bounds.height // Bottom
) { ) {
// Window is offscreen // Window is offscreen
if (is.dev()) { if (is.dev()) {
@ -431,7 +473,7 @@ async function createMainWindow() {
...defaultTitleBarOverlayOptions, ...defaultTitleBarOverlayOptions,
height: Math.floor( height: Math.floor(
defaultTitleBarOverlayOptions.height! * defaultTitleBarOverlayOptions.height! *
win.webContents.getZoomFactor(), win.webContents.getZoomFactor(),
), ),
}); });
} }
@ -444,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',
); );
} }
}); });
@ -468,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;
@ -518,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);
@ -607,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) {
@ -630,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) => {
@ -651,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(
@ -663,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

@ -1,5 +1,7 @@
function skipAd(target: Element) { function skipAd(target: Element) {
const skipButton = target.querySelector<HTMLButtonElement>('button.ytp-ad-skip-button-modern'); const skipButton = target.querySelector<HTMLButtonElement>(
'button.ytp-ad-skip-button-modern',
);
if (skipButton) { if (skipButton) {
skipButton.click(); skipButton.click();
} }
@ -17,7 +19,7 @@ function speedUpAndMute(player: Element, isAdShowing: boolean) {
} }
} }
export const loadAdSpeedup = async () => { export const loadAdSpeedup = () => {
const player = document.querySelector<HTMLVideoElement>('#movie_player'); const player = document.querySelector<HTMLVideoElement>('#movie_player');
if (!player) return; if (!player) return;
@ -53,4 +55,4 @@ export const loadAdSpeedup = async () => {
player.classList.contains('ad-interrupting'); player.classList.contains('ad-interrupting');
speedUpAndMute(player, isAdShowing); speedUpAndMute(player, isAdShowing);
skipAd(player); skipAd(player);
} };

View File

@ -10,11 +10,11 @@ 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';
import type { BrowserWindow } from 'electron'; import type { BrowserWindow } from 'electron';
import { loadAdSpeedup } from './adSpeedup';
interface AdblockerConfig { interface AdblockerConfig {
/** /**
@ -74,12 +74,12 @@ export default createPlugin({
]; ];
}, },
renderer: { renderer: {
async onPlayerApiReady(_, {getConfig}) { async onPlayerApiReady(_, { getConfig }) {
const config = await getConfig(); const config = await getConfig();
if (config.blocker === blockers.AdSpeedup) { if (config.blocker === blockers.AdSpeedup) {
await loadAdSpeedup(); await loadAdSpeedup();
} }
} },
}, },
backend: { backend: {
mainWindow: null as BrowserWindow | null, mainWindow: null as BrowserWindow | null,
@ -118,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

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

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

@ -53,10 +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 = await waitForElement<HTMLVideoElement>('.html5-video-container > video'); 'yt-img-shadow > img',
);
const video = await waitForElement<HTMLVideoElement>(
'.html5-video-container > video',
);
const videoWrapper = document.querySelector('#song-video > .player-wrapper'); const videoWrapper = document.querySelector(
'#song-video > .player-wrapper',
);
const injectBlurImage = () => { const injectBlurImage = () => {
if (!songImage || !image) return null; if (!songImage || !image) return null;
@ -95,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;
@ -109,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;
} }
@ -137,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');
@ -151,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);
@ -159,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);
@ -198,11 +218,20 @@ 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;

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

@ -7,4 +7,4 @@ export type AmbientModePluginConfig = {
size: number; size: number;
opacity: number; opacity: number;
fullscreen: boolean; fullscreen: boolean;
}; };

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

@ -202,9 +202,9 @@ 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);

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,7 +78,7 @@ export const onRendererLoad = ({
applyLyricsTabState(); applyLyricsTabState();
} }
}; };
const applyLyricsTabState = () => { const applyLyricsTabState = () => {
if (lyrics) { if (lyrics) {
tabs.lyrics.removeAttribute('disabled'); tabs.lyrics.removeAttribute('disabled');

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

View File

@ -10,13 +10,16 @@ export type VideoData = {
}; };
export type Permission = 'host-only' | 'playlist' | 'all'; export type Permission = 'host-only' | 'playlist' | 'all';
export const getDefaultProfile = (connectionID: string, id: string = Date.now().toString()): Profile => { export const getDefaultProfile = (
connectionID: string,
id: string = Date.now().toString(),
): Profile => {
const name = `Guest ${id.slice(0, 4)}`; const name = `Guest ${id.slice(0, 4)}`;
return { return {
id: connectionID, id: connectionID,
handleId: `#music-together:${id}`, handleId: `#music-together:${id}`,
name, name,
thumbnail: `https://ui-avatars.com/api/?name=${name}&background=random` thumbnail: `https://ui-avatars.com/api/?name=${name}&background=random`,
}; };
}; };

View File

@ -7,7 +7,6 @@ import { createStatus } from '../ui/status';
import IconOff from '../icons/off.svg?raw'; import IconOff from '../icons/off.svg?raw';
export type GuestPopupProps = { export type GuestPopupProps = {
onItemClick: (id: string) => void; onItemClick: (id: string) => void;
}; };
@ -33,7 +32,7 @@ export const createGuestPopup = (props: GuestPopupProps) => {
}, },
], ],
anchorAt: 'bottom-right', anchorAt: 'bottom-right',
popupAt: 'top-right' popupAt: 'top-right',
}); });
return { return {

View File

@ -22,7 +22,7 @@ export const createHostPopup = (props: HostPopupProps) => {
element: status.element, element: status.element,
}, },
{ {
type: 'divider' type: 'divider',
}, },
{ {
id: 'music-together-copy-id', id: 'music-together-copy-id',
@ -35,7 +35,9 @@ export const createHostPopup = (props: HostPopupProps) => {
id: 'music-together-permission', id: 'music-together-permission',
type: 'item', type: 'item',
icon: ElementFromHtml(IconTune), icon: ElementFromHtml(IconTune),
text: t('plugins.music-together.menu.set-permission', { permission: t('plugins.music-together.menu.permission.host-only') }), text: t('plugins.music-together.menu.set-permission', {
permission: t('plugins.music-together.menu.permission.host-only'),
}),
onClick: () => props.onItemClick('music-together-permission'), onClick: () => props.onItemClick('music-together-permission'),
}, },
{ {

View File

@ -39,7 +39,7 @@ export const createSettingPopup = (props: SettingPopupProps) => {
}, },
], ],
anchorAt: 'bottom-right', anchorAt: 'bottom-right',
popupAt: 'top-right' popupAt: 'top-right',
}); });
return { return {

View File

@ -7,17 +7,27 @@ import type { Permission, Profile } from '../types';
export const createStatus = () => { export const createStatus = () => {
const element = ElementFromHtml(statusHTML); const element = ElementFromHtml(statusHTML);
const icon = document.querySelector<HTMLImageElement>('ytmusic-settings-button > tp-yt-paper-icon-button > tp-yt-iron-icon#icon img'); const icon = document.querySelector<HTMLImageElement>(
'ytmusic-settings-button > tp-yt-paper-icon-button > tp-yt-iron-icon#icon img',
);
const profile = element.querySelector<HTMLImageElement>('.music-together-profile')!; const profile = element.querySelector<HTMLImageElement>(
const statusLabel = element.querySelector<HTMLSpanElement>('#music-together-status-label')!; '.music-together-profile',
const permissionLabel = element.querySelector<HTMLMarqueeElement>('#music-together-permission-label')!; )!;
const statusLabel = element.querySelector<HTMLSpanElement>(
'#music-together-status-label',
)!;
const permissionLabel = element.querySelector<HTMLMarqueeElement>(
'#music-together-permission-label',
)!;
profile.src = icon?.src ?? ''; profile.src = icon?.src ?? '';
const setStatus = (status: 'disconnected' | 'host' | 'guest') => { const setStatus = (status: 'disconnected' | 'host' | 'guest') => {
if (status === 'disconnected') { if (status === 'disconnected') {
statusLabel.textContent = t('plugins.music-together.menu.status.disconnected'); statusLabel.textContent = t(
'plugins.music-together.menu.status.disconnected',
);
statusLabel.style.color = 'rgba(255, 255, 255, 0.5)'; statusLabel.style.color = 'rgba(255, 255, 255, 0.5)';
} }
@ -34,17 +44,23 @@ export const createStatus = () => {
const setPermission = (permission: Permission) => { const setPermission = (permission: Permission) => {
if (permission === 'host-only') { if (permission === 'host-only') {
permissionLabel.textContent = t('plugins.music-together.menu.permission.host-only'); permissionLabel.textContent = t(
'plugins.music-together.menu.permission.host-only',
);
permissionLabel.style.color = 'rgba(255, 255, 255, 0.5)'; permissionLabel.style.color = 'rgba(255, 255, 255, 0.5)';
} }
if (permission === 'playlist') { if (permission === 'playlist') {
permissionLabel.textContent = t('plugins.music-together.menu.permission.playlist'); permissionLabel.textContent = t(
'plugins.music-together.menu.permission.playlist',
);
permissionLabel.style.color = 'rgba(255, 255, 255, 0.75)'; permissionLabel.style.color = 'rgba(255, 255, 255, 0.75)';
} }
if (permission === 'all') { if (permission === 'all') {
permissionLabel.textContent = t('plugins.music-together.menu.permission.all'); permissionLabel.textContent = t(
'plugins.music-together.menu.permission.all',
);
permissionLabel.style.color = 'rgba(255, 255, 255, 1)'; permissionLabel.style.color = 'rgba(255, 255, 255, 1)';
} }
}; };
@ -54,7 +70,9 @@ export const createStatus = () => {
}; };
const setUsers = (users: Profile[]) => { const setUsers = (users: Profile[]) => {
const container = element.querySelector<HTMLDivElement>('.music-together-user-container')!; const container = element.querySelector<HTMLDivElement>(
'.music-together-user-container',
)!;
const empty = element.querySelector<HTMLElement>('.music-together-empty')!; const empty = element.querySelector<HTMLElement>('.music-together-empty')!;
for (const child of Array.from(container.children)) { for (const child of Array.from(container.children)) {
if (child !== empty) child.remove(); if (child !== empty) child.remove();

View File

@ -68,7 +68,9 @@ const observer = 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;

View File

@ -22,7 +22,8 @@ const updatePlayBackSpeed = () => {
const playbackSpeedElement = document.querySelector('#playback-speed-value'); const playbackSpeedElement = document.querySelector('#playback-speed-value');
if (playbackSpeedElement) { if (playbackSpeedElement) {
playbackSpeedElement.innerHTML = String(playbackSpeed); const targetHtml = String(playbackSpeed);
playbackSpeedElement.innerHTML = window.trustedTypes?.defaultPolicy ? trustedTypes.defaultPolicy.createHTML(targetHtml) : targetHtml;
} }
}; };
@ -53,10 +54,7 @@ const observePopupContainer = () => {
menu = getSongMenu(); menu = getSongMenu();
} }
if ( if (menu && !menu.contains(slider)) {
menu &&
!menu.contains(slider)
) {
menu.prepend(slider); menu.prepend(slider);
setupSliderListener(); setupSliderListener();
} }

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