Compare commits

...

73 Commits

Author SHA1 Message Date
0e94c72eef Bump version to 3.2.1 2024-01-01 09:22:46 +09:00
c055641351 chore(i18n): Translated using Weblate (Korean)
Currently translated at 100.0% (328 of 328 strings)

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

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

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

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

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

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

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

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

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

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

* fix(music-together): fix

* test fix

* wow

* test

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

* fix: adblocker

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

* feat(music-together): add host UI

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

* feat(music-together): inject panel

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

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

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

* fix(music-together): fix some bug

* fix(music-together): fix sync queue

* feat(music-together): support i18n

* feat(music-together): improve sync queue

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

* refactor(music-together): refactor structure

* feat(music-together): add permission

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

* fix(music-together): fix some bugs

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

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

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

* fix(music-together): fix

* feat(music-together): improve video injection

* fix(music-together): fix injection code

* fix(music-together): fix broadcast guest

* feat(music-together): add more permission

* fix(music-together): fix injector

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

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

* fix(music-together): fix connection issue

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

* feat(music-together): reserve playlist

* fix(music-together): exclude automix songs

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

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

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

* fix: apply fix from eslint

* feat(util): add `ImageElementFromSrc`

* chore(util): update jsdoc

* feat(music-together): add owner name

* chore(music-together): add translation

* feat(music-together): add progress sync

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

---------

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Translation: th-ch/youtube-music/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/lt/
2023-12-19 07:21:28 +01:00
27a2955bba fix: fix homebrew cask
- resolve #1514
2023-12-18 22:02:38 +09:00
cc940e2020 Update changelog for v3.1.1 2023-12-18 12:57:28 +00:00
65 changed files with 3882 additions and 525 deletions

View File

@ -49,9 +49,10 @@ this [wiki page](https://wiki.archlinux.org/index.php/Arch_User_Repository#Insta
### MacOS ### MacOS
You can install the app using Homebrew: You can install the app using Homebrew (see the [cask definition](https://github.com/th-ch/homebrew-youtube-music)):
```bash ```bash
brew install --cask https://raw.githubusercontent.com/th-ch/youtube-music/master/youtube-music.rb brew install th-ch/youtube-music/youtube-music
``` ```
If you install the app manually and get an error "is damaged and cant be opened." when launching the app, run the following in the Terminal: If you install the app manually and get an error "is damaged and cant be opened." when launching the app, run the following in the Terminal:

View File

@ -2,8 +2,42 @@
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.2.0](https://github.com/th-ch/youtube-music/compare/v3.1.1...v3.2.0)
- feat(album-color-theme): improve `Album Color Theme` style [`#1571`](https://github.com/th-ch/youtube-music/pull/1571)
- feat(menu): add more detail in Menu [`#1570`](https://github.com/th-ch/youtube-music/pull/1570)
- feat(music-together): Add new plugin `Music Together` [`#1562`](https://github.com/th-ch/youtube-music/pull/1562)
- chore(deps): update dependency rollup to v4.9.2 [`#1567`](https://github.com/th-ch/youtube-music/pull/1567)
- fix(deps): update dependency i18next to v23.7.13 [`#1569`](https://github.com/th-ch/youtube-music/pull/1569)
- feat: Add new plugin `Album actions` [`#1515`](https://github.com/th-ch/youtube-music/pull/1515)
- fix(deps): update dependency i18next to v23.7.12 [`#1564`](https://github.com/th-ch/youtube-music/pull/1564)
- fix: Only apply scale factor on Windows [`#1565`](https://github.com/th-ch/youtube-music/pull/1565)
- chore(deps): update dependency @typescript-eslint/eslint-plugin to v6.16.0 [`#1556`](https://github.com/th-ch/youtube-music/pull/1556)
- chore(deps): update pnpm to v8.13.1 [`#1557`](https://github.com/th-ch/youtube-music/pull/1557)
- chore(deps): update dependency ws to v8.16.0 [`#1559`](https://github.com/th-ch/youtube-music/pull/1559)
- fix(deps): update dependency youtubei.js to v8.1.0 [`#1560`](https://github.com/th-ch/youtube-music/pull/1560)
- fix(deps): update dependency node-html-parser to v6.1.12 [`#1554`](https://github.com/th-ch/youtube-music/pull/1554)
- Revert "fix(deps): update dependency @xhayper/discord-rpc to v1.1.2" [`#1552`](https://github.com/th-ch/youtube-music/pull/1552)
- feat(ambient-mode): support ambient mode on `Song section` [`#1555`](https://github.com/th-ch/youtube-music/issues/1555)
- fix: fixed an issue with the download button disappearing [`#1551`](https://github.com/th-ch/youtube-music/issues/1551)
- fix: fix `homebrew cask` [`#1514`](https://github.com/th-ch/youtube-music/issues/1514)
- fix: pnpm build error [`13ef856`](https://github.com/th-ch/youtube-music/commit/13ef8560ff43353030537403be7da82542ba535e)
- chore(i18n): Translated using Weblate (Czech) [`0dc9c6a`](https://github.com/th-ch/youtube-music/commit/0dc9c6a1a90bce6505614617b827e816cbaaf875)
- chore(deps): update dependency @typescript-eslint/eslint-plugin to v6.15.0 [`c5bcd89`](https://github.com/th-ch/youtube-music/commit/c5bcd89f164b51d7380486a8ae35edd0caeea842)
#### [v3.1.1](https://github.com/th-ch/youtube-music/compare/v3.1.0...v3.1.1)
> 18 December 2023
- fix: fix renderer plugin load timing [`#1522`](https://github.com/th-ch/youtube-music/issues/1522)
- chore(i18n): Translated using Weblate (Lithuanian) [`fc1a7cd`](https://github.com/th-ch/youtube-music/commit/fc1a7cda62b6e33e5f5d57a5a6e0adef6a32bf9a)
- chore(i18n): Translated using Weblate (Chinese (Simplified)) [`eba7026`](https://github.com/th-ch/youtube-music/commit/eba7026b89bbfdd3ac07cf728a66ba9bdd274ec0)
- chore(deps): update dependency rollup to v4.8.0 [`a601d0b`](https://github.com/th-ch/youtube-music/commit/a601d0b3d2dee0fabad79a18e1a7dd0ca84ccf01)
#### [v3.1.0](https://github.com/th-ch/youtube-music/compare/v3.0.2...v3.1.0) #### [v3.1.0](https://github.com/th-ch/youtube-music/compare/v3.0.2...v3.1.0)
> 11 December 2023
- chore(deps): update dependency electron to v28 [`#1498`](https://github.com/th-ch/youtube-music/pull/1498) - chore(deps): update dependency electron to v28 [`#1498`](https://github.com/th-ch/youtube-music/pull/1498)
- Enable/Disable Navigation without restart [`#1507`](https://github.com/th-ch/youtube-music/pull/1507) - Enable/Disable Navigation without restart [`#1507`](https://github.com/th-ch/youtube-music/pull/1507)
- Turkish(tr)_lang_file [`#1513`](https://github.com/th-ch/youtube-music/pull/1513) - Turkish(tr)_lang_file [`#1513`](https://github.com/th-ch/youtube-music/pull/1513)

View File

@ -258,7 +258,7 @@ import style from './style.css?inline'; // 스타일을 인라인으로 가져
import { createPlugin } from '@/utils'; import { createPlugin } from '@/utils';
const builder = createPlugin({ export default createPlugin({
name: 'Plugin Label', name: 'Plugin Label',
restartNeeded: true, // 값이 true면, YTM은 재시작 다이얼로그를 표시합니다 restartNeeded: true, // 값이 true면, YTM은 재시작 다이얼로그를 표시합니다
config: { config: {
@ -274,7 +274,7 @@ const builder = createPlugin({
```typescript ```typescript
import { createPlugin } from '@/utils'; import { createPlugin } from '@/utils';
const builder = createPlugin({ export default createPlugin({
name: 'Plugin Label', name: 'Plugin Label',
restartNeeded: true, // 값이 true면, YTM은 재시작 다이얼로그를 표시합니다 restartNeeded: true, // 값이 true면, YTM은 재시작 다이얼로그를 표시합니다
config: { config: {

View File

@ -1,7 +1,7 @@
{ {
"name": "youtube-music", "name": "youtube-music",
"productName": "YouTube Music", "productName": "YouTube Music",
"version": "3.1.1", "version": "3.2.1",
"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",
@ -105,17 +105,17 @@
"dev": "electron-vite dev --watch", "dev": "electron-vite dev --watch",
"dev:debug": "cross-env ELECTRON_ENABLE_LOGGING=1 pnpm dev", "dev:debug": "cross-env ELECTRON_ENABLE_LOGGING=1 pnpm dev",
"clean": "del-cli dist && del-cli pack && del-cli .vite-inspect", "clean": "del-cli dist && del-cli pack && del-cli .vite-inspect",
"dist": "pnpm clean && pnpm build && electron-builder --win --mac --linux -p never", "dist": "pnpm clean && pnpm build && pnpm electron-builder --win --mac --linux -p never",
"dist:linux": "pnpm clean && pnpm build && electron-builder --linux -p never", "dist:linux": "pnpm clean && pnpm build && pnpm electron-builder --linux -p never",
"dist:mac": "pnpm clean && pnpm build && electron-builder --mac dmg:x64 -p never", "dist:mac": "pnpm clean && pnpm build && pnpm electron-builder --mac dmg:x64 -p never",
"dist:mac:arm64": "pnpm clean && pnpm build && electron-builder --mac dmg:arm64 -p never", "dist:mac:arm64": "pnpm clean && pnpm build && pnpm electron-builder --mac dmg:arm64 -p never",
"dist:win": "pnpm clean && pnpm build && electron-builder --win -p never", "dist:win": "pnpm clean && pnpm build && pnpm electron-builder --win -p never",
"dist:win:x64": "pnpm clean && pnpm build && electron-builder --win nsis-web:x64 -p never", "dist:win:x64": "pnpm clean && pnpm build && pnpm electron-builder --win nsis-web:x64 -p never",
"lint": "eslint .", "lint": "eslint .",
"changelog": "npx --yes auto-changelog", "changelog": "npx --yes auto-changelog",
"release:linux": "pnpm clean && pnpm build && electron-builder --linux -p always -c.snap.publish=github", "release:linux": "pnpm clean && pnpm build && pnpm electron-builder --linux -p always -c.snap.publish=github",
"release:mac": "pnpm clean && pnpm build && electron-builder --mac -p always", "release:mac": "pnpm clean && pnpm build && pnpm electron-builder --mac -p always",
"release:win": "pnpm clean && pnpm build && electron-builder --win -p always", "release:win": "pnpm clean && pnpm build && pnpm electron-builder --win -p always",
"typecheck": "tsc -p tsconfig.json --noEmit" "typecheck": "tsc -p tsconfig.json --noEmit"
}, },
"engines": { "engines": {
@ -125,12 +125,12 @@
"overrides": { "overrides": {
"esbuild": "0.18.20", "esbuild": "0.18.20",
"usocket": "1.0.1", "usocket": "1.0.1",
"rollup": "4.9.1", "rollup": "4.9.2",
"node-gyp": "10.0.1", "node-gyp": "10.0.1",
"xml2js": "0.6.2", "xml2js": "0.6.2",
"node-fetch": "3.3.2", "node-fetch": "3.3.2",
"@electron/universal": "2.0.0", "@electron/universal": "2.0.1",
"@babel/runtime": "7.23.2" "@babel/runtime": "7.23.7"
} }
}, },
"dependencies": { "dependencies": {
@ -140,13 +140,14 @@
"@electron/remote": "2.1.1", "@electron/remote": "2.1.1",
"@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",
"@foobar404/wave": "2.0.4", "@foobar404/wave": "2.0.5",
"@jellybrick/electron-better-web-request": "1.0.4", "@jellybrick/electron-better-web-request": "1.0.4",
"@jellybrick/mpris-service": "2.1.4", "@jellybrick/mpris-service": "2.1.4",
"@xhayper/discord-rpc": "1.1.1", "@xhayper/discord-rpc": "1.1.1",
"async-mutex": "0.4.0", "async-mutex": "0.4.0",
"butterchurn": "3.0.0-beta.4", "butterchurn": "3.0.0-beta.4",
"butterchurn-presets": "3.0.0-beta.4", "butterchurn-presets": "3.0.0-beta.4",
"color": "4.2.3",
"conf": "10.2.0", "conf": "10.2.0",
"custom-electron-prompt": "1.5.7", "custom-electron-prompt": "1.5.7",
"dbus-next": "0.10.2", "dbus-next": "0.10.2",
@ -162,49 +163,53 @@
"filenamify": "6.0.0", "filenamify": "6.0.0",
"howler": "2.2.4", "howler": "2.2.4",
"html-to-text": "9.0.5", "html-to-text": "9.0.5",
"i18next": "23.7.11", "i18next": "23.7.13",
"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.11", "node-html-parser": "6.1.12",
"node-id3": "0.2.6", "node-id3": "0.2.6",
"peerjs": "1.5.2",
"semver": "7.5.4",
"serve": "14.2.1", "serve": "14.2.1",
"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",
"ts-morph": "21.0.1", "ts-morph": "21.0.1",
"vudio": "2.1.1", "vudio": "2.1.1",
"x11": "2.3.0", "x11": "2.3.0",
"youtubei.js": "8.0.0" "youtubei.js": "8.1.0"
}, },
"devDependencies": { "devDependencies": {
"@playwright/test": "1.41.0-alpha-dec-18-2023", "@playwright/test": "1.41.0-alpha-dec-18-2023",
"@total-typescript/ts-reset": "0.5.1", "@total-typescript/ts-reset": "0.5.1",
"@types/color": "3.0.6",
"@types/electron-localshortcut": "3.1.3", "@types/electron-localshortcut": "3.1.3",
"@types/howler": "2.2.11", "@types/howler": "2.2.11",
"@types/html-to-text": "9.0.4", "@types/html-to-text": "9.0.4",
"@typescript-eslint/eslint-plugin": "6.14.0", "@types/semver": "7.5.6",
"@typescript-eslint/eslint-plugin": "6.16.0",
"bufferutil": "4.0.8", "bufferutil": "4.0.8",
"builtin-modules": "3.3.0", "builtin-modules": "3.3.0",
"cross-env": "7.0.3", "cross-env": "7.0.3",
"del-cli": "5.1.0", "del-cli": "5.1.0",
"electron": "28.0.0", "electron": "28.1.0",
"electron-builder": "24.9.1", "electron-builder": "24.9.1",
"electron-devtools-installer": "3.2.0", "electron-devtools-installer": "3.2.0",
"electron-vite": "2.0.0-beta.1", "electron-vite": "2.0.0-beta.2",
"esbuild": "0.18.20", "esbuild": "0.18.20",
"eslint": "8.56.0", "eslint": "8.56.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.1",
"eslint-plugin-import": "2.29.1", "eslint-plugin-import": "2.29.1",
"eslint-plugin-prettier": "5.0.1", "eslint-plugin-prettier": "5.1.2",
"glob": "10.3.10", "glob": "10.3.10",
"node-gyp": "10.0.1", "node-gyp": "10.0.1",
"playwright": "1.41.0-alpha-dec-18-2023", "playwright": "1.41.0-alpha-dec-18-2023",
"rollup": "4.9.1", "rollup": "4.9.2",
"typescript": "5.3.3", "typescript": "5.3.3",
"utf-8-validate": "6.0.3", "utf-8-validate": "6.0.3",
"vite": "5.0.10", "vite": "5.0.10",
"vite-plugin-inspect": "0.8.1", "vite-plugin-inspect": "0.8.1",
"vite-plugin-resolve": "2.5.1", "vite-plugin-resolve": "2.5.1",
"ws": "8.15.1" "ws": "8.16.0"
}, },
"auto-changelog": { "auto-changelog": {
"hideCredit": true, "hideCredit": true,
@ -212,5 +217,5 @@
"unreleased": true, "unreleased": true,
"output": "changelog.md" "output": "changelog.md"
}, },
"packageManager": "pnpm@8.12.1" "packageManager": "pnpm@8.13.1"
} }

470
pnpm-lock.yaml generated
View File

@ -7,26 +7,26 @@ settings:
overrides: overrides:
esbuild: 0.18.20 esbuild: 0.18.20
usocket: 1.0.1 usocket: 1.0.1
rollup: 4.9.1 rollup: 4.9.2
node-gyp: 10.0.1 node-gyp: 10.0.1
xml2js: 0.6.2 xml2js: 0.6.2
node-fetch: 3.3.2 node-fetch: 3.3.2
'@electron/universal': 2.0.0 '@electron/universal': 2.0.1
'@babel/runtime': 7.23.2 '@babel/runtime': 7.23.7
dependencies: dependencies:
'@cliqz/adblocker-electron': '@cliqz/adblocker-electron':
specifier: 1.26.12 specifier: 1.26.12
version: 1.26.12(electron@28.0.0) version: 1.26.12(electron@28.1.0)
'@cliqz/adblocker-electron-preload': '@cliqz/adblocker-electron-preload':
specifier: 1.26.12 specifier: 1.26.12
version: 1.26.12(electron@28.0.0) version: 1.26.12(electron@28.1.0)
'@electron-toolkit/tsconfig': '@electron-toolkit/tsconfig':
specifier: 1.0.1 specifier: 1.0.1
version: 1.0.1(@types/node@20.10.5) version: 1.0.1(@types/node@20.10.5)
'@electron/remote': '@electron/remote':
specifier: 2.1.1 specifier: 2.1.1
version: 2.1.1(electron@28.0.0) version: 2.1.1(electron@28.1.0)
'@ffmpeg.wasm/core-mt': '@ffmpeg.wasm/core-mt':
specifier: 0.12.0 specifier: 0.12.0
version: 0.12.0 version: 0.12.0
@ -34,8 +34,8 @@ dependencies:
specifier: 0.12.0 specifier: 0.12.0
version: 0.12.0 version: 0.12.0
'@foobar404/wave': '@foobar404/wave':
specifier: 2.0.4 specifier: 2.0.5
version: 2.0.4 version: 2.0.5
'@jellybrick/electron-better-web-request': '@jellybrick/electron-better-web-request':
specifier: 1.0.4 specifier: 1.0.4
version: 1.0.4 version: 1.0.4
@ -54,12 +54,15 @@ dependencies:
butterchurn-presets: butterchurn-presets:
specifier: 3.0.0-beta.4 specifier: 3.0.0-beta.4
version: 3.0.0-beta.4 version: 3.0.0-beta.4
color:
specifier: 4.2.3
version: 4.2.3
conf: conf:
specifier: 10.2.0 specifier: 10.2.0
version: 10.2.0 version: 10.2.0
custom-electron-prompt: custom-electron-prompt:
specifier: 1.5.7 specifier: 1.5.7
version: 1.5.7(electron@28.0.0) version: 1.5.7(electron@28.1.0)
dbus-next: dbus-next:
specifier: 0.10.2 specifier: 0.10.2
version: 0.10.2 version: 0.10.2
@ -100,8 +103,8 @@ dependencies:
specifier: 9.0.5 specifier: 9.0.5
version: 9.0.5 version: 9.0.5
i18next: i18next:
specifier: 23.7.11 specifier: 23.7.13
version: 23.7.11 version: 23.7.13
keyboardevent-from-electron-accelerator: keyboardevent-from-electron-accelerator:
specifier: 2.0.0 specifier: 2.0.0
version: 2.0.0 version: 2.0.0
@ -109,11 +112,17 @@ dependencies:
specifier: 0.2.2 specifier: 0.2.2
version: 0.2.2 version: 0.2.2
node-html-parser: node-html-parser:
specifier: 6.1.11 specifier: 6.1.12
version: 6.1.11 version: 6.1.12
node-id3: node-id3:
specifier: 0.2.6 specifier: 0.2.6
version: 0.2.6 version: 0.2.6
peerjs:
specifier: 1.5.2
version: 1.5.2
semver:
specifier: 7.5.4
version: 7.5.4
serve: serve:
specifier: 14.2.1 specifier: 14.2.1
version: 14.2.1 version: 14.2.1
@ -130,8 +139,8 @@ dependencies:
specifier: 2.3.0 specifier: 2.3.0
version: 2.3.0 version: 2.3.0
youtubei.js: youtubei.js:
specifier: 8.0.0 specifier: 8.1.0
version: 8.0.0 version: 8.1.0
devDependencies: devDependencies:
'@playwright/test': '@playwright/test':
@ -140,6 +149,9 @@ devDependencies:
'@total-typescript/ts-reset': '@total-typescript/ts-reset':
specifier: 0.5.1 specifier: 0.5.1
version: 0.5.1 version: 0.5.1
'@types/color':
specifier: 3.0.6
version: 3.0.6
'@types/electron-localshortcut': '@types/electron-localshortcut':
specifier: 3.1.3 specifier: 3.1.3
version: 3.1.3 version: 3.1.3
@ -149,9 +161,12 @@ devDependencies:
'@types/html-to-text': '@types/html-to-text':
specifier: 9.0.4 specifier: 9.0.4
version: 9.0.4 version: 9.0.4
'@types/semver':
specifier: 7.5.6
version: 7.5.6
'@typescript-eslint/eslint-plugin': '@typescript-eslint/eslint-plugin':
specifier: 6.14.0 specifier: 6.16.0
version: 6.14.0(@typescript-eslint/parser@6.14.0)(eslint@8.56.0)(typescript@5.3.3) version: 6.16.0(@typescript-eslint/parser@6.14.0)(eslint@8.56.0)(typescript@5.3.3)
bufferutil: bufferutil:
specifier: 4.0.8 specifier: 4.0.8
version: 4.0.8 version: 4.0.8
@ -165,8 +180,8 @@ devDependencies:
specifier: 5.1.0 specifier: 5.1.0
version: 5.1.0 version: 5.1.0
electron: electron:
specifier: 28.0.0 specifier: 28.1.0
version: 28.0.0 version: 28.1.0
electron-builder: electron-builder:
specifier: 24.9.1 specifier: 24.9.1
version: 24.9.1 version: 24.9.1
@ -174,8 +189,8 @@ devDependencies:
specifier: 3.2.0 specifier: 3.2.0
version: 3.2.0 version: 3.2.0
electron-vite: electron-vite:
specifier: 2.0.0-beta.1 specifier: 2.0.0-beta.2
version: 2.0.0-beta.1(vite@5.0.10) version: 2.0.0-beta.2(vite@5.0.10)
esbuild: esbuild:
specifier: 0.18.20 specifier: 0.18.20
version: 0.18.20 version: 0.18.20
@ -192,8 +207,8 @@ devDependencies:
specifier: 2.29.1 specifier: 2.29.1
version: 2.29.1(@typescript-eslint/parser@6.14.0)(eslint-import-resolver-typescript@3.6.1)(eslint@8.56.0) version: 2.29.1(@typescript-eslint/parser@6.14.0)(eslint-import-resolver-typescript@3.6.1)(eslint@8.56.0)
eslint-plugin-prettier: eslint-plugin-prettier:
specifier: 5.0.1 specifier: 5.1.2
version: 5.0.1(eslint@8.56.0)(prettier@3.1.1) version: 5.1.2(eslint@8.56.0)(prettier@3.1.1)
glob: glob:
specifier: 10.3.10 specifier: 10.3.10
version: 10.3.10 version: 10.3.10
@ -204,8 +219,8 @@ devDependencies:
specifier: 1.41.0-alpha-dec-18-2023 specifier: 1.41.0-alpha-dec-18-2023
version: 1.41.0-alpha-dec-18-2023 version: 1.41.0-alpha-dec-18-2023
rollup: rollup:
specifier: 4.9.1 specifier: 4.9.2
version: 4.9.1 version: 4.9.2
typescript: typescript:
specifier: 5.3.3 specifier: 5.3.3
version: 5.3.3 version: 5.3.3
@ -217,13 +232,13 @@ devDependencies:
version: 5.0.10(@types/node@20.10.5) version: 5.0.10(@types/node@20.10.5)
vite-plugin-inspect: vite-plugin-inspect:
specifier: 0.8.1 specifier: 0.8.1
version: 0.8.1(rollup@4.9.1)(vite@5.0.10) version: 0.8.1(rollup@4.9.2)(vite@5.0.10)
vite-plugin-resolve: vite-plugin-resolve:
specifier: 2.5.1 specifier: 2.5.1
version: 2.5.1 version: 2.5.1
ws: ws:
specifier: 8.15.1 specifier: 8.16.0
version: 8.15.1(bufferutil@4.0.8)(utf-8-validate@6.0.3) version: 8.16.0(bufferutil@4.0.8)(utf-8-validate@6.0.3)
packages: packages:
@ -422,8 +437,8 @@ packages:
'@babel/helper-plugin-utils': 7.22.5 '@babel/helper-plugin-utils': 7.22.5
dev: true dev: true
/@babel/runtime@7.23.2: /@babel/runtime@7.23.7:
resolution: {integrity: sha512-mM8eg4yl5D6i3lu2QKPuPH4FArvJ8KhTofbE7jwMUv9KX5mBvwPAqnV3MlyBNqdp9RyRKP6Yck8TrfYrPvX3bg==} resolution: {integrity: sha512-w06OXVOFso7LcbzMiDGt+3X7Rh7Ho8MmgPoWU3rarH+8upf+wSU/grlGbWzQyr3DkdN6ZeuMFjpdwW0Q+HxobA==}
engines: {node: '>=6.9.0'} engines: {node: '>=6.9.0'}
dependencies: dependencies:
regenerator-runtime: 0.14.1 regenerator-runtime: 0.14.1
@ -465,29 +480,77 @@ packages:
to-fast-properties: 2.0.0 to-fast-properties: 2.0.0
dev: true dev: true
/@cbor-extract/cbor-extract-darwin-arm64@2.1.1:
resolution: {integrity: sha512-blVBy5MXz6m36Vx0DfLd7PChOQKEs8lK2bD1WJn/vVgG4FXZiZmZb2GECHFvVPA5T7OnODd9xZiL3nMCv6QUhA==}
cpu: [arm64]
os: [darwin]
requiresBuild: true
dev: false
optional: true
/@cbor-extract/cbor-extract-darwin-x64@2.1.1:
resolution: {integrity: sha512-h6KFOzqk8jXTvkOftyRIWGrd7sKQzQv2jVdTL9nKSf3D2drCvQB/LHUxAOpPXo3pv2clDtKs3xnHalpEh3rDsw==}
cpu: [x64]
os: [darwin]
requiresBuild: true
dev: false
optional: true
/@cbor-extract/cbor-extract-linux-arm64@2.1.1:
resolution: {integrity: sha512-SxAaRcYf8S0QHaMc7gvRSiTSr7nUYMqbUdErBEu+HYA4Q6UNydx1VwFE68hGcp1qvxcy9yT5U7gA+a5XikfwSQ==}
cpu: [arm64]
os: [linux]
requiresBuild: true
dev: false
optional: true
/@cbor-extract/cbor-extract-linux-arm@2.1.1:
resolution: {integrity: sha512-ds0uikdcIGUjPyraV4oJqyVE5gl/qYBpa/Wnh6l6xLE2lj/hwnjT2XcZCChdXwW/YFZ1LUHs6waoYN8PmK0nKQ==}
cpu: [arm]
os: [linux]
requiresBuild: true
dev: false
optional: true
/@cbor-extract/cbor-extract-linux-x64@2.1.1:
resolution: {integrity: sha512-GVK+8fNIE9lJQHAlhOROYiI0Yd4bAZ4u++C2ZjlkS3YmO6hi+FUxe6Dqm+OKWTcMpL/l71N6CQAmaRcb4zyJuA==}
cpu: [x64]
os: [linux]
requiresBuild: true
dev: false
optional: true
/@cbor-extract/cbor-extract-win32-x64@2.1.1:
resolution: {integrity: sha512-2Niq1C41dCRIDeD8LddiH+mxGlO7HJ612Ll3D/E73ZWBmycued+8ghTr/Ho3CMOWPUEr08XtyBMVXAjqF+TcKw==}
cpu: [x64]
os: [win32]
requiresBuild: true
dev: false
optional: true
/@cliqz/adblocker-content@1.26.12: /@cliqz/adblocker-content@1.26.12:
resolution: {integrity: sha512-4LWW3kntWuTDo10u24uuk0GmTzegkw9cZ8eDBzzDvHOtRVRMUv4fuoaWCwnB6UpA1VH7iU5nCbRlXNvjnnUA2Q==} resolution: {integrity: sha512-4LWW3kntWuTDo10u24uuk0GmTzegkw9cZ8eDBzzDvHOtRVRMUv4fuoaWCwnB6UpA1VH7iU5nCbRlXNvjnnUA2Q==}
dependencies: dependencies:
'@cliqz/adblocker-extended-selectors': 1.26.12 '@cliqz/adblocker-extended-selectors': 1.26.12
dev: false dev: false
/@cliqz/adblocker-electron-preload@1.26.12(electron@28.0.0): /@cliqz/adblocker-electron-preload@1.26.12(electron@28.1.0):
resolution: {integrity: sha512-R9ZL5d6M1qKBed9/BCmIh3+RWpoO9MnDDxeVFySfpHM9pdLkRDniZURuP2FTQ43JD2GtWopHgYmfWj3Hc46huw==} resolution: {integrity: sha512-R9ZL5d6M1qKBed9/BCmIh3+RWpoO9MnDDxeVFySfpHM9pdLkRDniZURuP2FTQ43JD2GtWopHgYmfWj3Hc46huw==}
peerDependencies: peerDependencies:
electron: '>11' electron: '>11'
dependencies: dependencies:
'@cliqz/adblocker-content': 1.26.12 '@cliqz/adblocker-content': 1.26.12
electron: 28.0.0 electron: 28.1.0
dev: false dev: false
/@cliqz/adblocker-electron@1.26.12(electron@28.0.0): /@cliqz/adblocker-electron@1.26.12(electron@28.1.0):
resolution: {integrity: sha512-KcteTxbOvnnNSjYobRnJmUKWitIxBvJqN9GTrHYTygJzOtm0te7/QexCP2/wIBbbD56c+9Fn0FsdDU4gZAIyWA==} resolution: {integrity: sha512-KcteTxbOvnnNSjYobRnJmUKWitIxBvJqN9GTrHYTygJzOtm0te7/QexCP2/wIBbbD56c+9Fn0FsdDU4gZAIyWA==}
peerDependencies: peerDependencies:
electron: '>11' electron: '>11'
dependencies: dependencies:
'@cliqz/adblocker': 1.26.12 '@cliqz/adblocker': 1.26.12
'@cliqz/adblocker-electron-preload': 1.26.12(electron@28.0.0) '@cliqz/adblocker-electron-preload': 1.26.12(electron@28.1.0)
electron: 28.0.0 electron: 28.1.0
tldts-experimental: 6.1.1 tldts-experimental: 6.1.1
dev: false dev: false
@ -576,16 +639,16 @@ packages:
- supports-color - supports-color
dev: true dev: true
/@electron/remote@2.1.1(electron@28.0.0): /@electron/remote@2.1.1(electron@28.1.0):
resolution: {integrity: sha512-Lfxul2yBxL+FBVaKszNAkuUqSIDbUQ1I7BC394iRXyqA2XGz7im2bAxroNIM51jhySSPKUaOLHaFLxfV6pC9VQ==} resolution: {integrity: sha512-Lfxul2yBxL+FBVaKszNAkuUqSIDbUQ1I7BC394iRXyqA2XGz7im2bAxroNIM51jhySSPKUaOLHaFLxfV6pC9VQ==}
peerDependencies: peerDependencies:
electron: '>= 13.0.0' electron: '>= 13.0.0'
dependencies: dependencies:
electron: 28.0.0 electron: 28.1.0
dev: false dev: false
/@electron/universal@2.0.0: /@electron/universal@2.0.1:
resolution: {integrity: sha512-Kps3RG6mXtEvoGYmpazMRRTZ1Zklba7oeYiaSaVCR18iKyP0A7WV9t1w3hu1PlzLnGunLJ2I10WvJC++KNbkIQ==} resolution: {integrity: sha512-fKpv9kg4SPmt+hY7SVBnIYULE9QJl8L3sCfcBsnqbJwwBwAeTLokJ9TRt9y7bK0JAzIW2y78TVVjvnQEms/yyA==}
engines: {node: '>=16.4'} engines: {node: '>=16.4'}
dependencies: dependencies:
'@electron/asar': 3.2.8 '@electron/asar': 3.2.8
@ -852,8 +915,8 @@ packages:
regenerator-runtime: 0.13.11 regenerator-runtime: 0.13.11
dev: false dev: false
/@foobar404/wave@2.0.4: /@foobar404/wave@2.0.5:
resolution: {integrity: sha512-FEyg37hDvQtrQVlFxbit7ov5e487BjsR32bZfJ4oAb5i+NnlbGaNyy6iYBZ8ocVHo8fgug+SL+mFdDTzqjvPww==} resolution: {integrity: sha512-V/ydadtv5ObCw8aEg+Qy3YSq1eyinEWzJfRI43Ovmj7VmAvEdWAdL7MatoMbiIVYPATkNDVF7GOxX1xirxM9dA==}
dev: false dev: false
/@humanwhocodes/config-array@0.11.13: /@humanwhocodes/config-array@0.11.13:
@ -953,6 +1016,11 @@ packages:
- supports-color - supports-color
dev: true dev: true
/@msgpack/msgpack@2.8.0:
resolution: {integrity: sha512-h9u4u/jiIRKbq25PM+zymTyW6bhTzELvOoUd+AvYriWOAKpLGnIamaET3pnHYoI5iYphAHBI4ayx0MehR+VVPQ==}
engines: {node: '>= 10'}
dev: false
/@nodelib/fs.scandir@2.1.5: /@nodelib/fs.scandir@2.1.5:
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
engines: {node: '>= 8'} engines: {node: '>= 8'}
@ -1053,11 +1121,11 @@ packages:
resolution: {integrity: sha512-yvwa+aCyYI/UjeD39BnpMypG8N06l86wIDW1/PAc6ihBRnodIfZDwccxQN3n1t74wduzaz74m4ZMHZnB06567Q==} resolution: {integrity: sha512-yvwa+aCyYI/UjeD39BnpMypG8N06l86wIDW1/PAc6ihBRnodIfZDwccxQN3n1t74wduzaz74m4ZMHZnB06567Q==}
dev: false dev: false
/@rollup/pluginutils@5.1.0(rollup@4.9.1): /@rollup/pluginutils@5.1.0(rollup@4.9.2):
resolution: {integrity: sha512-XTIWOPPcpvyKI6L1NHo0lFlCyznUEyPmPY1mc3KpPVDYulHSTvyeLNVW00QTLIAFNhR3kYnJTQHeGqU4M3n09g==} resolution: {integrity: sha512-XTIWOPPcpvyKI6L1NHo0lFlCyznUEyPmPY1mc3KpPVDYulHSTvyeLNVW00QTLIAFNhR3kYnJTQHeGqU4M3n09g==}
engines: {node: '>=14.0.0'} engines: {node: '>=14.0.0'}
peerDependencies: peerDependencies:
rollup: 4.9.1 rollup: 4.9.2
peerDependenciesMeta: peerDependenciesMeta:
rollup: rollup:
optional: true optional: true
@ -1065,107 +1133,107 @@ packages:
'@types/estree': 1.0.5 '@types/estree': 1.0.5
estree-walker: 2.0.2 estree-walker: 2.0.2
picomatch: 2.3.1 picomatch: 2.3.1
rollup: 4.9.1 rollup: 4.9.2
dev: true dev: true
/@rollup/rollup-android-arm-eabi@4.9.1: /@rollup/rollup-android-arm-eabi@4.9.2:
resolution: {integrity: sha512-6vMdBZqtq1dVQ4CWdhFwhKZL6E4L1dV6jUjuBvsavvNJSppzi6dLBbuV+3+IyUREaj9ZFvQefnQm28v4OCXlig==} resolution: {integrity: sha512-RKzxFxBHq9ysZ83fn8Iduv3A283K7zPPYuhL/z9CQuyFrjwpErJx0h4aeb/bnJ+q29GRLgJpY66ceQ/Wcsn3wA==}
cpu: [arm] cpu: [arm]
os: [android] os: [android]
requiresBuild: true requiresBuild: true
dev: true dev: true
optional: true optional: true
/@rollup/rollup-android-arm64@4.9.1: /@rollup/rollup-android-arm64@4.9.2:
resolution: {integrity: sha512-Jto9Fl3YQ9OLsTDWtLFPtaIMSL2kwGyGoVCmPC8Gxvym9TCZm4Sie+cVeblPO66YZsYH8MhBKDMGZ2NDxuk/XQ==} resolution: {integrity: sha512-yZ+MUbnwf3SHNWQKJyWh88ii2HbuHCFQnAYTeeO1Nb8SyEiWASEi5dQUygt3ClHWtA9My9RQAYkjvrsZ0WK8Xg==}
cpu: [arm64] cpu: [arm64]
os: [android] os: [android]
requiresBuild: true requiresBuild: true
dev: true dev: true
optional: true optional: true
/@rollup/rollup-darwin-arm64@4.9.1: /@rollup/rollup-darwin-arm64@4.9.2:
resolution: {integrity: sha512-LtYcLNM+bhsaKAIGwVkh5IOWhaZhjTfNOkGzGqdHvhiCUVuJDalvDxEdSnhFzAn+g23wgsycmZk1vbnaibZwwA==} resolution: {integrity: sha512-vqJ/pAUh95FLc/G/3+xPqlSBgilPnauVf2EXOQCZzhZJCXDXt/5A8mH/OzU6iWhb3CNk5hPJrh8pqJUPldN5zw==}
cpu: [arm64] cpu: [arm64]
os: [darwin] os: [darwin]
requiresBuild: true requiresBuild: true
dev: true dev: true
optional: true optional: true
/@rollup/rollup-darwin-x64@4.9.1: /@rollup/rollup-darwin-x64@4.9.2:
resolution: {integrity: sha512-KyP/byeXu9V+etKO6Lw3E4tW4QdcnzDG/ake031mg42lob5tN+5qfr+lkcT/SGZaH2PdW4Z1NX9GHEkZ8xV7og==} resolution: {integrity: sha512-otPHsN5LlvedOprd3SdfrRNhOahhVBwJpepVKUN58L0RnC29vOAej1vMEaVU6DadnpjivVsNTM5eNt0CcwTahw==}
cpu: [x64] cpu: [x64]
os: [darwin] os: [darwin]
requiresBuild: true requiresBuild: true
dev: true dev: true
optional: true optional: true
/@rollup/rollup-linux-arm-gnueabihf@4.9.1: /@rollup/rollup-linux-arm-gnueabihf@4.9.2:
resolution: {integrity: sha512-Yqz/Doumf3QTKplwGNrCHe/B2p9xqDghBZSlAY0/hU6ikuDVQuOUIpDP/YcmoT+447tsZTmirmjgG3znvSCR0Q==} resolution: {integrity: sha512-ewG5yJSp+zYKBYQLbd1CUA7b1lSfIdo9zJShNTyc2ZP1rcPrqyZcNlsHgs7v1zhgfdS+kW0p5frc0aVqhZCiYQ==}
cpu: [arm] cpu: [arm]
os: [linux] os: [linux]
requiresBuild: true requiresBuild: true
dev: true dev: true
optional: true optional: true
/@rollup/rollup-linux-arm64-gnu@4.9.1: /@rollup/rollup-linux-arm64-gnu@4.9.2:
resolution: {integrity: sha512-u3XkZVvxcvlAOlQJ3UsD1rFvLWqu4Ef/Ggl40WAVCuogf4S1nJPHh5RTgqYFpCOvuGJ7H5yGHabjFKEZGExk5Q==} resolution: {integrity: sha512-pL6QtV26W52aCWTG1IuFV3FMPL1m4wbsRG+qijIvgFO/VBsiXJjDPE/uiMdHBAO6YcpV4KvpKtd0v3WFbaxBtg==}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
requiresBuild: true requiresBuild: true
dev: true dev: true
optional: true optional: true
/@rollup/rollup-linux-arm64-musl@4.9.1: /@rollup/rollup-linux-arm64-musl@4.9.2:
resolution: {integrity: sha512-0XSYN/rfWShW+i+qjZ0phc6vZ7UWI8XWNz4E/l+6edFt+FxoEghrJHjX1EY/kcUGCnZzYYRCl31SNdfOi450Aw==} resolution: {integrity: sha512-On+cc5EpOaTwPSNetHXBuqylDW+765G/oqB9xGmWU3npEhCh8xu0xqHGUA+4xwZLqBbIZNcBlKSIYfkBm6ko7g==}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
requiresBuild: true requiresBuild: true
dev: true dev: true
optional: true optional: true
/@rollup/rollup-linux-riscv64-gnu@4.9.1: /@rollup/rollup-linux-riscv64-gnu@4.9.2:
resolution: {integrity: sha512-LmYIO65oZVfFt9t6cpYkbC4d5lKHLYv5B4CSHRpnANq0VZUQXGcCPXHzbCXCz4RQnx7jvlYB1ISVNCE/omz5cw==} resolution: {integrity: sha512-Wnx/IVMSZ31D/cO9HSsU46FjrPWHqtdF8+0eyZ1zIB5a6hXaZXghUKpRrC4D5DcRTZOjml2oBhXoqfGYyXKipw==}
cpu: [riscv64] cpu: [riscv64]
os: [linux] os: [linux]
requiresBuild: true requiresBuild: true
dev: true dev: true
optional: true optional: true
/@rollup/rollup-linux-x64-gnu@4.9.1: /@rollup/rollup-linux-x64-gnu@4.9.2:
resolution: {integrity: sha512-kr8rEPQ6ns/Lmr/hiw8sEVj9aa07gh1/tQF2Y5HrNCCEPiCBGnBUt9tVusrcBBiJfIt1yNaXN6r1CCmpbFEDpg==} resolution: {integrity: sha512-ym5x1cj4mUAMBummxxRkI4pG5Vht1QMsJexwGP8547TZ0sox9fCLDHw9KCH9c1FO5d9GopvkaJsBIOkTKxksdw==}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
requiresBuild: true requiresBuild: true
dev: true dev: true
optional: true optional: true
/@rollup/rollup-linux-x64-musl@4.9.1: /@rollup/rollup-linux-x64-musl@4.9.2:
resolution: {integrity: sha512-t4QSR7gN+OEZLG0MiCgPqMWZGwmeHhsM4AkegJ0Kiy6TnJ9vZ8dEIwHw1LcZKhbHxTY32hp9eVCMdR3/I8MGRw==} resolution: {integrity: sha512-m0hYELHGXdYx64D6IDDg/1vOJEaiV8f1G/iO+tejvRCJNSwK4jJ15e38JQy5Q6dGkn1M/9KcyEOwqmlZ2kqaZg==}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
requiresBuild: true requiresBuild: true
dev: true dev: true
optional: true optional: true
/@rollup/rollup-win32-arm64-msvc@4.9.1: /@rollup/rollup-win32-arm64-msvc@4.9.2:
resolution: {integrity: sha512-7XI4ZCBN34cb+BH557FJPmh0kmNz2c25SCQeT9OiFWEgf8+dL6ZwJ8f9RnUIit+j01u07Yvrsuu1rZGxJCc51g==} resolution: {integrity: sha512-x1CWburlbN5JjG+juenuNa4KdedBdXLjZMp56nHFSHTOsb/MI2DYiGzLtRGHNMyydPGffGId+VgjOMrcltOksA==}
cpu: [arm64] cpu: [arm64]
os: [win32] os: [win32]
requiresBuild: true requiresBuild: true
dev: true dev: true
optional: true optional: true
/@rollup/rollup-win32-ia32-msvc@4.9.1: /@rollup/rollup-win32-ia32-msvc@4.9.2:
resolution: {integrity: sha512-yE5c2j1lSWOH5jp+Q0qNL3Mdhr8WuqCNVjc6BxbVfS5cAS6zRmdiw7ktb8GNpDCEUJphILY6KACoFoRtKoqNQg==} resolution: {integrity: sha512-VVzCB5yXR1QlfsH1Xw1zdzQ4Pxuzv+CPr5qpElpKhVxlxD3CRdfubAG9mJROl6/dmj5gVYDDWk8sC+j9BI9/kQ==}
cpu: [ia32] cpu: [ia32]
os: [win32] os: [win32]
requiresBuild: true requiresBuild: true
dev: true dev: true
optional: true optional: true
/@rollup/rollup-win32-x64-msvc@4.9.1: /@rollup/rollup-win32-x64-msvc@4.9.2:
resolution: {integrity: sha512-PyJsSsafjmIhVgaI1Zdj7m8BB8mMckFah/xbpplObyHfiXzKcI5UOUXRyOdHW7nz4DpMCuzLnF7v5IWHenCwYA==} resolution: {integrity: sha512-SYRedJi+mweatroB+6TTnJYLts0L0bosg531xnQWtklOI6dezEagx4Q0qDyvRdK+qgdA3YZpjjGuPFtxBmddBA==}
cpu: [x64] cpu: [x64]
os: [win32] os: [win32]
requiresBuild: true requiresBuild: true
@ -1212,7 +1280,7 @@ packages:
dependencies: dependencies:
'@types/http-cache-semantics': 4.0.4 '@types/http-cache-semantics': 4.0.4
'@types/keyv': 3.1.4 '@types/keyv': 3.1.4
'@types/node': 18.19.3 '@types/node': 20.10.5
'@types/responselike': 1.0.3 '@types/responselike': 1.0.3
/@types/chrome@0.0.248: /@types/chrome@0.0.248:
@ -1222,6 +1290,22 @@ packages:
'@types/har-format': 1.2.15 '@types/har-format': 1.2.15
dev: false dev: false
/@types/color-convert@2.0.3:
resolution: {integrity: sha512-2Q6wzrNiuEvYxVQqhh7sXM2mhIhvZR/Paq4FdsQkOMgWsCIkKvSGj8Le1/XalulrmgOzPMqNa0ix+ePY4hTrfg==}
dependencies:
'@types/color-name': 1.1.3
dev: true
/@types/color-name@1.1.3:
resolution: {integrity: sha512-87W6MJCKZYDhLAx/J1ikW8niMvmGRyY+rpUxWpL1cO7F8Uu5CHuQoFv+R0/L5pgNdW4jTyda42kv60uwVIPjLw==}
dev: true
/@types/color@3.0.6:
resolution: {integrity: sha512-NMiNcZFRUAiUUCCf7zkAelY8eV3aKqfbzyFQlXpPIEeoNDbsEHGpb854V3gzTsGKYj830I5zPuOwU/TP5/cW6A==}
dependencies:
'@types/color-convert': 2.0.3
dev: true
/@types/debug@4.1.12: /@types/debug@4.1.12:
resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==}
dependencies: dependencies:
@ -1231,7 +1315,7 @@ packages:
/@types/electron-localshortcut@3.1.3: /@types/electron-localshortcut@3.1.3:
resolution: {integrity: sha512-D+CRdDTRZ4/9UmcSaZ5qvW4uq2VyyVmqsH9cdNReB4CL6MSIgyhr9w2PKeNEb0J/ZS7db7irJM/+ZiA5uSQsLw==} resolution: {integrity: sha512-D+CRdDTRZ4/9UmcSaZ5qvW4uq2VyyVmqsH9cdNReB4CL6MSIgyhr9w2PKeNEb0J/ZS7db7irJM/+ZiA5uSQsLw==}
dependencies: dependencies:
electron: 28.0.0 electron: 28.1.0
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
dev: true dev: true
@ -1286,7 +1370,7 @@ packages:
/@types/keyv@3.1.4: /@types/keyv@3.1.4:
resolution: {integrity: sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==} resolution: {integrity: sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==}
dependencies: dependencies:
'@types/node': 18.19.3 '@types/node': 20.10.5
/@types/minimist@1.2.5: /@types/minimist@1.2.5:
resolution: {integrity: sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag==} resolution: {integrity: sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag==}
@ -1322,7 +1406,7 @@ packages:
/@types/responselike@1.0.3: /@types/responselike@1.0.3:
resolution: {integrity: sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==} resolution: {integrity: sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==}
dependencies: dependencies:
'@types/node': 18.19.3 '@types/node': 20.10.5
/@types/semver@7.5.6: /@types/semver@7.5.6:
resolution: {integrity: sha512-dn1l8LaMea/IjDoHNd9J52uBbInB796CDffS6VdIxvqYCPSG0V0DzHp76GpaWnlhg88uYyPbXCDIowa86ybd5A==} resolution: {integrity: sha512-dn1l8LaMea/IjDoHNd9J52uBbInB796CDffS6VdIxvqYCPSG0V0DzHp76GpaWnlhg88uYyPbXCDIowa86ybd5A==}
@ -1338,11 +1422,11 @@ packages:
resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==} resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==}
requiresBuild: true requiresBuild: true
dependencies: dependencies:
'@types/node': 18.19.3 '@types/node': 20.10.5
optional: true optional: true
/@typescript-eslint/eslint-plugin@6.14.0(@typescript-eslint/parser@6.14.0)(eslint@8.56.0)(typescript@5.3.3): /@typescript-eslint/eslint-plugin@6.16.0(@typescript-eslint/parser@6.14.0)(eslint@8.56.0)(typescript@5.3.3):
resolution: {integrity: sha512-1ZJBykBCXaSHG94vMMKmiHoL0MhNHKSVlcHVYZNw+BKxufhqQVTOawNpwwI1P5nIFZ/4jLVop0mcY6mJJDFNaw==} resolution: {integrity: sha512-O5f7Kv5o4dLWQtPX4ywPPa+v9G+1q1x8mz0Kr0pXUtKsevo+gIJHLkGc8RxaZWtP8RrhwhSNIWThnW42K9/0rQ==}
engines: {node: ^16.0.0 || >=18.0.0} engines: {node: ^16.0.0 || >=18.0.0}
peerDependencies: peerDependencies:
'@typescript-eslint/parser': ^6.0.0 || ^6.0.0-alpha '@typescript-eslint/parser': ^6.0.0 || ^6.0.0-alpha
@ -1354,10 +1438,10 @@ packages:
dependencies: dependencies:
'@eslint-community/regexpp': 4.10.0 '@eslint-community/regexpp': 4.10.0
'@typescript-eslint/parser': 6.14.0(eslint@8.56.0)(typescript@5.3.3) '@typescript-eslint/parser': 6.14.0(eslint@8.56.0)(typescript@5.3.3)
'@typescript-eslint/scope-manager': 6.14.0 '@typescript-eslint/scope-manager': 6.16.0
'@typescript-eslint/type-utils': 6.14.0(eslint@8.56.0)(typescript@5.3.3) '@typescript-eslint/type-utils': 6.16.0(eslint@8.56.0)(typescript@5.3.3)
'@typescript-eslint/utils': 6.14.0(eslint@8.56.0)(typescript@5.3.3) '@typescript-eslint/utils': 6.16.0(eslint@8.56.0)(typescript@5.3.3)
'@typescript-eslint/visitor-keys': 6.14.0 '@typescript-eslint/visitor-keys': 6.16.0
debug: 4.3.4 debug: 4.3.4
eslint: 8.56.0 eslint: 8.56.0
graphemer: 1.4.0 graphemer: 1.4.0
@ -1399,8 +1483,16 @@ packages:
'@typescript-eslint/visitor-keys': 6.14.0 '@typescript-eslint/visitor-keys': 6.14.0
dev: true dev: true
/@typescript-eslint/type-utils@6.14.0(eslint@8.56.0)(typescript@5.3.3): /@typescript-eslint/scope-manager@6.16.0:
resolution: {integrity: sha512-x6OC9Q7HfYKqjnuNu5a7kffIYs3No30isapRBJl1iCHLitD8O0lFbRcVGiOcuyN837fqXzPZ1NS10maQzZMKqw==} resolution: {integrity: sha512-0N7Y9DSPdaBQ3sqSCwlrm9zJwkpOuc6HYm7LpzLAPqBL7dmzAUimr4M29dMkOP/tEwvOCC/Cxo//yOfJD3HUiw==}
engines: {node: ^16.0.0 || >=18.0.0}
dependencies:
'@typescript-eslint/types': 6.16.0
'@typescript-eslint/visitor-keys': 6.16.0
dev: true
/@typescript-eslint/type-utils@6.16.0(eslint@8.56.0)(typescript@5.3.3):
resolution: {integrity: sha512-ThmrEOcARmOnoyQfYkHw/DX2SEYBalVECmoldVuH6qagKROp/jMnfXpAU/pAIWub9c4YTxga+XwgAkoA0pxfmg==}
engines: {node: ^16.0.0 || >=18.0.0} engines: {node: ^16.0.0 || >=18.0.0}
peerDependencies: peerDependencies:
eslint: ^7.0.0 || ^8.0.0 eslint: ^7.0.0 || ^8.0.0
@ -1409,8 +1501,8 @@ packages:
typescript: typescript:
optional: true optional: true
dependencies: dependencies:
'@typescript-eslint/typescript-estree': 6.14.0(typescript@5.3.3) '@typescript-eslint/typescript-estree': 6.16.0(typescript@5.3.3)
'@typescript-eslint/utils': 6.14.0(eslint@8.56.0)(typescript@5.3.3) '@typescript-eslint/utils': 6.16.0(eslint@8.56.0)(typescript@5.3.3)
debug: 4.3.4 debug: 4.3.4
eslint: 8.56.0 eslint: 8.56.0
ts-api-utils: 1.0.3(typescript@5.3.3) ts-api-utils: 1.0.3(typescript@5.3.3)
@ -1424,6 +1516,11 @@ packages:
engines: {node: ^16.0.0 || >=18.0.0} engines: {node: ^16.0.0 || >=18.0.0}
dev: true dev: true
/@typescript-eslint/types@6.16.0:
resolution: {integrity: sha512-hvDFpLEvTJoHutVl87+MG/c5C8I6LOgEx05zExTSJDEVU7hhR3jhV8M5zuggbdFCw98+HhZWPHZeKS97kS3JoQ==}
engines: {node: ^16.0.0 || >=18.0.0}
dev: true
/@typescript-eslint/typescript-estree@6.14.0(typescript@5.3.3): /@typescript-eslint/typescript-estree@6.14.0(typescript@5.3.3):
resolution: {integrity: sha512-yPkaLwK0yH2mZKFE/bXkPAkkFgOv15GJAUzgUVonAbv0Hr4PK/N2yaA/4XQbTZQdygiDkpt5DkxPELqHguNvyw==} resolution: {integrity: sha512-yPkaLwK0yH2mZKFE/bXkPAkkFgOv15GJAUzgUVonAbv0Hr4PK/N2yaA/4XQbTZQdygiDkpt5DkxPELqHguNvyw==}
engines: {node: ^16.0.0 || >=18.0.0} engines: {node: ^16.0.0 || >=18.0.0}
@ -1445,8 +1542,30 @@ packages:
- supports-color - supports-color
dev: true dev: true
/@typescript-eslint/utils@6.14.0(eslint@8.56.0)(typescript@5.3.3): /@typescript-eslint/typescript-estree@6.16.0(typescript@5.3.3):
resolution: {integrity: sha512-XwRTnbvRr7Ey9a1NT6jqdKX8y/atWG+8fAIu3z73HSP8h06i3r/ClMhmaF/RGWGW1tHJEwij1uEg2GbEmPYvYg==} resolution: {integrity: sha512-VTWZuixh/vr7nih6CfrdpmFNLEnoVBF1skfjdyGnNwXOH1SLeHItGdZDHhhAIzd3ACazyY2Fg76zuzOVTaknGA==}
engines: {node: ^16.0.0 || >=18.0.0}
peerDependencies:
typescript: '*'
peerDependenciesMeta:
typescript:
optional: true
dependencies:
'@typescript-eslint/types': 6.16.0
'@typescript-eslint/visitor-keys': 6.16.0
debug: 4.3.4
globby: 11.1.0
is-glob: 4.0.3
minimatch: 9.0.3
semver: 7.5.4
ts-api-utils: 1.0.3(typescript@5.3.3)
typescript: 5.3.3
transitivePeerDependencies:
- supports-color
dev: true
/@typescript-eslint/utils@6.16.0(eslint@8.56.0)(typescript@5.3.3):
resolution: {integrity: sha512-T83QPKrBm6n//q9mv7oiSvy/Xq/7Hyw9SzSEhMHJwznEmQayfBM87+oAlkNAMEO7/MjIwKyOHgBJbxB0s7gx2A==}
engines: {node: ^16.0.0 || >=18.0.0} engines: {node: ^16.0.0 || >=18.0.0}
peerDependencies: peerDependencies:
eslint: ^7.0.0 || ^8.0.0 eslint: ^7.0.0 || ^8.0.0
@ -1454,9 +1573,9 @@ packages:
'@eslint-community/eslint-utils': 4.4.0(eslint@8.56.0) '@eslint-community/eslint-utils': 4.4.0(eslint@8.56.0)
'@types/json-schema': 7.0.15 '@types/json-schema': 7.0.15
'@types/semver': 7.5.6 '@types/semver': 7.5.6
'@typescript-eslint/scope-manager': 6.14.0 '@typescript-eslint/scope-manager': 6.16.0
'@typescript-eslint/types': 6.14.0 '@typescript-eslint/types': 6.16.0
'@typescript-eslint/typescript-estree': 6.14.0(typescript@5.3.3) '@typescript-eslint/typescript-estree': 6.16.0(typescript@5.3.3)
eslint: 8.56.0 eslint: 8.56.0
semver: 7.5.4 semver: 7.5.4
transitivePeerDependencies: transitivePeerDependencies:
@ -1472,6 +1591,14 @@ packages:
eslint-visitor-keys: 3.4.3 eslint-visitor-keys: 3.4.3
dev: true dev: true
/@typescript-eslint/visitor-keys@6.16.0:
resolution: {integrity: sha512-QSFQLruk7fhs91a/Ep/LqRdbJCZ1Rq03rqBdKT5Ky17Sz8zRLUksqIe9DW0pKtg/Z35/ztbLQ6qpOCN6rOC11A==}
engines: {node: ^16.0.0 || >=18.0.0}
dependencies:
'@typescript-eslint/types': 6.16.0
eslint-visitor-keys: 3.4.3
dev: true
/@ungap/structured-clone@1.2.0: /@ungap/structured-clone@1.2.0:
resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==}
dev: true dev: true
@ -1481,7 +1608,7 @@ packages:
engines: {node: '>=14.18.0'} engines: {node: '>=14.18.0'}
dependencies: dependencies:
axios: 1.6.2 axios: 1.6.2
ws: 8.15.1(bufferutil@4.0.8)(utf-8-validate@6.0.3) ws: 8.16.0(bufferutil@4.0.8)(utf-8-validate@6.0.3)
transitivePeerDependencies: transitivePeerDependencies:
- bufferutil - bufferutil
- debug - debug
@ -1644,7 +1771,7 @@ packages:
'@develar/schema-utils': 2.6.5 '@develar/schema-utils': 2.6.5
'@electron/notarize': 2.1.0 '@electron/notarize': 2.1.0
'@electron/osx-sign': 1.0.5 '@electron/osx-sign': 1.0.5
'@electron/universal': 2.0.0 '@electron/universal': 2.0.1
'@malept/flatpak-bundler': 0.4.0 '@malept/flatpak-bundler': 0.4.0
'@types/fs-extra': 9.0.13 '@types/fs-extra': 9.0.13
async-exit-hook: 2.0.1 async-exit-hook: 2.0.1
@ -1974,14 +2101,14 @@ packages:
/butterchurn-presets@3.0.0-beta.4: /butterchurn-presets@3.0.0-beta.4:
resolution: {integrity: sha512-TbQLUPvGOYMZAtWKoCmBtludh9aQZ6NaMGQU4lvPeadBPy3Du3yNmwBjlTMLP5c5mRWElxQPjTL1PtR7FZK3OQ==} resolution: {integrity: sha512-TbQLUPvGOYMZAtWKoCmBtludh9aQZ6NaMGQU4lvPeadBPy3Du3yNmwBjlTMLP5c5mRWElxQPjTL1PtR7FZK3OQ==}
dependencies: dependencies:
'@babel/runtime': 7.23.2 '@babel/runtime': 7.23.7
dev: false dev: false
/butterchurn@3.0.0-beta.4: /butterchurn@3.0.0-beta.4:
resolution: {integrity: sha512-hiY1ktHYHQ8MT65nnZi7GjrgZZ6sl/ipT5rBqEfaYJd90L4SvOtB6lVxtKadtzAyJo2TQJc4gJfEca4cpZo0DA==} resolution: {integrity: sha512-hiY1ktHYHQ8MT65nnZi7GjrgZZ6sl/ipT5rBqEfaYJd90L4SvOtB6lVxtKadtzAyJo2TQJc4gJfEca4cpZo0DA==}
dependencies: dependencies:
'@assemblyscript/loader': 0.17.14 '@assemblyscript/loader': 0.17.14
'@babel/runtime': 7.23.2 '@babel/runtime': 7.23.7
ecma-proposal-math-extensions: 0.0.2 ecma-proposal-math-extensions: 0.0.2
eel-wasm: 0.0.15 eel-wasm: 0.0.15
dev: false dev: false
@ -2065,6 +2192,28 @@ packages:
resolution: {integrity: sha512-+3e0ASu4sw1SWaoCtvPeyXp+5PsjigkSt8OXZbF9StH5pQWbxEjLAZE3n8Aup5udop1uRiKA7a4utUk/uoSpUw==} resolution: {integrity: sha512-+3e0ASu4sw1SWaoCtvPeyXp+5PsjigkSt8OXZbF9StH5pQWbxEjLAZE3n8Aup5udop1uRiKA7a4utUk/uoSpUw==}
dev: true dev: true
/cbor-extract@2.1.1:
resolution: {integrity: sha512-1UX977+L+zOJHsp0mWFG13GLwO6ucKgSmSW6JTl8B9GUvACvHeIVpFqhU92299Z6PfD09aTXDell5p+lp1rUFA==}
hasBin: true
requiresBuild: true
dependencies:
node-gyp-build-optional-packages: 5.0.3
optionalDependencies:
'@cbor-extract/cbor-extract-darwin-arm64': 2.1.1
'@cbor-extract/cbor-extract-darwin-x64': 2.1.1
'@cbor-extract/cbor-extract-linux-arm': 2.1.1
'@cbor-extract/cbor-extract-linux-arm64': 2.1.1
'@cbor-extract/cbor-extract-linux-x64': 2.1.1
'@cbor-extract/cbor-extract-win32-x64': 2.1.1
dev: false
optional: true
/cbor-x@1.5.4:
resolution: {integrity: sha512-PVKILDn+Rf6MRhhcyzGXi5eizn1i0i3F8Fe6UMMxXBnWkalq9+C5+VTmlIjAYM4iF2IYF2N+zToqAfYOp+3rfw==}
optionalDependencies:
cbor-extract: 2.1.1
dev: false
/chalk-template@0.4.0: /chalk-template@0.4.0:
resolution: {integrity: sha512-/ghrgmhfY8RaSdeo43hNXxpoHAtxdbskUHjPpfqUWGttFgycUhYPGx3YZBCnUCvOa7Doivn1IZec3DEGFoMgLg==} resolution: {integrity: sha512-/ghrgmhfY8RaSdeo43hNXxpoHAtxdbskUHjPpfqUWGttFgycUhYPGx3YZBCnUCvOa7Doivn1IZec3DEGFoMgLg==}
engines: {node: '>=12'} engines: {node: '>=12'}
@ -2180,6 +2329,21 @@ packages:
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
requiresBuild: true requiresBuild: true
/color-string@1.9.1:
resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==}
dependencies:
color-name: 1.1.4
simple-swizzle: 0.2.2
dev: false
/color@4.2.3:
resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==}
engines: {node: '>=12.5.0'}
dependencies:
color-convert: 2.0.1
color-string: 1.9.1
dev: false
/combined-stream@1.0.8: /combined-stream@1.0.8:
resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
engines: {node: '>= 0.8'} engines: {node: '>= 0.8'}
@ -2302,12 +2466,12 @@ packages:
engines: {node: '>= 6'} engines: {node: '>= 6'}
dev: false dev: false
/custom-electron-prompt@1.5.7(electron@28.0.0): /custom-electron-prompt@1.5.7(electron@28.1.0):
resolution: {integrity: sha512-ptRPJr6CpT06GWLMtg3GD2Lr7gWfXdWI+hR1S39eq+m/mUa2E118YmX6mPCbHdg5QB/W9UVhSpRqBM8FUh1G8w==} resolution: {integrity: sha512-ptRPJr6CpT06GWLMtg3GD2Lr7gWfXdWI+hR1S39eq+m/mUa2E118YmX6mPCbHdg5QB/W9UVhSpRqBM8FUh1G8w==}
peerDependencies: peerDependencies:
electron: '>=10.0.0' electron: '>=10.0.0'
dependencies: dependencies:
electron: 28.0.0 electron: 28.1.0
dev: false dev: false
/data-uri-to-buffer@4.0.1: /data-uri-to-buffer@4.0.1:
@ -2762,8 +2926,8 @@ packages:
- supports-color - supports-color
dev: false dev: false
/electron-vite@2.0.0-beta.1(vite@5.0.10): /electron-vite@2.0.0-beta.2(vite@5.0.10):
resolution: {integrity: sha512-4KNb6+yWYEnmHG2exTUiS470Z38dllgkYy7rIV9kkIT4dVLVXu0ShBGct/94tDW004o++vCtd/Blqogv9nUI6Q==} resolution: {integrity: sha512-E7eEUWj1oSKOPLVj2adUMNUbH2Nr5WRwox5s7DAfAOMXekEvEIFR7iaT3pizpdNJez6NTCJC16RPW1OeSCuRkA==}
engines: {node: ^18.0.0 || >=20.0.0} engines: {node: ^18.0.0 || >=20.0.0}
hasBin: true hasBin: true
peerDependencies: peerDependencies:
@ -2784,8 +2948,8 @@ packages:
- supports-color - supports-color
dev: true dev: true
/electron@28.0.0: /electron@28.1.0:
resolution: {integrity: sha512-eDhnCFBvG0PGFVEpNIEdBvyuGUBsFdlokd+CtuCe2ER3P+17qxaRfWRxMmksCOKgDHb5Wif5UxqOkZSlA4snlw==} resolution: {integrity: sha512-82Y7o4PSWPn1o/aVwYPsgmBw6Gyf2lVHpaBu3Ef8LrLWXxytg7ZRZr/RtDqEMOzQp3+mcuy3huH84MyjdmP50Q==}
engines: {node: '>= 12.20.55'} engines: {node: '>= 12.20.55'}
hasBin: true hasBin: true
requiresBuild: true requiresBuild: true
@ -3095,8 +3259,8 @@ packages:
- supports-color - supports-color
dev: true dev: true
/eslint-plugin-prettier@5.0.1(eslint@8.56.0)(prettier@3.1.1): /eslint-plugin-prettier@5.1.2(eslint@8.56.0)(prettier@3.1.1):
resolution: {integrity: sha512-m3u5RnR56asrwV/lDC4GHorlW75DsFfmUcjfCYylTUs85dBRnB7VM6xG8eCMJdeDRnppzmxZVf1GEPJvl1JmNg==} resolution: {integrity: sha512-dhlpWc9vOwohcWmClFcA+HjlvUpuyynYs0Rf+L/P6/0iQE6vlHW9l5bkfzN62/Stm9fbq8ku46qzde76T1xlSg==}
engines: {node: ^14.18.0 || >=16.0.0} engines: {node: ^14.18.0 || >=16.0.0}
peerDependencies: peerDependencies:
'@types/eslint': '>=8.0.0' '@types/eslint': '>=8.0.0'
@ -3224,6 +3388,10 @@ packages:
through: 2.3.8 through: 2.3.8
dev: false dev: false
/eventemitter3@4.0.7:
resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==}
dev: false
/execa@5.1.1: /execa@5.1.1:
resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==}
engines: {node: '>=10'} engines: {node: '>=10'}
@ -3829,10 +3997,10 @@ packages:
engines: {node: '>=14.18.0'} engines: {node: '>=14.18.0'}
dev: true dev: true
/i18next@23.7.11: /i18next@23.7.13:
resolution: {integrity: sha512-A/vOkw8vY99YHU9A1Td3I1dcTiYaPnwBWzrpVzfXUXSYgogK3cmBcmop/0cnXPc6QpUWIyqaugKNxRUEZVk9Nw==} resolution: {integrity: sha512-DbCPlw6VmURSZa43iOnycxq9o15e+WuBWDBZ3aj+gQZcDz4sgnuKwrcwmP1n8gSSCwCN7CRFGTpnwTd93A16Mg==}
dependencies: dependencies:
'@babel/runtime': 7.23.2 '@babel/runtime': 7.23.7
dev: false dev: false
/iconv-corefoundation@1.1.7: /iconv-corefoundation@1.1.7:
@ -3940,6 +4108,10 @@ packages:
resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==}
dev: true dev: true
/is-arrayish@0.3.2:
resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==}
dev: false
/is-bigint@1.0.4: /is-bigint@1.0.4:
resolution: {integrity: sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==} resolution: {integrity: sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==}
dependencies: dependencies:
@ -4661,6 +4833,13 @@ packages:
formdata-polyfill: 4.0.10 formdata-polyfill: 4.0.10
dev: false dev: false
/node-gyp-build-optional-packages@5.0.3:
resolution: {integrity: sha512-k75jcVzk5wnnc/FMxsf4udAoTEUv2jY3ycfdSd3yWu6Cnd1oee6/CfZJApyscA4FJOmdoixWwiwOyf16RzD5JA==}
hasBin: true
requiresBuild: true
dev: false
optional: true
/node-gyp-build@4.7.1: /node-gyp-build@4.7.1:
resolution: {integrity: sha512-wTSrZ+8lsRRa3I3H8Xr65dLWSgCvY2l4AOnaeKdPA9TB/WYMPaTcrzf3rXvFoVvjKNVnu0CcWSx54qq9GKRUYg==} resolution: {integrity: sha512-wTSrZ+8lsRRa3I3H8Xr65dLWSgCvY2l4AOnaeKdPA9TB/WYMPaTcrzf3rXvFoVvjKNVnu0CcWSx54qq9GKRUYg==}
hasBin: true hasBin: true
@ -4683,8 +4862,8 @@ packages:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
/node-html-parser@6.1.11: /node-html-parser@6.1.12:
resolution: {integrity: sha512-FAgwwZ6h0DSDWxfD0Iq1tsDcBCxdJB1nXpLPPxX8YyVWzbfCjKWEzaynF4gZZ/8hziUmp7ZSaKylcn0iKhufUQ==} resolution: {integrity: sha512-/bT/Ncmv+fbMGX96XG9g05vFt43m/+SYKIs9oAemQVYyVcZmDAI2Xq/SbNcpOA35eF0Zk2av3Ksf+Xk8Vt8abA==}
dependencies: dependencies:
css-select: 5.1.0 css-select: 5.1.0
he: 1.2.0 he: 1.2.0
@ -4981,6 +5160,22 @@ packages:
resolution: {integrity: sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ==} resolution: {integrity: sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ==}
dev: false dev: false
/peerjs-js-binarypack@2.1.0:
resolution: {integrity: sha512-YIwCC+pTzp3Bi8jPI9UFKO0t0SLo6xALnHkiNt/iUFmUUZG0fEEmEyFKvjsDKweiFitzHRyhuh6NvyJZ4nNxMg==}
engines: {node: '>= 14.0.0'}
dev: false
/peerjs@1.5.2:
resolution: {integrity: sha512-pPrtNwPyWJHRPxy2y+rHcdlrG8UwUBB1nl+3Yj6r7FLwcbBpcB2NvGNvLvcrxAVGGGX9fsdA5VT5zBKTZcm1DQ==}
engines: {node: '>= 14'}
dependencies:
'@msgpack/msgpack': 2.8.0
cbor-x: 1.5.4
eventemitter3: 4.0.7
peerjs-js-binarypack: 2.1.0
webrtc-adapter: 8.2.3
dev: false
/pend@1.2.0: /pend@1.2.0:
resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==} resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==}
@ -5260,24 +5455,24 @@ packages:
sprintf-js: 1.1.3 sprintf-js: 1.1.3
optional: true optional: true
/rollup@4.9.1: /rollup@4.9.2:
resolution: {integrity: sha512-pgPO9DWzLoW/vIhlSoDByCzcpX92bKEorbgXuZrqxByte3JFk2xSW2JEeAcyLc9Ru9pqcNNW+Ob7ntsk2oT/Xw==} resolution: {integrity: sha512-66RB8OtFKUTozmVEh3qyNfH+b+z2RXBVloqO2KCC/pjFaGaHtxP9fVfOQKPSGXg2mElmjmxjW/fZ7iKrEpMH5Q==}
engines: {node: '>=18.0.0', npm: '>=8.0.0'} engines: {node: '>=18.0.0', npm: '>=8.0.0'}
hasBin: true hasBin: true
optionalDependencies: optionalDependencies:
'@rollup/rollup-android-arm-eabi': 4.9.1 '@rollup/rollup-android-arm-eabi': 4.9.2
'@rollup/rollup-android-arm64': 4.9.1 '@rollup/rollup-android-arm64': 4.9.2
'@rollup/rollup-darwin-arm64': 4.9.1 '@rollup/rollup-darwin-arm64': 4.9.2
'@rollup/rollup-darwin-x64': 4.9.1 '@rollup/rollup-darwin-x64': 4.9.2
'@rollup/rollup-linux-arm-gnueabihf': 4.9.1 '@rollup/rollup-linux-arm-gnueabihf': 4.9.2
'@rollup/rollup-linux-arm64-gnu': 4.9.1 '@rollup/rollup-linux-arm64-gnu': 4.9.2
'@rollup/rollup-linux-arm64-musl': 4.9.1 '@rollup/rollup-linux-arm64-musl': 4.9.2
'@rollup/rollup-linux-riscv64-gnu': 4.9.1 '@rollup/rollup-linux-riscv64-gnu': 4.9.2
'@rollup/rollup-linux-x64-gnu': 4.9.1 '@rollup/rollup-linux-x64-gnu': 4.9.2
'@rollup/rollup-linux-x64-musl': 4.9.1 '@rollup/rollup-linux-x64-musl': 4.9.2
'@rollup/rollup-win32-arm64-msvc': 4.9.1 '@rollup/rollup-win32-arm64-msvc': 4.9.2
'@rollup/rollup-win32-ia32-msvc': 4.9.1 '@rollup/rollup-win32-ia32-msvc': 4.9.2
'@rollup/rollup-win32-x64-msvc': 4.9.1 '@rollup/rollup-win32-x64-msvc': 4.9.2
fsevents: 2.3.3 fsevents: 2.3.3
dev: true dev: true
@ -5330,6 +5525,10 @@ packages:
/sax@1.3.0: /sax@1.3.0:
resolution: {integrity: sha512-0s+oAmw9zLl1V1cS9BtZN7JAd0cW5e0QH4W3LWEK6a4LaLEA2OTpGYWDY+6XasBLtz6wkm3u1xRw95mRuJ59WA==} resolution: {integrity: sha512-0s+oAmw9zLl1V1cS9BtZN7JAd0cW5e0QH4W3LWEK6a4LaLEA2OTpGYWDY+6XasBLtz6wkm3u1xRw95mRuJ59WA==}
/sdp@3.2.0:
resolution: {integrity: sha512-d7wDPgDV3DDiqulJjKiV2865wKsJ34YI+NDREbm+FySq6WuKOikwyNQcm+doLAZ1O6ltdO0SeKle2xMpN3Brgw==}
dev: false
/selderee@0.11.0: /selderee@0.11.0:
resolution: {integrity: sha512-5TF+l7p4+OsnP8BCCvSyZiSPc4x4//p5uPwK8TCnVPJYRmU2aYKMpOXvw8zM5a5JvuuCGN1jmsMwuU2W02ukfA==} resolution: {integrity: sha512-5TF+l7p4+OsnP8BCCvSyZiSPc4x4//p5uPwK8TCnVPJYRmU2aYKMpOXvw8zM5a5JvuuCGN1jmsMwuU2W02ukfA==}
dependencies: dependencies:
@ -5450,6 +5649,12 @@ packages:
resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
engines: {node: '>=14'} engines: {node: '>=14'}
/simple-swizzle@0.2.2:
resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==}
dependencies:
is-arrayish: 0.3.2
dev: false
/simple-update-notifier@2.0.0: /simple-update-notifier@2.0.0:
resolution: {integrity: sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==} resolution: {integrity: sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==}
engines: {node: '>=10'} engines: {node: '>=10'}
@ -6030,7 +6235,7 @@ packages:
dev: true dev: true
optional: true optional: true
/vite-plugin-inspect@0.8.1(rollup@4.9.1)(vite@5.0.10): /vite-plugin-inspect@0.8.1(rollup@4.9.2)(vite@5.0.10):
resolution: {integrity: sha512-oPBPVGp6tBd5KdY/qY6lrbLXqrbHRG0hZLvEaJfiZ/GQfDB+szRuLHblQh1oi1Hhh8GeLit/50l4xfs2SA+TCA==} resolution: {integrity: sha512-oPBPVGp6tBd5KdY/qY6lrbLXqrbHRG0hZLvEaJfiZ/GQfDB+szRuLHblQh1oi1Hhh8GeLit/50l4xfs2SA+TCA==}
engines: {node: '>=14'} engines: {node: '>=14'}
peerDependencies: peerDependencies:
@ -6041,7 +6246,7 @@ packages:
optional: true optional: true
dependencies: dependencies:
'@antfu/utils': 0.7.7 '@antfu/utils': 0.7.7
'@rollup/pluginutils': 5.1.0(rollup@4.9.1) '@rollup/pluginutils': 5.1.0(rollup@4.9.2)
debug: 4.3.4 debug: 4.3.4
error-stack-parser-es: 0.1.1 error-stack-parser-es: 0.1.1
fs-extra: 11.2.0 fs-extra: 11.2.0
@ -6091,7 +6296,7 @@ packages:
'@types/node': 20.10.5 '@types/node': 20.10.5
esbuild: 0.18.20 esbuild: 0.18.20
postcss: 8.4.32 postcss: 8.4.32
rollup: 4.9.1 rollup: 4.9.2
optionalDependencies: optionalDependencies:
fsevents: 2.3.3 fsevents: 2.3.3
dev: true dev: true
@ -6105,6 +6310,13 @@ packages:
engines: {node: '>= 8'} engines: {node: '>= 8'}
dev: false dev: false
/webrtc-adapter@8.2.3:
resolution: {integrity: sha512-gnmRz++suzmvxtp3ehQts6s2JtAGPuDPjA1F3a9ckNpG1kYdYuHWYpazoAnL9FS5/B21tKlhkorbdCXat0+4xQ==}
engines: {node: '>=6.0.0', npm: '>=3.10.0'}
dependencies:
sdp: 3.2.0
dev: false
/which-boxed-primitive@1.0.2: /which-boxed-primitive@1.0.2:
resolution: {integrity: sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==} resolution: {integrity: sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==}
dependencies: dependencies:
@ -6173,8 +6385,8 @@ packages:
/wrappy@1.0.2: /wrappy@1.0.2:
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
/ws@8.15.1(bufferutil@4.0.8)(utf-8-validate@6.0.3): /ws@8.16.0(bufferutil@4.0.8)(utf-8-validate@6.0.3):
resolution: {integrity: sha512-W5OZiCjXEmk0yZ66ZN82beM5Sz7l7coYxpRkzS+p9PP+ToQry8szKh+61eNktr7EA9DOwvFGhfC605jDHbP6QQ==} resolution: {integrity: sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==}
engines: {node: '>=10.0.0'} engines: {node: '>=10.0.0'}
peerDependencies: peerDependencies:
bufferutil: ^4.0.1 bufferutil: ^4.0.1
@ -6263,8 +6475,8 @@ packages:
engines: {node: '>=10'} engines: {node: '>=10'}
dev: true dev: true
/youtubei.js@8.0.0: /youtubei.js@8.1.0:
resolution: {integrity: sha512-kUwHvqoB5vfaGaY1quAGcX5JPIyjr5fjj9Zj/ZwUDCrermz/r5uIkNiJ5cNHkmAJbZP9fdygzNMvGHd7fM445g==} resolution: {integrity: sha512-KxyeRF5JI7b/z2y4Q0Jb1+WlWxF3VModBVhkBhatzyALAW/OUavh/tAJBU55pmKlfEvnBjwCiGZrX7zb7BHnlQ==}
dependencies: dependencies:
jintr: 1.1.0 jintr: 1.1.0
tslib: 2.6.2 tslib: 2.6.2

View File

@ -2,9 +2,9 @@
"common": { "common": {
"console": { "console": {
"plugins": { "plugins": {
"execute-failed": "Selhalo execute pluginu {{pluginName}}::{{contextName}}", "execute-failed": "Selhalo spuštění pluginu {{pluginName}}::{{contextName}}",
"executed-at-ms": "Plugin {{pluginName}}::{{contextName}} executed at {{ms}}ms", "executed-at-ms": "Plugin {{pluginName}}::{{contextName}} spuštěn za {{ms}}ms",
"initialize-failed": "Selhala initialize \"{{pluginName}}\" pluginu", "initialize-failed": "Selhalo zapnutí \"{{pluginName}}\" pluginu",
"load-all": "Načítání všech pluginů", "load-all": "Načítání všech pluginů",
"load-failed": "Selhalo načtení \"{{pluginName}}\" pluginu", "load-failed": "Selhalo načtení \"{{pluginName}}\" pluginu",
"loaded": "Plugin \"{{pluginName}}\" načten", "loaded": "Plugin \"{{pluginName}}\" načten",
@ -32,20 +32,20 @@
"css-file-not-found": "CSS soubor \"{{cssFile}}\" neexistuje, ignorováno" "css-file-not-found": "CSS soubor \"{{cssFile}}\" neexistuje, ignorováno"
}, },
"unresponsive": { "unresponsive": {
"details": "Unresponsive chyba!\n{{error}}" "details": "Chyba - Aplikace nereaguje!\n{{error}}"
}, },
"when-ready": { "when-ready": {
"clearing-cache-after-20s": "Čištění mezipaměti aplikace" "clearing-cache-after-20s": "Čištění mezipaměti aplikace"
}, },
"window": { "window": {
"tried-to-render-offscreen": "Okno se pokusilo render na pozadí, Velikost okna ={{windowSize}}, displaySize={{displaySize}}, position={{position}}" "tried-to-render-offscreen": "Okno se pokusilo vykreslit na pozadí, velikost okna = {{windowSize}}, display velikost = {{displaySize}}, pozice = {{position}}"
} }
}, },
"dialog": { "dialog": {
"hide-menu-enabled": { "hide-menu-enabled": {
"detail": "Menu je skryté, stiskněte 'Alt' k zobrazení (nebo 'Escape', pokud používáte in-app-menu)", "detail": "Menu je skryté, stiskněte 'Alt' k jeho zobrazení (nebo 'Escape', pokud používáte in-app-menu)",
"message": "Skrýt Menu je povoleno", "message": "Skrýt menu je povoleno",
"title": "Skrýt Menu Povolené" "title": "Skrýt menu Povolené"
}, },
"need-to-restart": { "need-to-restart": {
"buttons": { "buttons": {
@ -72,9 +72,9 @@
"download": "Stáhnout", "download": "Stáhnout",
"ok": "OK" "ok": "OK"
}, },
"detail": "Nová verze je k dispozici a lze stáhnout na {{downloadLink}}", "detail": "Nová verze je k dispozici a lze ji stáhnout na {{downloadLink}}",
"message": "Nová verze je dostupná", "message": "Nová verze je dostupná",
"title": "Aktualizace k dispozici" "title": "Aktualizace je k dispozici"
} }
}, },
"menu": { "menu": {
@ -82,7 +82,7 @@
"navigation": { "navigation": {
"label": "Navigace", "label": "Navigace",
"submenu": { "submenu": {
"copy-current-url": "Kopírovat aktuální URL adresu", "copy-current-url": "Zkopírovat aktuální URL adresu",
"go-back": "Jít zpátky", "go-back": "Jít zpátky",
"go-forward": "Jít dopředu", "go-forward": "Jít dopředu",
"quit": "Ukončit", "quit": "Ukončit",
@ -95,28 +95,28 @@
"advanced-options": { "advanced-options": {
"label": "Pokročilé možnosti", "label": "Pokročilé možnosti",
"submenu": { "submenu": {
"auto-reset-app-cache": "Při spuštění aplikace, se resetuje její mezipaměť", "auto-reset-app-cache": "Při spuštění aplikace se resetuje její mezipaměť",
"disable-hardware-acceleration": "Vypnout hardware zrychlení", "disable-hardware-acceleration": "Vypnout hardware zrychlení",
"edit-config-json": "Upravit config.json", "edit-config-json": "Upravit config.json",
"override-user-agent": "Přepsat User-Agent", "override-user-agent": "Přepsat uživatelského agenta",
"restart-on-config-changes": "Restartovat na změny v konfiguraci", "restart-on-config-changes": "Restartovat aplikaci na změny v konfiguraci",
"set-proxy": { "set-proxy": {
"label": "Nastavit proxy", "label": "Nastavit proxy",
"prompt": { "prompt": {
"label": "Zadejte adresu proxy: (nechejte prázdné to disable)", "label": "Zadejte adresu proxy: (k vypnutí nechte pole prázdné)",
"placeholder": "Příklad: socks5://127.0.0.1:9999", "placeholder": "Příklad: SOCKS5://127.0.0.1:9999",
"title": "Nastavit proxy" "title": "Nastavit proxy"
} }
}, },
"toggle-dev-tools": "Toggle Vývojářské nástroje" "toggle-dev-tools": "Přepínat vývojářské nástroje"
} }
}, },
"always-on-top": "Vždy na vrchu", "always-on-top": "Vždy na vrchu",
"auto-update": "Automatické aktualizace", "auto-update": "Automatické aktualizace",
"hide-menu": { "hide-menu": {
"dialog": { "dialog": {
"message": "Menu bude skryto na dalším launch, use [Alt] to show it (nebo backtick [`] pokud používáte in-app-menu)", "message": "Menu bude skryto na dalším spuštěním, použijte [Alt] k jeho zobrazení (nebo backtick [`] pokud používáte in-app-menu)",
"title": "Skrýt Menu Povoleno" "title": "Skrýt menu Povoleno"
}, },
"label": "Skrýt menu" "label": "Skrýt menu"
}, },
@ -130,15 +130,17 @@
"to-help-translate": "Chcete pomoc s překladem? Klikněte zde" "to-help-translate": "Chcete pomoc s překladem? Klikněte zde"
} }
}, },
"resume-on-start": "Resume poslední písničku při spuštění aplikace", "resume-on-start": "Při spuštění aplikace, pokračovat na poslední písničce",
"single-instance-lock": "Zámek pro jednu instanci", "single-instance-lock": "Zámek pro jednu instanci",
"start-at-login": "Zapnutí aplikace po přihlášení",
"starting-page": { "starting-page": {
"label": "Úvodní stránka", "label": "Úvodní stránka",
"unset": "Nenastaveno" "unset": "Nenastaveno"
}, },
"tray": { "tray": {
"label": "Tray",
"submenu": { "submenu": {
"disabled": "Vypnuté", "disabled": "Vypnuto",
"enabled-and-hide-app": "Povolit a skrýt aplikaci", "enabled-and-hide-app": "Povolit a skrýt aplikaci",
"enabled-and-show-app": "Enabled a show aplikaci", "enabled-and-show-app": "Enabled a show aplikaci",
"play-pause-on-click": "Přehrát/Pozastavit na kliknutí" "play-pause-on-click": "Přehrát/Pozastavit na kliknutí"
@ -149,15 +151,15 @@
"submenu": { "submenu": {
"like-buttons": { "like-buttons": {
"default": "Výchozí", "default": "Výchozí",
"force-show": "Vynutit show", "force-show": "Vynutit zobrazení",
"hide": "Schovat", "hide": "Skrýt",
"label": "Like tlačítka" "label": "Like tlačítka"
}, },
"remove-upgrade-button": "Odebrat upgrade tlačítko", "remove-upgrade-button": "Odebrat upgrade tlačítko",
"theme": { "theme": {
"label": "Motiv", "label": "Motiv",
"submenu": { "submenu": {
"import-css-file": "Import vlastní CSS soubor", "import-css-file": "Vložit vlastní CSS soubor",
"no-theme": "Žádný motiv" "no-theme": "Žádný motiv"
} }
} }
@ -167,14 +169,15 @@
}, },
"plugins": { "plugins": {
"enabled": "Povoleno", "enabled": "Povoleno",
"label": "Pluginy" "label": "Pluginy",
"new": "NOVÉ"
}, },
"view": { "view": {
"label": "Zobrazení", "label": "Zobrazení",
"submenu": { "submenu": {
"force-reload": "Vynutit znovu načtení", "force-reload": "Vynutit znovu načtení",
"reload": "Obnovit", "reload": "Obnovit",
"reset-zoom": "Actual velikost", "reset-zoom": "Skutečná velikost",
"toggle-fullscreen": "Přepnout režim celé obrazovky", "toggle-fullscreen": "Přepnout režim celé obrazovky",
"zoom-in": "Přiblížit", "zoom-in": "Přiblížit",
"zoom-out": "Oddálit" "zoom-out": "Oddálit"
@ -187,7 +190,7 @@
"previous": "Minulý", "previous": "Minulý",
"quit": "Ukončit", "quit": "Ukončit",
"restart": "Restartovat aplikaci", "restart": "Restartovat aplikaci",
"show": "Ukázat okno" "show": "Zobrazit okno"
} }
}, },
"plugins": { "plugins": {
@ -198,14 +201,19 @@
}, },
"name": "Blokovač reklam" "name": "Blokovač reklam"
}, },
"album-actions": {
"description": "Přidává Undislike, Dislike, Like, a Unlike tlačítka k apply this ke všem písničkám v seznamu písniček nebo albumu.",
"name": "Album akce"
},
"album-color-theme": { "album-color-theme": {
"description": "Použije dynamický motiv a vizuální efekty na základě palety barev alba", "description": "Používá dynamický motiv a vizuální efekty na základě palety barev alba",
"name": "Motiv podle barvy Alba" "name": "Motiv podle barvy Alba"
}, },
"ambient-mode": { "ambient-mode": {
"description": "Applies a lighting efekty pomocí casting gentle barvy z videa, do vašeho screens pozadí.", "description": "Applies bleskové efekty pomocí casting jemných barev z videa, do vašeho pozadí obrazovky.",
"menu": { "menu": {
"blur-amount": { "blur-amount": {
"label": "Množství rozmazání",
"submenu": { "submenu": {
"pixels": "{{blurAmount}} pixelů" "pixels": "{{blurAmount}} pixelů"
} }
@ -237,18 +245,22 @@
"smoothness-transition": { "smoothness-transition": {
"label": "Plynulý přechod", "label": "Plynulý přechod",
"submenu": { "submenu": {
"during": "Během {{interpolationTime}}s" "during": "Během {{interpolationTime}} s"
} }
},
"use-fullscreen": {
"label": "Používání režimu celé obrazovky"
} }
}, },
"name": "Ambientní režim" "name": "Ambientní režim"
}, },
"audio-compressor": { "audio-compressor": {
"description": "Apply compression k audiu (snižuje hlasitost nejhlasitěších částí signálu and zvyšuje hlasitost nejjemnějších částí)",
"name": "Audio kompresor" "name": "Audio kompresor"
}, },
"blur-nav-bar": { "blur-nav-bar": {
"description": "Udělá navigační panel průhledným a rozmazaným", "description": "Udělá navigační panel průhledný a rozmazaný",
"name": "Rozmazaný navigační Bar" "name": "Rozmazaný navigační panel"
}, },
"bypass-age-restrictions": { "bypass-age-restrictions": {
"description": "Obejít ověření věku na YouTube", "description": "Obejít ověření věku na YouTube",
@ -273,11 +285,11 @@
} }
}, },
"compact-sidebar": { "compact-sidebar": {
"description": "Vždy set the sidebar v kompaktním režimu", "description": "Vždy nastavit postranní panel do kompaktního režimu",
"name": "Kompaktní Sidebar" "name": "Kompaktní postranní panel"
}, },
"crossfade": { "crossfade": {
"description": "Crossfade mezi písničkami", "description": "Prolínání mezi písničkami",
"menu": { "menu": {
"advanced": "Pokročilý" "advanced": "Pokročilý"
}, },
@ -296,26 +308,35 @@
} }
}, },
"disable-autoplay": { "disable-autoplay": {
"description": "Spustí písničku v režimu \"pozastaveno\"",
"menu": { "menu": {
"apply-once": "Applies jenom na spuštění aplikace" "apply-once": "Applies jenom na spuštění aplikace"
}, },
"name": "Zrušit automatické přehrávání" "name": "Vypnout automatické přehrávání"
}, },
"discord": { "discord": {
"backend": { "backend": {
"already-connected": "Pokusilo se spojit s aktivním spojením",
"connected": "Připojeno k Discordu", "connected": "Připojeno k Discordu",
"disconnected": "Odpojeno od Discordu" "disconnected": "Odpojeno od Discordu"
}, },
"description": "Ukažte svým přátelům, co posloucháte s Rich Presence", "description": "Ukažte svým přátelům, co posloucháte s Bohatou přítomností",
"menu": { "menu": {
"auto-reconnect": "Automaticky znovu připojit",
"clear-activity": "Vymazat aktivitu",
"clear-activity-after-timeout": "Vymazat aktivitu po timeout",
"connected": "Připojeno", "connected": "Připojeno",
"disconnected": "Odpojeno", "disconnected": "Odpojeno",
"hide-duration-left": "Skrýt zbývající duration",
"hide-github-button": "Skrýt tlačítko s odkazem na GitHub", "hide-github-button": "Skrýt tlačítko s odkazem na GitHub",
"play-on-youtube-music": "Hrát na YouTube Music" "play-on-youtube-music": "Hrát na YouTube Music",
"set-inactivity-timeout": "Nastavit timeout pro neaktivitu"
}, },
"name": "Discord Bohatá přítomnost",
"prompt": { "prompt": {
"set-inactivity-timeout": { "set-inactivity-timeout": {
"label": "Zadejte inactivity timeout v sekundách:" "label": "Zadejte timeout neaktivity v sekundách:",
"title": "Nastavit timeout pro neaktivitu"
} }
} }
}, },
@ -334,70 +355,122 @@
"ok": "OK" "ok": "OK"
}, },
"detail": "({{playlistSize}} písničky)", "detail": "({{playlistSize}} písničky)",
"message": "Stahování seznamu skladeb {{playlistTitle}}", "message": "Stahování seznamu písniček {{playlistTitle}}",
"title": "Stahování začalo" "title": "Stahování začalo"
} }
}, },
"feedback": { "feedback": {
"conversion-progress": "Konverze: {{percent}}%",
"done": "Hotovo: {{filePath}}", "done": "Hotovo: {{filePath}}",
"download-info": "Stahování {{artist}} - {{title}} [{{videoId}}", "download-info": "Stahování {{artist}} - {{title}} [{{videoId}}",
"download-progress": "Stahování: {{percent}}%", "download-progress": "Stahování: {{percent}}%",
"downloading": "Stahování…", "downloading": "Stahování…",
"downloading-counter": "Stahování {{current}}/{{total}}…", "downloading-counter": "Stahování {{current}}/{{total}}…",
"downloading-playlist": "Stahování seznamu skladeb \"{{playlistTitle}}\" - {{playlistSize}} písničky ({{playlistId}})", "downloading-playlist": "Stahování seznamu písniček \"{{playlistTitle}}\" - {{playlistSize}} písničky ({{playlistId}})",
"error-while-downloading": "Chyba při stahování \"{{author}} - {{title}}\": {{error}}", "error-while-downloading": "Chyba při stahování \"{{author}} - {{title}}\": {{error}}",
"folder-already-exists": "Složka {{playlistFolder}} již existuje", "folder-already-exists": "Složka {{playlistFolder}} již existuje",
"getting-playlist-info": "Getting informace o seznamu skladeb…", "getting-playlist-info": "Získávání informací o seznamu písniček…",
"loading": "Načítání…", "loading": "Načítání…",
"playlist-has-only-one-song": "Seznam skladeb má pouze jednu položku, downloading it directly", "playlist-has-only-one-song": "Seznam písniček má pouze jednu položku, stahuje se přímo",
"playlist-id-not-found": "Žádné ID seznamu skladeb nenalezeno", "playlist-id-not-found": "Žádné ID seznamu písnček nenalezeno",
"playlist-is-empty": "Seznam skladeb je prázdný", "playlist-is-empty": "Seznam písniček je prázdný",
"playlist-is-mix-or-private": "Chyba při získávání informací o seznamu písniček: ujistite se, že se nejedná o soukromý nebo \"Namíchaný pro vás\" seznam písniček\n\n{{error}}",
"preparing-file": "Připravování souboru…", "preparing-file": "Připravování souboru…",
"saving": "Ukládání…", "saving": "Ukládání…",
"trying-to-get-playlist-id": "Trying to get ID seznamu skladeb: {{playlistId}}", "trying-to-get-playlist-id": "Trying se získat ID seznamu písniček: {{playlistId}}",
"video-id-not-found": "Video nebylo nalezeno" "video-id-not-found": "Video nebylo nalezeno",
"writing-id3": "Psaní ID3 značek…"
} }
}, },
"description": "Stahuje MP3 / source audio přímo z rozhraní", "description": "Stahuje MP3 / source audio přímo z rozhraní",
"menu": { "menu": {
"choose-download-folder": "Vybrat download složku", "choose-download-folder": "Vybrat složku pro stahování",
"download-playlist": "Stáhnout seznam skladeb", "download-playlist": "Stáhnout seznam písniček",
"presets": "Předvolby",
"skip-existing": "Přeskočit existující soubory" "skip-existing": "Přeskočit existující soubory"
}, },
"name": "Stahovač", "name": "Stahovač",
"renderer": {
"can-not-update-progress": "Progress nemůže být aktualizován"
},
"templates": { "templates": {
"button": "Stáhnout" "button": "Stáhnout"
} }
}, },
"exponential-volume": { "exponential-volume": {
"description": "Dělá posuvník hlasitosti exponenciální, takže je snazší vybrat nižší hlasitost.",
"name": "Exponenciální hlasitost" "name": "Exponenciální hlasitost"
}, },
"in-app-menu": { "in-app-menu": {
"description": "Dává menu-bars a fancy, tmavý nebo album-color vzhled" "description": "Dává menu panelům fancy, tmavý nebo album-color vzhled",
"menu": {
"hide-dom-window-controls": "Skrýt DOM window controls"
}
}, },
"last-fm": { "last-fm": {
"description": "Přidat scrobbling podporu pro Last.fm", "description": "Přidat scrobbling podporu pro Last.fm",
"name": "Last.fm" "name": "Last.fm"
}, },
"lumiastream": { "lumiastream": {
"description": "Přidává Lumia Stream podporu" "description": "Přidává Lumia Stream podporu",
"name": "Lumia Stream [beta]"
}, },
"lyrics-genius": { "lyrics-genius": {
"description": "Přidat lyrics podporu pro většinu písniček", "description": "Přidává lyrics podporu pro většinu písniček",
"renderer": { "renderer": {
"fetched-lyrics": "Fetched lyrics pro Genius" "fetched-lyrics": "Fetched lyrics pro Genius"
} }
}, },
"music-together": {
"dialog": {
"enter-host": "Zadejte Host ID"
},
"internal": {
"save": "Uložit",
"unknown-user": "Neznámý uživatel"
},
"menu": {
"close": "Zavřít Hudba Spolu",
"connected-users": "Připojení uživatelé",
"disconnect": "Odpojit od Hudby Spolu",
"empty-user": "Žadní připojení uživatelé",
"host": "Hudba Spolu Host",
"join": "Připojit se k Hudbě Spolu",
"permission": {
"playlist": "Seznam písniček Control"
},
"set-permission": "Změnit Control oprávnění",
"status": {
"disconnected": "Odpojen",
"guest": "Připojený/á jako Guest",
"host": "Připojený/á jako Host"
}
},
"name": "Hudba Spolu [Beta]",
"toast": {
"add-song-failed": "Selhalo přidání písničky",
"closed": "Hudba Spolu zavřena",
"disconnected": "Hudba Spolu odpojena",
"host-failed": "Selhalo hostování Hudby Spolu",
"id-copied": "Host ID zkopírováno do schránky",
"join-failed": "Selhalo připojení k Hudba Spolu",
"joined": "Připojil/a jste se k Hudbě Spolu",
"permission-changed": "Oprávnění Hudby Spolu se změnilo na \"{{permission}}\"",
"remove-song-failed": "Selhalo odstranění písničky",
"user-connected": "{{name}} se připojil/a k Hudbě Spolu",
"user-disconnected": "{{name}} odpustil/a Hudba Spolu"
}
},
"navigation": { "navigation": {
"description": "Další/Zpátky navigační šipky přímo integrovány do rozhraní, jako ve vašem oblíbeném prohlížeči", "description": "Další/Zpátky navigační šipky přímo integrovány do rozhraní, jako ve vašem oblíbeném prohlížeči",
"name": "Navigace" "name": "Navigace"
}, },
"no-google-login": { "no-google-login": {
"description": "Odstranit Google login tlačítka a odkazy z rozhraní", "description": "Odstranit tlačítka Google přihlášení a odkazy z rozhraní",
"name": "Žádné Google přihlášení" "name": "Žádné Google přihlášení"
}, },
"notifications": { "notifications": {
"description": "Display oznámení when a písnička starts hraje (interactive notifications are available on Windows)", "description": "Zobrazit oznámení, když písnička začne hrát (interaktiv notifikace jsou dostupné na Windows)",
"menu": { "menu": {
"interactive": "Interaktivní oznámení", "interactive": "Interaktivní oznámení",
"interactive-settings": { "interactive-settings": {
@ -405,7 +478,7 @@
"submenu": { "submenu": {
"hide-button-text": "Skrýt text tlačítka", "hide-button-text": "Skrýt text tlačítka",
"refresh-on-play-pause": "Refresh na Přehrát/Pozastavit", "refresh-on-play-pause": "Refresh na Přehrát/Pozastavit",
"tray-controls": "Otevřít/Zavřít on tray click" "tray-controls": "Otevřít/Zavřít aplikaci na kliknutí na tray ikonu"
} }
}, },
"priority": "Priorita Oznámení", "priority": "Priorita Oznámení",
@ -414,6 +487,7 @@
"name": "Oznámení" "name": "Oznámení"
}, },
"picture-in-picture": { "picture-in-picture": {
"description": "Povoluje switch aplikaci do režimu obrázek v obrázku",
"menu": { "menu": {
"always-on-top": "Vždy na vrchu", "always-on-top": "Vždy na vrchu",
"hotkey": { "hotkey": {
@ -422,8 +496,8 @@
"keybind-options": { "keybind-options": {
"hotkey": "Klávesová zkratka" "hotkey": "Klávesová zkratka"
}, },
"label": "Vybrat klávesovou zkratku pro toggle obrázek v obrázku", "label": "Vybrat klávesovou zkratku pro přepínání obrázek v obrázku",
"title": "Obrázek v obrázku klávesová zkratka" "title": "klávesová zkratka pro obrázek v obrázku"
} }
}, },
"save-window-position": "Uložit pozici okna", "save-window-position": "Uložit pozici okna",
@ -436,14 +510,16 @@
} }
}, },
"playback-speed": { "playback-speed": {
"description": "Posloiuchej rychle, poslouchej pomalu! Adds a slider, který kontroluje rychlost písníčky", "description": "Poslouchej rychle, poslouchej pomalu! Přidává slider, který kontroluje rychlost písníčky",
"name": "Rychlost přehrávání", "name": "Rychlost přehrávání",
"templates": { "templates": {
"button": "Rychlost" "button": "Rychlost"
} }
}, },
"precise-volume": { "precise-volume": {
"description": "Přesná kontrola hlasitosti pomocí kolečka myši/klávesnicových zkratek, s vlastní HUD a customizable hlasitostních steps",
"menu": { "menu": {
"custom-volume-steps": "Nastavit vlastní hlasitostní steps",
"global-shortcuts": "Globální klávesové zkratky" "global-shortcuts": "Globální klávesové zkratky"
}, },
"name": "Přesná hlasitost", "name": "Přesná hlasitost",
@ -452,7 +528,13 @@
"keybind-options": { "keybind-options": {
"decrease": "Snížit hlasitost", "decrease": "Snížit hlasitost",
"increase": "Zvýšit hlasitost" "increase": "Zvýšit hlasitost"
} },
"label": "Vybrat globální klávesnicové zkratky:",
"title": "Globální klávesnicové zkratky hlasitosti"
},
"volume-steps": {
"label": "Vybrat Zvýšení/Snížení hlasitost Steps",
"title": "Hlasitostní steps"
} }
} }
}, },
@ -465,11 +547,15 @@
"title": "Vybrat kvalitu videa" "title": "Vybrat kvalitu videa"
} }
} }
} },
"description": "Umožňuje měnit kvalitu videa pomocí tlačítka na video overlay",
"name": "Měnič kvality videa"
}, },
"shortcuts": { "shortcuts": {
"description": "Dovoluje nastavit globální klávesové zkratky pro playback (přehrát/pozastavit/další/předchozí) a vypínání media OSD pomocí přepisování media klíčů, zapínání Ctrl/CMD + F k vyhledávání, zapínání Linux MPRIS podporu pro media klíče, a vlastní klávesové zkratky pro pokročilé uživatele.",
"menu": { "menu": {
"override-media-keys": "Přepsat Media Keys" "override-media-keys": "Přepsat media klíče",
"set-keybinds": "Nastavit globální Controls písniček"
}, },
"name": "Zkratky (& MPRIS)", "name": "Zkratky (& MPRIS)",
"prompt": { "prompt": {
@ -478,16 +564,23 @@
"next": "Další", "next": "Další",
"play-pause": "Přehrát / Pozastavit", "play-pause": "Přehrát / Pozastavit",
"previous": "Předchozí" "previous": "Předchozí"
} },
"label": "Vybrat globální klávesnicové zkratky pro ovládání písniček:",
"title": "Globální klávesnicové zkratky"
} }
} }
}, },
"skip-disliked-songs": {
"description": "Přeskakovat disliked písničky",
"name": "Přeskočit Disliked písničky"
},
"skip-silences": { "skip-silences": {
"description": "Automaticky přeskakovat tichá místa v písničkách", "description": "Automaticky přeskakovat tichá místa v písničkách",
"name": "Přeskočit Tichá místa" "name": "Přeskakovat Tichá místa"
}, },
"sponsorblock": { "sponsorblock": {
"description": "Automaticky přeskakuje non-music části jako intro/outro nebo části of music videos, kde nehraje písnčka" "description": "Automaticky přeskakuje nehudební části jako intro/outro nebo části hudebních videí, kde nehraje písnčka",
"name": "SponsorBlock"
}, },
"taskbar-mediacontrol": { "taskbar-mediacontrol": {
"description": "Ovládejte přehrávání z vašeho hlavního panelu Windows", "description": "Ovládejte přehrávání z vašeho hlavního panelu Windows",
@ -498,10 +591,11 @@
"name": "Touch Bar" "name": "Touch Bar"
}, },
"tuna-obs": { "tuna-obs": {
"description": "Integrace s OBS's plugin Tuna" "description": "Integrace s OBS's plugin Tuna",
"name": "Tuna OBS"
}, },
"video-toggle": { "video-toggle": {
"description": "Přidává tlačítko switch mezi videem/písničkou mode. Může také optionally remove celou video kartu", "description": "Přidává tlačítko k switch mezi video/písničko režimem. Může také odstranit celou video kartu",
"menu": { "menu": {
"align": { "align": {
"label": "Zarovnání", "label": "Zarovnání",
@ -513,9 +607,15 @@
}, },
"force-hide": "Vynutit odstranění karty videa", "force-hide": "Vynutit odstranění karty videa",
"mode": { "mode": {
"label": "Režim" "label": "Režim",
"submenu": {
"custom": "Vlastní přepínač",
"disabled": "Vypnuto",
"native": "Původní přepínač"
}
} }
}, },
"name": "Přepínač videa",
"templates": { "templates": {
"button": "Písnička" "button": "Písnička"
} }

View File

@ -170,7 +170,8 @@
}, },
"plugins": { "plugins": {
"enabled": "Enabled", "enabled": "Enabled",
"label": "Plugins" "label": "Plugins",
"new": "NEW"
}, },
"view": { "view": {
"label": "View", "label": "View",
@ -201,6 +202,10 @@
}, },
"name": "Adblocker" "name": "Adblocker"
}, },
"album-actions": {
"description": "Adds Undislike, Dislike, Like, and Unlike buttons to apply this to all songs in a playlist or album.",
"name": "Album actions"
},
"album-color-theme": { "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" "name": "Album Color Theme"
@ -426,6 +431,51 @@
"fetched-lyrics": "Fetched lyrics for Genius" "fetched-lyrics": "Fetched lyrics for Genius"
} }
}, },
"music-together": {
"description": "Share a playlist with others. When the host plays a song, everyone else will hear the same song",
"dialog": {
"enter-host": "Enter Host ID"
},
"internal": {
"save": "Save",
"track-source": "Track Source",
"unknown-user": "Unknown User"
},
"menu": {
"click-to-copy-id": "Copy Host ID",
"close": "Close Music Together",
"connected-users": "Connected Users",
"disconnect": "Disconnect Music Together",
"empty-user": "No connected users",
"host": "Music Together Host",
"join": "Join Music Together",
"permission": {
"all": "Allow guests to control playlist and player",
"host-only": "Only the host can control playlist and player",
"playlist": "Allow guests to control playlist"
},
"set-permission": "Change Control Permission",
"status": {
"disconnected": "Disconnected",
"guest": "Connected as Guest",
"host": "Connected as Host"
}
},
"name": "Music Together [Beta]",
"toast": {
"add-song-failed": "Failed to add song",
"closed": "Music Together closed",
"disconnected": "Music Together disconnected",
"host-failed": "Failed to host Music Together",
"id-copied": "Host ID copied to clipboard",
"join-failed": "Failed to join Music Together",
"joined": "Joined Music Together",
"permission-changed": "Music Together permission changed to \"{{permission}}\"",
"remove-song-failed": "Failed to remove song",
"user-connected": "{{name}} joined Music Together",
"user-disconnected": "{{name}} left Music Together"
}
},
"navigation": { "navigation": {
"description": "Next/Back navigation arrows directly integrated in the interface, like in your favorite browser", "description": "Next/Back navigation arrows directly integrated in the interface, like in your favorite browser",
"name": "Navigation" "name": "Navigation"

View File

@ -170,7 +170,8 @@
}, },
"plugins": { "plugins": {
"enabled": "Habilitado", "enabled": "Habilitado",
"label": "Plugins" "label": "Plugins",
"new": "NUEVO"
}, },
"view": { "view": {
"label": "Ver", "label": "Ver",
@ -201,6 +202,10 @@
}, },
"name": "Adblocker" "name": "Adblocker"
}, },
"album-actions": {
"description": "Añade los botones \"No me gusta\", \"No me gusta\", \"Me gusta\" y \"No me gusta\" para aplicarlos a todas las canciones de una lista de reproducción o un álbum.",
"name": "Acciones del álbum"
},
"album-color-theme": { "album-color-theme": {
"description": "Aplica un tema dinámico y efectos visuales basados en la paleta de colores del álbum", "description": "Aplica un tema dinámico y efectos visuales basados en la paleta de colores del álbum",
"name": "Color del álbum" "name": "Color del álbum"
@ -426,6 +431,51 @@
"fetched-lyrics": "Letras recuperadas de Genius" "fetched-lyrics": "Letras recuperadas de Genius"
} }
}, },
"music-together": {
"description": "Comparte una lista de reproducción con los demás. Cuando el anfitrión reproduzca una canción, todos los demás escucharán la misma",
"dialog": {
"enter-host": "Introduzca el ID del host"
},
"internal": {
"save": "Guardar",
"track-source": "Fuente de la pista",
"unknown-user": "Usuario desconocido"
},
"menu": {
"click-to-copy-id": "Copiar el ID del host",
"close": "Cerrar Music Together",
"connected-users": "Usuarios conectados",
"disconnect": "Desactivar Music Together",
"empty-user": "No hay usuarios conectados",
"host": "Host de Music Together",
"join": "Únase a Music Together",
"permission": {
"all": "Todo el control",
"host-only": "Solo anfitrión",
"playlist": "Control de las listas de reproducción"
},
"set-permission": "Permiso de control de cambios",
"status": {
"disconnected": "Desconectado",
"guest": "Conectado como invitado",
"host": "Conectado como anfitrión"
}
},
"name": "Music Together [Beta]",
"toast": {
"add-song-failed": "No se puede añadir la canción",
"closed": "Music Together cerrado",
"disconnected": "Music Together desconectados",
"host-failed": "Fallo el host de Music Together",
"id-copied": "ID del host copiado en el portapapeles",
"join-failed": "Fallo en la unión a Music Together",
"joined": "Unido a Music Together",
"permission-changed": "Permiso de Music Together cambiado a \"{{permission}}\"",
"remove-song-failed": "Error al eliminar la canción",
"user-connected": "{{name}} se unió a Music Together",
"user-disconnected": "{{name}} dejó Music Together"
}
},
"navigation": { "navigation": {
"description": "Flechas de navegación Siguiente/Atrás directamente integradas en la interfaz, como en tu navegador favorito", "description": "Flechas de navegación Siguiente/Atrás directamente integradas en la interfaz, como en tu navegador favorito",
"name": "Navegación" "name": "Navegación"

View File

@ -7,7 +7,9 @@
"initialize-failed": "Gagal dalam menginisialisasi plugin \"{{pluginName}}\"", "initialize-failed": "Gagal dalam menginisialisasi plugin \"{{pluginName}}\"",
"load-all": "Memuat semua plugin", "load-all": "Memuat semua plugin",
"load-failed": "Gagal memuat plugin \"{{pluginName}}\"", "load-failed": "Gagal memuat plugin \"{{pluginName}}\"",
"loaded": "Plugin \"{{pluginName}}\" dimuat" "loaded": "Plugin \"{{pluginName}}\" dimuat",
"unload-failed": "Gagal untuk memuat plugin \"{{pluginName}}\"",
"unloaded": "Plugin \"{{pluginName}}\" telah dikeluarkan"
} }
} }
}, },

View File

@ -170,7 +170,8 @@
}, },
"plugins": { "plugins": {
"enabled": "활성화", "enabled": "활성화",
"label": "확장" "label": "확장",
"new": "NEW"
}, },
"view": { "view": {
"label": "보기", "label": "보기",
@ -201,6 +202,10 @@
}, },
"name": "광고 차단기" "name": "광고 차단기"
}, },
"album-actions": {
"description": "좋아요, 싫어요 버튼을 추가하고, 결과를 재생 목록 또는 앨범의 모든 노래에 적용합니다.",
"name": "앨범 액션"
},
"album-color-theme": { "album-color-theme": {
"description": "앨범 색상 팔레트를 기반으로 동적 테마 및 시각 효과를 적용합니다", "description": "앨범 색상 팔레트를 기반으로 동적 테마 및 시각 효과를 적용합니다",
"name": "앨범 컬러 기반 테마" "name": "앨범 컬러 기반 테마"
@ -426,6 +431,51 @@
"fetched-lyrics": "Genius에서 가사 불러옴" "fetched-lyrics": "Genius에서 가사 불러옴"
} }
}, },
"music-together": {
"description": "여러명과 함께 플레이리스트를 공유합니다. 호스트가 음악을 재생하면, 다른 사용자들도 같은 노래를 들을 수 있습니다",
"dialog": {
"enter-host": "호스트 아이디를 입력하세요"
},
"internal": {
"save": "저장",
"track-source": "재생 중인 트랙 출처",
"unknown-user": "알 수 없는 사용자"
},
"menu": {
"click-to-copy-id": "호스트 아이디 복사",
"close": "Music Together 닫기",
"connected-users": "연결된 사용자",
"disconnect": "Music Together 연결 끊기",
"empty-user": "연결된 사용자 없음",
"host": "Music Together 호스트",
"join": "Music Together 참여",
"permission": {
"all": "게스트가 모두 제어 가능",
"host-only": "호스트만 제어 가능",
"playlist": "게스트가 재생목록 제어 가능"
},
"set-permission": "제어 권한 변경",
"status": {
"disconnected": "연결 끊김",
"guest": "게스트로 연결됨",
"host": "호스트로 연결됨"
}
},
"name": "Music Together [베타]",
"toast": {
"add-song-failed": "노래 추가 실패",
"closed": "Music Together가 닫혔습니다",
"disconnected": "Music Together 연결이 끊어졌습니다",
"host-failed": "Music Together를 열 수 없습니다",
"id-copied": "호스트 아이디가 클립보드에 복사되었습니다",
"join-failed": "Music Together에 참여할 수 없습니다",
"joined": "Music Together에 참여했습니다",
"permission-changed": "Music Together 제어 권한이 \"{{permission}}\"(으)로 변경되었습니다",
"remove-song-failed": "노래 제거 실패",
"user-connected": "{{name}}님이 Music Together에 참여했습니다",
"user-disconnected": "{{name}}님이 Music Together에서 나갔습니다"
}
},
"navigation": { "navigation": {
"description": "브라우저에서처럼, UI에 직접 통합된 앞으로/뒤로 탐색하는 화살표", "description": "브라우저에서처럼, UI에 직접 통합된 앞으로/뒤로 탐색하는 화살표",
"name": "탐색" "name": "탐색"

View File

@ -99,7 +99,7 @@
"auto-reset-app-cache": "Perkrauti programos talpyklą, kai programa paleidžiama", "auto-reset-app-cache": "Perkrauti programos talpyklą, kai programa paleidžiama",
"disable-hardware-acceleration": "Išjungti aparatūros pagreitį", "disable-hardware-acceleration": "Išjungti aparatūros pagreitį",
"edit-config-json": "Redaguoti config.json", "edit-config-json": "Redaguoti config.json",
"override-user-agent": "Perrašyti User-Agent", "override-user-agent": "Perrašyti \"User-Agent\"",
"restart-on-config-changes": "Perkrauti po config pasikeitimo", "restart-on-config-changes": "Perkrauti po config pasikeitimo",
"set-proxy": { "set-proxy": {
"label": "Nustatyti įgaliotajį serverį", "label": "Nustatyti įgaliotajį serverį",
@ -116,7 +116,7 @@
"auto-update": "Automatinis Atnaujinimas", "auto-update": "Automatinis Atnaujinimas",
"hide-menu": { "hide-menu": {
"dialog": { "dialog": {
"message": "Meniu bus paslėpta per kitą paleidimą, naudokite [Alt], kad ją parodyti (arba [`] jei naudojama programos meniu)", "message": "Meniu bus paslėpta per kitą paleidimą, naudokite [Alt], kad ją parodyti (arba kairinio kirčio ženklą [`] jei naudojama programos meniu)",
"title": "\"Paslėpti Meniu\" įjungtas" "title": "\"Paslėpti Meniu\" įjungtas"
}, },
"label": "Paslėpti Meniu" "label": "Paslėpti Meniu"
@ -144,7 +144,7 @@
"disabled": "Išjungta", "disabled": "Išjungta",
"enabled-and-hide-app": "Įjungta ir slėpti programos langą", "enabled-and-hide-app": "Įjungta ir slėpti programos langą",
"enabled-and-show-app": "Įjungta ir rodyti programos langą", "enabled-and-show-app": "Įjungta ir rodyti programos langą",
"play-pause-on-click": "Leisti/Sustabdyti ant paspaudimo" "play-pause-on-click": "Paleisti/Pristabdyti ant paspaudimo"
} }
}, },
"visual-tweaks": { "visual-tweaks": {
@ -175,7 +175,7 @@
"view": { "view": {
"label": "Vaizdas", "label": "Vaizdas",
"submenu": { "submenu": {
"force-reload": "Priverstinis perkrovimas", "force-reload": "Priverstinai perkrauti",
"reload": "Perkrauti", "reload": "Perkrauti",
"reset-zoom": "Tikras dydis", "reset-zoom": "Tikras dydis",
"toggle-fullscreen": "Įjungti/Išjungti Pilną Ekraną", "toggle-fullscreen": "Įjungti/Išjungti Pilną Ekraną",
@ -186,7 +186,7 @@
}, },
"tray": { "tray": {
"next": "Kitas", "next": "Kitas",
"play-pause": "Leisti/Sustabdyti", "play-pause": "Paleisti/Pristabdyti",
"previous": "Ankstesnis", "previous": "Ankstesnis",
"quit": "Išeiti", "quit": "Išeiti",
"restart": "Perkrauti programą", "restart": "Perkrauti programą",
@ -239,7 +239,7 @@
} }
}, },
"smoothness-transition": { "smoothness-transition": {
"label": "Perėjimo švelnumas", "label": "Perliejimo švelnumas",
"submenu": { "submenu": {
"during": "Per {{interpolationTime}}s" "during": "Per {{interpolationTime}}s"
} }
@ -307,7 +307,7 @@
} }
}, },
"disable-autoplay": { "disable-autoplay": {
"description": "Pradeda dainą sustabdytame rėžime", "description": "Pradeda dainą pristabdytame rėžime",
"menu": { "menu": {
"apply-once": "Pritaiko tik per programos paleidimą" "apply-once": "Pritaiko tik per programos paleidimą"
}, },
@ -319,11 +319,11 @@
"connected": "Prisijungta prie \"Discord\"", "connected": "Prisijungta prie \"Discord\"",
"disconnected": "Atsijungta nuo \"Discord\"" "disconnected": "Atsijungta nuo \"Discord\""
}, },
"description": "Parodyk savo draugams ko tu klausaisi su \"Turtingas Buvimas\"", "description": "Parodyk savo draugams ko tu klausaisi su \"Turtingas Buvimas\" (Rich Presence)",
"menu": { "menu": {
"auto-reconnect": "Automatiškai prisijungti", "auto-reconnect": "Automatiškai prisijungti",
"clear-activity": "Išvalyti veiksmus", "clear-activity": "Išvalyti veik",
"clear-activity-after-timeout": "Išvalyti veiksmus po skirtojo laiko", "clear-activity-after-timeout": "Išvalyti veik po skirtojo laiko",
"connected": "Prisijungta", "connected": "Prisijungta",
"disconnected": "Atsijungta", "disconnected": "Atsijungta",
"hide-duration-left": "Slėpti kiek liko laiko", "hide-duration-left": "Slėpti kiek liko laiko",
@ -331,7 +331,7 @@
"play-on-youtube-music": "Leisti ant \"Youtube Music\"", "play-on-youtube-music": "Leisti ant \"Youtube Music\"",
"set-inactivity-timeout": "Nustatyti neveiklumo laiką" "set-inactivity-timeout": "Nustatyti neveiklumo laiką"
}, },
"name": "\"Discord\" Turtingas Buvimas", "name": "\"Discord\" Turtingas Buvimas (Rich Presence)",
"prompt": { "prompt": {
"set-inactivity-timeout": { "set-inactivity-timeout": {
"label": "Įveskite neveiklumo skirtąjį laiką sekundėmis:", "label": "Įveskite neveiklumo skirtąjį laiką sekundėmis:",
@ -371,10 +371,10 @@
"folder-already-exists": "Aplankas {{playlistFolder}} jau egzistuoja", "folder-already-exists": "Aplankas {{playlistFolder}} jau egzistuoja",
"getting-playlist-info": "Gaunama grojaraščio informacija…", "getting-playlist-info": "Gaunama grojaraščio informacija…",
"loading": "Kraunama…", "loading": "Kraunama…",
"playlist-has-only-one-song": "Grojaraštis turi tik vieną daiktą, jis atsisiunčiamas tiesiogiai", "playlist-has-only-one-song": "Grojaraštis turi tik vieną elementą, jis atsisiunčiamas tiesiogiai",
"playlist-id-not-found": "Grojaraščio ID nerastas", "playlist-id-not-found": "Grojaraščio ID nerastas",
"playlist-is-empty": "Grojaraštis yra tuščias", "playlist-is-empty": "Grojaraštis yra tuščias",
"playlist-is-mix-or-private": "Paklaida gaunant grojaraščio informaciją: Pasitikrink, kad nėra privatus ar \"Surinkta specialiai jums\" grojaraštis\n\n{{error}}", "playlist-is-mix-or-private": "Paklaida gaunant grojaraščio informaciją: Pasitikrink, kad jis nėra privatus ar \"Surinkta specialiai jums\" grojaraštis\n\n{{error}}",
"preparing-file": "Failas paruošiamas…", "preparing-file": "Failas paruošiamas…",
"saving": "Išsaugojama…", "saving": "Išsaugojama…",
"trying-to-get-playlist-id": "Bandoma gauti grojaraščio ID: {{playlistId}}", "trying-to-get-playlist-id": "Bandoma gauti grojaraščio ID: {{playlistId}}",
@ -423,7 +423,7 @@
}, },
"name": "\"Genius\" Žodžių tekstai", "name": "\"Genius\" Žodžių tekstai",
"renderer": { "renderer": {
"fetched-lyrics": "Gauti žodžiai iš „Genius“." "fetched-lyrics": "Gauti žodžiai iš „Genius“"
} }
}, },
"navigation": { "navigation": {
@ -442,7 +442,7 @@
"label": "Interaktyvūs nustatymai", "label": "Interaktyvūs nustatymai",
"submenu": { "submenu": {
"hide-button-text": "Paslėpti mygtuko tekstą", "hide-button-text": "Paslėpti mygtuko tekstą",
"refresh-on-play-pause": "Atnaujinti ant Leisti/Sustabdyti", "refresh-on-play-pause": "Atnaujinti ant Paleidimo/Pristabdymo",
"tray-controls": "Atidaryti/Uždaryti ant padėklo paspaudimo" "tray-controls": "Atidaryti/Uždaryti ant padėklo paspaudimo"
} }
}, },
@ -455,7 +455,7 @@
"picture-in-picture": { "picture-in-picture": {
"description": "Leidžia pakeisti programą į \"picture-in-picture\" rėžimą", "description": "Leidžia pakeisti programą į \"picture-in-picture\" rėžimą",
"menu": { "menu": {
"always-on-top": "Visada viršuje", "always-on-top": "Visada ant viršaus",
"hotkey": { "hotkey": {
"label": "Spartusis klavišas", "label": "Spartusis klavišas",
"prompt": { "prompt": {
@ -543,7 +543,7 @@
}, },
"skip-silences": { "skip-silences": {
"description": "Automatiškai praleisti tylos dalis dainose", "description": "Automatiškai praleisti tylos dalis dainose",
"name": "Praleisti Tylas" "name": "Praleisti Tylumas"
}, },
"sponsorblock": { "sponsorblock": {
"description": "Automatiškai praleidžia ne muzikines dalis, pvz., įžangą/užvedimą arba muzikinių vaizdo įrašų dalis, kuriose daina negrojama", "description": "Automatiškai praleidžia ne muzikines dalis, pvz., įžangą/užvedimą arba muzikinių vaizdo įrašų dalis, kuriose daina negrojama",

View File

@ -105,7 +105,7 @@
"label": "Ustaw proxy", "label": "Ustaw proxy",
"prompt": { "prompt": {
"label": "Podaj adres Proxy: (zostaw pusty aby wyłączyć)", "label": "Podaj adres Proxy: (zostaw pusty aby wyłączyć)",
"placeholder": "Przykład: socks5://127.0.0.1:9999", "placeholder": "Przykład: SOCKS5://127.0.0.1:9999",
"title": "Ustaw proxy" "title": "Ustaw proxy"
} }
}, },
@ -241,7 +241,7 @@
"smoothness-transition": { "smoothness-transition": {
"label": "Płynność przejścia", "label": "Płynność przejścia",
"submenu": { "submenu": {
"during": "W czasie {{interpolationTime}}s" "during": "W czasie {{interpolationTime}} s"
} }
}, },
"use-fullscreen": { "use-fullscreen": {
@ -293,8 +293,8 @@
"prompt": { "prompt": {
"options": { "options": {
"multi-input": { "multi-input": {
"fade-in-duration": "Czas wnikania (milisekundy)", "fade-in-duration": "Czas wnikania (ms)",
"fade-out-duration": "Czas zanikania (milisekundy)", "fade-out-duration": "Czas zanikania (ms)",
"fade-scaling": { "fade-scaling": {
"label": "Skalowanie zanikania", "label": "Skalowanie zanikania",
"linear": "Liniowe", "linear": "Liniowe",

View File

@ -99,6 +99,7 @@
"auto-reset-app-cache": "Очищать кеш при запуске приложения", "auto-reset-app-cache": "Очищать кеш при запуске приложения",
"disable-hardware-acceleration": "Отключить аппаратное ускорение", "disable-hardware-acceleration": "Отключить аппаратное ускорение",
"edit-config-json": "Редактировать config.json", "edit-config-json": "Редактировать config.json",
"override-user-agent": "Переопределить User-Agent",
"restart-on-config-changes": "Перезапускать при изменениях конфига", "restart-on-config-changes": "Перезапускать при изменениях конфига",
"set-proxy": { "set-proxy": {
"label": "Задать прокси", "label": "Задать прокси",
@ -131,6 +132,7 @@
} }
}, },
"resume-on-start": "Продолжать последнюю песню при запуске приложения", "resume-on-start": "Продолжать последнюю песню при запуске приложения",
"single-instance-lock": "Запрет запуска нескольких экземпляров",
"start-at-login": "Запуск при включении компьютера", "start-at-login": "Запуск при включении компьютера",
"starting-page": { "starting-page": {
"label": "Стартовая страница", "label": "Стартовая страница",
@ -399,17 +401,37 @@
"description": "Делает слайдер громкости расширенным чтобы было легче выбирать низкие уровни.", "description": "Делает слайдер громкости расширенным чтобы было легче выбирать низкие уровни.",
"name": "Расширенная громкость" "name": "Расширенная громкость"
}, },
"in-app-menu": {
"description": "Придает меню модный вид"
},
"last-fm": { "last-fm": {
"name": "Last.fm" "name": "Last.fm"
}, },
"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": {
"description": "Позволяет переключить приложение в режим «картинка в картинке»",
"menu": {
"always-on-top": "Всегда наверху",
"hotkey": {
"label": "Горячая клавиша",
"prompt": {
"keybind-options": {
"hotkey": "Горячая клавиша"
},
"label": "Выберите горячую клавишу для переключения режима изображения в изображении"
}
},
"save-window-position": "Сохранить положение окна",
"save-window-size": "Сохранить размер окна"
}
}, },
"shortcuts": { "shortcuts": {
"prompt": { "prompt": {
@ -437,15 +459,26 @@
"right": "Right" "right": "Right"
} }
}, },
"force-hide": "Скрыть обложку",
"mode": { "mode": {
"label": "Mode" "label": "Mode",
"submenu": {
"custom": "Кастомный переключатель",
"disabled": "Отключен",
"native": "Стандартный переключатель"
}
} }
}, },
"name": "Переключатель видео",
"templates": { "templates": {
"button": "Song" "button": "Song"
} }
}, },
"visualizer": { "visualizer": {
"description": "Заменяет обложку визуализатором музыки",
"menu": {
"visualizer-type": "Вид визуализации"
},
"name": "Визуалайзер" "name": "Визуалайзер"
} }
} }

View File

@ -7,7 +7,9 @@
"initialize-failed": "\"{{pluginName}}\" eklentisi başlatılamadı", "initialize-failed": "\"{{pluginName}}\" eklentisi başlatılamadı",
"load-all": "Tüm eklentiler yükleniyor", "load-all": "Tüm eklentiler yükleniyor",
"load-failed": "\"{{pluginName}}\" eklentisi yüklenemedi", "load-failed": "\"{{pluginName}}\" eklentisi yüklenemedi",
"loaded": "\"{{pluginName}}\" eklentisi yüklendi" "loaded": "\"{{pluginName}}\" eklentisi yüklendi",
"unload-failed": "\"{{pluginName}}\" eklentisi kaldırılamadı.",
"unloaded": "\"{{pluginName}}\" eklentisi kaldırıldı"
} }
} }
}, },
@ -23,11 +25,33 @@
}, },
"i18n": { "i18n": {
"loaded": "i18n yüklendi" "loaded": "i18n yüklendi"
},
"second-instance": {
"receive-command": "Protokol üzerinden alınan komut: \"{{command}}\""
},
"theme": {
"css-file-not-found": "\"{{cssFile}}\" adlı CSS dosyası bulunamadı, yok sayılıyor"
},
"unresponsive": {
"details": "Yanıt verilmedi!\n{{error}}"
},
"when-ready": {
"clearing-cache-after-20s": "Uygulama ön belleği temizleme"
},
"window": {
"tried-to-render-offscreen": "Pencere ekranın dışında oluşturulmaya çalışıldı, windowSize={{windowSize}}, displaySize={{displaySize}}, position={{position}}"
} }
}, },
"dialog": { "dialog": {
"hide-menu-enabled": { "hide-menu-enabled": {
"message": "Gizli menüyü etkinleştir" "detail": "Menü gizli, göstermek için 'Alt' tuşunu kullanın (veya Uygulama İçi Menüyü kullanıyorsanız 'Escape' tuşunu kullanın)",
"message": "Menüyü gizle etkinleştirildi",
"title": "Menüyü gizle etkinleştirildi"
},
"need-to-restart": {
"buttons": {
"restart-now": "Şimdi yeniden başlat"
}
}, },
"update-available": { "update-available": {
"buttons": { "buttons": {

View File

@ -402,7 +402,63 @@
"name": "Експоненціальний обсяг" "name": "Експоненціальний обсяг"
}, },
"in-app-menu": { "in-app-menu": {
"description": "Надає меню-барам вишуканого, темного або кольору альбому вигляду" "description": "Надає меню-барам вишуканого, темного або кольору альбому вигляду",
"menu": {
"hide-dom-window-controls": "Сховати елементи керування вікном DOM"
},
"name": "Меню в програмі"
},
"last-fm": {
"description": "Додати підтримку прокрутки для Last.fm",
"name": "Last.fm"
},
"lumiastream": {
"description": "Додано підтримку для Lumia Stream",
"name": "Lumia Stream [бета-версія]"
},
"lyrics-genius": {
"description": "Додає підтримку текстів для більшості пісень",
"menu": {
"romanized-lyrics": "Романізована лірика"
}
},
"navigation": {
"name": "Навігація"
},
"no-google-login": {
"name": "Без входу в Google"
},
"notifications": {
"description": "Відображати сповіщення, коли пісня починає грати (інтерактивні сповіщення доступні в Windows)",
"menu": {
"interactive": "Інтерактивні сповіщення",
"interactive-settings": {
"label": "Інтерактивні налаштування",
"submenu": {
"hide-button-text": "Сховати текст кнопки"
}
}
},
"name": "Сповіщення"
},
"picture-in-picture": {
"description": "Дозволяє перемикати програму в режим «картинка в картинці»",
"menu": {
"always-on-top": "Завжди наверху",
"hotkey": {
"label": "Гаряча клавіша",
"prompt": {
"keybind-options": {
"hotkey": "Гаряча клавіша"
},
"label": "Оберіть гарячу клавішу для перемикання режиму зображення в зображенні",
"title": "Гаряча клавіша для режиму зображення в зображенні"
}
},
"save-window-position": "Зберегти положення вікна",
"save-window-size": "Зберегти розмір вікна"
},
"name": "Зображення в зображенні"
} }
} }
} }

View File

@ -53,9 +53,9 @@
"later": "稍後", "later": "稍後",
"restart-now": "立即重啟" "restart-now": "立即重啟"
}, },
"detail": "外掛「{{pluginName}}」需要程式重新啟動之後才會生效", "detail": "\"{{pluginName}}\" 外掛需要重啟應用之後才會生效",
"message": "{{pluginName}}」需要重新啟動", "message": "\"{{pluginName}}\" 需要重啟應用",
"title": "需要重新啟動" "title": "需要重啟應用"
}, },
"unresponsive": { "unresponsive": {
"buttons": { "buttons": {
@ -87,7 +87,7 @@
"go-back": "回到上一頁", "go-back": "回到上一頁",
"go-forward": "回到下一頁", "go-forward": "回到下一頁",
"quit": "退出", "quit": "退出",
"restart": "重新啟動應用程式" "restart": "重啟應用"
} }
}, },
"options": { "options": {
@ -131,12 +131,12 @@
"to-help-translate": "想要協助翻譯?按一下這裡" "to-help-translate": "想要協助翻譯?按一下這裡"
} }
}, },
"resume-on-start": "繼續上次關閉應用程式前的音樂", "resume-on-start": "應用啟動時繼續上次播放的歌曲",
"single-instance-lock": "禁止多開應用程式", "single-instance-lock": "單視窗鎖定",
"start-at-login": "開機時啟動", "start-at-login": "開機時啟動",
"starting-page": { "starting-page": {
"label": "啟動頁面", "label": "啟動頁面",
"unset": "未設定" "unset": "不指定"
}, },
"tray": { "tray": {
"label": "系統閘圖式", "label": "系統閘圖式",
@ -148,7 +148,7 @@
} }
}, },
"visual-tweaks": { "visual-tweaks": {
"label": "視覺設定", "label": "介面設定",
"submenu": { "submenu": {
"like-buttons": { "like-buttons": {
"default": "預設", "default": "預設",
@ -413,7 +413,7 @@
"name": "Last.fm" "name": "Last.fm"
}, },
"lumiastream": { "lumiastream": {
"description": "新增對Lumia Stream的支援", "description": "新增對 Lumia Stream 的支援",
"name": "Lumia Stream [Beta]" "name": "Lumia Stream [Beta]"
}, },
"lyrics-genius": { "lyrics-genius": {
@ -558,7 +558,7 @@
"name": "觸控列 (Touchbar) 支援" "name": "觸控列 (Touchbar) 支援"
}, },
"tuna-obs": { "tuna-obs": {
"description": "與OBS外掛Tuna連接", "description": "與 OBSTuna 外掛連接",
"name": "Tuna OBS" "name": "Tuna OBS"
}, },
"video-toggle": { "video-toggle": {

View File

@ -299,7 +299,7 @@ async function createMainWindow() {
const { x: windowX, y: windowY } = windowPosition; const { x: windowX, y: windowY } = windowPosition;
const winSize = win.getSize(); const winSize = win.getSize();
const display = screen.getDisplayNearestPoint(windowPosition); const display = screen.getDisplayNearestPoint(windowPosition);
const scaleFactor = display.scaleFactor; const scaleFactor = is.windows() ? 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);

View File

@ -9,6 +9,7 @@ import {
shell, shell,
} from 'electron'; } from 'electron';
import prompt from 'custom-electron-prompt'; import prompt from 'custom-electron-prompt';
import { satisfies } from 'semver';
import { allPlugins } from 'virtual:plugins'; import { allPlugins } from 'virtual:plugins';
@ -23,6 +24,8 @@ import promptOptions from './providers/prompt-options';
import { getAllMenuTemplate, loadAllMenuPlugins } from './loader/menu'; import { getAllMenuTemplate, loadAllMenuPlugins } from './loader/menu';
import { setLanguage, t } from '@/i18n'; import { setLanguage, t } from '@/i18n';
import packageJson from '../package.json';
export type MenuTemplate = Electron.MenuItemConstructorOptions[]; export type MenuTemplate = Electron.MenuItemConstructorOptions[];
// True only if in-app-menu was loaded on launch // True only if in-app-menu was loaded on launch
@ -31,10 +34,14 @@ const inAppMenuActive = config.plugins.isEnabled('in-app-menu');
const pluginEnabledMenu = ( const pluginEnabledMenu = (
plugin: string, plugin: string,
label = '', label = '',
description: string | undefined = undefined,
isNew = false,
hasSubmenu = false, hasSubmenu = false,
refreshMenu: (() => void) | undefined = undefined, refreshMenu: (() => void) | undefined = undefined,
): Electron.MenuItemConstructorOptions => ({ ): Electron.MenuItemConstructorOptions => ({
label: label || plugin, label: label || plugin,
sublabel: isNew ? t('main.menu.plugins.new') : undefined,
toolTip: description,
type: 'checkbox', type: 'checkbox',
checked: config.plugins.isEnabled(plugin), checked: config.plugins.isEnabled(plugin),
click(item: Electron.MenuItem) { click(item: Electron.MenuItem) {
@ -66,12 +73,15 @@ export const mainMenuTemplate = async (
const menuResult = Object.entries(getAllMenuTemplate()).map( const menuResult = Object.entries(getAllMenuTemplate()).map(
([id, template]) => { ([id, template]) => {
const pluginLabel = allPlugins[id]?.name?.() ?? id; const plugin = allPlugins[id];
const pluginLabel = plugin?.name?.() ?? id;
const pluginDescription = plugin?.description?.() ?? undefined;
const isNew = plugin?.addedVersion ? satisfies(packageJson.version, plugin.addedVersion) : false;
if (!config.plugins.isEnabled(id)) { if (!config.plugins.isEnabled(id)) {
return [ return [
id, id,
pluginEnabledMenu(id, pluginLabel, true, innerRefreshMenu), pluginEnabledMenu(id, pluginLabel, pluginDescription, isNew, true, innerRefreshMenu),
] as const; ] as const;
} }
@ -79,10 +89,14 @@ export const mainMenuTemplate = async (
id, id,
{ {
label: pluginLabel, label: pluginLabel,
sublabel: isNew ? t('main.menu.plugins.new') : undefined,
toolTip: pluginDescription,
submenu: [ submenu: [
pluginEnabledMenu( pluginEnabledMenu(
id, id,
t('main.menu.plugins.enabled'), t('main.menu.plugins.enabled'),
undefined,
false,
true, true,
innerRefreshMenu, innerRefreshMenu,
), ),
@ -106,9 +120,12 @@ export const mainMenuTemplate = async (
const predefinedTemplate = menuResult.find((it) => it[0] === id); const predefinedTemplate = menuResult.find((it) => it[0] === id);
if (predefinedTemplate) return predefinedTemplate[1]; if (predefinedTemplate) return predefinedTemplate[1];
const pluginLabel = allPlugins[id]?.name?.() ?? id; const plugin = allPlugins[id];
const pluginLabel = plugin?.name?.() ?? id;
const pluginDescription = plugin?.description?.() ?? undefined;
const isNew = plugin?.addedVersion ? satisfies(packageJson.version, plugin.addedVersion) : false;
return pluginEnabledMenu(id, pluginLabel, true, innerRefreshMenu); return pluginEnabledMenu(id, pluginLabel, pluginDescription, isNew, true, innerRefreshMenu);
}); });
const availableLanguages = Object.keys(languageResources); const availableLanguages = Object.keys(languageResources);

View File

@ -109,7 +109,7 @@ export default createPlugin({
}, },
}, },
preload: { preload: {
script: 'window.JSON = window._proxyJson; window._proxyJson = undefined; window.Response = window._proxyResponse; window._proxyResponse = undefined; 0', script: 'window.JSON.parse = window._proxyJsonParse; window._proxyJsonParse = undefined; window.Response.prototype.json = window._proxyResponseJson; window._proxyResponseJson = undefined; 0',
async start({ getConfig }) { async start({ getConfig }) {
const config = await getConfig(); const config = await getConfig();

View File

@ -32,37 +32,17 @@ export const inject = (contextBridge) => {
return o; return o;
}; };
contextBridge.exposeInMainWorld('_proxyJson', { contextBridge.exposeInMainWorld('_proxyJsonParse', new Proxy(JSON.parse, {
parse: new Proxy(JSON.parse, { apply() {
apply() { return pruner(Reflect.apply(...arguments));
return pruner(Reflect.apply(...arguments)); },
}, }));
}),
stringify: JSON.stringify,
[Symbol.toStringTag]: JSON[Symbol.toStringTag],
});
const withPrototype = (obj) => { contextBridge.exposeInMainWorld('_proxyResponseJson', new Proxy(Response.prototype.json, {
const protos = Object.getPrototypeOf(obj);
for (const [key, value] of Object.entries(protos)) {
if (Object.prototype.hasOwnProperty.call(obj, key)) continue;
if (typeof value === 'function') {
obj[key] = function (...args) {
return value.call(obj, ...args);
}
} else {
obj[key] = value;
}
}
return obj;
};
Response.prototype.json = new Proxy(Response.prototype.json, {
apply() { apply() {
return Reflect.apply(...arguments).then((o) => pruner(o)); return Reflect.apply(...arguments).then((o) => pruner(o));
}, },
}); }));
contextBridge.exposeInMainWorld('_proxyResponse', withPrototype(Response));
} }
(function () { (function () {

View File

@ -0,0 +1,179 @@
import { t } from '@/i18n';
import { createPlugin } from '@/utils';
import { ElementFromHtml } from '@/plugins/utils/renderer';
import undislikeHTML from './templates/undislike.html?raw';
import dislikeHTML from './templates/dislike.html?raw';
import likeHTML from './templates/like.html?raw';
import unlikeHTML from './templates/unlike.html?raw';
export default createPlugin({
name: () => t('plugins.album-actions.name'),
description: () => t('plugins.album-actions.description'),
restartNeeded: false,
addedVersion: '3.2.0',
config: {
enabled: false,
},
renderer: {
observer: null as MutationObserver | null,
loadObserver: null as MutationObserver | null,
changeObserver: null as MutationObserver | null,
waiting: false as boolean,
start() {
//Waits for pagechange
this.onPageChange();
this.observer = new MutationObserver(() => {
this.onPageChange();
});
this.observer.observe(document.querySelector('#browse-page'), {
attributes: false,
childList: true,
subtree: true,
});
},
onPageChange() {
if (this.waiting) {
return;
} else {
this.waiting = true;
}
this.waitForElem('#continuations').then((continuations: HTMLElement) => {
this.waiting = false;
//Gets the for buttons
let buttons: Array<HTMLElement> = [
ElementFromHtml(undislikeHTML),
ElementFromHtml(dislikeHTML),
ElementFromHtml(likeHTML),
ElementFromHtml(unlikeHTML),
];
//Finds the playlist
const playlist =
document.querySelector('ytmusic-shelf-renderer') ??
document.querySelector('ytmusic-playlist-shelf-renderer');
//Adds an observer for every button so it gets updated when one is clicked
this.changeObserver?.disconnect();
this.changeObserver = new MutationObserver(() => {
this.stop();
this.start();
});
const allButtons = playlist.querySelectorAll(
'yt-button-shape.ytmusic-like-button-renderer',
);
for (const btn of allButtons)
this.changeObserver.observe(btn, {
attributes: true,
childList: false,
subtree: false,
});
//Determine if button is needed and colors the percentage
const listsLength = playlist.querySelectorAll(
'#button-shape-dislike > button',
).length;
if (continuations.children.length == 0 && listsLength > 0) {
const counts = [
playlist?.querySelectorAll(
'#button-shape-dislike[aria-pressed=true] > button',
).length,
playlist?.querySelectorAll(
'#button-shape-dislike[aria-pressed=false] > button',
).length,
playlist?.querySelectorAll(
'#button-shape-like[aria-pressed=false] > button',
).length,
playlist?.querySelectorAll(
'#button-shape-like[aria-pressed=true] > button',
).length,
];
let i = 0;
for (const count of counts) {
if (count == 0) {
buttons.splice(i, 1);
i--;
} else {
buttons[i].children[0].children[0].style.setProperty(
'-webkit-mask-size',
`100% ${100 - (count / listsLength) * 100}%`,
);
}
i++;
}
}
const menu = document.querySelector('.detail-page-menu');
if (menu && !document.querySelector('.like-menu')) {
for (const button of buttons) {
menu.appendChild(button);
button.addEventListener('click', this.loadFullList);
}
}
});
},
loadFullList(event) {
event.stopPropagation();
const id: string = event.currentTarget.id,
loader = document.getElementById('continuations');
this.loadObserver = new MutationObserver(() => {
this.applyToList(id, loader);
});
this.applyToList(id, loader);
this.loadObserver.observe(loader, {
attributes: true,
childList: true,
subtree: true,
});
loader?.style.setProperty('top', '0');
loader?.style.setProperty('left', '50%');
loader?.style.setProperty('position', 'absolute');
},
applyToList(id: string, loader: HTMLElement) {
if (loader.children.length != 0) return;
this.loadObserver?.disconnect();
let playlistbuttons: NodeListOf<Element> | undefined;
const playlist = document.querySelector('ytmusic-shelf-renderer')
? document.querySelector('ytmusic-shelf-renderer')
: document.querySelector('ytmusic-playlist-shelf-renderer');
switch (id) {
case 'allundislike':
playlistbuttons = playlist?.querySelectorAll(
'#button-shape-dislike[aria-pressed=true] > button',
);
break;
case 'alldislike':
playlistbuttons = playlist?.querySelectorAll(
'#button-shape-dislike[aria-pressed=false] > button',
);
break;
case 'alllike':
playlistbuttons = playlist?.querySelectorAll(
'#button-shape-like[aria-pressed=false] > button',
);
break;
case 'allunlike':
playlistbuttons = playlist?.querySelectorAll(
'#button-shape-like[aria-pressed=true] > button',
);
break;
default:
}
playlistButtons?.forEach((elem) => elem.click());
},
stop() {
this.observer?.disconnect();
this.changeObserver?.disconnect();
for (const button of document.querySelectorAll('.like-menu')) {
button.remove();
}
},
waitForElem(selector: string) {
return new Promise((resolve) => {
const interval = setInterval(() => {
const elem = document.querySelector(selector);
if (!elem) return;
clearInterval(interval);
resolve(elem);
});
});
},
},
});

View File

@ -0,0 +1,74 @@
<button
id="alldislike"
data-type="dislike"
data-filled="false"
class="like-menu yt-spec-button-shape-next yt-spec-button-shape-next--text yt-spec-button-shape-next--mono yt-spec-button-shape-next--size-m yt-spec-button-shape-next--icon-button"
aria-pressed="false"
aria-label="Dislike all"
>
<div
class="yt-spec-button-shape-next__icon"
style="color: var(--ytmusic-setting-item-toggle-active)"
aria-hidden="true"
>
<div
class="yt-spec-button-shape-next__icon"
style="
color: white;
-webkit-mask: linear-gradient(grey, grey);
-webkit-mask-size: 100% 50%;
-webkit-mask-repeat: no-repeat;
z-index: 1;
position: absolute;
"
aria-hidden="true"
>
<div style="width: 24px; height: 24px">
<svg
viewBox="0 0 24 24"
preserveAspectRatio="xMidYMid meet"
focusable="false"
class="style-scope yt-icon"
style="
pointer-events: none;
display: block;
width: 100%;
height: 100%;
"
>
<g class="style-scope yt-icon">
<path
d="M18,4h3v10h-3V4z M5.23,14h4.23l-1.52,4.94C7.62,19.97,8.46,21,9.62,21c0.58,0,1.14-0.24,1.52-0.65L17,14V4H6.57 C5.5,4,4.59,4.67,4.38,5.61l-1.34,6C2.77,12.85,3.82,14,5.23,14z"
class="style-scope yt-icon"
></path>
</g>
</svg>
</div>
</div>
<div style="width: 24px; height: 24px">
<svg
viewBox="0 0 24 24"
preserveAspectRatio="xMidYMid meet"
focusable="false"
class="style-scope yt-icon"
style="pointer-events: none; display: block; width: 100%; height: 100%"
>
<g class="style-scope yt-icon">
<path
d="M18,4h3v10h-3V4z M5.23,14h4.23l-1.52,4.94C7.62,19.97,8.46,21,9.62,21c0.58,0,1.14-0.24,1.52-0.65L17,14V4H6.57 C5.5,4,4.59,4.67,4.38,5.61l-1.34,6C2.77,12.85,3.82,14,5.23,14z"
class="style-scope yt-icon"
></path>
</g>
</svg>
</div>
</div>
<yt-touch-feedback-shape style="border-radius: inherit">
<div
class="yt-spec-touch-feedback-shape yt-spec-touch-feedback-shape--touch-response"
aria-hidden="true"
>
<div class="yt-spec-touch-feedback-shape__stroke"></div>
<div class="yt-spec-touch-feedback-shape__fill"></div>
</div>
</yt-touch-feedback-shape>
</button>

View File

@ -0,0 +1,74 @@
<button
id="alllike"
data-type="like"
data-filled="false"
class="like-menu yt-spec-button-shape-next yt-spec-button-shape-next--text yt-spec-button-shape-next--mono yt-spec-button-shape-next--size-m yt-spec-button-shape-next--icon-button"
aria-pressed="false"
aria-label="Like all"
>
<div
class="yt-spec-button-shape-next__icon"
style="color: var(--ytmusic-setting-item-toggle-active)"
aria-hidden="true"
>
<div
class="yt-spec-button-shape-next__icon"
style="
color: white;
-webkit-mask: linear-gradient(grey, grey);
-webkit-mask-size: 100% 50%;
-webkit-mask-repeat: no-repeat;
z-index: 1;
position: absolute;
"
aria-hidden="true"
>
<div style="width: 24px; height: 24px">
<svg
viewBox="0 0 24 24"
preserveAspectRatio="xMidYMid meet"
focusable="false"
class="style-scope yt-icon"
style="
pointer-events: none;
display: block;
width: 100%;
height: 100%;
"
>
<g class="style-scope yt-icon">
<path
d="M3,11h3v10H3V11z M18.77,11h-4.23l1.52-4.94C16.38,5.03,15.54,4,14.38,4c-0.58,0-1.14,0.24-1.52,0.65L7,11v10h10.43 c1.06,0,1.98-0.67,2.19-1.61l1.34-6C21.23,12.15,20.18,11,18.77,11z"
class="style-scope yt-icon"
></path>
</g>
</svg>
</div>
</div>
<div style="width: 24px; height: 24px">
<svg
viewBox="0 0 24 24"
preserveAspectRatio="xMidYMid meet"
focusable="false"
class="style-scope yt-icon"
style="pointer-events: none; display: block; width: 100%; height: 100%"
>
<g class="style-scope yt-icon">
<path
d="M3,11h3v10H3V11z M18.77,11h-4.23l1.52-4.94C16.38,5.03,15.54,4,14.38,4c-0.58,0-1.14,0.24-1.52,0.65L7,11v10h10.43 c1.06,0,1.98-0.67,2.19-1.61l1.34-6C21.23,12.15,20.18,11,18.77,11z"
class="style-scope yt-icon"
></path>
</g>
</svg>
</div>
</div>
<yt-touch-feedback-shape style="border-radius: inherit">
<div
class="yt-spec-touch-feedback-shape yt-spec-touch-feedback-shape--touch-response"
aria-hidden="true"
>
<div class="yt-spec-touch-feedback-shape__stroke"></div>
<div class="yt-spec-touch-feedback-shape__fill"></div>
</div>
</yt-touch-feedback-shape>
</button>

View File

@ -0,0 +1,74 @@
<button
id="allundislike"
data-type="dislike"
data-filled="true"
class="like-menu yt-spec-button-shape-next yt-spec-button-shape-next--text yt-spec-button-shape-next--mono yt-spec-button-shape-next--size-m yt-spec-button-shape-next--icon-button"
aria-pressed="false"
aria-label="Undislike all"
>
<div
class="yt-spec-button-shape-next__icon"
style="color: var(--ytmusic-setting-item-toggle-active)"
aria-hidden="true"
>
<div
class="yt-spec-button-shape-next__icon"
style="
color: white;
-webkit-mask: linear-gradient(grey, grey);
-webkit-mask-size: 100% 50%;
-webkit-mask-repeat: no-repeat;
z-index: 1;
position: absolute;
"
aria-hidden="true"
>
<div style="width: 24px; height: 24px">
<svg
viewBox="0 0 24 24"
preserveAspectRatio="xMidYMid meet"
focusable="false"
class="style-scope yt-icon"
style="
pointer-events: none;
display: block;
width: 100%;
height: 100%;
"
>
<g class="style-scope yt-icon">
<path
d="M17,4h-1H6.57C5.5,4,4.59,4.67,4.38,5.61l-1.34,6C2.77,12.85,3.82,14,5.23,14h4.23l-1.52,4.94C7.62,19.97,8.46,21,9.62,21 c0.58,0,1.14-0.24,1.52-0.65L17,14h4V4H17z M10.4,19.67C10.21,19.88,9.92,20,9.62,20c-0.26,0-0.5-0.11-0.63-0.3 c-0.07-0.1-0.15-0.26-0.09-0.47l1.52-4.94l0.4-1.29H9.46H5.23c-0.41,0-0.8-0.17-1.03-0.46c-0.12-0.15-0.25-0.4-0.18-0.72l1.34-6 C5.46,5.35,5.97,5,6.57,5H16v8.61L10.4,19.67z M20,13h-3V5h3V13z"
class="style-scope yt-icon"
></path>
</g>
</svg>
</div>
</div>
<div style="width: 24px; height: 24px">
<svg
viewBox="0 0 24 24"
preserveAspectRatio="xMidYMid meet"
focusable="false"
class="style-scope yt-icon"
style="pointer-events: none; display: block; width: 100%; height: 100%"
>
<g class="style-scope yt-icon">
<path
d="M17,4h-1H6.57C5.5,4,4.59,4.67,4.38,5.61l-1.34,6C2.77,12.85,3.82,14,5.23,14h4.23l-1.52,4.94C7.62,19.97,8.46,21,9.62,21 c0.58,0,1.14-0.24,1.52-0.65L17,14h4V4H17z M10.4,19.67C10.21,19.88,9.92,20,9.62,20c-0.26,0-0.5-0.11-0.63-0.3 c-0.07-0.1-0.15-0.26-0.09-0.47l1.52-4.94l0.4-1.29H9.46H5.23c-0.41,0-0.8-0.17-1.03-0.46c-0.12-0.15-0.25-0.4-0.18-0.72l1.34-6 C5.46,5.35,5.97,5,6.57,5H16v8.61L10.4,19.67z M20,13h-3V5h3V13z"
class="style-scope yt-icon"
></path>
</g>
</svg>
</div>
</div>
<yt-touch-feedback-shape style="border-radius: inherit">
<div
class="yt-spec-touch-feedback-shape yt-spec-touch-feedback-shape--touch-response"
aria-hidden="true"
>
<div class="yt-spec-touch-feedback-shape__stroke"></div>
<div class="yt-spec-touch-feedback-shape__fill"></div>
</div>
</yt-touch-feedback-shape>
</button>

View File

@ -0,0 +1,74 @@
<button
id="allunlike"
data-type="like"
data-filled="true"
class="like-menu yt-spec-button-shape-next yt-spec-button-shape-next--text yt-spec-button-shape-next--mono yt-spec-button-shape-next--size-m yt-spec-button-shape-next--icon-button"
aria-pressed="false"
aria-label="Unlike all"
>
<div
class="yt-spec-button-shape-next__icon"
style="color: var(--ytmusic-setting-item-toggle-active)"
aria-hidden="true"
>
<div
class="yt-spec-button-shape-next__icon"
style="
color: white;
-webkit-mask: linear-gradient(grey, grey);
-webkit-mask-size: 100% 50%;
-webkit-mask-repeat: no-repeat;
z-index: 1;
position: absolute;
"
aria-hidden="true"
>
<div style="width: 24px; height: 24px">
<svg
viewBox="0 0 24 24"
preserveAspectRatio="xMidYMid meet"
focusable="false"
class="style-scope yt-icon"
style="
pointer-events: none;
display: block;
width: 100%;
height: 100%;
"
>
<g class="style-scope yt-icon">
<path
d="M18.77,11h-4.23l1.52-4.94C16.38,5.03,15.54,4,14.38,4c-0.58,0-1.14,0.24-1.52,0.65L7,11H3v10h4h1h9.43 c1.06,0,1.98-0.67,2.19-1.61l1.34-6C21.23,12.15,20.18,11,18.77,11z M7,20H4v-8h3V20z M19.98,13.17l-1.34,6 C18.54,19.65,18.03,20,17.43,20H8v-8.61l5.6-6.06C13.79,5.12,14.08,5,14.38,5c0.26,0,0.5,0.11,0.63,0.3 c0.07,0.1,0.15,0.26,0.09,0.47l-1.52,4.94L13.18,12h1.35h4.23c0.41,0,0.8,0.17,1.03,0.46C19.92,12.61,20.05,12.86,19.98,13.17z"
class="style-scope yt-icon"
></path>
</g>
</svg>
</div>
</div>
<div style="width: 24px; height: 24px">
<svg
viewBox="0 0 24 24"
preserveAspectRatio="xMidYMid meet"
focusable="false"
class="style-scope yt-icon"
style="pointer-events: none; display: block; width: 100%; height: 100%"
>
<g class="style-scope yt-icon">
<path
d="M18.77,11h-4.23l1.52-4.94C16.38,5.03,15.54,4,14.38,4c-0.58,0-1.14,0.24-1.52,0.65L7,11H3v10h4h1h9.43 c1.06,0,1.98-0.67,2.19-1.61l1.34-6C21.23,12.15,20.18,11,18.77,11z M7,20H4v-8h3V20z M19.98,13.17l-1.34,6 C18.54,19.65,18.03,20,17.43,20H8v-8.61l5.6-6.06C13.79,5.12,14.08,5,14.38,5c0.26,0,0.5,0.11,0.63,0.3 c0.07,0.1,0.15,0.26,0.09,0.47l-1.52,4.94L13.18,12h1.35h4.23c0.41,0,0.8,0.17,1.03,0.46C19.92,12.61,20.05,12.86,19.98,13.17z"
class="style-scope yt-icon"
></path>
</g>
</svg>
</div>
</div>
<yt-touch-feedback-shape style="border-radius: inherit">
<div
class="yt-spec-touch-feedback-shape yt-spec-touch-feedback-shape--touch-response"
aria-hidden="true"
>
<div class="yt-spec-touch-feedback-shape__stroke"></div>
<div class="yt-spec-touch-feedback-shape__fill"></div>
</div>
</yt-touch-feedback-shape>
</button>

View File

@ -1,11 +1,13 @@
import { FastAverageColor } from 'fast-average-color'; import { FastAverageColor } from 'fast-average-color';
import Color from 'color';
import style from './style.css?inline'; import style from './style.css?inline';
import { createPlugin } from '@/utils'; import { createPlugin } from '@/utils';
import { t } from '@/i18n'; import { t } from '@/i18n';
import type { VideoDataChanged } from '@/types/video-data-changed'; const COLOR_KEY = '--ytmusic-album-color';
const DARK_COLOR_KEY = '--ytmusic-album-color-dark';
export default createPlugin({ export default createPlugin({
name: () => t('plugins.album-color-theme.name'), name: () => t('plugins.album-color-theme.name'),
@ -16,69 +18,8 @@ export default createPlugin({
}, },
stylesheets: [style], stylesheets: [style],
renderer: { renderer: {
hexToHSL: (H: string) => { color: null as Color | null,
// Convert hex to RGB first darkColor: null as Color | null,
let r = 0;
let g = 0;
let b = 0;
if (H.length == 4) {
r = Number('0x' + H[1] + H[1]);
g = Number('0x' + H[2] + H[2]);
b = Number('0x' + H[3] + H[3]);
} else if (H.length == 7) {
r = Number('0x' + H[1] + H[2]);
g = Number('0x' + H[3] + H[4]);
b = Number('0x' + H[5] + H[6]);
}
// Then to HSL
r /= 255;
g /= 255;
b /= 255;
const cmin = Math.min(r, g, b);
const cmax = Math.max(r, g, b);
const delta = cmax - cmin;
let h: number;
let s: number;
let l: number;
if (delta == 0) {
h = 0;
} else if (cmax == r) {
h = ((g - b) / delta) % 6;
} else if (cmax == g) {
h = ((b - r) / delta) + 2;
} else {
h = ((r - g) / delta) + 4;
}
h = Math.round(h * 60);
if (h < 0) {
h += 360;
}
l = (cmax + cmin) / 2;
s = delta == 0 ? 0 : delta / (1 - Math.abs((2 * l) - 1));
s = +(s * 100).toFixed(1);
l = +(l * 100).toFixed(1);
//return "hsl(" + h + "," + s + "%," + l + "%)";
return [h, s, l];
},
hue: 0,
saturation: 0,
lightness: 0,
changeElementColor: (
element: HTMLElement | null,
hue: number,
saturation: number,
lightness: number,
) => {
if (element) {
element.style.backgroundColor = `hsl(${hue}, ${saturation}%, ${lightness}%)`;
}
},
playerPage: null as HTMLElement | null, playerPage: null as HTMLElement | null,
navBarBackground: null as HTMLElement | null, navBarBackground: null as HTMLElement | null,
@ -103,113 +44,66 @@ export default createPlugin({
'#mini-guide-background', '#mini-guide-background',
); );
this.ytmusicAppLayout = document.querySelector<HTMLElement>('#layout'); this.ytmusicAppLayout = document.querySelector<HTMLElement>('#layout');
const observer = new MutationObserver((mutationsList) => {
for (const mutation of mutationsList) {
if (mutation.type === 'attributes') {
const isPageOpen =
this.ytmusicAppLayout?.hasAttribute('player-page-open');
if (isPageOpen) {
this.changeElementColor(
this.sidebarSmall,
this.hue,
this.saturation,
this.lightness - 30,
);
} else {
if (this.sidebarSmall) {
this.sidebarSmall.style.backgroundColor = 'black';
}
}
}
}
});
if (this.playerPage) {
observer.observe(this.playerPage, { attributes: true });
}
}, },
onPlayerApiReady(playerApi) { onPlayerApiReady(playerApi) {
const fastAverageColor = new FastAverageColor(); const fastAverageColor = new FastAverageColor();
document.addEventListener( document.addEventListener('videodatachange', async (event) => {
'videodatachange', if (event.detail.name !== 'dataloaded') return;
(event: CustomEvent<VideoDataChanged>) => {
if (event.detail.name === 'dataloaded') { const playerResponse = playerApi.getPlayerResponse();
const playerResponse = playerApi.getPlayerResponse(); const thumbnail = playerResponse?.videoDetails?.thumbnail?.thumbnails?.at(0);
const thumbnail = if (!thumbnail) return;
playerResponse?.videoDetails?.thumbnail?.thumbnails?.at(0);
if (thumbnail) { const albumColor = await fastAverageColor.getColorAsync(thumbnail.url)
fastAverageColor .catch((err) => {
.getColorAsync(thumbnail.url) console.error(err);
.then((albumColor) => { return null;
if (albumColor) { });
const [hue, saturation, lightness] = ([
this.hue, if (albumColor) {
this.saturation, const target = Color(albumColor.hex);
this.lightness,
] = this.hexToHSL(albumColor.hex)); this.darkColor = target.darken(0.3).rgb();
this.changeElementColor( this.color = target.darken(0.15).rgb();
this.playerPage,
hue, while (this.color.luminosity() > 0.5) {
saturation, this.color = this.color?.darken(0.05);
lightness - 30, this.darkColor = this.darkColor?.darken(0.05);
);
this.changeElementColor(
this.navBarBackground,
hue,
saturation,
lightness - 15,
);
this.changeElementColor(
this.ytmusicPlayerBar,
hue,
saturation,
lightness - 15,
);
this.changeElementColor(
this.playerBarBackground,
hue,
saturation,
lightness - 15,
);
this.changeElementColor(
this.sidebarBig,
hue,
saturation,
lightness - 15,
);
if (
this.ytmusicAppLayout?.hasAttribute('player-page-open')
) {
this.changeElementColor(
this.sidebarSmall,
hue,
saturation,
lightness - 30,
);
}
const ytRightClickList =
document.querySelector<HTMLElement>(
'tp-yt-paper-listbox',
);
this.changeElementColor(
ytRightClickList,
hue,
saturation,
lightness - 15,
);
} else {
if (this.playerPage) {
this.playerPage.style.backgroundColor = '#000000';
}
}
})
.catch((e) => console.error(e));
}
} }
},
); document.documentElement.style.setProperty(COLOR_KEY, `${~~this.color.red()}, ${~~this.color.green()}, ${~~this.color.blue()}`);
document.documentElement.style.setProperty(DARK_COLOR_KEY, `${~~this.darkColor.red()}, ${~~this.darkColor.green()}, ${~~this.darkColor.blue()}`);
} else {
document.documentElement.style.setProperty(COLOR_KEY, '0, 0, 0');
document.documentElement.style.setProperty(DARK_COLOR_KEY, '0, 0, 0');
}
this.updateColor();
});
},
getColor(key: string, alpha = 1) {
return `rgba(var(${key}), ${alpha})`;
},
updateColor() {
const change = (element: HTMLElement | null, color: string) => {
if (element) {
element.style.backgroundColor = color;
}
};
change(this.playerPage, this.getColor(DARK_COLOR_KEY));
change(this.navBarBackground, this.getColor(COLOR_KEY));
change(this.ytmusicPlayerBar, this.getColor(COLOR_KEY));
change(this.playerBarBackground, this.getColor(COLOR_KEY));
change(this.sidebarBig, this.getColor(COLOR_KEY));
if (this.ytmusicAppLayout?.hasAttribute('player-page-open')) {
change(this.sidebarSmall, this.getColor(DARK_COLOR_KEY));
}
const ytRightClickList = document.querySelector<HTMLElement>('tp-yt-paper-listbox');
change(ytRightClickList, this.getColor(COLOR_KEY));
}, },
}, },
}); });

View File

@ -4,28 +4,24 @@ yt-page-navigation-progress {
} }
#player-page { #player-page {
transition: transition: transform 300ms,
transform 300ms, background-color 300ms cubic-bezier(0.2, 0, 0.6, 1) !important;
background-color 300ms cubic-bezier(0.2, 0, 0.6, 1) !important;
} }
#nav-bar-background { #nav-bar-background {
transition: transition: opacity 200ms,
opacity 200ms, background-color 300ms cubic-bezier(0.2, 0, 0.6, 1) !important;
background-color 300ms cubic-bezier(0.2, 0, 0.6, 1) !important;
} }
#mini-guide-background { #mini-guide-background {
transition: transition: opacity 200ms,
opacity 200ms, background-color 300ms cubic-bezier(0.2, 0, 0.6, 1) !important;
background-color 300ms cubic-bezier(0.2, 0, 0.6, 1) !important;
border-right: 0px !important; border-right: 0px !important;
} }
#guide-wrapper { #guide-wrapper {
transition: transition: opacity 200ms,
opacity 200ms, background-color 300ms cubic-bezier(0.2, 0, 0.6, 1) !important;
background-color 300ms cubic-bezier(0.2, 0, 0.6, 1) !important;
} }
#img, #img,
@ -37,3 +33,35 @@ yt-page-navigation-progress {
#items { #items {
border-radius: 10px !important; border-radius: 10px !important;
} }
/* fix blur navigation bar */
ytmusic-app-layout > [slot='player-page'] {
padding-top: 90px;
margin-top: calc(-90px + var(--menu-bar-height, 0px)) !important;
}
/* fix icon color */
.duration.ytmusic-player-queue-item, .byline.ytmusic-player-queue-item {
color: rgba(255, 255, 255, 0.5) !important;
--yt-endpoint-color: rgba(255, 255, 255, 0.5) !important;
--yt-endpoint-hover-color: rgba(255, 255, 255, 0.5) !important;
--yt-endpoint-visited-color: rgba(255, 255, 255, 0.5) !important;
}
.icon.ytmusic-menu-navigation-item-renderer {
color: rgba(255, 255, 255, 0.5) !important;
}
.menu.ytmusic-player-bar {
--iron-icon-fill-color: rgba(255, 255, 255, 0.5) !important;
}
ytmusic-player-bar {
color: rgba(255, 255, 255, 0.5) !important;
}
.time-info.ytmusic-player-bar {
color: rgba(255, 255, 255, 0.5) !important;
}
.volume-slider.ytmusic-player-bar, .expand-volume-slider.ytmusic-player-bar {
--paper-slider-container-color: rgba(255, 255, 255, 0.5) !important;
}

View File

@ -145,6 +145,73 @@ export default createPlugin({
observer: null as MutationObserver | null, observer: null as MutationObserver | null,
start() { start() {
const injectBlurImage = () => {
const songImage = document.querySelector<HTMLImageElement>(
'#song-image',
);
const image = document.querySelector<HTMLImageElement>(
'#song-image yt-img-shadow > img',
);
if (!songImage) return null;
if (!image) return null;
const blurImage = document.createElement('img');
blurImage.classList.add('html5-blur-image');
blurImage.src = image.src;
const applyImageAttribute = () => {
const rect = image.getBoundingClientRect();
const newWidth = Math.floor(image.width || rect.width);
const newHeight = Math.floor(image.height || rect.height);
if (newWidth === 0 || newHeight === 0) return;
if (this.isFullscreen) blurImage.classList.add('fullscreen');
else blurImage.classList.remove('fullscreen');
const leftOffset = (newWidth * (this.sizeRatio - 1)) / 2;
const topOffset = (newHeight * (this.sizeRatio - 1)) / 2;
blurImage.style.setProperty('--left', `${-1 * leftOffset}px`);
blurImage.style.setProperty('--top', `${-1 * topOffset}px`);
blurImage.style.setProperty('--width', `${newWidth * this.sizeRatio}px`);
blurImage.style.setProperty('--height', `${newHeight * this.sizeRatio}px`);
blurImage.style.setProperty('--blur', `${this.blur}px`);
blurImage.style.setProperty('--opacity', `${this.opacity}`);
};
this.update = applyImageAttribute;
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.type === 'attributes') {
applyImageAttribute();
}
});
});
const resizeObserver = new ResizeObserver(() => {
applyImageAttribute();
});
applyImageAttribute();
observer.observe(songImage, { attributes: true });
resizeObserver.observe(songImage);
window.addEventListener('resize', applyImageAttribute);
/* injecting */
songImage.prepend(blurImage);
/* cleanup */
return () => {
observer.disconnect();
resizeObserver.disconnect();
window.removeEventListener('resize', applyImageAttribute);
if (blurImage.isConnected) blurImage.remove();
};
};
const injectBlurVideo = (): (() => void) | null => { const injectBlurVideo = (): (() => void) | null => {
const songVideo = document.querySelector<HTMLDivElement>('#song-video'); const songVideo = document.querySelector<HTMLDivElement>('#song-video');
const video = document.querySelector<HTMLVideoElement>( const video = document.querySelector<HTMLVideoElement>(
@ -172,7 +239,6 @@ export default createPlugin({
cancelAnimationFrame(lastEffectWorkId); cancelAnimationFrame(lastEffectWorkId);
lastEffectWorkId = requestAnimationFrame(() => { lastEffectWorkId = requestAnimationFrame(() => {
// console.log('context', context);
if (!context) return; if (!context) return;
const width = this.qualityRatio; const width = this.qualityRatio;
@ -280,13 +346,20 @@ export default createPlugin({
}; };
}; };
const isVideoMode = () => {
const songVideo = document.querySelector<HTMLDivElement>('#song-video');
if (!songVideo) return false;
return getComputedStyle(songVideo).display !== 'none';
};
const playerPage = document.querySelector<HTMLElement>('#player-page'); const playerPage = document.querySelector<HTMLElement>('#player-page');
const ytmusicAppLayout = document.querySelector<HTMLElement>('#layout'); const ytmusicAppLayout = document.querySelector<HTMLElement>('#layout');
const isPageOpen = ytmusicAppLayout?.hasAttribute('player-page-open'); const isPageOpen = ytmusicAppLayout?.hasAttribute('player-page-open');
if (isPageOpen) { if (isPageOpen) {
this.unregister?.(); this.unregister?.();
this.unregister = injectBlurVideo() ?? null; this.unregister = (isVideoMode() ? injectBlurVideo() : injectBlurImage()) ?? null;
} }
const observer = new MutationObserver((mutationsList) => { const observer = new MutationObserver((mutationsList) => {
@ -296,7 +369,7 @@ export default createPlugin({
ytmusicAppLayout?.hasAttribute('player-page-open'); ytmusicAppLayout?.hasAttribute('player-page-open');
if (isPageOpen) { if (isPageOpen) {
this.unregister?.(); this.unregister?.();
this.unregister = injectBlurVideo() ?? null; this.unregister = (isVideoMode() ? injectBlurVideo() : injectBlurImage()) ?? null;
} else { } else {
this.unregister?.(); this.unregister?.();
this.unregister = null; this.unregister = null;

View File

@ -24,3 +24,17 @@
#song-video .html5-video-container > video { #song-video .html5-video-container > video {
top: 0 !important; top: 0 !important;
} }
#song-image .html5-blur-image {
position: absolute;
left: var(--left, 0px);
top: var(--top, 0px);
width: var(--width, 100%) !important;
height: var(--height, 100%) !important;
filter: blur(var(--blur, 100px));
opacity: var(--opacity, 1);
pointer-events: none;
}

View File

@ -33,7 +33,7 @@ const menuObserver = new MutationObserver(() => {
} }
const menuUrl = document.querySelector<HTMLAnchorElement>( const menuUrl = document.querySelector<HTMLAnchorElement>(
'tp-yt-paper-listbox [tabindex="-1"] #navigation-endpoint', 'tp-yt-paper-listbox [tabindex="0"] #navigation-endpoint',
)?.href; )?.href;
if (!menuUrl?.includes('watch?') && doneFirstLoad) { if (!menuUrl?.includes('watch?') && doneFirstLoad) {
return; return;
@ -42,11 +42,9 @@ const menuObserver = new MutationObserver(() => {
menu.prepend(downloadButton); menu.prepend(downloadButton);
progress = document.querySelector('#ytmcustom-download'); progress = document.querySelector('#ytmcustom-download');
if (doneFirstLoad) { if (!doneFirstLoad) {
return; setTimeout(() => (doneFirstLoad ||= true), 500);
} }
setTimeout(() => (doneFirstLoad ||= true), 500);
}); });
export const onRendererLoad = ({ export const onRendererLoad = ({
@ -56,7 +54,7 @@ export const onRendererLoad = ({
let videoUrl = getSongMenu() let videoUrl = getSongMenu()
// Selector of first button which is always "Start Radio" // Selector of first button which is always "Start Radio"
?.querySelector( ?.querySelector(
'ytmusic-menu-navigation-item-renderer[tabindex="-1"] #navigation-endpoint', 'ytmusic-menu-navigation-item-renderer[tabindex="0"] #navigation-endpoint',
) )
?.getAttribute('href'); ?.getAttribute('href');
if (videoUrl) { if (videoUrl) {

View File

@ -7,13 +7,14 @@ import type { MenuItem } from 'electron';
interface PanelOptions { interface PanelOptions {
placement?: 'bottom' | 'right'; placement?: 'bottom' | 'right';
order?: number; order?: number;
openOnHover?: boolean;
} }
export const createPanel = ( export const createPanel = (
parent: HTMLElement, parent: HTMLElement,
anchor: HTMLElement, anchor: HTMLElement,
items: MenuItem[], items: MenuItem[],
options: PanelOptions = { placement: 'bottom', order: 0 }, options: PanelOptions = { placement: 'bottom', order: 0, openOnHover: false },
) => { ) => {
const childPanels: HTMLElement[] = []; const childPanels: HTMLElement[] = [];
const panel = document.createElement('menu-panel'); const panel = document.createElement('menu-panel');
@ -51,6 +52,29 @@ export const createPanel = (
menu.appendChild(iconWrapper); menu.appendChild(iconWrapper);
menu.append(item.label); menu.append(item.label);
if (item.sublabel) {
menu.classList.add('badge');
const menuBadge = document.createElement('menu-item-badge');
menuBadge.append(item.sublabel);
menu.append(menuBadge);
}
if (item.toolTip) {
const menuTooltip = document.createElement('menu-item-tooltip');
menuTooltip.append(item.toolTip);
menu.addEventListener('mouseenter', () => {
const rect = menu.getBoundingClientRect();
menuTooltip.style.setProperty('max-width', `${rect.width - 8}px`);
menuTooltip.style.setProperty('--x', `${rect.left}px`);
menuTooltip.style.setProperty('--y', `${rect.top + rect.height}px`);
menuTooltip.classList.add('show');
});
menu.addEventListener('mouseleave', () => {
menuTooltip.classList.remove('show');
});
parent.append(menuTooltip);
}
menu.addEventListener('click', async () => { menu.addEventListener('click', async () => {
await window.ipcRenderer.invoke('menu-event', item.commandId); await window.ipcRenderer.invoke('menu-event', item.commandId);
const menuItem = (await window.ipcRenderer.invoke( const menuItem = (await window.ipcRenderer.invoke(
@ -93,11 +117,12 @@ export const createPanel = (
{ {
placement: 'right', placement: 'right',
order: (options?.order ?? 0) + 1, order: (options?.order ?? 0) + 1,
openOnHover: true,
}, },
); );
childPanels.push(child); childPanels.push(child);
children.push(...children); childPanels.push(...children);
} }
return panel.appendChild(menu); return panel.appendChild(menu);
@ -132,6 +157,46 @@ export const createPanel = (
} }
}; };
if (options.openOnHover) {
let timeout: number | null = null;
anchor.addEventListener('mouseenter', () => {
if (timeout) window.clearTimeout(timeout);
timeout = window.setTimeout(() => {
if (!isOpened()) open();
}, 225);
});
anchor.addEventListener('mouseleave', () => {
if (timeout) window.clearTimeout(timeout);
let mouseX = 0, mouseY = 0;
const onMouseMove = (event: MouseEvent) => {
mouseX = event.clientX;
mouseY = event.clientY;
};
document.addEventListener('mousemove', onMouseMove);
timeout = window.setTimeout(() => {
document.removeEventListener('mousemove', onMouseMove);
const now = document.elementFromPoint(mouseX, mouseY);
if (now === panel || panel.contains(now)) {
const onLeave = () => {
document.addEventListener('mousemove', onMouseMove);
timeout = window.setTimeout(() => {
document.removeEventListener('mousemove', onMouseMove);
const now = document.elementFromPoint(mouseX, mouseY);
if (now === panel || panel.contains(now) || childPanels.some((it) => it.contains(now))) return;
if (isOpened()) close();
panel.removeEventListener('mouseleave', onLeave);
}, 225);
};
panel.addEventListener('mouseleave', onLeave);
return;
}
if (isOpened()) close();
}, 225);
});
}
anchor.addEventListener('click', () => { anchor.addEventListener('click', () => {
if (isOpened()) close(); if (isOpened()) close();
else open(); else open();

View File

@ -22,8 +22,7 @@ title-bar {
color: #f1f1f1; color: #f1f1f1;
font-size: 12px; font-size: 12px;
padding: 4px 12px; padding: 4px 12px 4px var(--offset-left, 12px);
padding-left: var(--offset-left, 12px);
background-color: var(--titlebar-background-color, #030303); background-color: var(--titlebar-background-color, #030303);
user-select: none; user-select: none;
@ -97,6 +96,8 @@ menu-panel.position-by-bottom {
} }
menu-item { menu-item {
position: relative;
-webkit-app-region: none; -webkit-app-region: none;
min-height: 32px; min-height: 32px;
height: 32px; height: 32px;
@ -109,6 +110,9 @@ menu-item {
border-radius: 4px; border-radius: 4px;
cursor: pointer; cursor: pointer;
} }
menu-item.badge {
grid-template-columns: 32px 1fr auto minmax(32px, auto);
}
menu-item:hover { menu-item:hover {
background-color: rgba(255, 255, 255, 0.1); background-color: rgba(255, 255, 255, 0.1);
} }
@ -128,6 +132,56 @@ menu-separator {
background-color: rgba(255, 255, 255, 0.2); background-color: rgba(255, 255, 255, 0.2);
} }
menu-item-badge {
display: flex;
justify-content: center;
align-items: center;
min-width: 16px;
height: 16px;
padding: 0 4px;
margin-left: 8px;
border-radius: 4px;
background-color: rgba(255, 255, 255, 0.2);
color: #f1f1f1;
font-size: 10px;
font-weight: 500;
line-height: 1;
}
menu-item-tooltip {
position: fixed;
left: var(--x, 0);
top: var(--y, 0);
display: flex;
justify-content: center;
align-items: center;
min-width: 32px;
padding: 4px;
border-radius: 4px;
background-color: rgba(25, 25, 25, 0.8);
color: #f1f1f1;
font-size: 10px;
pointer-events: none;
z-index: 1000;
opacity: 0;
scale: 0.9;
transform-origin: 50% 0;
transition: opacity 0.225s ease-out, scale 0.225s ease-out;
}
menu-item-tooltip.show {
opacity: 1;
scale: 1.0;
}
/* classes */ /* classes */
.title-bar-icon { .title-bar-icon {

View File

@ -0,0 +1,149 @@
import { DataConnection, Peer } from 'peerjs';
import type { Permission, Profile, VideoData } from './types';
export type ConnectionEventMap = {
ADD_SONGS: { videoList: VideoData[], index?: number };
REMOVE_SONG: { index: number };
MOVE_SONG: { fromIndex: number; toIndex: number };
IDENTIFY: { profile: Profile } | undefined;
SYNC_PROFILE: { profiles: Record<string, Profile> } | undefined;
SYNC_QUEUE: { videoList: VideoData[] } | undefined;
SYNC_PROGRESS: { progress?: number; state?: number; index?: number; } | undefined;
PERMISSION: Permission | undefined;
};
export type ConnectionEventUnion = {
[Event in keyof ConnectionEventMap]: {
type: Event;
payload: ConnectionEventMap[Event];
after?: ConnectionEventUnion[];
};
}[keyof ConnectionEventMap];
type PromiseUtil<T> = {
promise: Promise<T>;
resolve: (id: T) => void;
reject: (err: unknown) => void;
}
export type ConnectionListener = (event: ConnectionEventUnion, conn: DataConnection) => void;
export type ConnectionMode = 'host' | 'guest' | 'disconnected';
export class Connection {
private peer: Peer;
private _mode: ConnectionMode = 'disconnected';
private connections: Record<string, DataConnection> = {};
private waitOpen: PromiseUtil<string> = {} as PromiseUtil<string>;
private listeners: ConnectionListener[] = [];
private connectionListeners: ((connection?: DataConnection) => void)[] = [];
constructor() {
this.peer = new Peer({ debug: 0 });
this.waitOpen.promise = new Promise<string>((resolve, reject) => {
this.waitOpen.resolve = resolve;
this.waitOpen.reject = reject;
});
this.peer.on('open', (id) => {
this._mode = 'host';
this.waitOpen.resolve(id);
});
this.peer.on('connection', (conn) => {
this._mode = 'host';
this.registerConnection(conn);
});
this.peer.on('error', (err) => {
this._mode = 'disconnected';
this.waitOpen.reject(err);
this.connectionListeners.forEach((listener) => listener());
console.log(err);
});
}
/* public */
async waitForReady() {
return this.waitOpen.promise;
}
async connect(id: string) {
this._mode = 'guest';
const conn = this.peer.connect(id);
await this.registerConnection(conn);
return conn;
}
async disconnect() {
if (this._mode === 'disconnected') throw new Error('Already disconnected');
this._mode = 'disconnected';
this.connections = {};
this.peer.destroy();
}
/* utils */
public get id() {
return this.peer.id;
}
public get mode() {
return this._mode;
}
public getConnections() {
return Object.values(this.connections);
}
public async broadcast<Event extends keyof ConnectionEventMap>(type: Event, payload: ConnectionEventMap[Event]) {
await Promise.all(
this.getConnections().map((conn) => conn.send({ type, payload }))
);
}
public on(listener: ConnectionListener) {
this.listeners.push(listener);
}
public onConnections(listener: (connections?: DataConnection) => void) {
this.connectionListeners.push(listener);
}
/* privates */
private async registerConnection(conn: DataConnection) {
return new Promise<DataConnection>((resolve, reject) => {
this.peer.once('error', (err) => {
this._mode = 'disconnected';
reject(err);
this.connectionListeners.forEach((listener) => listener());
});
conn.on('open', () => {
this.connections[conn.connectionId] = conn;
resolve(conn);
this.connectionListeners.forEach((listener) => listener(conn));
conn.on('data', (data) => {
if (!data || typeof data !== 'object' || !('type' in data) || !('payload' in data) || !data.type) {
console.warn('Music Together: Invalid data', data);
return;
}
for (const listener of this.listeners) {
listener(data as ConnectionEventUnion, conn);
}
});
});
const onClose = (err?: Error) => {
if (err) reject(err);
delete this.connections[conn.connectionId];
this.connectionListeners.forEach((listener) => listener(conn));
};
conn.on('error', onClose);
conn.on('close', onClose);
});
}
}

View File

@ -0,0 +1,138 @@
import { ElementFromHtml } from '@/plugins/utils/renderer';
import itemHTML from './templates/item.html?raw';
import popupHTML from './templates/popup.html?raw';
type Placement =
'top'
| 'bottom'
| 'right'
| 'left'
| 'center'
| 'middle'
| 'center-middle'
| 'top-left'
| 'top-right'
| 'bottom-left'
| 'bottom-right';
type PopupItem = (ItemRendererProps & { type: 'item'; })
| { type: 'divider'; }
| { type: 'custom'; element: HTMLElement; };
type PopupProps = {
data: PopupItem[];
anchorAt?: Placement;
popupAt?: Placement;
}
export const Popup = (props: PopupProps) => {
const popup = ElementFromHtml(popupHTML);
const container = popup.querySelector<HTMLElement>('.music-together-popup-container')!;
const items = props.data
.map((props) => {
if (props.type === 'item') return {
type: 'item' as const,
...ItemRenderer(props),
};
if (props.type === 'divider') return {
type: 'divider' as const,
element: ElementFromHtml('<div class="music-together-divider horizontal"></div>'),
};
if (props.type === 'custom') return {
type: 'custom' as const,
element: props.element,
};
return null;
})
.filter(Boolean);
container.append(...items.map(({ element }) => element));
popup.style.setProperty('opacity', '0');
popup.style.setProperty('pointer-events', 'none');
document.body.append(popup);
return {
element: popup,
container,
items,
show(x: number, y: number, anchor?: HTMLElement) {
let left = x;
let top = y;
if (anchor) {
if (props.anchorAt?.includes('right')) left += anchor.clientWidth;
if (props.anchorAt?.includes('bottom')) top += anchor.clientHeight;
if (props.anchorAt?.includes('center')) left += anchor.clientWidth / 2;
if (props.anchorAt?.includes('middle')) top += anchor.clientHeight / 2;
}
if (props.popupAt?.includes('right')) left -= popup.clientWidth;
if (props.popupAt?.includes('bottom')) top -= popup.clientHeight;
if (props.popupAt?.includes('center')) left -= popup.clientWidth / 2;
if (props.popupAt?.includes('middle')) top -= popup.clientHeight / 2;
popup.style.setProperty('left', `${left}px`);
popup.style.setProperty('top', `${top}px`);
popup.style.setProperty('opacity', '1');
popup.style.setProperty('pointer-events', 'unset');
setTimeout(() => {
const onClose = (event: MouseEvent) => {
const isPopupClick = event.composedPath().some((element) => element === popup);
if (!isPopupClick) {
this.dismiss();
document.removeEventListener('click', onClose);
}
};
document.addEventListener('click', onClose);
}, 16);
},
showAtAnchor(anchor: HTMLElement) {
const { x, y } = anchor.getBoundingClientRect();
this.show(x, y, anchor);
},
isShowing() {
return popup.style.getPropertyValue('opacity') === '1';
},
dismiss() {
popup.style.setProperty('opacity', '0');
popup.style.setProperty('pointer-events', 'none');
}
};
};
type ItemRendererProps = {
id?: string;
icon?: Element;
text: string;
onClick?: () => void;
};
export const ItemRenderer = (props: ItemRendererProps) => {
const element = ElementFromHtml(itemHTML);
const iconContainer = element.querySelector<HTMLElement>('div.icon')!;
const textContainer = element.querySelector<HTMLElement>('div.text')!;
if (props.icon) iconContainer.appendChild(props.icon);
textContainer.append(props.text);
if (props.onClick) {
element.addEventListener('click', () => {
props.onClick?.();
});
}
if (props.id) element.id = props.id;
return {
element,
setIcon(icon: Element) {
iconContainer.replaceChildren(icon);
},
setText(text: string) {
textContainer.replaceChildren(text);
},
id: props.id
};
};

View File

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24">
<path d="M480-640 280-440l56 56 104-103v407h80v-407l104 103 56-56-200-200ZM146-260q-32-49-49-105T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 59-17 115t-49 105l-58-58q22-37 33-78t11-84q0-134-93-227t-227-93q-134 0-227 93t-93 227q0 43 11 84t33 78l-58 58Z"/>
</svg>

After

Width:  |  Height:  |  Size: 408 B

View File

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24">
<path
d="M280-400q-33 0-56.5-23.5T200-480q0-33 23.5-56.5T280-560q33 0 56.5 23.5T360-480q0 33-23.5 56.5T280-400Zm0 160q-100 0-170-70T40-480q0-100 70-170t170-70q67 0 121.5 33t86.5 87h352l120 120-180 180-80-60-80 60-85-60h-47q-32 54-86.5 87T280-240Zm0-80q56 0 98.5-34t56.5-86h125l58 41 82-61 71 55 75-75-40-40H435q-14-52-56.5-86T280-640q-66 0-113 47t-47 113q0 66 47 113t113 47Z"/>
</svg>

After

Width:  |  Height:  |  Size: 480 B

View File

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24">
<path d="M560-160q-66 0-113-47t-47-113q0-66 47-113t113-47q23 0 42.5 5.5T640-458v-342h240v120H720v360q0 66-47 113t-113 47ZM80-320q0-99 38-186.5T221-659q65-65 152.5-103T560-800v80q-82 0-155 31.5t-127.5 86q-54.5 54.5-86 127T160-320H80Zm160 0q0-66 25.5-124.5t69-102Q378-590 436-615t124-25v80q-100 0-170 70t-70 170h-80Z"/>
</svg>

After

Width:  |  Height:  |  Size: 416 B

View File

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24">
<path
d="M792-56 686-160H260q-92 0-156-64T40-380q0-77 47.5-137T210-594q3-8 6-15.5t6-16.5L56-792l56-56 736 736-56 56ZM260-240h346L284-562q-2 11-3 21t-1 21h-20q-58 0-99 41t-41 99q0 58 41 99t99 41Zm185-161Zm419 191-58-56q17-14 25.5-32.5T840-340q0-42-29-71t-71-29h-60v-80q0-83-58.5-141.5T480-720q-27 0-52 6.5T380-693l-58-58q35-24 74.5-36.5T480-800q117 0 198.5 81.5T760-520q69 8 114.5 59.5T920-340q0 39-15 72.5T864-210ZM593-479Z"/>
</svg>

After

Width:  |  Height:  |  Size: 529 B

View File

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24">
<path d="M440-120v-240h80v80h320v80H520v80h-80Zm-320-80v-80h240v80H120Zm160-160v-80H120v-80h160v-80h80v240h-80Zm160-80v-80h400v80H440Zm160-160v-240h80v80h160v80H680v80h-80Zm-480-80v-80h400v80H120Z"/>
</svg>

After

Width:  |  Height:  |  Size: 298 B

View File

@ -0,0 +1,681 @@
import prompt from 'custom-electron-prompt';
import { t } from '@/i18n';
import { createPlugin } from '@/utils';
import promptOptions from '@/providers/prompt-options';
import { AppAPI, getDefaultProfile, Permission, Profile, VideoData } from './types';
import { Queue } from './queue';
import { Connection, ConnectionEventUnion } from './connection';
import { createHostPopup } from './ui/host';
import { createGuestPopup } from './ui/guest';
import { createSettingPopup } from './ui/setting';
import settingHTML from './templates/setting.html?raw';
import style from './style.css?inline';
import type { YoutubePlayer } from '@/types/youtube-player';
import type { RendererContext } from '@/types/contexts';
import type { VideoDataChanged } from '@/types/video-data-changed';
import { DataConnection } from 'peerjs';
type RawAccountData = {
accountName: {
runs: { text: string }[];
};
accountPhoto: {
thumbnails: { url: string; width: number; height: number; }[];
};
settingsEndpoint: unknown;
manageAccountTitle: unknown;
trackingParams: string;
channelHandle: {
runs: { text: string }[];
};
};
export default createPlugin({
name: () => t('plugins.music-together.name'),
description: () => t('plugins.music-together.description'),
restartNeeded: false,
addedVersion: '3.2.0',
config: {
enabled: false
},
stylesheets: [style],
backend: {
async start({ ipc }) {
ipc.handle('music-together:prompt', async (title: string, label: string) => prompt({
title,
label,
type: 'input',
...promptOptions()
}));
}
},
renderer: {
connection: null as Connection | null,
ipc: null as RendererContext<never>['ipc'] | null,
api: null as (HTMLElement & AppAPI) | null,
queue: null as Queue | null,
playerApi: null as YoutubePlayer | null,
showPrompt: (async () => null) as ((title: string, label: string) => Promise<string | null>),
elements: {} as {
setting: HTMLElement;
icon: SVGElement;
spinner: HTMLElement;
},
popups: {} as {
host: ReturnType<typeof createHostPopup>;
guest: ReturnType<typeof createGuestPopup>;
setting: ReturnType<typeof createSettingPopup>;
},
stateInterval: null as number | null,
updateNext: false,
ignoreChange: false,
rollbackInjector: null as (() => void) | null,
me: null as Omit<Profile, 'id'> | null,
profiles: {} as Record<string, Profile>,
permission: 'playlist' as Permission,
/* events */
videoChangeListener(event: CustomEvent<VideoDataChanged>) {
if (event.detail.name === 'dataloaded' || this.updateNext) {
if (this.connection?.mode === 'host') {
const videoList: VideoData[] = this.queue?.flatItems.map((it: any) => ({
videoId: it.videoId,
ownerId: this.connection!.id
} satisfies VideoData)) ?? [];
this.queue?.setVideoList(videoList, false);
this.queue?.syncQueueOwner();
this.connection.broadcast('SYNC_QUEUE', {
videoList
});
this.updateNext = event.detail.name === 'dataloaded';
}
}
},
videoStateChangeListener() {
if (this.connection?.mode !== 'guest') return;
if (this.ignoreChange) return;
if (this.permission !== 'all') return;
const state = this.playerApi?.getPlayerState();
if (state !== 1 && state !== 2) return;
this.connection.broadcast('SYNC_PROGRESS', {
// progress: this.playerApi?.getCurrentTime(),
state: this.playerApi?.getPlayerState()
// index: this.queue?.selectedIndex ?? 0,
});
},
/* connection */
async onHost() {
this.connection = new Connection();
const wait = await this.connection.waitForReady().catch(() => null);
if (!wait) return false;
if (!this.me) this.me = getDefaultProfile(this.connection.id);
const rawItems = this.queue?.flatItems?.map((it: any) => ({
videoId: it.videoId,
ownerId: this.connection!.id
} satisfies VideoData)) ?? [];
this.queue?.setOwner({
id: this.connection.id,
...this.me
});
this.queue?.setVideoList(rawItems, false);
this.queue?.syncQueueOwner();
this.queue?.initQueue();
this.queue?.injection();
this.profiles = {};
this.connection.onConnections((connection) => {
if (!connection) {
this.api?.openToast(t('plugins.music-together.toast.disconnected'));
this.onStop();
return;
}
if (!connection.open) {
this.api?.openToast(t('plugins.music-together.toast.user-disconnected', {
name: this.profiles[connection.peer]?.name
}));
this.putProfile(connection.peer, undefined);
}
});
this.putProfile(this.connection.id, {
id: this.connection.id,
...this.me
});
const listener = async (event: ConnectionEventUnion, conn?: DataConnection) => {
this.ignoreChange = true;
switch (event.type) {
case 'ADD_SONGS': {
if (conn && this.permission === 'host-only') return;
await this.queue?.addVideos(event.payload.videoList, event.payload.index);
await this.connection?.broadcast('ADD_SONGS', event.payload);
break;
}
case 'REMOVE_SONG': {
if (conn && this.permission === 'host-only') return;
await this.queue?.removeVideo(event.payload.index);
await this.connection?.broadcast('REMOVE_SONG', event.payload);
break;
}
case 'MOVE_SONG': {
if (conn && this.permission === 'host-only') {
await this.connection?.broadcast('SYNC_QUEUE', {
videoList: this.queue?.videoList ?? []
});
break;
}
this.queue?.moveItem(event.payload.fromIndex, event.payload.toIndex);
await this.connection?.broadcast('MOVE_SONG', event.payload);
break;
}
case 'IDENTIFY': {
if (!event.payload || !conn) {
console.warn('Music Together [Host]: Received "IDENTIFY" event without payload or connection');
break;
}
this.api?.openToast(t('plugins.music-together.toast.user-connected', { name: event.payload.profile.name }));
this.putProfile(conn.peer, event.payload.profile);
break;
}
case 'SYNC_PROFILE': {
await this.connection?.broadcast('SYNC_PROFILE', { profiles: this.profiles });
break;
}
case 'PERMISSION': {
await this.connection?.broadcast('PERMISSION', this.permission);
this.popups.guest.setPermission(this.permission);
this.popups.host.setPermission(this.permission);
this.popups.setting.setPermission(this.permission);
break;
}
case 'SYNC_QUEUE': {
await this.connection?.broadcast('SYNC_QUEUE', {
videoList: this.queue?.videoList ?? []
});
break;
}
case 'SYNC_PROGRESS': {
let permissionLevel = 0;
if (this.permission === 'all') permissionLevel = 2;
if (this.permission === 'playlist') permissionLevel = 1;
if (this.permission === 'host-only') permissionLevel = 0;
if (!conn) permissionLevel = 3;
if (permissionLevel >= 2) {
if (typeof event.payload?.progress === 'number') {
const currentTime = this.playerApi?.getCurrentTime() ?? 0;
if (Math.abs(event.payload.progress - currentTime) > 3) this.playerApi?.seekTo(event.payload.progress);
}
if (this.playerApi?.getPlayerState() !== event.payload?.state) {
if (event.payload?.state === 2) this.playerApi?.pauseVideo();
if (event.payload?.state === 1) this.playerApi?.playVideo();
}
}
if (permissionLevel >= 1) {
if (typeof event.payload?.index === 'number') {
const nowIndex = this.queue?.selectedIndex ?? 0;
if (nowIndex !== event.payload.index) {
this.queue?.setIndex(event.payload.index);
}
}
}
break;
}
default: {
console.warn('Music Together [Host]: Unknown Event', event);
break;
}
}
if (event.after) {
const now = event.after.shift();
if (now) {
now.after = event.after;
await listener(now, conn);
}
}
};
this.connection.on(listener);
this.queue?.on(listener);
setTimeout(() => {
this.ignoreChange = false;
}, 16); // wait 1 frame
return true;
},
async onJoin() {
this.connection = new Connection();
const wait = await this.connection.waitForReady().catch(() => null);
if (!wait) return false;
this.profiles = {};
const id = await this.showPrompt(t('plugins.music-together.name'), t('plugins.music-together.dialog.enter-host'));
if (typeof id !== 'string') return false;
const connection = await this.connection.connect(id).catch(() => false);
if (!connection) return false;
this.connection.onConnections((connection) => {
if (!connection?.open) {
this.api?.openToast(t('plugins.music-together.toast.disconnected'));
this.onStop();
}
});
let resolveIgnore: number | null = null;
const listener = async (event: ConnectionEventUnion) => {
this.ignoreChange = true;
switch (event.type) {
case 'ADD_SONGS': {
await this.queue?.addVideos(event.payload.videoList, event.payload.index);
break;
}
case 'REMOVE_SONG': {
await this.queue?.removeVideo(event.payload.index);
break;
}
case 'MOVE_SONG': {
await this.queue?.moveItem(event.payload.fromIndex, event.payload.toIndex);
break;
}
case 'IDENTIFY': {
console.warn('Music Together [Guest]: Received "IDENTIFY" event from guest');
break;
}
case 'SYNC_QUEUE': {
if (Array.isArray(event.payload?.videoList)) {
await this.queue?.setVideoList(event.payload.videoList);
}
break;
}
case 'SYNC_PROFILE': {
if (!event.payload) {
console.warn('Music Together [Guest]: Received "SYNC_PROFILE" event without payload');
break;
}
Object.entries(event.payload.profiles).forEach(([id, profile]) => {
this.putProfile(id, profile);
});
break;
}
case 'SYNC_PROGRESS': {
if (typeof event.payload?.progress === 'number') {
const currentTime = this.playerApi?.getCurrentTime() ?? 0;
if (Math.abs(event.payload.progress - currentTime) > 3) this.playerApi?.seekTo(event.payload.progress);
}
if (this.playerApi?.getPlayerState() !== event.payload?.state) {
if (event.payload?.state === 2) this.playerApi?.pauseVideo();
if (event.payload?.state === 1) this.playerApi?.playVideo();
}
if (typeof event.payload?.index === 'number') {
const nowIndex = this.queue?.selectedIndex ?? 0;
if (nowIndex !== event.payload.index) {
this.queue?.setIndex(event.payload.index);
}
}
break;
}
case 'PERMISSION': {
if (!event.payload) {
console.warn('Music Together [Guest]: Received "PERMISSION" event without payload');
break;
}
this.permission = event.payload;
this.popups.guest.setPermission(this.permission);
this.popups.host.setPermission(this.permission);
this.popups.setting.setPermission(this.permission);
const permissionLabel = t(`plugins.music-together.menu.permission.${this.permission}`);
this.api?.openToast(t('plugins.music-together.toast.permission-changed', { permission: permissionLabel }));
break;
}
default: {
console.warn('Music Together [Guest]: Unknown Event', event);
break;
}
}
if (typeof resolveIgnore === 'number') clearTimeout(resolveIgnore);
resolveIgnore = window.setTimeout(() => {
this.ignoreChange = false;
}, 16); // wait 1 frame
};
this.connection.on(listener);
this.queue?.on(async (event: ConnectionEventUnion) => {
this.ignoreChange = true;
switch (event.type) {
case 'ADD_SONGS': {
await this.connection?.broadcast('ADD_SONGS', event.payload);
await this.connection?.broadcast('SYNC_QUEUE', undefined);
break;
}
case 'REMOVE_SONG': {
await this.connection?.broadcast('REMOVE_SONG', event.payload);
break;
}
case 'MOVE_SONG': {
await this.connection?.broadcast('MOVE_SONG', event.payload);
await this.connection?.broadcast('SYNC_QUEUE', undefined);
break;
}
case 'SYNC_PROGRESS': {
if (this.permission === 'host-only') await this.connection?.broadcast('SYNC_QUEUE', undefined);
else await this.connection?.broadcast('SYNC_PROGRESS', event.payload);
break;
}
}
if (typeof resolveIgnore === 'number') clearTimeout(resolveIgnore);
resolveIgnore = window.setTimeout(() => {
this.ignoreChange = false;
}, 16); // wait 1 frame
});
if (!this.me) this.me = getDefaultProfile(this.connection.id);
this.queue?.injection();
this.queue?.setOwner({
id: this.connection.id,
...this.me
});
const progress = Array.from(document.querySelectorAll<HTMLElement & {
_update: (...args: unknown[]) => void
}>('tp-yt-paper-progress'));
const rollbackList = progress.map((progress) => {
const original = progress._update;
progress._update = (...args) => {
const now = args[0];
if (this.permission === 'all' && typeof now === 'number') {
const currentTime = this.playerApi?.getCurrentTime() ?? 0;
if (Math.abs(now - currentTime) > 3) this.connection?.broadcast('SYNC_PROGRESS', {
progress: now,
state: this.playerApi?.getPlayerState()
});
}
original.call(progress, ...args);
};
return () => {
progress._update = original;
};
});
this.rollbackInjector = () => {
rollbackList.forEach((rollback) => rollback());
};
this.connection.broadcast('IDENTIFY', {
profile: {
id: this.connection.id,
handleId: this.me.handleId,
name: this.me.name,
thumbnail: this.me.thumbnail
}
});
this.connection.broadcast('SYNC_PROFILE', undefined);
this.connection.broadcast('PERMISSION', undefined);
this.queue?.clear();
this.queue?.syncQueueOwner();
this.queue?.initQueue();
this.connection.broadcast('SYNC_QUEUE', undefined);
return true;
},
onStop() {
this.connection?.disconnect();
this.queue?.rollbackInjection();
this.queue?.removeQueueOwner();
if (this.rollbackInjector) {
this.rollbackInjector();
this.rollbackInjector = null;
}
this.profiles = {};
this.popups.host.setUsers(Object.values(this.profiles));
this.popups.guest.setUsers(Object.values(this.profiles));
this.popups.host.dismiss();
this.popups.guest.dismiss();
this.popups.setting.dismiss();
},
/* methods */
putProfile(id: string, profile?: Profile) {
if (profile === undefined) {
delete this.profiles[id];
} else {
this.profiles[id] = profile;
}
this.popups.host.setUsers(Object.values(this.profiles));
this.popups.guest.setUsers(Object.values(this.profiles));
},
showSpinner() {
this.elements.icon.style.setProperty('display', 'none');
this.elements.spinner.removeAttribute('hidden');
this.elements.spinner.setAttribute('active', '');
},
hideSpinner() {
this.elements.icon.style.removeProperty('display');
this.elements.spinner.removeAttribute('active');
this.elements.spinner.setAttribute('hidden', '');
},
initMyProfile() {
const accountButton = document.querySelector<HTMLElement & {
onButtonTap: () => void
}>('ytmusic-settings-button');
accountButton?.onButtonTap();
setTimeout(() => {
accountButton?.onButtonTap();
const renderer = document.querySelector<HTMLElement & { data: unknown }>('ytd-active-account-header-renderer');
if (!accountButton || !renderer) {
console.warn('Music Together: Cannot find account');
this.me = getDefaultProfile(this.connection?.id ?? '');
return;
}
const accountData = renderer.data as RawAccountData;
this.me = {
handleId: accountData.channelHandle.runs[0].text,
name: accountData.accountName.runs[0].text,
thumbnail: accountData.accountPhoto.thumbnails[0].url
};
if (this.me.thumbnail) {
this.popups.host.setProfile(this.me.thumbnail);
this.popups.guest.setProfile(this.me.thumbnail);
this.popups.setting.setProfile(this.me.thumbnail);
}
}, 0);
},
/* hooks */
start({ ipc }) {
this.ipc = ipc;
this.showPrompt = async (title: string, label: string) => ipc.invoke('music-together:prompt', title, label);
this.api = document.querySelector<HTMLElement & AppAPI>('ytmusic-app');
/* setup */
document.querySelector('#right-content > ytmusic-settings-button')?.insertAdjacentHTML('beforebegin', settingHTML);
const setting = document.querySelector<HTMLElement>('#music-together-setting-button');
const icon = document.querySelector<SVGElement>('#music-together-setting-button > svg');
const spinner = document.querySelector<HTMLElement>('#music-together-setting-button > tp-yt-paper-spinner-lite');
if (!setting || !icon || !spinner) {
console.warn('Music Together: Cannot inject html');
console.log(setting, icon, spinner);
return;
}
this.elements = {
setting,
icon,
spinner
};
this.stateInterval = window.setInterval(() => {
if (this.connection?.mode !== 'host') return;
const index = this.queue?.selectedIndex ?? 0;
this.connection.broadcast('SYNC_PROGRESS', {
progress: this.playerApi?.getCurrentTime(),
state: this.playerApi?.getPlayerState(),
index
});
}, 1000);
/* UI */
const hostPopup = createHostPopup({
onItemClick: (id) => {
if (id === 'music-together-close') {
this.onStop();
this.api?.openToast(t('plugins.music-together.toast.closed'));
hostPopup.dismiss();
}
if (id === 'music-together-copy-id') {
navigator.clipboard.writeText(this.connection?.id ?? '');
this.api?.openToast(t('plugins.music-together.toast.id-copied'));
hostPopup.dismiss();
}
if (id === 'music-together-permission') {
if (this.permission === 'all') this.permission = 'host-only';
else if (this.permission === 'playlist') this.permission = 'all';
else if (this.permission === 'host-only') this.permission = 'playlist';
this.connection?.broadcast('PERMISSION', this.permission);
hostPopup.setPermission(this.permission);
guestPopup.setPermission(this.permission);
settingPopup.setPermission(this.permission);
const permissionLabel = t(`plugins.music-together.menu.permission.${this.permission}`);
this.api?.openToast(t('plugins.music-together.toast.permission-changed', { permission: permissionLabel }));
const item = hostPopup.items.find((it) => it?.element.id === id);
if (item?.type === 'item') {
item.setText(t('plugins.music-together.menu.set-permission'));
}
}
}
});
const guestPopup = createGuestPopup({
onItemClick: (id) => {
if (id === 'music-together-disconnect') {
this.onStop();
this.api?.openToast(t('plugins.music-together.toast.disconnected'));
guestPopup.dismiss();
}
}
});
const settingPopup = createSettingPopup({
onItemClick: async (id) => {
if (id === 'music-together-host') {
settingPopup.dismiss();
this.showSpinner();
const result = await this.onHost();
this.hideSpinner();
if (result) {
navigator.clipboard.writeText(this.connection?.id ?? '');
this.api?.openToast(t('plugins.music-together.toast.id-copied'));
hostPopup.showAtAnchor(setting);
} else {
this.api?.openToast(t('plugins.music-together.toast.host-failed'));
}
}
if (id === 'music-together-join') {
settingPopup.dismiss();
this.showSpinner();
const result = await this.onJoin();
this.hideSpinner();
if (result) {
this.api?.openToast(t('plugins.music-together.toast.joined'));
guestPopup.showAtAnchor(setting);
} else {
this.api?.openToast(t('plugins.music-together.toast.join-failed'));
}
}
}
});
this.popups = {
host: hostPopup,
guest: guestPopup,
setting: settingPopup
};
setting.addEventListener('click', async () => {
let popup = settingPopup;
if (this.connection?.mode === 'host') popup = hostPopup;
if (this.connection?.mode === 'guest') popup = guestPopup;
if (popup.isShowing()) popup.dismiss();
else popup.showAtAnchor(setting);
});
/* account data getter */
this.initMyProfile();
},
onPlayerApiReady(playerApi) {
this.queue = new Queue({
owner: {
id: this.connection?.id ?? '',
...this.me!
},
getProfile: (id) => this.profiles[id]
});
this.playerApi = playerApi;
this.playerApi.addEventListener('onStateChange', this.videoStateChangeListener);
document.addEventListener('videodatachange', this.videoChangeListener);
},
stop() {
const dividers = Array.from(document.querySelectorAll('.music-together-divider'));
dividers.forEach((divider) => divider.remove());
this.elements.setting?.remove();
this.onStop();
if (typeof this.stateInterval === 'number') clearInterval(this.stateInterval);
if (this.playerApi) this.playerApi.removeEventListener('onStateChange', this.videoStateChangeListener);
if (this.videoChangeListener) document.removeEventListener('videodatachange', this.videoChangeListener);
}
}
});

View File

@ -0,0 +1,40 @@
import { SHA1Hash } from './sha1hash';
export const extractToken = (cookie = document.cookie) => cookie.match(/SAPISID=([^;]+);/)?.[1] ?? cookie.match(/__Secure\-3PAPISID=([^;]+);/)?.[1];
export const getHash = (papisid: string, millis = Date.now(), origin: string = 'https://music.youtube.com') => {
const hash = SHA1Hash();
hash.update(`${millis} ${papisid} ${origin}`);
return hash.digestString().toLowerCase();
};
export const getAuthorizationHeader = (papisid: string, millis = Date.now(), origin: string = 'https://music.youtube.com') => {
return `SAPISIDHASH ${millis}_${getHash(papisid, millis, origin)}`;
};
export const getClient = () => {
return {
hl: navigator.language.split('-')[0] ?? 'en',
gl: navigator.language.split('-')[1] ?? 'US',
deviceMake: '',
deviceModel: '',
userAgent: navigator.userAgent,
clientName: 'WEB_REMIX',
clientVersion: '1.20231208.05.02',
osName: '',
osVersion: '',
platform: 'DESKTOP',
timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
locationInfo: {
locationPermissionAuthorizationStatus: 'LOCATION_PERMISSION_AUTHORIZATION_STATUS_UNSUPPORTED',
},
musicAppInfo: {
pwaInstallabilityStatus: 'PWA_INSTALLABILITY_STATUS_UNKNOWN',
webDisplayMode: 'WEB_DISPLAY_MODE_BROWSER',
storeDigitalGoodsApiSupportStatus: {
playStoreDigitalGoodsApiSupportStatus: 'DIGITAL_GOODS_API_SUPPORT_STATUS_UNSUPPORTED',
},
},
utcOffsetMinutes: -1 * (new Date()).getTimezoneOffset(),
};
};

View File

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

View File

@ -0,0 +1,429 @@
import { getMusicQueueRenderer } from './song';
import { mapQueueItem } from './utils';
import type { Profile, QueueAPI, VideoData } from '../types';
import { ConnectionEventUnion } from '@/plugins/music-together/connection';
import { t } from '@/i18n';
const getHeaderPayload = (() => {
let payload: unknown = null;
return () => {
if (!payload) {
payload = {
title: {
runs: [
{
text: t('plugins.music-together.internal.track-source')
}
]
},
subtitle: {
runs: [
{
text: t('plugins.music-together.name')
}
]
},
buttons: [
{
chipCloudChipRenderer: {
style: {
styleType: 'STYLE_TRANSPARENT'
},
text: {
runs: [
{
text: t('plugins.music-together.internal.save')
}
]
},
navigationEndpoint: {
saveQueueToPlaylistCommand: {}
},
icon: {
iconType: 'ADD_TO_PLAYLIST'
},
accessibilityData: {
accessibilityData: {
label: t('plugins.music-together.internal.save')
}
},
isSelected: false,
uniqueId: t('plugins.music-together.internal.save')
}
}
]
};
}
return payload;
}
})();
export type QueueOptions = {
videoList?: VideoData[];
owner?: Profile;
queue?: HTMLElement & QueueAPI;
getProfile: (id: string) => Profile | undefined;
}
export type QueueEventListener = (event: ConnectionEventUnion) => void;
export class Queue {
private queue: (HTMLElement & QueueAPI) | null = null;
private originalDispatch: ((obj: {
type: string;
payload?: unknown;
}) => void) | null = null;
private internalDispatch = false;
private ignoreFlag = false;
private listeners: QueueEventListener[] = [];
private owner: Profile | null = null;
private getProfile: (id: string) => Profile | undefined;
constructor(options: QueueOptions) {
this.getProfile = options.getProfile;
this.queue = options.queue ?? document.querySelector<HTMLElement & QueueAPI>('#queue');
this.owner = options.owner ?? null;
this._videoList = options.videoList ?? [];
}
private _videoList: VideoData[] = [];
/* utils */
get videoList() {
return this._videoList;
}
get selectedIndex() {
return mapQueueItem((it) => it?.selected, this.queue?.store.getState().queue.items).findIndex(Boolean) ?? 0;
}
get rawItems() {
return this.queue?.store.getState().queue.items;
}
get flatItems() {
return mapQueueItem((it) => it, this.rawItems);
}
setOwner(owner: Profile) {
this.owner = owner;
}
/* public */
async setVideoList(videoList: VideoData[], sync = true) {
this._videoList = videoList;
if (sync) await this.syncVideo();
}
async addVideos(videos: VideoData[], index?: number) {
const response = await getMusicQueueRenderer(videos.map((it) => it.videoId));
if (!response) return false;
const items = response.queueDatas.map((it) => it?.content).filter(Boolean);
if (!items) return false;
this.internalDispatch = true;
this._videoList.push(...videos);
this.queue?.dispatch({
type: 'ADD_ITEMS',
payload: {
nextQueueItemId: this.queue.store.getState().queue.nextQueueItemId,
index: index ?? this.queue.store.getState().queue.items.length ?? 0,
items,
shuffleEnabled: false,
shouldAssignIds: true
}
});
this.internalDispatch = false;
setTimeout(() => {
this.initQueue();
this.syncQueueOwner();
}, 0);
return true;
}
async removeVideo(index: number) {
this.internalDispatch = true;
this._videoList.splice(index, 1);
this.queue?.dispatch({
type: 'REMOVE_ITEM',
payload: index
});
this.internalDispatch = false;
setTimeout(() => {
this.initQueue();
this.syncQueueOwner();
}, 0);
}
setIndex(index: number) {
this.internalDispatch = true;
this.queue?.dispatch({
type: 'SET_INDEX',
payload: index
});
this.internalDispatch = false;
}
moveItem(fromIndex: number, toIndex: number) {
this.internalDispatch = true;
const data = this._videoList.splice(fromIndex, 1)[0];
this._videoList.splice(toIndex, 0, data);
this.queue?.dispatch({
type: 'MOVE_ITEM',
payload: {
fromIndex,
toIndex
}
});
this.internalDispatch = false;
setTimeout(() => {
this.initQueue();
this.syncQueueOwner();
}, 0);
}
clear() {
this.internalDispatch = true;
this._videoList = [];
this.queue?.dispatch({
type: 'CLEAR'
});
this.internalDispatch = false;
}
on(listener: QueueEventListener) {
this.listeners.push(listener);
}
off(listener: QueueEventListener) {
this.listeners = this.listeners.filter((it) => it !== listener);
}
rollbackInjection() {
if (!this.queue) {
console.error('Queue is not initialized!');
return;
}
if (this.originalDispatch) this.queue.store.dispatch = this.originalDispatch;
}
injection() {
if (!this.queue) {
console.error('Queue is not initialized!');
return;
}
this.originalDispatch = this.queue.store.dispatch;
this.queue.store.dispatch = (event) => {
if (!this.queue || !this.owner) {
console.error('Queue is not initialized!');
return;
}
if (!this.internalDispatch) {
if (event.type === 'CLEAR') {
this.ignoreFlag = true;
}
if (event.type === 'ADD_ITEMS') {
if (this.ignoreFlag) {
this.ignoreFlag = false;
const videoList = mapQueueItem((it: any) => ({
videoId: it.videoId,
ownerId: this.owner!.id
} satisfies VideoData), (event.payload as any).items);
const index = this._videoList.length + videoList.length - 1;
if (videoList.length > 0) {
this.broadcast({ // play
type: 'ADD_SONGS',
payload: {
videoList
},
after: [
{
type: 'SYNC_PROGRESS',
payload: {
index
}
}
]
});
}
} else if ((event.payload as any).items.length === 1) {
this.broadcast({ // add playlist
type: 'ADD_SONGS',
payload: {
// index: (event.payload as any).index,
videoList: mapQueueItem((it: any) => ({
videoId: it.videoId,
ownerId: this.owner!.id
} satisfies VideoData), (event.payload as any).items)
}
});
}
return;
}
if (event.type === 'MOVE_ITEM') {
this.broadcast({
type: 'MOVE_SONG',
payload: {
fromIndex: (event.payload as any).fromIndex,
toIndex: (event.payload as any).toIndex
}
});
return;
}
if (event.type === 'REMOVE_ITEM') {
this.broadcast({
type: 'REMOVE_SONG',
payload: {
index: event.payload as number
}
});
return;
}
if (event.type === 'SET_INDEX') {
this.broadcast({
type: 'SYNC_PROGRESS',
payload: {
index: event.payload as number
}
});
return;
}
if (event.type === 'SET_HEADER') event.payload = getHeaderPayload();
if (event.type === 'ADD_STEERING_CHIPS') {
event.type = 'CLEAR_STEERING_CHIPS';
event.payload = undefined;
}
if (event.type === 'SET_PLAYER_UI_STATE') {
if (event.payload === 'INACTIVE' && this.videoList.length > 0) {
return;
}
}
if (event.type === 'HAS_SHOWN_AUTOPLAY') return;
if (event.type === 'ADD_AUTOMIX_ITEMS') return;
}
const fakeContext = {
...this.queue,
store: {
...this.queue.store,
dispatch: this.originalDispatch
}
};
this.originalDispatch!.call(fakeContext, event);
};
}
/* sync */
async initQueue() {
if (!this.queue) return;
this.internalDispatch = true;
this.queue.dispatch({
type: 'HAS_SHOWN_AUTOPLAY',
payload: false
});
this.queue.dispatch({
type: 'SET_HEADER',
payload: getHeaderPayload(),
});
this.queue.dispatch({
type: 'CLEAR_STEERING_CHIPS'
});
this.internalDispatch = false;
}
async syncVideo() {
const response = await getMusicQueueRenderer(this._videoList.map((it) => it.videoId));
if (!response) return false;
const items = response.queueDatas.map((it) => it.content);
this.internalDispatch = true;
this.queue?.dispatch({
type: 'UPDATE_ITEMS',
payload: {
items: items,
nextQueueItemId: this.queue.store.getState().queue.nextQueueItemId,
shouldAssignIds: true,
currentIndex: -1
}
});
this.internalDispatch = false;
setTimeout(() => {
this.initQueue();
this.syncQueueOwner();
}, 0);
return true;
}
async syncQueueOwner() {
const allQueue = document.querySelectorAll('#queue');
allQueue.forEach((queue) => {
const list = Array.from(queue?.querySelectorAll<HTMLElement>('ytmusic-player-queue-item') ?? []);
list.forEach((item, index) => {
if (typeof index !== 'number') return;
const id = this._videoList[index]?.ownerId;
const data = this.getProfile(id);
const profile = item.querySelector<HTMLImageElement>('.music-together-owner') ?? document.createElement('img');
profile.classList.add('music-together-owner');
profile.dataset.id = id;
profile.dataset.index = index.toString();
const name = item.querySelector<HTMLElement>('.music-together-name') ?? document.createElement('div');
name.classList.add('music-together-name');
name.textContent = data?.name ?? t('plugins.music-together.internal.unknown-user');
if (data) {
profile.dataset.thumbnail = data.thumbnail ?? '';
profile.dataset.name = data.name ?? '';
profile.dataset.handleId = data.handleId ?? '';
profile.dataset.id = data.id ?? '';
profile.src = data.thumbnail ?? '';
profile.title = data.name ?? '';
profile.alt = data.handleId ?? '';
}
if (!profile.isConnected) item.append(profile);
if (!name.isConnected) item.append(name);
});
});
}
removeQueueOwner() {
const allQueue = document.querySelectorAll('#queue');
allQueue.forEach((queue) => {
const list = Array.from(queue?.querySelectorAll<HTMLElement>('ytmusic-player-queue-item') ?? []);
list.forEach((item) => {
const profile = item.querySelector<HTMLImageElement>('.music-together-owner');
const name = item.querySelector<HTMLElement>('.music-together-name');
profile?.remove();
name?.remove();
});
});
}
/* private */
private broadcast(event: ConnectionEventUnion) {
this.listeners.forEach((listener) => listener(event));
}
}

View File

@ -0,0 +1,117 @@
export function SHA1Hash(): {
reset: () => void,
update: (message: string | number[], length?: number) => void,
digest: () => number[],
digestString: () => string
} {
let hash: number[];
function initialize(): void {
hash = [1732584193, 4023233417, 2562383102, 271733878, 3285377520];
totalLength = currentLength = 0;
}
function processBlock(block: number[]): void {
let words: number[] = [];
for (let i = 0; i < 64; i += 4) {
words[i / 4] = block[i] << 24 | block[i + 1] << 16 | block[i + 2] << 8 | block[i + 3];
}
for (let i = 16; i < 80; i++) {
let temp = words[i - 3] ^ words[i - 8] ^ words[i - 14] ^ words[i - 16];
words[i] = (temp << 1 | temp >>> 31) & 4294967295;
}
let a = hash[0],
b = hash[1],
c = hash[2],
d = hash[3],
e = hash[4];
for (let i = 0; i < 80; i++) {
let f, k;
if (i < 20) {
f = d ^ b & (c ^ d);
k = 1518500249;
} else if (i < 40) {
f = b ^ c ^ d;
k = 1859775393;
} else if (i < 60) {
f = b & c | d & (b | c);
k = 2400959708;
} else {
f = b ^ c ^ d;
k = 3395469782;
}
let temp = ((a << 5 | a >>> 27) & 4294967295) + f + e + k + words[i] & 4294967295;
e = d;
d = c;
c = (b << 30 | b >>> 2) & 4294967295;
b = a;
a = temp;
}
hash[0] = hash[0] + a & 4294967295;
hash[1] = hash[1] + b & 4294967295;
hash[2] = hash[2] + c & 4294967295;
hash[3] = hash[3] + d & 4294967295;
hash[4] = hash[4] + e & 4294967295;
}
function update(message: string | number[], length?: number): void {
if ('string' === typeof message) {
message = unescape(encodeURIComponent(message));
let bytes: number[] = [];
for (let i = 0, len = message.length; i < len; ++i)
bytes.push(message.charCodeAt(i));
message = bytes;
}
length || (length = message.length);
let i = 0;
if (0 == currentLength)
for (; i + 64 < length;)
processBlock(message.slice(i, i + 64)),
i += 64,
totalLength += 64;
for (; i < length;)
if (buffer[currentLength++] = message[i++],
totalLength++,
64 == currentLength)
for (currentLength = 0,
processBlock(buffer); i + 64 < length;)
processBlock(message.slice(i, i + 64)),
i += 64,
totalLength += 64;
}
function finalize(): number[] {
let result: number[] = []
, bits = 8 * totalLength;
if (currentLength < 56)
update(padding, 56 - currentLength);
else
update(padding, 64 - (currentLength - 56));
for (let i = 63; i >= 56; i--)
buffer[i] = bits & 255,
bits >>>= 8;
processBlock(buffer);
for (let i = 0; i < 5; i++)
for (let j = 24; j >= 0; j -= 8)
result.push(hash[i] >> j & 255);
return result;
}
let buffer: number[] = [], padding: number[] = [128], totalLength: number, currentLength: number;
for (let i = 1; i < 64; ++i)
padding[i] = 0;
initialize();
return {
reset: initialize,
update: update,
digest: finalize,
digestString: function(): string {
let hash = finalize(), hex = '';
for (let i = 0; i < hash.length; i++)
hex += '0123456789ABCDEF'.charAt(Math.floor(hash[i] / 16)) + '0123456789ABCDEF'.charAt(hash[i] % 16);
return hex;
}
};
}

View File

@ -0,0 +1,48 @@
import { extractToken, getAuthorizationHeader, getClient } from './client';
type QueueRendererResponse = {
queueDatas: {
content: unknown;
}[];
responseContext: unknown;
trackingParams: string;
};
export const getMusicQueueRenderer = async (videoIds: string[]): Promise<QueueRendererResponse | null> => {
const token = extractToken();
if (!token) return null;
const response = await fetch(
'https://music.youtube.com/youtubei/v1/music/get_queue?key=AIzaSyC9XL3ZjWddXya6X74dJoCTL-WEYFDNX30&prettyPrint=false',
{
method: 'POST',
credentials: 'include',
body: JSON.stringify({
context: {
client: getClient(),
request: {
useSsl: true,
internalExperimentFlags: [],
consistencyTokenJars: [],
},
user: {
lockedSafetyMode: false,
},
},
videoIds,
}),
headers: {
'Content-Type': 'application/json',
Origin: 'https://music.youtube.com',
Authorization: getAuthorizationHeader(token)
}
}
);
const text = await response.text();
try {
return JSON.parse(text) as QueueRendererResponse;
} catch {}
return null;
};

View File

@ -0,0 +1,15 @@
export const mapQueueItem = <T>(map: (item: any | null) => T, array: any[]): T[] => array
.map((item) => {
if ('playlistPanelVideoWrapperRenderer' in item) {
const keys = Object.keys(item.playlistPanelVideoWrapperRenderer.primaryRenderer);
return item.playlistPanelVideoWrapperRenderer.primaryRenderer[keys[0]];
}
if ('playlistPanelVideoRenderer' in item) {
return item.playlistPanelVideoRenderer;
}
console.error('Music Together: Unknown item', item);
return null;
})
.map(map);

View File

@ -0,0 +1,160 @@
.music-together-button {
display: inline-flex;
cursor: pointer;
margin-left: 8px;
& svg {
width: 24px;
height: 24px;
fill: rgba(255, 255, 255, .5);
}
&:hover svg:hover {
fill: #fff;
}
}
#right-content > .music-together-divider {
width: 1px;
height: 26px;
margin-left: 16px;
margin-right: 8px;
}
.music-together-divider {
background-color: rgba(255, 255, 255, .15);
}
.music-together-divider.horizontal {
width: 100%;
height: 1px;
}
.music-together-divider.vertical {
width: 1px;
height: 100%;
}
.music-together-tool {
position: absolute;
display: flex;
align-items: center;
gap: 8px;
opacity: 0;
translate: 50%;
pointer-events: none;
transition: all 0.225s ease-out;
&.open {
position: unset;
opacity: 1;
translate: 0;
pointer-events: all;
}
}
.music-together-spinner {
}
.music-together-popup {
position: fixed;
z-index: 1000;
}
.music-together-popup-container {
border-radius: 10px !important;
}
.music-together-item {
display: flex;
height: 48px;
align-items: center;
padding: 0 8px;
--iron-icon-fill-color: #fff;
&:not([is-disabled]) {
cursor: pointer;
}
&:hover {
background-color: var(--ytmusic-menu-item-hover-background-color, rgba(255,255,255,0.05));
}
}
.music-together-status {
display: flex;
flex-direction: column;
align-items: stretch;
padding: 16px;
}
.music-together-profile {
width: 24px;
height: 24px;
border-radius: 50%;
overflow: hidden;
flex-shrink: 0;
}
.music-together-profile.big {
width: 32px;
height: 32px;
}
.music-together-status-container {
flex: 1;
display: flex;
flex-direction: row;
align-items: flex-start;
gap: 16px;
}
.music-together-status-item {
display: inline-flex;
flex-direction: column;
align-items: flex-start;
gap: 4px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
font-size: 14px;
font-weight: 400;
}
.music-together-user-container {
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
gap: 8px;
padding-top: 16px;
font-size: 14px;
}
.music-together-empty {
width: 100%;
font-size: 14px;
color: rgba(255, 255, 255, .5);
text-align: center;
}
.music-together-owner {
width: 24px;
height: 24px;
flex-shrink: 0;
border-radius: 50%;
margin-left: 8px;
}
.music-together-name {
display: none;
color: #fff;
font-size: 14px;
margin-left: 8px;
}
ytmusic-player-queue-item:hover .music-together-name {
display: unset;
}

View File

@ -0,0 +1,8 @@
<div class="style-scope music-together-item">
<div class="icon style-scope ytmusic-menu-service-item-renderer">
<!-- icon -->
</div>
<div class="text style-scope ytmusic-menu-service-item-renderer">
<!-- text -->
</div>
</div>

View File

@ -0,0 +1,5 @@
<div class="music-together-popup">
<tp-yt-paper-listbox class="style-scope ytmusic-menu-popup-renderer music-together-popup-container">
</tp-yt-paper-listbox>
</div>

View File

@ -0,0 +1,7 @@
<div id="music-together-setting-button" class="music-together-button style-scope ytmusic-nav-bar">
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24">
<path d="M0-240v-63q0-43 44-70t116-27q13 0 25 .5t23 2.5q-14 21-21 44t-7 48v65H0Zm240 0v-65q0-32 17.5-58.5T307-410q32-20 76.5-30t96.5-10q53 0 97.5 10t76.5 30q32 20 49 46.5t17 58.5v65H240Zm540 0v-65q0-26-6.5-49T754-397q11-2 22.5-2.5t23.5-.5q72 0 116 26.5t44 70.5v63H780Zm-455-80h311q-10-20-55.5-35T480-370q-55 0-100.5 15T325-320ZM160-440q-33 0-56.5-23.5T80-520q0-34 23.5-57t56.5-23q34 0 57 23t23 57q0 33-23 56.5T160-440Zm640 0q-33 0-56.5-23.5T720-520q0-34 23.5-57t56.5-23q34 0 57 23t23 57q0 33-23 56.5T800-440Zm-320-40q-50 0-85-35t-35-85q0-51 35-85.5t85-34.5q51 0 85.5 34.5T600-600q0 50-34.5 85T480-480Zm0-80q17 0 28.5-11.5T520-600q0-17-11.5-28.5T480-640q-17 0-28.5 11.5T440-600q0 17 11.5 28.5T480-560Zm1 240Zm-1-280Z"/>
</svg>
<tp-yt-paper-spinner-lite id="music-together-host-spinner" hidden class="loading-indicator style-scope music-together-spinner"></tp-yt-paper-spinner-lite>
</div>
<div class="music-together-divider"></div>

View File

@ -0,0 +1,23 @@
<div class="music-together-status">
<div class="music-together-status-container">
<img class="music-together-profile big">
<div class="music-together-status-item">
<ytmd-trans key="plugins.music-together.name"></ytmd-trans>
<span id="music-together-status-label">
<ytmd-trans key="plugins.music-together.menu.status.disconnected"></ytmd-trans>
</span>
<span id="music-together-permission-label">
<ytmd-trans key="plugins.music-together.menu.permission.playlist" style="color: rgba(255, 255, 255, 0.75)"></ytmd-trans>
</span>
</div>
</div>
<div class="music-together-divider horizontal" style="margin: 16px 0;"></div>
<div class="music-together-status-item">
<ytmd-trans key="plugins.music-together.menu.connected-users"></ytmd-trans>
</div>
<div class="music-together-user-container">
<span class="music-together-empty">
<ytmd-trans key="plugins.music-together.menu.empty-user"></ytmd-trans>
</span>
</div>
</div>

View File

@ -0,0 +1,54 @@
import { YoutubePlayer } from '@/types/youtube-player';
type StoreState = any;
type Store = {
dispatch: (obj: {
type: string;
payload?: unknown;
}) => void;
getState: () => StoreState;
replaceReducer: (param1: unknown) => unknown;
subscribe: (callback: () => void) => unknown;
};
export type QueueAPI = {
dispatch(obj: {
type: string;
payload?: unknown;
}): void;
getItems(): unknown[];
store: Store;
continuation?: string;
autoPlaying?: boolean;
};
export type AppAPI = {
queue_: QueueAPI;
playerApi_: YoutubePlayer;
openToast: (message: string) => void;
// TODO: Add more
};
export type Profile = {
id: string;
handleId: string;
name: string;
thumbnail: string;
};
export type VideoData = {
videoId: string;
ownerId: string;
};
export type Permission = 'host-only' | 'playlist' | 'all';
export const getDefaultProfile = (connectionID: string, id: string = Date.now().toString()): Profile => {
const name = `Guest ${id.slice(0, 4)}`;
return {
id: connectionID,
handleId: `#music-together:${id}`,
name,
thumbnail: `https://ui-avatars.com/api/?name=${name}&background=random`
};
};

View File

@ -0,0 +1,43 @@
import { ElementFromHtml } from '@/plugins/utils/renderer';
import { t } from '@/i18n';
import { Popup } from '../element';
import { createStatus } from '../ui/status';
import IconOff from '../icons/off.svg?raw';
export type GuestPopupProps = {
onItemClick: (id: string) => void;
};
export const createGuestPopup = (props: GuestPopupProps) => {
const status = createStatus();
status.setStatus('guest');
const result = Popup({
data: [
{
type: 'custom',
element: status.element,
},
{
type: 'divider',
},
{
type: 'item',
id: 'music-together-disconnect',
icon: ElementFromHtml(IconOff),
text: t('plugins.music-together.menu.disconnect'),
onClick: () => props.onItemClick('music-together-disconnect'),
},
],
anchorAt: 'bottom-right',
popupAt: 'top-right'
});
return {
...status,
...result,
};
};

View File

@ -0,0 +1,60 @@
import { t } from '@/i18n';
import { ElementFromHtml } from '@/plugins/utils/renderer';
import { Popup } from '../element';
import { createStatus } from '../ui/status';
import IconKey from '../icons/key.svg?raw';
import IconOff from '../icons/off.svg?raw';
import IconTune from '../icons/tune.svg?raw';
export type HostPopupProps = {
onItemClick: (id: string) => void;
};
export const createHostPopup = (props: HostPopupProps) => {
const status = createStatus();
status.setStatus('host');
const result = Popup({
data: [
{
type: 'custom',
element: status.element,
},
{
type: 'divider'
},
{
id: 'music-together-copy-id',
type: 'item',
icon: ElementFromHtml(IconKey),
text: t('plugins.music-together.menu.click-to-copy-id'),
onClick: () => props.onItemClick('music-together-copy-id'),
},
{
id: 'music-together-permission',
type: 'item',
icon: ElementFromHtml(IconTune),
text: t('plugins.music-together.menu.set-permission', { permission: t('plugins.music-together.menu.permission.host-only') }),
onClick: () => props.onItemClick('music-together-permission'),
},
{
type: 'divider',
},
{
type: 'item',
id: 'music-together-close',
icon: ElementFromHtml(IconOff),
text: t('plugins.music-together.menu.close'),
onClick: () => props.onItemClick('music-together-close'),
},
],
anchorAt: 'bottom-right',
popupAt: 'top-right',
});
return {
...status,
...result,
};
};

View File

@ -0,0 +1,49 @@
import { Popup } from '@/plugins/music-together/element';
import { ElementFromHtml } from '@/plugins/utils/renderer';
import { createStatus } from './status';
import { t } from '@/i18n';
import IconMusicCast from '../icons/music-cast.svg?raw';
import IconConnect from '../icons/connect.svg?raw';
export type SettingPopupProps = {
onItemClick: (id: string) => void;
};
export const createSettingPopup = (props: SettingPopupProps) => {
const status = createStatus();
status.setStatus('disconnected');
const result = Popup({
data: [
{
type: 'custom',
element: status.element,
},
{
type: 'divider',
},
{
id: 'music-together-host',
type: 'item',
icon: ElementFromHtml(IconMusicCast),
text: t('plugins.music-together.menu.host'),
onClick: () => props.onItemClick('music-together-host'),
},
{
type: 'item',
icon: ElementFromHtml(IconConnect),
text: t('plugins.music-together.menu.join'),
onClick: () => props.onItemClick('music-together-join'),
},
],
anchorAt: 'bottom-right',
popupAt: 'top-right'
});
return {
...status,
...result,
};
};

View File

@ -0,0 +1,82 @@
import { ElementFromHtml } from '@/plugins/utils/renderer';
import statusHTML from '../templates/status.html?raw';
import { t } from '@/i18n';
import type { Permission, Profile } from '../types';
export const createStatus = () => {
const element = ElementFromHtml(statusHTML);
const icon = document.querySelector<HTMLImageElement>('ytmusic-settings-button > tp-yt-paper-icon-button > tp-yt-iron-icon#icon img');
const profile = element.querySelector<HTMLImageElement>('.music-together-profile')!;
const statusLabel = element.querySelector<HTMLSpanElement>('#music-together-status-label')!;
const permissionLabel = element.querySelector<HTMLSpanElement>('#music-together-permission-label')!;
profile.src = icon?.src ?? '';
const setStatus = (status: 'disconnected' | 'host' | 'guest') => {
if (status === 'disconnected') {
statusLabel.textContent = t('plugins.music-together.menu.status.disconnected');
statusLabel.style.color = 'rgba(255, 255, 255, 0.5)';
}
if (status === 'host') {
statusLabel.textContent = t('plugins.music-together.menu.status.host');
statusLabel.style.color = 'rgba(255, 0, 0, 1)';
}
if (status === 'guest') {
statusLabel.textContent = t('plugins.music-together.menu.status.guest');
statusLabel.style.color = 'rgba(255, 255, 255, 1)';
}
};
const setPermission = (permission: Permission) => {
if (permission === 'host-only') {
permissionLabel.textContent = t('plugins.music-together.menu.permission.host-only');
permissionLabel.style.color = 'rgba(255, 255, 255, 0.5)';
}
if (permission === 'playlist') {
permissionLabel.textContent = t('plugins.music-together.menu.permission.playlist');
permissionLabel.style.color = 'rgba(255, 255, 255, 0.75)';
}
if (permission === 'all') {
permissionLabel.textContent = t('plugins.music-together.menu.permission.all');
permissionLabel.style.color = 'rgba(255, 255, 255, 1)';
}
};
const setProfile = (src: string) => {
profile.src = src;
};
const setUsers = (users: Profile[]) => {
const container = element.querySelector<HTMLDivElement>('.music-together-user-container')!;
const empty = element.querySelector<HTMLElement>('.music-together-empty')!;
for (const child of Array.from(container.children)) {
if (child !== empty) child.remove();
}
if (users.length === 0) empty.style.display = 'block';
else empty.style.display = 'none';
for (const user of users) {
const img = document.createElement('img');
img.classList.add('music-together-profile');
img.src = user.thumbnail ?? '';
img.title = user.name;
img.alt = `${user.name} (${user.id})`;
container.append(img);
}
};
return {
element,
setStatus,
setUsers,
setProfile,
setPermission,
};
};

View File

@ -18,6 +18,8 @@ let useNativePiP = false;
let menu: Element | null = null; let menu: Element | null = null;
const pipButton = ElementFromHtml(pipHTML); const pipButton = ElementFromHtml(pipHTML);
let doneFirstLoad = false;
// Will also clone // Will also clone
function replaceButton(query: string, button: Element) { function replaceButton(query: string, button: Element) {
const svg = button.querySelector('#icon svg')?.cloneNode(true); const svg = button.querySelector('#icon svg')?.cloneNode(true);
@ -61,11 +63,15 @@ const observer = new MutationObserver(() => {
const menuUrl = $<HTMLAnchorElement>( const menuUrl = $<HTMLAnchorElement>(
'tp-yt-paper-listbox [tabindex="0"] #navigation-endpoint', 'tp-yt-paper-listbox [tabindex="0"] #navigation-endpoint',
)?.href; )?.href;
if (!menuUrl?.includes('watch?')) { if (!menuUrl?.includes('watch?') && doneFirstLoad) {
return; return;
} }
menu.prepend(pipButton); menu.prepend(pipButton);
if (!doneFirstLoad) {
setTimeout(() => (doneFirstLoad ||= true), 500);
}
}); });
const togglePictureInPicture = async () => { const togglePictureInPicture = async () => {

View File

@ -12,7 +12,7 @@
tabindex="-1" tabindex="-1"
> >
<div <div
class="icon menu-icon style-scope ytmusic-menu-navigation-item-renderer" class="icon ytmd-menu-item style-scope ytmusic-menu-navigation-item-renderer"
> >
<svg <svg
id="Layer_1" id="Layer_1"

View File

@ -1,4 +1,8 @@
// Creates a DOM element from an HTML string /**
* Creates a DOM element from an HTML string
* @param html The HTML string
* @returns The DOM element
*/
export const ElementFromHtml = (html: string): HTMLElement => { export const ElementFromHtml = (html: string): HTMLElement => {
const template = document.createElement('template'); const template = document.createElement('template');
html = html.trim(); // Never return a text node of whitespace as the result html = html.trim(); // Never return a text node of whitespace as the result
@ -6,3 +10,14 @@ export const ElementFromHtml = (html: string): HTMLElement => {
return template.content.firstElementChild as HTMLElement; return template.content.firstElementChild as HTMLElement;
}; };
/**
* Creates a DOM element from a src string
* @param src The source of the image
* @returns The image element
*/
export const ImageElementFromSrc = (src: string): HTMLImageElement => {
const image = document.createElement('img');
image.src = src;
return image;
};

View File

@ -257,4 +257,5 @@ export interface PlayerAPIEvents {
videodatachange: { videodatachange: {
value: VideoDataChangeValue; value: VideoDataChangeValue;
} & ({ name: 'dataloaded' } | { name: 'dataupdated ' }); } & ({ name: 'dataloaded' } | { name: 'dataupdated ' });
onStateChange: number;
} }

View File

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

View File

@ -262,7 +262,23 @@ export interface YoutubePlayer {
showControls: () => void; showControls: () => void;
hideControls: () => void; hideControls: () => void;
cancelPlayback: () => void; cancelPlayback: () => void;
getProgressState: <Return>() => Return; getProgressState: () => {
airingEnd: number;
airingStart: number;
allowSeeking: boolean;
clipEnd: number;
clipStart: number;
current: number;
displayedStart: number;
duration: number;
ingestionTime: number;
isAtLiveHead: boolean;
loaded: number;
offset: number;
seekableEnd: number;
seekableStart: number;
viewerLivestreamJoinMediaTime: number;
};
isInline: () => boolean; isInline: () => boolean;
setInline: (isInline: boolean) => void; setInline: (isInline: boolean) => void;
setLoopVideo: (value: boolean) => void; setLoopVideo: (value: boolean) => void;
@ -320,6 +336,10 @@ export interface YoutubePlayer {
getVolume: () => number; getVolume: () => number;
seekTo: (seconds: number) => void; seekTo: (seconds: number) => void;
getPlayerMode: <Return>() => Return; getPlayerMode: <Return>() => Return;
/**
* 1: playing
* 2: paused
*/
getPlayerState: () => number; getPlayerState: () => number;
getAvailablePlaybackRates: () => number[]; getAvailablePlaybackRates: () => number[];
getPlaybackQuality: () => string; getPlaybackQuality: () => string;

View File

@ -24,10 +24,6 @@ ytmusic-app-layout {
--ytmusic-nav-bar-height: 90px; --ytmusic-nav-bar-height: 90px;
} }
ytmusic-search-box.ytmusic-nav-bar {
margin-top: 15px;
}
/* Blocking annoying elements */ /* Blocking annoying elements */
ytmusic-mealbar-promo-renderer { ytmusic-mealbar-promo-renderer {
display: none !important; display: none !important;

View File

@ -15,6 +15,11 @@ declare module '*.svg?inline' {
export default base64; export default base64;
} }
declare module '*.svg?raw' {
const html: string;
export default html;
}
declare module '*.png' { declare module '*.png' {
const element: HTMLImageElement; const element: HTMLImageElement;

View File

@ -1,28 +0,0 @@
require 'json'
require 'open-uri'
cask "youtube-music" do
desc "YouTube Music Desktop App"
homepage "https://github.com/th-ch/youtube-music"
# Fetch the latest release version from GitHub API
latest_release = JSON.parse(URI.open("https://api.github.com/repos/th-ch/youtube-music/releases/latest").read)['tag_name']
version latest_release
base_url = "https://github.com/th-ch/youtube-music/releases/download/#{latest_release}/YouTube-Music-#{latest_release.delete_prefix('v')}"
file_extension = Hardware::CPU.arm? ? "-arm64.dmg" : ".dmg"
url "#{base_url}#{file_extension}"
# TODO checksum
sha256 :no_check
app "YouTube Music.app"
postflight do
print("Removing quarantine attribute from YouTube Music.app.\n")
system "xattr -cr '/Applications/YouTube Music.app'"
end
auto_updates true
end