mirror of
https://github.com/th-ch/youtube-music.git
synced 2026-01-13 11:21:46 +00:00
Compare commits
72 Commits
v3.7.2
...
refactor/m
| Author | SHA1 | Date | |
|---|---|---|---|
| 507a70015e | |||
| 86c77d141f | |||
| 754ca3caaa | |||
| 61ea104d7b | |||
| 573bdfae03 | |||
| cca493b7d5 | |||
| f47262d27b | |||
| 65bf9129ea | |||
| 87e9b9f7a8 | |||
| 07bc4f05fd | |||
| e3a6808087 | |||
| e9184e5d60 | |||
| a5b32d96f8 | |||
| 040db7539c | |||
| da646c1d53 | |||
| 5f5917f972 | |||
| eccb0d2f08 | |||
| 4cd9dd17df | |||
| 5de07b9a96 | |||
| 151f067beb | |||
| c68a7bd19f | |||
| b87e5e31df | |||
| 03229d61c8 | |||
| b6330eed18 | |||
| b254812ac2 | |||
| 7e243e2fbf | |||
| 307e52cc89 | |||
| f7b7ea916f | |||
| 3eccf8daca | |||
| aa48944212 | |||
| 4d51f1a412 | |||
| d3c9f76582 | |||
| d638a6cf28 | |||
| f93651b219 | |||
| cb8c6c69fe | |||
| 4e7266fb1b | |||
| 8de75ff3a5 | |||
| 3236c88eb2 | |||
| c9f0ad14c2 | |||
| 0a9199c92b | |||
| 3ffcff7d9c | |||
| ddf614d362 | |||
| 6f1a77bbb9 | |||
| 8595f9761e | |||
| cc442182fd | |||
| b827a05eea | |||
| 250abab8bc | |||
| 8e45518ccf | |||
| 7485e065ed | |||
| c1d88f91d4 | |||
| 124a2bd8d0 | |||
| f8f94f9665 | |||
| 5e98a82b23 | |||
| a5c20a66b3 | |||
| cc84116ad1 | |||
| a2e2031708 | |||
| 5001eabf23 | |||
| aac2974430 | |||
| f7f005bb3d | |||
| e1f6d5b7f2 | |||
| b6b607897e | |||
| 651ebb2b1a | |||
| 9fa24deed2 | |||
| c81022d373 | |||
| b726dc7580 | |||
| 471aa7d0a6 | |||
| f34d645ac3 | |||
| d2a11a560e | |||
| 9d185872db | |||
| d0ff71aa66 | |||
| bc8999585f | |||
| 1e1582e31f |
4
.github/workflows/build.yml
vendored
4
.github/workflows/build.yml
vendored
@ -23,7 +23,7 @@ jobs:
|
|||||||
- name: Install pnpm
|
- name: Install pnpm
|
||||||
uses: pnpm/action-setup@v4
|
uses: pnpm/action-setup@v4
|
||||||
with:
|
with:
|
||||||
version: 9
|
version: 10
|
||||||
run_install: false
|
run_install: false
|
||||||
|
|
||||||
- name: Setup NodeJS
|
- name: Setup NodeJS
|
||||||
@ -98,7 +98,7 @@ jobs:
|
|||||||
- name: Install pnpm
|
- name: Install pnpm
|
||||||
uses: pnpm/action-setup@v4
|
uses: pnpm/action-setup@v4
|
||||||
with:
|
with:
|
||||||
version: 9
|
version: 10
|
||||||
run_install: false
|
run_install: false
|
||||||
|
|
||||||
- name: Setup NodeJS
|
- name: Setup NodeJS
|
||||||
|
|||||||
102
changelog.md
102
changelog.md
@ -2,8 +2,110 @@
|
|||||||
|
|
||||||
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.7.3](https://github.com/th-ch/youtube-music/compare/v3.7.2...v3.7.3)
|
||||||
|
|
||||||
|
- fix(downloader): use the upgrade button to check for premium status [`#2987`](https://github.com/th-ch/youtube-music/pull/2987)
|
||||||
|
- chore(deps): update dependency electron-vite to v3 [`#2986`](https://github.com/th-ch/youtube-music/pull/2986)
|
||||||
|
- chore(deps): update dependency @babel/runtime to v7.26.9 [`#2980`](https://github.com/th-ch/youtube-music/pull/2980)
|
||||||
|
- fix(vite): set server.cors.origin [`#2981`](https://github.com/th-ch/youtube-music/pull/2981)
|
||||||
|
- chore(deps-dev): bump esbuild from 0.24.2 to 0.25.0 [`#2973`](https://github.com/th-ch/youtube-music/pull/2973)
|
||||||
|
- fix(deps): update dependency solid-transition-group to v0.3.0 [`#2949`](https://github.com/th-ch/youtube-music/pull/2949)
|
||||||
|
- fix: remove disable-gpu-memory-buffer-video-frames flag [`#2963`](https://github.com/th-ch/youtube-music/pull/2963)
|
||||||
|
- fix(deps): update dependency semver to v7.7.0 [`#2948`](https://github.com/th-ch/youtube-music/pull/2948)
|
||||||
|
- chore(deps): update playwright monorepo to v1.50.1 [`#2943`](https://github.com/th-ch/youtube-music/pull/2943)
|
||||||
|
- fix(deps): update dependency @hono/node-server to v1.13.8 [`#2944`](https://github.com/th-ch/youtube-music/pull/2944)
|
||||||
|
- fix(deps): update dependency electron-store to v10.0.1 [`#2945`](https://github.com/th-ch/youtube-music/pull/2945)
|
||||||
|
- chore(deps): update dependency rollup to v4.34.1 [`#2946`](https://github.com/th-ch/youtube-music/pull/2946)
|
||||||
|
- chore(deps): update dependency typescript-eslint to v8.22.0 [`#2947`](https://github.com/th-ch/youtube-music/pull/2947)
|
||||||
|
- fix(synced-lyrics): Fix reverse direction of synced lyrics for persian or other rtl languages [`#2940`](https://github.com/th-ch/youtube-music/pull/2940)
|
||||||
|
- chore(deps): update dependency electron to v34.0.2 [`#2942`](https://github.com/th-ch/youtube-music/pull/2942)
|
||||||
|
- chore(deps): update dependency discord-api-types to v0.37.119 [`#2941`](https://github.com/th-ch/youtube-music/pull/2941)
|
||||||
|
- fix(deps): update dependency hono to v4.6.20 [`#2932`](https://github.com/th-ch/youtube-music/pull/2932)
|
||||||
|
- chore(deps): update eslint monorepo to v9.19.0 [`#2935`](https://github.com/th-ch/youtube-music/pull/2935)
|
||||||
|
- fix(deps): update dependency bgutils-js to v3.1.3 [`#2934`](https://github.com/th-ch/youtube-music/pull/2934)
|
||||||
|
- fix(deps): update dependency i18next to v24.2.2 [`#2933`](https://github.com/th-ch/youtube-music/pull/2933)
|
||||||
|
- fix(deps): update dependency happy-dom to v16.8.1 [`#2936`](https://github.com/th-ch/youtube-music/pull/2936)
|
||||||
|
- chore(deps): update dependency @babel/runtime to v7.26.7 [`#2924`](https://github.com/th-ch/youtube-music/pull/2924)
|
||||||
|
- chore(config): migrate renovate config [`#2925`](https://github.com/th-ch/youtube-music/pull/2925)
|
||||||
|
- fix(deps): update dependency @ghostery/adblocker-electron-preload to v2.5.0 [`#2923`](https://github.com/th-ch/youtube-music/pull/2923)
|
||||||
|
- fix(deps): update dependency @ghostery/adblocker-electron to v2.5.0 [`#2922`](https://github.com/th-ch/youtube-music/pull/2922)
|
||||||
|
- chore(deps): update playwright monorepo to v1.50.0 [`#2921`](https://github.com/th-ch/youtube-music/pull/2921)
|
||||||
|
- chore(deps): update dependency vite-plugin-inspect to v10.1.0 [`#2920`](https://github.com/th-ch/youtube-music/pull/2920)
|
||||||
|
- chore(deps): update dependency rollup to v4.32.0 [`#2919`](https://github.com/th-ch/youtube-music/pull/2919)
|
||||||
|
- fix(deps): update dependency hono to v4.6.18 [`#2918`](https://github.com/th-ch/youtube-music/pull/2918)
|
||||||
|
- fix(deps): update dependency deepmerge-ts to v7.1.4 [`#2917`](https://github.com/th-ch/youtube-music/pull/2917)
|
||||||
|
- chore(deps): update dependency vite to v6.0.11 [`#2894`](https://github.com/th-ch/youtube-music/pull/2894)
|
||||||
|
- chore(deps): update dependency electron to v34.0.1 [`#2916`](https://github.com/th-ch/youtube-music/pull/2916)
|
||||||
|
- chore(deps): update dependency electron-builder-squirrel-windows to v26.0.0-alpha.10 [`#2899`](https://github.com/th-ch/youtube-music/pull/2899)
|
||||||
|
- chore(deps): update dependency electron-builder to v26.0.0-alpha.10 [`#2898`](https://github.com/th-ch/youtube-music/pull/2898)
|
||||||
|
- chore(deps): update dependency typescript-eslint to v8.21.0 [`#2901`](https://github.com/th-ch/youtube-music/pull/2901)
|
||||||
|
- chore(deps): update dependency discord-api-types to v0.37.117 [`#2895`](https://github.com/th-ch/youtube-music/pull/2895)
|
||||||
|
- fix(deps): update dependency youtubei.js to v13 [`#2904`](https://github.com/th-ch/youtube-music/pull/2904)
|
||||||
|
- chore(deps): update dependency vite to v6.0.9 [security] [`#2907`](https://github.com/th-ch/youtube-music/pull/2907)
|
||||||
|
- fix(deps): update dependency happy-dom to v16.7.2 [`#2902`](https://github.com/th-ch/youtube-music/pull/2902)
|
||||||
|
- fix(discord-plugin): handle album name padding if length < 2 [`#2903`](https://github.com/th-ch/youtube-music/pull/2903)
|
||||||
|
- feat(navigation): added nav icon padding [`#2905`](https://github.com/th-ch/youtube-music/pull/2905)
|
||||||
|
- chore(deps): update dependency rollup to v4.31.0 [`#2891`](https://github.com/th-ch/youtube-music/pull/2891)
|
||||||
|
- chore(deps): update dependency eslint-plugin-prettier to v5.2.3 [`#2889`](https://github.com/th-ch/youtube-music/pull/2889)
|
||||||
|
- chore(deps): update dependency vite-plugin-inspect to v10.0.7 [`#2882`](https://github.com/th-ch/youtube-music/pull/2882)
|
||||||
|
- fix(deps): update dependency hono to v4.6.17 [`#2883`](https://github.com/th-ch/youtube-music/pull/2883)
|
||||||
|
- fix: bump deps [`e9184e5`](https://github.com/th-ch/youtube-music/commit/e9184e5d60c2495473a7c3226ce9748ba89fceb3)
|
||||||
|
- fix(deps): fix pnpm [`040db75`](https://github.com/th-ch/youtube-music/commit/040db7539ccd1ae40f2632fdf38168cdaa26f112)
|
||||||
|
- chore(i18n): Translated using Weblate (Persian) [`9d18587`](https://github.com/th-ch/youtube-music/commit/9d185872dba5b56dabc691e56eafb13dc192b9cd)
|
||||||
|
|
||||||
|
#### [v3.7.2](https://github.com/th-ch/youtube-music/compare/v3.7.1...v3.7.2)
|
||||||
|
|
||||||
|
> 18 January 2025
|
||||||
|
|
||||||
|
- feat(api-server): add endpoint to get shuffle state [`#2792`](https://github.com/th-ch/youtube-music/pull/2792)
|
||||||
|
- chore(deps): update dependency discord-api-types to v0.37.116 [`#2877`](https://github.com/th-ch/youtube-music/pull/2877)
|
||||||
|
- chore(deps): update dependency eslint-plugin-prettier to v5.2.2 [`#2875`](https://github.com/th-ch/youtube-music/pull/2875)
|
||||||
|
- chore(deps): update eslint monorepo to v9.18.0 [`#2858`](https://github.com/th-ch/youtube-music/pull/2858)
|
||||||
|
- chore(deps): update dependency glob to v11.0.1 [`#2857`](https://github.com/th-ch/youtube-music/pull/2857)
|
||||||
|
- chore(deps): update dependency electron-builder-squirrel-windows to v26.0.0-alpha.9 [`#2874`](https://github.com/th-ch/youtube-music/pull/2874)
|
||||||
|
- chore(deps): update dependency electron to v34 [`#2867`](https://github.com/th-ch/youtube-music/pull/2867)
|
||||||
|
- chore(deps): update dependency eslint-config-prettier to v10 [`#2866`](https://github.com/th-ch/youtube-music/pull/2866)
|
||||||
|
- chore(deps): update dependency @stylistic/eslint-plugin-js to v2.13.0 [`#2864`](https://github.com/th-ch/youtube-music/pull/2864)
|
||||||
|
- chore(deps): update dependency typescript-eslint to v8.20.0 [`#2865`](https://github.com/th-ch/youtube-music/pull/2865)
|
||||||
|
- chore(deps): update dependency electron-builder to v26.0.0-alpha.9 [`#2869`](https://github.com/th-ch/youtube-music/pull/2869)
|
||||||
|
- fix: fix build.linux.desktop.entry [`#2859`](https://github.com/th-ch/youtube-music/pull/2859)
|
||||||
|
- feat(api-server): add endpoint to get volume state [`#2813`](https://github.com/th-ch/youtube-music/pull/2813)
|
||||||
|
- chore(deps): update dependency vite-plugin-inspect to v10 [`#2856`](https://github.com/th-ch/youtube-music/pull/2856)
|
||||||
|
- chore(deps): update dependency typescript to v5.7.3 [`#2855`](https://github.com/th-ch/youtube-music/pull/2855)
|
||||||
|
- fix(deps): update dependency @floating-ui/dom to v1.6.13 [`#2846`](https://github.com/th-ch/youtube-music/pull/2846)
|
||||||
|
- chore(deps): bump nanoid from 3.3.7 to 3.3.8 [`#2854`](https://github.com/th-ch/youtube-music/pull/2854)
|
||||||
|
- chore(deps): update dependency electron to v33.3.1 [`#2841`](https://github.com/th-ch/youtube-music/pull/2841)
|
||||||
|
- fix(deps): update dependency i18next to v24.2.1 [`#2840`](https://github.com/th-ch/youtube-music/pull/2840)
|
||||||
|
- chore(deps): update dependency typescript-eslint to v8.19.1 [`#2836`](https://github.com/th-ch/youtube-music/pull/2836)
|
||||||
|
- chore(deps): update dependency rollup to v4.30.1 [`#2833`](https://github.com/th-ch/youtube-music/pull/2833)
|
||||||
|
- fix(deps): update dependency solid-js to v1.9.4 [`#2849`](https://github.com/th-ch/youtube-music/pull/2849)
|
||||||
|
- fix(deps): update dependency fast-equals to v5.2.2 [`#2842`](https://github.com/th-ch/youtube-music/pull/2842)
|
||||||
|
- chore: Update README.md [`#2845`](https://github.com/th-ch/youtube-music/pull/2845)
|
||||||
|
- chore: Fixing the Content section in the README-ru.md file. [`#2847`](https://github.com/th-ch/youtube-music/pull/2847)
|
||||||
|
- chore: Create youtube-music-hu.svg [`#2844`](https://github.com/th-ch/youtube-music/pull/2844)
|
||||||
|
- chore: Create Transalated README-hu.md [`#2843`](https://github.com/th-ch/youtube-music/pull/2843)
|
||||||
|
- chore(deps): update dependency vite to v6.0.7 [`#2819`](https://github.com/th-ch/youtube-music/pull/2819)
|
||||||
|
- chore(deps): update dependency discord-api-types to v0.37.115 [`#2818`](https://github.com/th-ch/youtube-music/pull/2818)
|
||||||
|
- fix(deps): update dependency hono to v4.6.16 [`#2829`](https://github.com/th-ch/youtube-music/pull/2829)
|
||||||
|
- chore(deps): update dependency rollup to v4.29.2 [`#2832`](https://github.com/th-ch/youtube-music/pull/2832)
|
||||||
|
- fix(deps): update dependency fast-equals to v5.2.0 [`#2822`](https://github.com/th-ch/youtube-music/pull/2822)
|
||||||
|
- feat(api-server): add `insertPosition` for `addSongToQueue` [`#2808`](https://github.com/th-ch/youtube-music/pull/2808)
|
||||||
|
- chore(deps): update dependency typescript-eslint to v8.19.0 [`#2812`](https://github.com/th-ch/youtube-music/pull/2812)
|
||||||
|
- fix(deps): update dependency ts-morph to v25 [`#2810`](https://github.com/th-ch/youtube-music/pull/2810)
|
||||||
|
- fix(renderer): update event handler from onVolumeTap to onVolumeClick [`#2791`](https://github.com/th-ch/youtube-music/pull/2791)
|
||||||
|
- fix(deps): update dependency hono to v4.6.15 [`#2796`](https://github.com/th-ch/youtube-music/pull/2796)
|
||||||
|
- chore(deps): update dependency bufferutil to v4.0.9 [`#2787`](https://github.com/th-ch/youtube-music/pull/2787)
|
||||||
|
- feat: Refactor Menu Navigation and Update Media Control Icons [`#2783`](https://github.com/th-ch/youtube-music/pull/2783)
|
||||||
|
- fix(synced-lyrics): Revert font-size behavior for non-fancy modes [`#2788`](https://github.com/th-ch/youtube-music/pull/2788)
|
||||||
|
- fix(downloader): apply poToken [`#2863`](https://github.com/th-ch/youtube-music/issues/2863) [`#2780`](https://github.com/th-ch/youtube-music/issues/2780)
|
||||||
|
- chore(deps): update dependency electron-builder to v26 [`67fc0a4`](https://github.com/th-ch/youtube-music/commit/67fc0a415cae231a11f2846aadf01edb04f5c677)
|
||||||
|
- fix: fix lock file [`3339f99`](https://github.com/th-ch/youtube-music/commit/3339f997e3c2d4d2c32b3aee95c65d561f123fcb)
|
||||||
|
- chore(i18n): Translated using Weblate (Romanian) [`845dac3`](https://github.com/th-ch/youtube-music/commit/845dac3c0393dadea8efdd03ba1f41b1b36e6191)
|
||||||
|
|
||||||
#### [v3.7.1](https://github.com/th-ch/youtube-music/compare/v3.7.0...v3.7.1)
|
#### [v3.7.1](https://github.com/th-ch/youtube-music/compare/v3.7.0...v3.7.1)
|
||||||
|
|
||||||
|
> 27 December 2024
|
||||||
|
|
||||||
- fix(deps): update dependency node-html-parser to v7 [`#2776`](https://github.com/th-ch/youtube-music/pull/2776)
|
- fix(deps): update dependency node-html-parser to v7 [`#2776`](https://github.com/th-ch/youtube-music/pull/2776)
|
||||||
- chore(deps): update dependency vite to v6.0.6 [`#2774`](https://github.com/th-ch/youtube-music/pull/2774)
|
- chore(deps): update dependency vite to v6.0.6 [`#2774`](https://github.com/th-ch/youtube-music/pull/2774)
|
||||||
- feat(api-server): Add queue api [`#2767`](https://github.com/th-ch/youtube-music/pull/2767)
|
- feat(api-server): Add queue api [`#2767`](https://github.com/th-ch/youtube-music/pull/2767)
|
||||||
|
|||||||
@ -147,6 +147,11 @@ export default defineConfig({
|
|||||||
resolve: {
|
resolve: {
|
||||||
alias: resolveAlias,
|
alias: resolveAlias,
|
||||||
},
|
},
|
||||||
|
server: {
|
||||||
|
cors: {
|
||||||
|
origin: 'https://music.youtube.com',
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
if (mode === 'development') {
|
if (mode === 'development') {
|
||||||
|
|||||||
88
package.json
88
package.json
@ -2,7 +2,7 @@
|
|||||||
"name": "youtube-music",
|
"name": "youtube-music",
|
||||||
"desktopName": "com.github.th_ch.youtube_music",
|
"desktopName": "com.github.th_ch.youtube_music",
|
||||||
"productName": "YouTube Music",
|
"productName": "YouTube Music",
|
||||||
"version": "3.7.2",
|
"version": "3.7.3",
|
||||||
"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",
|
||||||
@ -222,18 +222,19 @@
|
|||||||
},
|
},
|
||||||
"pnpm": {
|
"pnpm": {
|
||||||
"overrides": {
|
"overrides": {
|
||||||
"vite": "6.0.7",
|
"vite": "6.1.0",
|
||||||
"node-gyp": "11.0.0",
|
"node-gyp": "11.1.0",
|
||||||
"xml2js": "0.6.2",
|
"xml2js": "0.6.2",
|
||||||
"node-fetch": "3.3.2",
|
"node-fetch": "3.3.2",
|
||||||
"@electron/universal": "2.0.1",
|
"@electron/universal": "2.0.1",
|
||||||
"@babel/runtime": "7.26.0"
|
"@babel/runtime": "7.26.9"
|
||||||
},
|
},
|
||||||
"patchedDependencies": {
|
"patchedDependencies": {
|
||||||
"vudio@2.1.1": "patches/vudio@2.1.1.patch",
|
"vudio@2.1.1": "patches/vudio@2.1.1.patch",
|
||||||
"app-builder-lib@26.0.0-alpha.9": "patches/app-builder-lib@26.0.0-alpha.9.patch",
|
"app-builder-lib@26.0.6": "patches/app-builder-lib@26.0.6.patch",
|
||||||
"@malept/flatpak-bundler": "patches/@malept__flatpak-bundler.patch"
|
"@malept/flatpak-bundler": "patches/@malept__flatpak-bundler.patch"
|
||||||
}
|
},
|
||||||
|
"neverBuiltDependencies": []
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@electron-toolkit/tsconfig": "1.0.1",
|
"@electron-toolkit/tsconfig": "1.0.1",
|
||||||
@ -242,12 +243,12 @@
|
|||||||
"@ffmpeg.wasm/main": "0.12.0",
|
"@ffmpeg.wasm/main": "0.12.0",
|
||||||
"@floating-ui/dom": "1.6.13",
|
"@floating-ui/dom": "1.6.13",
|
||||||
"@foobar404/wave": "2.0.5",
|
"@foobar404/wave": "2.0.5",
|
||||||
"@ghostery/adblocker-electron": "2.3.1",
|
"@ghostery/adblocker-electron": "2.5.0",
|
||||||
"@ghostery/adblocker-electron-preload": "2.3.1",
|
"@ghostery/adblocker-electron-preload": "2.5.0",
|
||||||
"@hono/node-server": "1.13.7",
|
"@hono/node-server": "1.13.8",
|
||||||
"@hono/swagger-ui": "0.5.0",
|
"@hono/swagger-ui": "0.5.0",
|
||||||
"@hono/zod-openapi": "0.18.3",
|
"@hono/zod-openapi": "0.18.4",
|
||||||
"@hono/zod-validator": "0.4.2",
|
"@hono/zod-validator": "0.4.3",
|
||||||
"@jellybrick/dbus-next": "0.10.3",
|
"@jellybrick/dbus-next": "0.10.3",
|
||||||
"@jellybrick/electron-better-web-request": "1.0.4",
|
"@jellybrick/electron-better-web-request": "1.0.4",
|
||||||
"@jellybrick/mpris-service": "2.1.5",
|
"@jellybrick/mpris-service": "2.1.5",
|
||||||
@ -255,52 +256,51 @@
|
|||||||
"@skyra/jaro-winkler": "1.1.1",
|
"@skyra/jaro-winkler": "1.1.1",
|
||||||
"@xhayper/discord-rpc": "1.2.0",
|
"@xhayper/discord-rpc": "1.2.0",
|
||||||
"async-mutex": "0.5.0",
|
"async-mutex": "0.5.0",
|
||||||
"bgutils-js": "3.1.2",
|
"bgutils-js": "3.1.3",
|
||||||
"butterchurn": "3.0.0-beta.4",
|
"butterchurn": "3.0.0-beta.4",
|
||||||
"butterchurn-presets": "3.0.0-beta.4",
|
"butterchurn-presets": "3.0.0-beta.4",
|
||||||
"color": "4.2.3",
|
"color": "5.0.0",
|
||||||
"conf": "13.1.0",
|
"conf": "13.1.0",
|
||||||
"custom-electron-prompt": "1.5.8",
|
"custom-electron-prompt": "1.5.8",
|
||||||
"deepmerge-ts": "7.1.3",
|
"deepmerge-ts": "7.1.4",
|
||||||
"electron-debug": "4.1.0",
|
"electron-debug": "4.1.0",
|
||||||
"electron-is": "3.0.0",
|
"electron-is": "3.0.0",
|
||||||
"electron-localshortcut": "3.2.1",
|
"electron-localshortcut": "3.2.1",
|
||||||
"electron-store": "10.0.0",
|
"electron-store": "10.0.1",
|
||||||
"electron-unhandled": "4.0.1",
|
"electron-unhandled": "4.0.1",
|
||||||
"electron-updater": "6.3.9",
|
"electron-updater": "6.3.9",
|
||||||
"fast-average-color": "9.4.0",
|
"fast-average-color": "9.4.0",
|
||||||
"fast-equals": "5.2.2",
|
"fast-equals": "5.2.2",
|
||||||
"filenamify": "6.0.0",
|
"filenamify": "6.0.0",
|
||||||
"happy-dom": "16.6.0",
|
"happy-dom": "17.1.0",
|
||||||
"hono": "4.6.16",
|
"hono": "4.7.1",
|
||||||
"howler": "2.2.4",
|
"howler": "2.2.4",
|
||||||
"html-to-text": "9.0.5",
|
"html-to-text": "9.0.5",
|
||||||
"i18next": "24.2.1",
|
"i18next": "24.2.2",
|
||||||
"jimp": "1.6.0",
|
"jimp": "1.6.0",
|
||||||
"keyboardevent-from-electron-accelerator": "2.0.0",
|
"keyboardevent-from-electron-accelerator": "2.0.0",
|
||||||
"keyboardevents-areequal": "0.2.2",
|
"keyboardevents-areequal": "0.2.2",
|
||||||
"node-html-parser": "7.0.1",
|
"node-html-parser": "7.0.1",
|
||||||
"node-id3": "0.2.6",
|
"node-id3": "0.2.7",
|
||||||
"peerjs": "1.5.4",
|
"peerjs": "1.5.4",
|
||||||
"semver": "7.6.3",
|
"semver": "7.7.1",
|
||||||
"serve": "14.2.4",
|
"serve": "14.2.4",
|
||||||
"simple-youtube-age-restriction-bypass": "github:organization/Simple-YouTube-Age-Restriction-Bypass#v2.5.9",
|
"simple-youtube-age-restriction-bypass": "github:organization/Simple-YouTube-Age-Restriction-Bypass#v2.5.9",
|
||||||
"solid-floating-ui": "0.3.1",
|
"solid-floating-ui": "0.3.1",
|
||||||
"solid-js": "1.9.4",
|
"solid-js": "1.9.4",
|
||||||
"solid-styled-components": "0.28.5",
|
"solid-styled-components": "0.28.5",
|
||||||
"solid-transition-group": "0.2.3",
|
"solid-transition-group": "0.3.0",
|
||||||
"ts-morph": "25.0.0",
|
"ts-morph": "25.0.1",
|
||||||
"vudio": "2.1.1",
|
"vudio": "2.1.1",
|
||||||
"x11": "2.3.0",
|
"x11": "2.3.0",
|
||||||
"youtubei.js": "12.2.0",
|
"youtubei.js": "13.0.0",
|
||||||
"zod": "3.24.1"
|
"zod": "3.24.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "9.18.0",
|
"@eslint/js": "9.20.0",
|
||||||
"@playwright/test": "1.49.1",
|
"@playwright/test": "1.50.1",
|
||||||
"@stylistic/eslint-plugin-js": "2.13.0",
|
"@stylistic/eslint-plugin-js": "3.1.0",
|
||||||
"@total-typescript/ts-reset": "0.6.1",
|
"@total-typescript/ts-reset": "0.6.1",
|
||||||
"@types/color": "4.2.0",
|
|
||||||
"@types/electron-localshortcut": "3.1.3",
|
"@types/electron-localshortcut": "3.1.3",
|
||||||
"@types/eslint__js": "8.42.3",
|
"@types/eslint__js": "8.42.3",
|
||||||
"@types/howler": "2.2.12",
|
"@types/howler": "2.2.12",
|
||||||
@ -311,30 +311,30 @@
|
|||||||
"builtin-modules": "4.0.0",
|
"builtin-modules": "4.0.0",
|
||||||
"cross-env": "7.0.3",
|
"cross-env": "7.0.3",
|
||||||
"del-cli": "6.0.0",
|
"del-cli": "6.0.0",
|
||||||
"discord-api-types": "0.37.116",
|
"discord-api-types": "0.37.119",
|
||||||
"electron": "34.0.0",
|
"electron": "34.2.0",
|
||||||
"electron-builder": "26.0.0-alpha.9",
|
"electron-builder": "26.0.6",
|
||||||
"electron-builder-squirrel-windows": "26.0.0-alpha.9",
|
"electron-builder-squirrel-windows": "26.0.6",
|
||||||
"electron-devtools-installer": "4.0.0",
|
"electron-devtools-installer": "4.0.0",
|
||||||
"electron-vite": "2.3.0",
|
"electron-vite": "3.0.0",
|
||||||
"esbuild": "0.24.2",
|
"esbuild": "0.25.0",
|
||||||
"eslint": "9.18.0",
|
"eslint": "9.20.1",
|
||||||
"eslint-config-prettier": "10.0.1",
|
"eslint-config-prettier": "10.0.1",
|
||||||
"eslint-import-resolver-exports": "1.0.0-beta.5",
|
"eslint-import-resolver-exports": "1.0.0-beta.5",
|
||||||
"eslint-import-resolver-typescript": "3.7.0",
|
"eslint-import-resolver-typescript": "3.8.0",
|
||||||
"eslint-plugin-import": "2.31.0",
|
"eslint-plugin-import": "2.31.0",
|
||||||
"eslint-plugin-prettier": "5.2.2",
|
"eslint-plugin-prettier": "5.2.3",
|
||||||
"glob": "11.0.1",
|
"glob": "11.0.1",
|
||||||
"node-gyp": "11.0.0",
|
"node-gyp": "11.1.0",
|
||||||
"playwright": "1.49.1",
|
"playwright": "1.50.1",
|
||||||
"rollup": "4.30.1",
|
"rollup": "4.34.7",
|
||||||
"typescript": "5.7.3",
|
"typescript": "5.7.3",
|
||||||
"typescript-eslint": "8.20.0",
|
"typescript-eslint": "8.24.0",
|
||||||
"utf-8-validate": "6.0.5",
|
"utf-8-validate": "6.0.5",
|
||||||
"vite": "6.0.7",
|
"vite": "6.1.0",
|
||||||
"vite-plugin-inspect": "10.0.6",
|
"vite-plugin-inspect": "10.2.1",
|
||||||
"vite-plugin-resolve": "2.5.2",
|
"vite-plugin-resolve": "2.5.2",
|
||||||
"vite-plugin-solid": "2.11.0",
|
"vite-plugin-solid": "2.11.1",
|
||||||
"ws": "8.18.0"
|
"ws": "8.18.0"
|
||||||
},
|
},
|
||||||
"auto-changelog": {
|
"auto-changelog": {
|
||||||
|
|||||||
3385
pnpm-lock.yaml
generated
3385
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -1,8 +1,6 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||||
"extends": [
|
"extends": ["config:recommended"],
|
||||||
"config:base"
|
|
||||||
],
|
|
||||||
"labels": ["dependencies"],
|
"labels": ["dependencies"],
|
||||||
"postUpdateOptions": ["pnpmDedupe"]
|
"postUpdateOptions": ["pnpmDedupe"]
|
||||||
}
|
}
|
||||||
|
|||||||
@ -745,7 +745,8 @@
|
|||||||
"label": "Efecto de la línea",
|
"label": "Efecto de la línea",
|
||||||
"submenu": {
|
"submenu": {
|
||||||
"fancy": {
|
"fancy": {
|
||||||
"label": "Elegante"
|
"label": "Elegante",
|
||||||
|
"tooltip": "Usar efectos grandes, similares a los de una aplicación, en la línea actual"
|
||||||
},
|
},
|
||||||
"focus": {
|
"focus": {
|
||||||
"label": "Enfoque",
|
"label": "Enfoque",
|
||||||
|
|||||||
@ -81,12 +81,12 @@
|
|||||||
"menu": {
|
"menu": {
|
||||||
"about": "درباره",
|
"about": "درباره",
|
||||||
"navigation": {
|
"navigation": {
|
||||||
"label": "ناوبری",
|
"label": "کنترلهای رابط",
|
||||||
"submenu": {
|
"submenu": {
|
||||||
"copy-current-url": "کپی کردن URL فعلی",
|
"copy-current-url": "کپی کردن لینک صفحه فعلی",
|
||||||
"go-back": "بازگشت",
|
"go-back": "صفحه قبل",
|
||||||
"go-forward": "حرکت به جلو",
|
"go-forward": "صفحه بعدی",
|
||||||
"quit": "خروجی",
|
"quit": "خروج از برنامه",
|
||||||
"restart": "راهاندازی مجدد برنامه"
|
"restart": "راهاندازی مجدد برنامه"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -98,8 +98,8 @@
|
|||||||
"submenu": {
|
"submenu": {
|
||||||
"auto-reset-app-cache": "ریست کردن حافظه کش برنامه هنگام شروع",
|
"auto-reset-app-cache": "ریست کردن حافظه کش برنامه هنگام شروع",
|
||||||
"disable-hardware-acceleration": "غیرفعال کردن شتاب سختافزاری",
|
"disable-hardware-acceleration": "غیرفعال کردن شتاب سختافزاری",
|
||||||
"edit-config-json": "ویرایش config.json",
|
"edit-config-json": "config.json ویرایش",
|
||||||
"override-user-agent": "تغییر User-Agent",
|
"override-user-agent": "User-Agent تغییر",
|
||||||
"restart-on-config-changes": "راهاندازی مجدد در صورت تغییرات در پیکربندی",
|
"restart-on-config-changes": "راهاندازی مجدد در صورت تغییرات در پیکربندی",
|
||||||
"set-proxy": {
|
"set-proxy": {
|
||||||
"label": "تنظیم پراکسی",
|
"label": "تنظیم پراکسی",
|
||||||
@ -109,7 +109,7 @@
|
|||||||
"title": "تنظیم پراکسی"
|
"title": "تنظیم پراکسی"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"toggle-dev-tools": "باز کردن DevTools"
|
"toggle-dev-tools": "DevTools باز کردن"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"always-on-top": "همیشه در بالا",
|
"always-on-top": "همیشه در بالا",
|
||||||
@ -168,7 +168,7 @@
|
|||||||
},
|
},
|
||||||
"label": "تم",
|
"label": "تم",
|
||||||
"submenu": {
|
"submenu": {
|
||||||
"import-css-file": "وارد کردن فایل CSS سفارشی",
|
"import-css-file": "سفارشی CSS وارد کردن فایل",
|
||||||
"no-theme": "بدون تم"
|
"no-theme": "بدون تم"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -177,7 +177,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"plugins": {
|
"plugins": {
|
||||||
"enabled": "فعال",
|
"enabled": "فعال/غیرفعال کردن",
|
||||||
"label": "افزونهها",
|
"label": "افزونهها",
|
||||||
"new": "جدید"
|
"new": "جدید"
|
||||||
},
|
},
|
||||||
@ -187,7 +187,7 @@
|
|||||||
"force-reload": "اجبار به بارگذاری مجدد",
|
"force-reload": "اجبار به بارگذاری مجدد",
|
||||||
"reload": "بارگذاری مجدد",
|
"reload": "بارگذاری مجدد",
|
||||||
"reset-zoom": "اندازه واقعی",
|
"reset-zoom": "اندازه واقعی",
|
||||||
"toggle-fullscreen": "تغییر به تمامصفحه",
|
"toggle-fullscreen": "تغییر به تمام صفحه",
|
||||||
"zoom-in": "بزرگنمایی",
|
"zoom-in": "بزرگنمایی",
|
||||||
"zoom-out": "کوچکنمایی"
|
"zoom-out": "کوچکنمایی"
|
||||||
}
|
}
|
||||||
@ -219,7 +219,7 @@
|
|||||||
"name": "مسدودکننده تبلیغات"
|
"name": "مسدودکننده تبلیغات"
|
||||||
},
|
},
|
||||||
"album-actions": {
|
"album-actions": {
|
||||||
"description": "افزودن دکمههای \"برگرفتن ناپسند\"، \"ناپسند\"، \"پسند\"، و \"حذف پسند\" برای اعمال آنها روی همه آهنگها در یک فهرست پخش یا آلبوم",
|
"description": "اضافه کردن دکمههای عدم پسندیدن، پسندیدن و لغو پسندیدن برای اعمال این تغییرات به تمامی آهنگهای یک فهرست پخش یا آلبوم",
|
||||||
"name": "عملیات آلبوم"
|
"name": "عملیات آلبوم"
|
||||||
},
|
},
|
||||||
"album-color-theme": {
|
"album-color-theme": {
|
||||||
@ -250,7 +250,10 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"opacity": {
|
"opacity": {
|
||||||
"label": "شفافیت"
|
"label": "شفافیت",
|
||||||
|
"submenu": {
|
||||||
|
"percent": "{{opacity}}%"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"quality": {
|
"quality": {
|
||||||
"label": "کیفیت",
|
"label": "کیفیت",
|
||||||
@ -259,7 +262,10 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"size": {
|
"size": {
|
||||||
"label": "اندازه"
|
"label": "اندازه",
|
||||||
|
"submenu": {
|
||||||
|
"percent": "{{size}}%"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"smoothness-transition": {
|
"smoothness-transition": {
|
||||||
"label": "انتقال نرمی",
|
"label": "انتقال نرمی",
|
||||||
@ -273,8 +279,15 @@
|
|||||||
},
|
},
|
||||||
"name": "حالت محیطی"
|
"name": "حالت محیطی"
|
||||||
},
|
},
|
||||||
|
"amuse": {
|
||||||
|
"description": "حالا ویجت Amuse از YouTube Music هم پشتیبانی میکنه! (توسط 6K Labs)",
|
||||||
|
"name": "Amuse",
|
||||||
|
"response": {
|
||||||
|
"query": "سرور Amuse فعال است. برای دریافت اطلاعات آهنگ، از آدرس /query استفاده کنید."
|
||||||
|
}
|
||||||
|
},
|
||||||
"api-server": {
|
"api-server": {
|
||||||
"description": "افزودن یک سرور API برای کنترل پخشکننده",
|
"description": "برای کنترل پخشکننده API افزودن یک سرور",
|
||||||
"dialog": {
|
"dialog": {
|
||||||
"request": {
|
"request": {
|
||||||
"buttons": {
|
"buttons": {
|
||||||
@ -304,14 +317,14 @@
|
|||||||
"label": "پورت"
|
"label": "پورت"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"name": "سرور API [بتا]",
|
"name": "[بتا]API سرور",
|
||||||
"prompt": {
|
"prompt": {
|
||||||
"hostname": {
|
"hostname": {
|
||||||
"label": "نام میزبان را برای سرور API وارد کنید (مثل 0.0.0.0):",
|
"label": "وارد کنید (مثل 0.0.0.0): API نام میزبان را برای سرور",
|
||||||
"title": "نام میزبان"
|
"title": "نام میزبان"
|
||||||
},
|
},
|
||||||
"port": {
|
"port": {
|
||||||
"label": "پورت را برای سرور API وارد کنید:",
|
"label": "وارد کنید: API پورت را برای سرور",
|
||||||
"title": "پورت"
|
"title": "پورت"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -321,8 +334,8 @@
|
|||||||
"name": "فشردهساز صدا"
|
"name": "فشردهساز صدا"
|
||||||
},
|
},
|
||||||
"blur-nav-bar": {
|
"blur-nav-bar": {
|
||||||
"description": "شفاف و محو کردن نوار ناوبری",
|
"description": "شفاف و محو کردن نوار کنترل",
|
||||||
"name": "محو کردن نوار ناوبری"
|
"name": "محو کردن نوار کنترل"
|
||||||
},
|
},
|
||||||
"bypass-age-restrictions": {
|
"bypass-age-restrictions": {
|
||||||
"description": "دور زدن تأیید سن یوتیوب",
|
"description": "دور زدن تأیید سن یوتیوب",
|
||||||
@ -381,27 +394,27 @@
|
|||||||
},
|
},
|
||||||
"discord": {
|
"discord": {
|
||||||
"backend": {
|
"backend": {
|
||||||
"already-connected": "تلاش برای اتصال با اتصال فعال",
|
"already-connected": "تلاش برای برقراری ارتباط با اتصال فعال",
|
||||||
"connected": "متصل به Discord",
|
"connected": "متصل به دیسکورد",
|
||||||
"disconnected": "قطع اتصال از Discord"
|
"disconnected": "ارتباط با دیسکورد قطع شد"
|
||||||
},
|
},
|
||||||
"description": "نمایش آنچه گوش میدهید به دوستان با Rich Presence",
|
"description": "Rich Presence نمایش آنچه گوش میدهید به دوستان با",
|
||||||
"menu": {
|
"menu": {
|
||||||
"auto-reconnect": "اتصال خودکار مجدد",
|
"auto-reconnect": "اتصال خودکار",
|
||||||
"clear-activity": "پاک کردن فعالیت",
|
"clear-activity": "پاک کردن فعالیت",
|
||||||
"clear-activity-after-timeout": "پاک کردن فعالیت پس از تایماوت",
|
"clear-activity-after-timeout": "حذف فعالیت پس از اتمام زمان تعیینشده",
|
||||||
"connected": "متصل",
|
"connected": "اتصال برقرار شد",
|
||||||
"disconnected": "قطع شده",
|
"disconnected": "اتصال قطع شد",
|
||||||
"hide-duration-left": "مخفی کردن مدت زمان باقیمانده",
|
"hide-duration-left": "مخفی کردن مدت زمان باقیمانده",
|
||||||
"hide-github-button": "مخفی کردن دکمه لینک GitHub",
|
"hide-github-button": "مخفی کردن دکمه لینک گیت هاب",
|
||||||
"play-on-youtube-music": "پخش در یوتیوب موسیقی",
|
"play-on-youtube-music": "پخش در یوتیوب موزیک",
|
||||||
"set-inactivity-timeout": "تنظیم تایماوت عدم فعالیت"
|
"set-inactivity-timeout": "تنظیم زمان عدم فعالیت"
|
||||||
},
|
},
|
||||||
"name": "Rich Presence در Discord",
|
"name": "Discord Rich Presence",
|
||||||
"prompt": {
|
"prompt": {
|
||||||
"set-inactivity-timeout": {
|
"set-inactivity-timeout": {
|
||||||
"label": "ورود تایماوت عدم فعالیت به ثانیه:",
|
"label": "محدودیت زمان عدم فعالیت را به ثانیه وارد کنید:",
|
||||||
"title": "تنظیم تایماوت عدم فعالیت"
|
"title": "تنظیم زمان عدم فعالیت"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -478,6 +491,18 @@
|
|||||||
"button": "دانلود"
|
"button": "دانلود"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"equalizer": {
|
||||||
|
"description": "اضافه کردن یک اکولایزر به پخشکننده",
|
||||||
|
"menu": {
|
||||||
|
"presets": {
|
||||||
|
"label": "تنظیمات از پیش تعیین شده",
|
||||||
|
"list": {
|
||||||
|
"bass-booster": "تقویتکننده باس صدا"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"name": "اکولایزر"
|
||||||
|
},
|
||||||
"exponential-volume": {
|
"exponential-volume": {
|
||||||
"description": "نوار لغزنده حجم را به صورت نمایی میسازد تا انتخاب حجمهای پایینتر آسانتر شود.",
|
"description": "نوار لغزنده حجم را به صورت نمایی میسازد تا انتخاب حجمهای پایینتر آسانتر شود.",
|
||||||
"name": "حجم نمایی"
|
"name": "حجم نمایی"
|
||||||
@ -490,17 +515,17 @@
|
|||||||
"name": "منوی داخل برنامه"
|
"name": "منوی داخل برنامه"
|
||||||
},
|
},
|
||||||
"lumiastream": {
|
"lumiastream": {
|
||||||
"description": "افزودن پشتیبانی از Lumia Stream",
|
"description": "Lumia Stream افزودن پشتیبانی از",
|
||||||
"name": "Lumia Stream [بتا]"
|
"name": "Lumia Stream [بتا]"
|
||||||
},
|
},
|
||||||
"lyrics-genius": {
|
"lyrics-genius": {
|
||||||
"description": "افزودن پشتیبانی از متن آهنگ برای بیشتر آهنگها",
|
"description": "افزودن متن ترانه پشتیبان برای اکثر ترانه ها",
|
||||||
"menu": {
|
"menu": {
|
||||||
"romanized-lyrics": "متن رومیشده"
|
"romanized-lyrics": "الفبای لاتین برای آهنگهایی با الفبای شرقی (فینگلیش)"
|
||||||
},
|
},
|
||||||
"name": "متن آهنگ Genius",
|
"name": "Genius متن آهنگ",
|
||||||
"renderer": {
|
"renderer": {
|
||||||
"fetched-lyrics": "متن آهنگ از Genius بازیابی شد"
|
"fetched-lyrics": "بازیابی شد Genius متن ترانه توسط"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"music-together": {
|
"music-together": {
|
||||||
@ -536,13 +561,13 @@
|
|||||||
"name": "Music Together [بتا]",
|
"name": "Music Together [بتا]",
|
||||||
"toast": {
|
"toast": {
|
||||||
"add-song-failed": "افزودن آهنگ با شکست مواجه شد",
|
"add-song-failed": "افزودن آهنگ با شکست مواجه شد",
|
||||||
"closed": "Music Together بسته شد",
|
"closed": "بسته شد Music Together",
|
||||||
"disconnected": "قطع اتصال Music Together",
|
"disconnected": "Music Together قطع اتصال",
|
||||||
"host-failed": "میزبانی Music Together با شکست مواجه شد",
|
"host-failed": "با شکست مواجه شد Music Together میزبانی",
|
||||||
"id-copied": "شناسه میزبان به کلیپبورد کپی شد",
|
"id-copied": "شناسه میزبان به کلیپبورد کپی شد",
|
||||||
"id-copy-failed": "کپی شناسه میزبان به کلیپبورد با شکست مواجه شد",
|
"id-copy-failed": "کپی شناسه میزبان به کلیپبورد با شکست مواجه شد",
|
||||||
"join-failed": "پیوستن به Music Together با شکست مواجه شد",
|
"join-failed": "با شکست مواجه شد Music Together پیوستن به",
|
||||||
"joined": "به Music Together پیوست",
|
"joined": "پیوست Music Together به",
|
||||||
"permission-changed": "مجوز Music Together به \"{{permission}}\" تغییر یافت",
|
"permission-changed": "مجوز Music Together به \"{{permission}}\" تغییر یافت",
|
||||||
"remove-song-failed": "حذف آهنگ با شکست مواجه شد",
|
"remove-song-failed": "حذف آهنگ با شکست مواجه شد",
|
||||||
"user-connected": "{{name}} به Music Together پیوست",
|
"user-connected": "{{name}} به Music Together پیوست",
|
||||||
@ -551,11 +576,11 @@
|
|||||||
},
|
},
|
||||||
"navigation": {
|
"navigation": {
|
||||||
"description": "بعدی/قبلی به طور مستقیم در رابط یکپارچه شدهاند، مانند مرورگر مورد علاقه شما",
|
"description": "بعدی/قبلی به طور مستقیم در رابط یکپارچه شدهاند، مانند مرورگر مورد علاقه شما",
|
||||||
"name": "ناوبری"
|
"name": "کنترل های رابط"
|
||||||
},
|
},
|
||||||
"no-google-login": {
|
"no-google-login": {
|
||||||
"description": "حذف دکمههای ورود به سیستم Google و لینکها از رابط",
|
"description": "حذف دکمهها و لینکهای ورود به گوگل از رابط کاربری",
|
||||||
"name": "بدون ورود به Google"
|
"name": "بدون ورود به گوگل"
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"description": "نمایش اعلان هنگامی که آهنگی شروع به پخش میکند (اعلانهای تعاملی در ویندوز در دسترس هستند)",
|
"description": "نمایش اعلان هنگامی که آهنگی شروع به پخش میکند (اعلانهای تعاملی در ویندوز در دسترس هستند)",
|
||||||
@ -566,11 +591,11 @@
|
|||||||
"submenu": {
|
"submenu": {
|
||||||
"hide-button-text": "مخفی کردن متن دکمه",
|
"hide-button-text": "مخفی کردن متن دکمه",
|
||||||
"refresh-on-play-pause": "تازهسازی در پخش/توقف",
|
"refresh-on-play-pause": "تازهسازی در پخش/توقف",
|
||||||
"tray-controls": "باز/بسته شدن با کلیک روی سینی"
|
"tray-controls": "باز/بسته شدن با کلیک روی آیکون در نوار وظیفه"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"priority": "اولویت اعلان",
|
"priority": "اولویت اعلان",
|
||||||
"toast-style": "سبک Toast",
|
"toast-style": "Toast سبک",
|
||||||
"unpause-notification": "نمایش اعلان هنگام از سرگیری پخش"
|
"unpause-notification": "نمایش اعلان هنگام از سرگیری پخش"
|
||||||
},
|
},
|
||||||
"name": "اعلانها"
|
"name": "اعلانها"
|
||||||
@ -578,8 +603,234 @@
|
|||||||
"picture-in-picture": {
|
"picture-in-picture": {
|
||||||
"description": "اجازه میدهد تا برنامه به حالت تصویر در تصویر تغییر کند",
|
"description": "اجازه میدهد تا برنامه به حالت تصویر در تصویر تغییر کند",
|
||||||
"menu": {
|
"menu": {
|
||||||
"always-on-top": "همیشه در بالا"
|
"always-on-top": "همیشه در بالا",
|
||||||
|
"hotkey": {
|
||||||
|
"label": "کلید میانبر",
|
||||||
|
"prompt": {
|
||||||
|
"keybind-options": {
|
||||||
|
"hotkey": "کلید میانبر"
|
||||||
|
},
|
||||||
|
"label": "یک کلید میانبر انتخاب کنید برای فعال/غیرفعال کردن حالت تصویر در تصویر",
|
||||||
|
"title": "کلید میانبر برای حالت تصویر در تصویر"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"save-window-position": "ذخیره موقعیت پنجره",
|
||||||
|
"save-window-size": "ذخیره اندازه پنجره",
|
||||||
|
"use-native-pip": "استفاده از حالت تصویر در تصویر اصلی مرورگر"
|
||||||
|
},
|
||||||
|
"name": "تصویر در تصویر",
|
||||||
|
"templates": {
|
||||||
|
"button": "تصویر در تصویر"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"playback-speed": {
|
||||||
|
"description": "به سرعت گوش بده، به آرامی گوش بده! یک دکمه کشویی برای تنظیم سرعت آهنگ اضافه شد",
|
||||||
|
"name": "سرعت پخش",
|
||||||
|
"templates": {
|
||||||
|
"button": "سرعت"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"precise-volume": {
|
||||||
|
"description": "کنترل دقیق صدا با استفاده از چرخ موس/میانبرها، همراه با HUD سفارشی و مراحل تنظیم حجم قابل تنظیم",
|
||||||
|
"menu": {
|
||||||
|
"arrows-shortcuts": "میانبرهای کلیدهای فلشی",
|
||||||
|
"custom-volume-steps": "مراحل تنظیم صدای دلخواه",
|
||||||
|
"global-shortcuts": "کلید های میانبر جهانی"
|
||||||
|
},
|
||||||
|
"name": "صدای دقیق",
|
||||||
|
"prompt": {
|
||||||
|
"global-shortcuts": {
|
||||||
|
"keybind-options": {
|
||||||
|
"decrease": "کاهش صدا",
|
||||||
|
"increase": "افزایش صدا"
|
||||||
|
},
|
||||||
|
"label": "انتخاب کلیدهای میانبر سراسری صدا:",
|
||||||
|
"title": "میانبرهای کلید سراسری صدا"
|
||||||
|
},
|
||||||
|
"volume-steps": {
|
||||||
|
"label": "مراحل انتخاب افزایش/کاهش صدا",
|
||||||
|
"title": "سطح صدا"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"quality-changer": {
|
||||||
|
"backend": {
|
||||||
|
"dialog": {
|
||||||
|
"quality-changer": {
|
||||||
|
"detail": "کیفیت کنونی: {{quality}}",
|
||||||
|
"message": "انتخاب کیفیت ویدیو:",
|
||||||
|
"title": "انتخاب کیفیت ویدیو"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"description": "امکان تغییر کیفیت ویدیو با استفاده از دکمه در رابط پخش ویدیو",
|
||||||
|
"name": "تغییر دهنده کیفیت ویدیو"
|
||||||
|
},
|
||||||
|
"scrobbler": {
|
||||||
|
"description": "اضافه کردن پشتیبانی از اسکرابلینگ (etc. last.fm, Listenbrainz)",
|
||||||
|
"dialog": {
|
||||||
|
"lastfm": {
|
||||||
|
"auth-failed": {
|
||||||
|
"message": "احراز هویت با Last.fm ناموفق بود\nپنجره شناور را تا راهاندازی مجدد بعدی مخفی کن.",
|
||||||
|
"title": "احراز هویت ناموفق بود"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"menu": {
|
||||||
|
"lastfm": {
|
||||||
|
"api-settings": "تنظیمات \"Last.fm \"API"
|
||||||
|
},
|
||||||
|
"listenbrainz": {
|
||||||
|
"token": "توکن کاربری ListenBrainz را وارد کنید"
|
||||||
|
},
|
||||||
|
"scrobble-other-media": "ردیابی رسانههای دیگر"
|
||||||
|
},
|
||||||
|
"name": "ابزار ثبتکنندهی آهنگ",
|
||||||
|
"prompt": {
|
||||||
|
"lastfm": {
|
||||||
|
"api-key": "کلید Last.fm API",
|
||||||
|
"api-secret": "API مخفی Last.fm"
|
||||||
|
},
|
||||||
|
"listenbrainz": {
|
||||||
|
"token": {
|
||||||
|
"label": "توکن کاربری ListenBrainz خود را وارد کنید:",
|
||||||
|
"title": "توکن ListenBrainz"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"shortcuts": {
|
||||||
|
"description": "امکان تنظیم میانبرهای سراسری برای کنترل (پخش/توقف/بعدی/قبلی) و خاموش کردن OSD رسانه با بازنویسی کلیدهای رسانهای، فعالسازی Ctrl/CMD + F برای جستجو، فعالسازی پشتیبانی MPRIS در لینوکس برای کلیدهای رسانهای، و میانبرهای سفارشی برای کاربران پیشرفته",
|
||||||
|
"menu": {
|
||||||
|
"override-media-keys": "تغییر عملکرد کلیدهای رسانه",
|
||||||
|
"set-keybinds": "تنظیم کنترلهای سراسری آهنگ"
|
||||||
|
},
|
||||||
|
"name": "میانبرها (& MPRIS)",
|
||||||
|
"prompt": {
|
||||||
|
"keybind": {
|
||||||
|
"keybind-options": {
|
||||||
|
"next": "بعدی",
|
||||||
|
"play-pause": "پخش / توقف",
|
||||||
|
"previous": "قبلی"
|
||||||
|
},
|
||||||
|
"label": "انتخاب میانبرهای سراسری برای کنترل آهنگها:",
|
||||||
|
"title": "میانبرهای کلیدی سراسری"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"skip-disliked-songs": {
|
||||||
|
"description": "خودکار آهنگ های غیر موردعلاقه رد میشن",
|
||||||
|
"name": "رد آهنگهای غیر مورد علاقه"
|
||||||
|
},
|
||||||
|
"skip-silences": {
|
||||||
|
"description": "رد خودکار بخشهای بی صدا آهنگ ها",
|
||||||
|
"name": "رد بخشهای بیصدا"
|
||||||
|
},
|
||||||
|
"sponsorblock": {
|
||||||
|
"description": "بهطور خودکار بخشهای غیرموسیقی مانند مقدمه/پایان یا قسمتهایی از ویدیوهای موسیقی که آهنگ در آن پخش نمیشود را رد میکند",
|
||||||
|
"name": "مسدودکننده اسپانسر"
|
||||||
|
},
|
||||||
|
"synced-lyrics": {
|
||||||
|
"description": "ارائه متن ترانهها به صورت هماهنگ با آهنگها، با استفاده از ارائهدهندگانی مانند LRClib.",
|
||||||
|
"errors": {
|
||||||
|
"fetch": "⚠️هنگام بارگیری متن ترانه خطایی رخ داده است.\n\tلطفاً بعداً دوباره تلاش کنید.",
|
||||||
|
"not-found": "⚠️ متنی برای این ترانه پیدا نشد."
|
||||||
|
},
|
||||||
|
"menu": {
|
||||||
|
"default-text-string": {
|
||||||
|
"label": "حرف/کاراکتر پیشفرض بین متنهای ترانه",
|
||||||
|
"tooltip": "حرف/کاراکتر پیشفرض را برای فاصله بین متنهای ترانه انتخاب کنید"
|
||||||
|
},
|
||||||
|
"line-effect": {
|
||||||
|
"label": "افکت خط متن",
|
||||||
|
"submenu": {
|
||||||
|
"fancy": {
|
||||||
|
"label": "شیک",
|
||||||
|
"tooltip": "استفاده از افکتهای بزرگ و شبیه به اپلیکیشنها برای خط فعلی"
|
||||||
|
},
|
||||||
|
"focus": {
|
||||||
|
"label": "تمرکز",
|
||||||
|
"tooltip": "فقط خط فعلی رو سفید کن"
|
||||||
|
},
|
||||||
|
"offset": {
|
||||||
|
"label": "جابجایی",
|
||||||
|
"tooltip": "جابجایی خط فعلی به سمت راست"
|
||||||
|
},
|
||||||
|
"scale": {
|
||||||
|
"label": "مقیاس",
|
||||||
|
"tooltip": "تغییر اندازه خط فعلی"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tooltip": "افکت مورد نظر را برای خط فعلی انتخاب کنید"
|
||||||
|
},
|
||||||
|
"precise-timing": {
|
||||||
|
"label": "هماهنگسازی کامل متن ترانه",
|
||||||
|
"tooltip": "محاسبه دقیق نمایش خط بعدی تا میلیثانیه (ممکن است تاثیر کمی بر عملکرد داشته باشد)"
|
||||||
|
},
|
||||||
|
"show-lyrics-even-if-inexact": {
|
||||||
|
"label": "نمایش متن ترانه ها حتی اگر دقیق نباشد",
|
||||||
|
"tooltip": "اگر آهنگ پیدا نشد، افزونه دوباره با یک جستجوی متفاوت امتحان میکند.\nنتیجهی این تلاش ممکن است دقیق نباشد."
|
||||||
|
},
|
||||||
|
"show-time-codes": {
|
||||||
|
"label": "نمایش زمانبندیها",
|
||||||
|
"tooltip": "نمایش زمانبندیها کنار متن ترانه"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"name": "متن ترانه هماهنگ شد",
|
||||||
|
"refetch-btn": {
|
||||||
|
"fetching": "در حال بارگذاری...",
|
||||||
|
"normal": "دریافت مجدد متن ترانه"
|
||||||
|
},
|
||||||
|
"warnings": {
|
||||||
|
"duration-mismatch": "⚠️ - ممکن است متن ترانه به دلیل عدم تطابق زمان با مشکل هماهنگی مواجه شود.",
|
||||||
|
"inexact": "⚠️ - ممکن است متن ترانه برای این آهنگ دقیق نباشد",
|
||||||
|
"instrumental": "⚠️ - این آهنگ بی کلام است"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"taskbar-mediacontrol": {
|
||||||
|
"description": "کنترل پخش از نوار وظیفه ویندوز(taskbar)",
|
||||||
|
"name": "کنترل رسانه از نوار وظیفه (taskbar)"
|
||||||
|
},
|
||||||
|
"touchbar": {
|
||||||
|
"description": "افزودن ویجت TouchBar برای کاربران macOS",
|
||||||
|
"name": "نوار لمسی"
|
||||||
|
},
|
||||||
|
"tuna-obs": {
|
||||||
|
"description": "ادغام با پلاگین Tuna در OBS",
|
||||||
|
"name": "Tuna OBS"
|
||||||
|
},
|
||||||
|
"video-toggle": {
|
||||||
|
"description": "دکمهای اضافه میکند برای جابجایی بین حالت ویدیو/آهنگ. همچنین به صورت اختیاری میتواند تب ویدیو را حذف کند",
|
||||||
|
"menu": {
|
||||||
|
"align": {
|
||||||
|
"label": "چینش",
|
||||||
|
"submenu": {
|
||||||
|
"left": "چپ",
|
||||||
|
"middle": "میانه",
|
||||||
|
"right": "راست"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"force-hide": "حذف اجباری تب ویدیو",
|
||||||
|
"mode": {
|
||||||
|
"label": "حالت",
|
||||||
|
"submenu": {
|
||||||
|
"custom": "حالت شخصیسازی شده",
|
||||||
|
"disabled": "غیرفعال",
|
||||||
|
"native": "حالت پیشفرض"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"name": "ویدیو به آهنگ",
|
||||||
|
"templates": {
|
||||||
|
"button": "ترانه"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"visualizer": {
|
||||||
|
"description": "اضافه کردن نمایشدهنده تصویری به پخشکننده",
|
||||||
|
"menu": {
|
||||||
|
"visualizer-type": "نوع نمایشدهنده تصویری"
|
||||||
|
},
|
||||||
|
"name": "نمایشدهنده تصویری"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -279,6 +279,12 @@
|
|||||||
},
|
},
|
||||||
"name": "Ambient Mode"
|
"name": "Ambient Mode"
|
||||||
},
|
},
|
||||||
|
"amuse": {
|
||||||
|
"description": "Nagdaragdag ng suporta sa YouTube Music para sa Amuse now playing widget ng 6K Labs",
|
||||||
|
"response": {
|
||||||
|
"query": "Tumatakbo ang Amuse API server. Gamitin ang GET /query para makuha ang impo ng kanta."
|
||||||
|
}
|
||||||
|
},
|
||||||
"api-server": {
|
"api-server": {
|
||||||
"description": "Nagdadagdag ng API Server upang kontrolin ang player",
|
"description": "Nagdadagdag ng API Server upang kontrolin ang player",
|
||||||
"dialog": {
|
"dialog": {
|
||||||
@ -468,6 +474,14 @@
|
|||||||
"button": "Mag-download"
|
"button": "Mag-download"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"equalizer": {
|
||||||
|
"description": "Nagdaragdag ng equalizer sa player",
|
||||||
|
"menu": {
|
||||||
|
"presets": {
|
||||||
|
"label": "Mga Preset"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"exponential-volume": {
|
"exponential-volume": {
|
||||||
"description": "Ginagawang exponential ang volume slider para mas madaling pumili ng mas mababang volume."
|
"description": "Ginagawang exponential ang volume slider para mas madaling pumili ng mas mababang volume."
|
||||||
},
|
},
|
||||||
@ -674,8 +688,8 @@
|
|||||||
"synced-lyrics": {
|
"synced-lyrics": {
|
||||||
"description": "Nagbibigay ng naka-sync na lyrics sa mga kanta, gamit ang mga provider tulad ng LRClib.",
|
"description": "Nagbibigay ng naka-sync na lyrics sa mga kanta, gamit ang mga provider tulad ng LRClib.",
|
||||||
"errors": {
|
"errors": {
|
||||||
"fetch": "⚠️ - Nagkaroon ng error habang kinukuha ang lyrics. Subukang muli mamaya.",
|
"fetch": "⚠️\t Nagkaroon ng error habang kinukuha ang lyrics.\n\t Subukang muli mamaya.",
|
||||||
"not-found": "⚠️ - Walang nakitang lyrics para sa kantang ito."
|
"not-found": "⚠️ Walang nakitang lyrics para sa kantang ito."
|
||||||
},
|
},
|
||||||
"menu": {
|
"menu": {
|
||||||
"default-text-string": {
|
"default-text-string": {
|
||||||
@ -685,6 +699,10 @@
|
|||||||
"line-effect": {
|
"line-effect": {
|
||||||
"label": "Effect ng Linya",
|
"label": "Effect ng Linya",
|
||||||
"submenu": {
|
"submenu": {
|
||||||
|
"fancy": {
|
||||||
|
"label": "Magarbo",
|
||||||
|
"tooltip": "Gumamit ng malaki, mala-app na effect sa kasalukuyang linya"
|
||||||
|
},
|
||||||
"focus": {
|
"focus": {
|
||||||
"tooltip": "Gawing puti lamang ang kasalukuyang linya"
|
"tooltip": "Gawing puti lamang ang kasalukuyang linya"
|
||||||
},
|
},
|
||||||
|
|||||||
@ -53,7 +53,8 @@
|
|||||||
"later": "אחר כך",
|
"later": "אחר כך",
|
||||||
"restart-now": "מתחיל את התוכנה מחדש עכשיו"
|
"restart-now": "מתחיל את התוכנה מחדש עכשיו"
|
||||||
},
|
},
|
||||||
"message": "נדרש אתחול",
|
"detail": "\"{{pluginName}}\" מצריך אתחול",
|
||||||
|
"message": "\"{{pluginName}}\" דורש אתחול",
|
||||||
"title": "נדרשת הפעלה מחדש"
|
"title": "נדרשת הפעלה מחדש"
|
||||||
},
|
},
|
||||||
"unresponsive": {
|
"unresponsive": {
|
||||||
@ -70,9 +71,10 @@
|
|||||||
"buttons": {
|
"buttons": {
|
||||||
"disable": "בטל עדכונים",
|
"disable": "בטל עדכונים",
|
||||||
"download": "הורדה",
|
"download": "הורדה",
|
||||||
"ok": "אוקי"
|
"ok": "אוקיי"
|
||||||
},
|
},
|
||||||
"message": "גירסא חדשה זמינה כעת",
|
"detail": "גרסה חדשה זמינה, ניתן להוריד אותה ב-{{downloadLink}}",
|
||||||
|
"message": "גירסה חדשה זמינה כעת",
|
||||||
"title": "קיים עדכון חדש"
|
"title": "קיים עדכון חדש"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@ -226,10 +226,50 @@
|
|||||||
"description": "एल्बम रंग पैलेट के आधार पर एक गतिशील थीम और दृश्य प्रभाव लागू करता है",
|
"description": "एल्बम रंग पैलेट के आधार पर एक गतिशील थीम और दृश्य प्रभाव लागू करता है",
|
||||||
"menu": {
|
"menu": {
|
||||||
"color-mix-ratio": {
|
"color-mix-ratio": {
|
||||||
|
"label": "रंग मिश्रण अनुपात",
|
||||||
"submenu": {
|
"submenu": {
|
||||||
"percent": "{{ratio}}%"
|
"percent": "{{ratio}}%"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"name": "एल्बम रंग थीम"
|
||||||
|
},
|
||||||
|
"ambient-mode": {
|
||||||
|
"description": "वीडियो से हल्के रंगों को आपकी स्क्रीन की पृष्ठभूमि में डालकर एक प्रकाश प्रभाव लागू करता है",
|
||||||
|
"menu": {
|
||||||
|
"blur-amount": {
|
||||||
|
"label": "धुंधलापन मात्रा",
|
||||||
|
"submenu": {
|
||||||
|
"pixels": "{{blurAmount}} पिक्सल"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"buffer": {
|
||||||
|
"label": "बफर",
|
||||||
|
"submenu": {
|
||||||
|
"buffer": "{{buffer}}"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"opacity": {
|
||||||
|
"label": "अस्पष्टता",
|
||||||
|
"submenu": {
|
||||||
|
"percent": "{{opacity}}%"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"quality": {
|
||||||
|
"label": "गुणवत्ता",
|
||||||
|
"submenu": {
|
||||||
|
"pixels": "{{quality}} पिक्सल"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"size": {
|
||||||
|
"label": "माप",
|
||||||
|
"submenu": {
|
||||||
|
"percent": "{{size}}%"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"smoothness-transition": {
|
||||||
|
"label": "चिकनाई संक्रमण"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"video-toggle": {
|
"video-toggle": {
|
||||||
|
|||||||
@ -279,6 +279,13 @@
|
|||||||
},
|
},
|
||||||
"name": "Mode ambient"
|
"name": "Mode ambient"
|
||||||
},
|
},
|
||||||
|
"amuse": {
|
||||||
|
"description": "Menambahkan dukungan YouTube Music untuk widget Amuse yang sedang diputar oleh 6K Labs",
|
||||||
|
"name": "Amuse",
|
||||||
|
"response": {
|
||||||
|
"query": "Server API Amuse sedang berjalan. GET /query untuk mendapatkan info lagu."
|
||||||
|
}
|
||||||
|
},
|
||||||
"api-server": {
|
"api-server": {
|
||||||
"description": "Menambahkan server API untuk mengontrol pemutar",
|
"description": "Menambahkan server API untuk mengontrol pemutar",
|
||||||
"dialog": {
|
"dialog": {
|
||||||
@ -726,8 +733,8 @@
|
|||||||
"synced-lyrics": {
|
"synced-lyrics": {
|
||||||
"description": "Menyediakan lirik lagu yang disinkronkan, menggunakan penyedia seperti LRClib.",
|
"description": "Menyediakan lirik lagu yang disinkronkan, menggunakan penyedia seperti LRClib.",
|
||||||
"errors": {
|
"errors": {
|
||||||
"fetch": "⚠️ - Terjadi kesalahan saat mengambil lirik. Coba lagi nanti.",
|
"fetch": "⚠️\tTerjadi kesalahan saat mengambil lirik.\n\tSilakan coba lagi nanti.",
|
||||||
"not-found": "⚠️ - Tidak ada lirik yang ditemukan untuk lagu ini."
|
"not-found": "⚠️ Tidak ada lirik yang ditemukan untuk lagu ini."
|
||||||
},
|
},
|
||||||
"menu": {
|
"menu": {
|
||||||
"default-text-string": {
|
"default-text-string": {
|
||||||
@ -737,6 +744,10 @@
|
|||||||
"line-effect": {
|
"line-effect": {
|
||||||
"label": "Efek garis",
|
"label": "Efek garis",
|
||||||
"submenu": {
|
"submenu": {
|
||||||
|
"fancy": {
|
||||||
|
"label": "Mewah",
|
||||||
|
"tooltip": "Gunakan efek besar seperti aplikasi pada baris saat ini"
|
||||||
|
},
|
||||||
"focus": {
|
"focus": {
|
||||||
"label": "Fokus",
|
"label": "Fokus",
|
||||||
"tooltip": "Jadikan hanya baris saat ini berwarna putih"
|
"tooltip": "Jadikan hanya baris saat ini berwarna putih"
|
||||||
|
|||||||
@ -541,7 +541,7 @@
|
|||||||
"menu": {
|
"menu": {
|
||||||
"click-to-copy-id": "호스트 아이디 복사",
|
"click-to-copy-id": "호스트 아이디 복사",
|
||||||
"close": "Music Together 닫기",
|
"close": "Music Together 닫기",
|
||||||
"connected-users": "연결된 사용자",
|
"connected-users": "연결된 사용자: {{count}}명",
|
||||||
"disconnect": "Music Together 연결 끊기",
|
"disconnect": "Music Together 연결 끊기",
|
||||||
"empty-user": "연결된 사용자 없음",
|
"empty-user": "연결된 사용자 없음",
|
||||||
"host": "Music Together 호스트",
|
"host": "Music Together 호스트",
|
||||||
|
|||||||
@ -279,6 +279,12 @@
|
|||||||
},
|
},
|
||||||
"name": "Chế độ Môi trường xung quanh"
|
"name": "Chế độ Môi trường xung quanh"
|
||||||
},
|
},
|
||||||
|
"amuse": {
|
||||||
|
"name": "Amuse",
|
||||||
|
"response": {
|
||||||
|
"query": "Máy chủ API của Amuse đang chạy. GET /query để lấy thông tin về bài hát."
|
||||||
|
}
|
||||||
|
},
|
||||||
"api-server": {
|
"api-server": {
|
||||||
"description": "Thêm máy chủ API để điều khiển trình phát",
|
"description": "Thêm máy chủ API để điều khiển trình phát",
|
||||||
"dialog": {
|
"dialog": {
|
||||||
@ -299,7 +305,7 @@
|
|||||||
"label": "Xác thực ngay yêu cầu đầu tiên"
|
"label": "Xác thực ngay yêu cầu đầu tiên"
|
||||||
},
|
},
|
||||||
"none": {
|
"none": {
|
||||||
"label": "Không/Chưa xác thực (Need context)"
|
"label": "Không xác thực"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@ -134,14 +134,6 @@ if (is.linux()) {
|
|||||||
// Overrides WM_CLASS for X11 to correspond to icon filename
|
// Overrides WM_CLASS for X11 to correspond to icon filename
|
||||||
app.setName('com.github.th_ch.youtube_music');
|
app.setName('com.github.th_ch.youtube_music');
|
||||||
|
|
||||||
// Workaround for issue #2248
|
|
||||||
if (
|
|
||||||
process.env.XDG_SESSION_TYPE === 'wayland' ||
|
|
||||||
process.env.WAYLAND_DISPLAY
|
|
||||||
) {
|
|
||||||
app.commandLine.appendSwitch('disable-gpu-memory-buffer-video-frames');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Stops chromium from launching its own MPRIS service
|
// Stops chromium from launching its own MPRIS service
|
||||||
if (config.plugins.isEnabled('shortcuts')) {
|
if (config.plugins.isEnabled('shortcuts')) {
|
||||||
app.commandLine.appendSwitch('disable-features', 'MediaSessionService');
|
app.commandLine.appendSwitch('disable-features', 'MediaSessionService');
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { FastAverageColor } from 'fast-average-color';
|
import { FastAverageColor } from 'fast-average-color';
|
||||||
import Color from 'color';
|
import Color, { ColorInstance } from 'color';
|
||||||
|
|
||||||
import style from './style.css?inline';
|
import style from './style.css?inline';
|
||||||
|
|
||||||
@ -14,8 +14,8 @@ export default createPlugin<
|
|||||||
unknown,
|
unknown,
|
||||||
unknown,
|
unknown,
|
||||||
{
|
{
|
||||||
color?: Color;
|
color?: ColorInstance;
|
||||||
darkColor?: Color;
|
darkColor?: ColorInstance;
|
||||||
|
|
||||||
playerPage: HTMLElement | null;
|
playerPage: HTMLElement | null;
|
||||||
navBarBackground: HTMLElement | null;
|
navBarBackground: HTMLElement | null;
|
||||||
|
|||||||
@ -720,7 +720,7 @@ export const register = (
|
|||||||
|
|
||||||
app.openapi(routes.addSongToQueue, (ctx) => {
|
app.openapi(routes.addSongToQueue, (ctx) => {
|
||||||
const { videoId, insertPosition } = ctx.req.valid('json');
|
const { videoId, insertPosition } = ctx.req.valid('json');
|
||||||
controller.addSongToQueue(videoId, insertPosition);
|
controller.addSongToQueue(videoId, { queueInsertPosition: insertPosition });
|
||||||
|
|
||||||
ctx.status(204);
|
ctx.status(204);
|
||||||
return ctx.body(null);
|
return ctx.body(null);
|
||||||
|
|||||||
@ -154,15 +154,14 @@ export const backend = createBackend<
|
|||||||
// @see https://discord.com/developers/docs/topics/gateway#activity-object
|
// @see https://discord.com/developers/docs/topics/gateway#activity-object
|
||||||
// not all options are transfered through https://github.com/discordjs/RPC/blob/6f83d8d812c87cb7ae22064acd132600407d7d05/src/client.js#L518-530
|
// not all options are transfered through https://github.com/discordjs/RPC/blob/6f83d8d812c87cb7ae22064acd132600407d7d05/src/client.js#L518-530
|
||||||
const hangulFillerUnicodeCharacter = '\u3164'; // This is an empty character
|
const hangulFillerUnicodeCharacter = '\u3164'; // This is an empty character
|
||||||
if (songInfo.title.length < 2) {
|
const paddedInfoKeys: (keyof SongInfo)[] = ['title', 'artist', 'album'];
|
||||||
songInfo.title += hangulFillerUnicodeCharacter.repeat(
|
for (const key of paddedInfoKeys) {
|
||||||
2 - songInfo.title.length,
|
const keyLength = (songInfo[key] as string)?.length;
|
||||||
);
|
if (keyLength < 2) {
|
||||||
}
|
(songInfo[key] as string) += hangulFillerUnicodeCharacter.repeat(
|
||||||
if (songInfo.artist.length < 2) {
|
2 - keyLength,
|
||||||
songInfo.artist += hangulFillerUnicodeCharacter.repeat(
|
);
|
||||||
2 - songInfo.title.length,
|
}
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// see https://github.com/th-ch/youtube-music/issues/1664
|
// see https://github.com/th-ch/youtube-music/issues/1664
|
||||||
|
|||||||
@ -55,10 +55,20 @@ let yt: Innertube;
|
|||||||
let win: BrowserWindow;
|
let win: BrowserWindow;
|
||||||
let playingUrl: string;
|
let playingUrl: string;
|
||||||
|
|
||||||
const isYouTubePremium = () =>
|
const isYouTubeMusicPremium = async () => {
|
||||||
win.webContents.executeJavaScript(
|
const upgradeBtnIconPathData = (await win.webContents.executeJavaScript(
|
||||||
'!document.querySelector(\'#endpoint[href="/music_premium"]\')',
|
'document.querySelector(\'iron-iconset-svg[name="yt-sys-icons"] #youtube_music_monochrome\')?.firstChild?.getAttribute("d")?.substring(0, 15)',
|
||||||
) as Promise<boolean>;
|
)) as string | null;
|
||||||
|
|
||||||
|
// Fallback to non-premium if the icon is not found
|
||||||
|
if (!upgradeBtnIconPathData) return false;
|
||||||
|
|
||||||
|
const selector = `ytmusic-guide-entry-renderer:has(> tp-yt-paper-item > yt-icon path[d^="${upgradeBtnIconPathData}"])`;
|
||||||
|
|
||||||
|
return (await win.webContents.executeJavaScript(
|
||||||
|
`!document.querySelector('${selector}')`,
|
||||||
|
)) as boolean;
|
||||||
|
};
|
||||||
|
|
||||||
const sendError = (error: Error, source?: string) => {
|
const sendError = (error: Error, source?: string) => {
|
||||||
win.setProgressBar(-1); // Close progress bar
|
win.setProgressBar(-1); // Close progress bar
|
||||||
@ -116,6 +126,11 @@ export const onMainLoad = async ({
|
|||||||
const visitorData = yt.session.context.client.visitorData;
|
const visitorData = yt.session.context.client.visitorData;
|
||||||
|
|
||||||
if (visitorData) {
|
if (visitorData) {
|
||||||
|
const cleanUp = (context: Partial<typeof globalThis>) => {
|
||||||
|
delete context.window;
|
||||||
|
delete context.document;
|
||||||
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const [width, height] = win.getSize();
|
const [width, height] = win.getSize();
|
||||||
// emulate jsdom using linkedom
|
// emulate jsdom using linkedom
|
||||||
@ -153,16 +168,16 @@ export const onMainLoad = async ({
|
|||||||
program: bgChallenge.program,
|
program: bgChallenge.program,
|
||||||
globalName: bgChallenge.globalName,
|
globalName: bgChallenge.globalName,
|
||||||
bgConfig,
|
bgConfig,
|
||||||
|
}).finally(() => {
|
||||||
|
cleanUp(globalThis);
|
||||||
});
|
});
|
||||||
|
|
||||||
yt.session.po_token = poTokenResult.poToken;
|
yt.session.po_token = poTokenResult.poToken;
|
||||||
|
} else {
|
||||||
|
cleanUp(globalThis);
|
||||||
}
|
}
|
||||||
} finally {
|
} catch {
|
||||||
// Bypass TypeScript checks
|
cleanUp(globalThis);
|
||||||
((x: Partial<typeof globalThis>) => {
|
|
||||||
delete x.window;
|
|
||||||
delete x.document;
|
|
||||||
})(globalThis);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -365,7 +380,7 @@ async function downloadSongUnsafe(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const downloadOptions: FormatOptions = {
|
const downloadOptions: FormatOptions = {
|
||||||
type: (await isYouTubePremium()) ? 'audio' : 'video+audio', // Audio, video or video+audio
|
type: (await isYouTubeMusicPremium()) ? 'audio' : 'video+audio', // Audio, video or video+audio
|
||||||
quality: 'best', // Best, bestefficiency, 144p, 240p, 480p, 720p and so on.
|
quality: 'best', // Best, bestefficiency, 144p, 240p, 480p, 720p and so on.
|
||||||
format: 'any', // Media container format
|
format: 'any', // Media container format
|
||||||
};
|
};
|
||||||
|
|||||||
117
src/plugins/music-together/api/guest.ts
Normal file
117
src/plugins/music-together/api/guest.ts
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
import { setQueue } from '../store/queue';
|
||||||
|
|
||||||
|
import { ConnectionEventUnion, MusicTogetherConfig, VideoData } from '../types';
|
||||||
|
import { setStatus } from '../store/status';
|
||||||
|
import { IPC } from '../constants';
|
||||||
|
import { Connection } from '../connection';
|
||||||
|
|
||||||
|
import type { AppElement } from '@/types/queue';
|
||||||
|
import type { RendererContext } from '@/types/contexts';
|
||||||
|
|
||||||
|
type BuildListenerOptions = {
|
||||||
|
ipc: RendererContext<MusicTogetherConfig>['ipc'];
|
||||||
|
app: AppElement;
|
||||||
|
};
|
||||||
|
export const Guest = {
|
||||||
|
buildListener: (_: Connection, { ipc, app }: BuildListenerOptions) => {
|
||||||
|
const listener = async (event: ConnectionEventUnion) => {
|
||||||
|
switch (event.type) {
|
||||||
|
case 'ADD_SONGS': {
|
||||||
|
await ipc.invoke(
|
||||||
|
IPC.addSongToQueue,
|
||||||
|
event.payload.videoList.map((v) => v.videoId),
|
||||||
|
{
|
||||||
|
index: event.payload.index,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
setQueue('queue', (queue) => {
|
||||||
|
const result: VideoData[] = [...queue];
|
||||||
|
|
||||||
|
if (event.payload.index) {
|
||||||
|
result.splice(event.payload.index, 0, ...event.payload.videoList);
|
||||||
|
} else {
|
||||||
|
result.push(...event.payload.videoList);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'REMOVE_SONG': {
|
||||||
|
await ipc.invoke(IPC.removeSongFromQueue, event.payload.index);
|
||||||
|
|
||||||
|
setQueue('queue', (queue) => {
|
||||||
|
const result: VideoData[] = [...queue];
|
||||||
|
result.splice(event.payload.index, 1);
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'MOVE_SONG': {
|
||||||
|
await ipc.invoke(
|
||||||
|
IPC.moveSongInQueue,
|
||||||
|
event.payload.fromIndex,
|
||||||
|
event.payload.toIndex,
|
||||||
|
);
|
||||||
|
|
||||||
|
setQueue('queue', (queue) => {
|
||||||
|
const result: VideoData[] = [...queue];
|
||||||
|
const [removed] = result.splice(event.payload.fromIndex, 1);
|
||||||
|
result.splice(event.payload.toIndex, 0, removed);
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'IDENTIFY': {
|
||||||
|
console.warn('Music Together [Guest]: Not allowed Event', event);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'SYNC_USER': {
|
||||||
|
setStatus('users', event.payload?.users ?? []);
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'PERMISSION': {
|
||||||
|
const permission = event.payload;
|
||||||
|
if (!permission) break;
|
||||||
|
|
||||||
|
setStatus('permission', permission);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'SYNC_QUEUE': {
|
||||||
|
await ipc.invoke(IPC.clearQueue);
|
||||||
|
await ipc.invoke(
|
||||||
|
IPC.addSongToQueue,
|
||||||
|
event.payload?.videoList.map((v) => v.videoId),
|
||||||
|
{
|
||||||
|
queueInsertPosition: 'INSERT_AT_END',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
setQueue('queue', event.payload?.videoList ?? []);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'SYNC_PROGRESS': {
|
||||||
|
if (typeof event.payload?.progress === 'number') {
|
||||||
|
app.playerApi?.seekTo(event.payload.progress);
|
||||||
|
}
|
||||||
|
if (app.playerApi?.getPlayerState() !== event.payload?.state) {
|
||||||
|
if (event.payload?.state === 2) app.playerApi?.pauseVideo();
|
||||||
|
if (event.payload?.state === 1) app.playerApi?.playVideo();
|
||||||
|
}
|
||||||
|
if (typeof event.payload?.index === 'number') {
|
||||||
|
await ipc.invoke(IPC.setQueueIndex, event.payload.index);
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
console.warn('Music Together [Host]: Unknown Event', event);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return listener;
|
||||||
|
},
|
||||||
|
};
|
||||||
140
src/plugins/music-together/api/host.ts
Normal file
140
src/plugins/music-together/api/host.ts
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
import { DataConnection } from 'peerjs';
|
||||||
|
|
||||||
|
import { RendererContext } from '@/types/contexts';
|
||||||
|
|
||||||
|
import { queue } from '@/plugins/music-together/store/queue';
|
||||||
|
|
||||||
|
import { AppElement } from '@/types/queue';
|
||||||
|
|
||||||
|
import { ConnectionEventUnion, MusicTogetherConfig } from '../types';
|
||||||
|
import { setStatus, status } from '../store/status';
|
||||||
|
import { IPC } from '../constants';
|
||||||
|
import { Connection } from '../connection';
|
||||||
|
|
||||||
|
type BuildListenerOptions = {
|
||||||
|
ipc: RendererContext<MusicTogetherConfig>['ipc'];
|
||||||
|
app: AppElement;
|
||||||
|
};
|
||||||
|
export const Host = {
|
||||||
|
buildListener: (conn: Connection, { ipc, app }: BuildListenerOptions) => {
|
||||||
|
const listener = async (
|
||||||
|
event: ConnectionEventUnion,
|
||||||
|
dataConnection?: DataConnection,
|
||||||
|
) => {
|
||||||
|
switch (event.type) {
|
||||||
|
case 'ADD_SONGS': {
|
||||||
|
if (dataConnection && status.permission === 'host-only') return;
|
||||||
|
|
||||||
|
await ipc.invoke(
|
||||||
|
IPC.addSongToQueue,
|
||||||
|
event.payload.videoList.map((v) => v.videoId),
|
||||||
|
{
|
||||||
|
index: event.payload.index,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
console.log('ADD_SONGS', event);
|
||||||
|
await conn?.broadcast(event.type, event.payload);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'REMOVE_SONG': {
|
||||||
|
if (dataConnection && status.permission === 'host-only') return;
|
||||||
|
|
||||||
|
await ipc.invoke(IPC.removeSongFromQueue, event.payload.index);
|
||||||
|
await conn?.broadcast(event.type, event.payload);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'MOVE_SONG': {
|
||||||
|
if (dataConnection && status.permission === 'host-only') {
|
||||||
|
// await conn.broadcast('SYNC_QUEUE', {
|
||||||
|
// videoList: queue?.videoList ?? [],
|
||||||
|
// });
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
await ipc.invoke(
|
||||||
|
IPC.moveSongInQueue,
|
||||||
|
event.payload.fromIndex,
|
||||||
|
event.payload.toIndex,
|
||||||
|
);
|
||||||
|
await conn?.broadcast(event.type, event.payload);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'IDENTIFY': {
|
||||||
|
const newUser = event.payload?.user;
|
||||||
|
if (!newUser) return;
|
||||||
|
|
||||||
|
// api?.toastService?.show(
|
||||||
|
// t('plugins.music-together.toast.user-connected', {
|
||||||
|
// name: event.payload.profile.name,
|
||||||
|
// }),
|
||||||
|
// );
|
||||||
|
|
||||||
|
setStatus('users', (users) => [...users, newUser]);
|
||||||
|
await conn?.broadcast('SYNC_USER', {
|
||||||
|
users: status.users,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'SYNC_USER': {
|
||||||
|
await conn?.broadcast('SYNC_USER', {
|
||||||
|
users: status.users,
|
||||||
|
});
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'PERMISSION': {
|
||||||
|
await conn?.broadcast('PERMISSION', status.permission);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'SYNC_QUEUE': {
|
||||||
|
await conn?.broadcast('SYNC_QUEUE', {
|
||||||
|
videoList: queue.queue,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'SYNC_PROGRESS': {
|
||||||
|
let permissionLevel = 0;
|
||||||
|
if (status.permission === 'all') permissionLevel = 2;
|
||||||
|
if (status.permission === 'playlist') permissionLevel = 1;
|
||||||
|
if (status.permission === 'host-only') permissionLevel = 0;
|
||||||
|
if (!conn) permissionLevel = 3;
|
||||||
|
|
||||||
|
if (permissionLevel >= 2) {
|
||||||
|
if (typeof event.payload?.progress === 'number') {
|
||||||
|
const currentTime = app.playerApi?.getCurrentTime() ?? 0;
|
||||||
|
const offset = Math.abs(event.payload.progress - currentTime);
|
||||||
|
if (offset > 3)
|
||||||
|
app.playerApi?.seekTo(event.payload.progress + offset);
|
||||||
|
}
|
||||||
|
if (app.playerApi?.getPlayerState() !== event.payload?.state) {
|
||||||
|
if (event.payload?.state === 2) app.playerApi?.pauseVideo();
|
||||||
|
if (event.payload?.state === 1) app.playerApi?.playVideo();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (permissionLevel >= 1) {
|
||||||
|
if (typeof event.payload?.index === 'number') {
|
||||||
|
await ipc.invoke(IPC.setQueueIndex, event.payload.index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await conn?.broadcast('SYNC_PROGRESS', event.payload);
|
||||||
|
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, dataConnection);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return listener;
|
||||||
|
},
|
||||||
|
};
|
||||||
0
src/plugins/music-together/api/queue.ts
Normal file
0
src/plugins/music-together/api/queue.ts
Normal file
53
src/plugins/music-together/backend.ts
Normal file
53
src/plugins/music-together/backend.ts
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import prompt from 'custom-electron-prompt';
|
||||||
|
|
||||||
|
import { MusicTogetherConfig } from './types';
|
||||||
|
|
||||||
|
import promptOptions from '@/providers/prompt-options';
|
||||||
|
|
||||||
|
import getSongControls from '@/providers/song-controls';
|
||||||
|
|
||||||
|
import { IPC } from './constants';
|
||||||
|
|
||||||
|
import type { BackendContext } from '@/types/contexts';
|
||||||
|
|
||||||
|
export const onMainLoad = ({
|
||||||
|
ipc,
|
||||||
|
window,
|
||||||
|
}: BackendContext<MusicTogetherConfig>) => {
|
||||||
|
const controller = getSongControls(window);
|
||||||
|
|
||||||
|
ipc.handle(IPC.prompt, async (title: string, label: string) =>
|
||||||
|
prompt({
|
||||||
|
title,
|
||||||
|
label,
|
||||||
|
type: 'input',
|
||||||
|
...promptOptions(),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
ipc.handle(IPC.play, () => controller.play());
|
||||||
|
ipc.handle(IPC.pause, () => controller.pause());
|
||||||
|
ipc.handle(IPC.previous, () => controller.previous());
|
||||||
|
ipc.handle(IPC.next, () => controller.next());
|
||||||
|
ipc.handle(IPC.seekTo, (seconds: number) => controller.seekTo(seconds));
|
||||||
|
ipc.handle(
|
||||||
|
IPC.addSongToQueue,
|
||||||
|
(
|
||||||
|
ids: string | string[],
|
||||||
|
options: {
|
||||||
|
queueInsertPosition?: 'INSERT_AT_END' | 'INSERT_AFTER_CURRENT_VIDEO';
|
||||||
|
index?: number;
|
||||||
|
},
|
||||||
|
) => controller.addSongToQueue(ids, options),
|
||||||
|
);
|
||||||
|
ipc.handle(IPC.removeSongFromQueue, (index: number) =>
|
||||||
|
controller.removeSongFromQueue(index),
|
||||||
|
);
|
||||||
|
ipc.handle(IPC.moveSongInQueue, (fromIndex: number, toIndex: number) =>
|
||||||
|
controller.moveSongInQueue(fromIndex, toIndex),
|
||||||
|
);
|
||||||
|
ipc.handle(IPC.clearQueue, () => controller.clearQueue());
|
||||||
|
ipc.handle(IPC.setQueueIndex, (index: number) =>
|
||||||
|
controller.setQueueIndex(index),
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -1,26 +1,6 @@
|
|||||||
import { DataConnection, Peer } from 'peerjs';
|
import { DataConnection, Peer } from 'peerjs';
|
||||||
|
|
||||||
import type { Permission, Profile, VideoData } from './types';
|
import { ConnectedState, ConnectionEventMap, ConnectionEventUnion } 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> = {
|
type PromiseUtil<T> = {
|
||||||
promise: Promise<T>;
|
promise: Promise<T>;
|
||||||
@ -32,10 +12,10 @@ export type ConnectionListener = (
|
|||||||
event: ConnectionEventUnion,
|
event: ConnectionEventUnion,
|
||||||
conn: DataConnection,
|
conn: DataConnection,
|
||||||
) => void;
|
) => void;
|
||||||
export type ConnectionMode = 'host' | 'guest' | 'disconnected';
|
|
||||||
export class Connection {
|
export class Connection {
|
||||||
private peer: Peer;
|
private peer: Peer;
|
||||||
private _mode: ConnectionMode = 'disconnected';
|
private _state: ConnectedState = 'disconnected';
|
||||||
private connections: Record<string, DataConnection> = {};
|
private connections: Record<string, DataConnection> = {};
|
||||||
|
|
||||||
private waitOpen: PromiseUtil<string> = {} as PromiseUtil<string>;
|
private waitOpen: PromiseUtil<string> = {} as PromiseUtil<string>;
|
||||||
@ -51,15 +31,15 @@ export class Connection {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.peer.on('open', (id) => {
|
this.peer.on('open', (id) => {
|
||||||
this._mode = 'host';
|
this._state = 'connecting';
|
||||||
this.waitOpen.resolve(id);
|
this.waitOpen.resolve(id);
|
||||||
});
|
});
|
||||||
this.peer.on('connection', (conn) => {
|
this.peer.on('connection', (conn) => {
|
||||||
this._mode = 'host';
|
this._state = 'host';
|
||||||
this.registerConnection(conn);
|
this.registerConnection(conn);
|
||||||
});
|
});
|
||||||
this.peer.on('error', (err) => {
|
this.peer.on('error', (err) => {
|
||||||
this._mode = 'disconnected';
|
this._state = 'disconnected';
|
||||||
|
|
||||||
this.waitOpen.reject(err);
|
this.waitOpen.reject(err);
|
||||||
this.connectionListeners.forEach((listener) => listener());
|
this.connectionListeners.forEach((listener) => listener());
|
||||||
@ -73,16 +53,16 @@ export class Connection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async connect(id: string) {
|
async connect(id: string) {
|
||||||
this._mode = 'guest';
|
this._state = 'guest';
|
||||||
const conn = this.peer.connect(id);
|
const conn = this.peer.connect(id);
|
||||||
await this.registerConnection(conn);
|
await this.registerConnection(conn);
|
||||||
return conn;
|
return conn;
|
||||||
}
|
}
|
||||||
|
|
||||||
disconnect() {
|
disconnect() {
|
||||||
if (this._mode === 'disconnected') throw new Error('Already disconnected');
|
if (this._state === 'disconnected') throw new Error('Already disconnected');
|
||||||
|
|
||||||
this._mode = 'disconnected';
|
this._state = 'disconnected';
|
||||||
this.connections = {};
|
this.connections = {};
|
||||||
this.peer.destroy();
|
this.peer.destroy();
|
||||||
}
|
}
|
||||||
@ -92,8 +72,8 @@ export class Connection {
|
|||||||
return this.peer.id;
|
return this.peer.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
public get mode() {
|
public get state() {
|
||||||
return this._mode;
|
return this._state;
|
||||||
}
|
}
|
||||||
|
|
||||||
public getConnections() {
|
public getConnections() {
|
||||||
@ -121,7 +101,7 @@ export class Connection {
|
|||||||
private async registerConnection(conn: DataConnection) {
|
private async registerConnection(conn: DataConnection) {
|
||||||
return new Promise<DataConnection>((resolve, reject) => {
|
return new Promise<DataConnection>((resolve, reject) => {
|
||||||
this.peer.once('error', (err) => {
|
this.peer.once('error', (err) => {
|
||||||
this._mode = 'disconnected';
|
this._state = 'disconnected';
|
||||||
|
|
||||||
reject(err);
|
reject(err);
|
||||||
this.connectionListeners.forEach((listener) => listener());
|
this.connectionListeners.forEach((listener) => listener());
|
||||||
@ -133,6 +113,12 @@ export class Connection {
|
|||||||
this.connectionListeners.forEach((listener) => listener(conn));
|
this.connectionListeners.forEach((listener) => listener(conn));
|
||||||
|
|
||||||
conn.on('data', (data) => {
|
conn.on('data', (data) => {
|
||||||
|
if (typeof data === 'string') {
|
||||||
|
try {
|
||||||
|
data = JSON.parse(data);
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!data ||
|
!data ||
|
||||||
typeof data !== 'object' ||
|
typeof data !== 'object' ||
|
||||||
@ -140,7 +126,7 @@ export class Connection {
|
|||||||
!('payload' in data) ||
|
!('payload' in data) ||
|
||||||
!data.type
|
!data.type
|
||||||
) {
|
) {
|
||||||
console.warn('Music Together: Invalid data', data);
|
console.warn('Music Together: Invalid data', data, typeof data);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
13
src/plugins/music-together/constants.ts
Normal file
13
src/plugins/music-together/constants.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
export const IPC = {
|
||||||
|
prompt: 'music-together:prompt',
|
||||||
|
play: 'music-together:play',
|
||||||
|
pause: 'music-together:pause',
|
||||||
|
previous: 'music-together:previous',
|
||||||
|
next: 'music-together:next',
|
||||||
|
seekTo: 'music-together:seekTo',
|
||||||
|
addSongToQueue: 'music-together:addSongToQueue',
|
||||||
|
removeSongFromQueue: 'music-together:removeSongFromQueue',
|
||||||
|
moveSongInQueue: 'music-together:moveSongInQueue',
|
||||||
|
clearQueue: 'music-together:clearQueue',
|
||||||
|
setQueueIndex: 'music-together:setQueueIndex',
|
||||||
|
};
|
||||||
31
src/plugins/music-together/context/RendererContext.tsx
Normal file
31
src/plugins/music-together/context/RendererContext.tsx
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import { createContext, JSX, splitProps, useContext } from 'solid-js';
|
||||||
|
|
||||||
|
import { MusicTogetherConfig } from '../types';
|
||||||
|
|
||||||
|
import { RendererContext } from '@/types/contexts';
|
||||||
|
|
||||||
|
export type RendererContextContextType = {
|
||||||
|
context: RendererContext<MusicTogetherConfig>;
|
||||||
|
};
|
||||||
|
export const RendererContextContext =
|
||||||
|
createContext<RendererContextContextType>();
|
||||||
|
|
||||||
|
export type RendererContextProviderProps = RendererContextContextType & {
|
||||||
|
children: JSX.Element;
|
||||||
|
};
|
||||||
|
export const RendererContextProvider = (
|
||||||
|
props: RendererContextProviderProps,
|
||||||
|
) => {
|
||||||
|
const [local, left] = splitProps(props, ['children']);
|
||||||
|
return (
|
||||||
|
<RendererContextContext.Provider value={left}>
|
||||||
|
{local.children}
|
||||||
|
</RendererContextContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export const useRendererContext = () => {
|
||||||
|
const context = useContext(RendererContextContext);
|
||||||
|
if (!context) throw Error('RendererContextProvider not found');
|
||||||
|
|
||||||
|
return context.context;
|
||||||
|
};
|
||||||
22
src/plugins/music-together/context/ToastContext.tsx
Normal file
22
src/plugins/music-together/context/ToastContext.tsx
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { createContext, JSX, useContext } from 'solid-js';
|
||||||
|
|
||||||
|
import { ToastService } from '@/types/queue';
|
||||||
|
|
||||||
|
export type ToastContextType = {
|
||||||
|
service: ToastService;
|
||||||
|
};
|
||||||
|
export const ToastContext = createContext<ToastContextType>();
|
||||||
|
|
||||||
|
export type ToastProviderProps = ToastContextType & {
|
||||||
|
children: JSX.Element;
|
||||||
|
};
|
||||||
|
export const ToastProvider = (props: ToastProviderProps) => (
|
||||||
|
<ToastContext.Provider value={props}>{props.children}</ToastContext.Provider>
|
||||||
|
);
|
||||||
|
export const useToast = () => {
|
||||||
|
const context = useContext(ToastContext);
|
||||||
|
|
||||||
|
return (message: string) => {
|
||||||
|
context?.service.show(message);
|
||||||
|
};
|
||||||
|
};
|
||||||
@ -1,148 +0,0 @@
|
|||||||
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,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@ -1,3 +0,0 @@
|
|||||||
<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>
|
|
||||||
|
Before Width: | Height: | Size: 408 B |
@ -1,4 +0,0 @@
|
|||||||
<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>
|
|
||||||
|
Before Width: | Height: | Size: 480 B |
@ -1,3 +0,0 @@
|
|||||||
<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>
|
|
||||||
|
Before Width: | Height: | Size: 416 B |
@ -1,4 +0,0 @@
|
|||||||
<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>
|
|
||||||
|
Before Width: | Height: | Size: 529 B |
@ -1,3 +0,0 @@
|
|||||||
<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>
|
|
||||||
|
Before Width: | Height: | Size: 298 B |
@ -1,84 +1,10 @@
|
|||||||
import prompt from 'custom-electron-prompt';
|
|
||||||
|
|
||||||
import { DataConnection } from 'peerjs';
|
|
||||||
|
|
||||||
import { t } from '@/i18n';
|
import { t } from '@/i18n';
|
||||||
import { createPlugin } from '@/utils';
|
import { createPlugin } from '@/utils';
|
||||||
import promptOptions from '@/providers/prompt-options';
|
|
||||||
|
|
||||||
import {
|
import { onMainLoad } from './backend';
|
||||||
getDefaultProfile,
|
import { onRendererLoad } from './src';
|
||||||
type Permission,
|
|
||||||
type Profile,
|
|
||||||
type VideoData,
|
|
||||||
} from './types';
|
|
||||||
import { Queue } from './queue';
|
|
||||||
import { Connection, type 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';
|
export default createPlugin({
|
||||||
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 type { AppElement } from '@/types/queue';
|
|
||||||
|
|
||||||
type RawAccountData = {
|
|
||||||
accountName: {
|
|
||||||
runs: { text: string }[];
|
|
||||||
};
|
|
||||||
accountPhoto: {
|
|
||||||
thumbnails: { url: string; width: number; height: number }[];
|
|
||||||
};
|
|
||||||
settingsEndpoint: unknown;
|
|
||||||
manageAccountTitle: unknown;
|
|
||||||
trackingParams: string;
|
|
||||||
channelHandle: {
|
|
||||||
runs: { text: string }[];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export default createPlugin<
|
|
||||||
unknown,
|
|
||||||
unknown,
|
|
||||||
{
|
|
||||||
connection?: Connection;
|
|
||||||
ipc?: RendererContext<never>['ipc'];
|
|
||||||
api: AppElement | null;
|
|
||||||
queue?: Queue;
|
|
||||||
playerApi?: YoutubePlayer;
|
|
||||||
showPrompt: (title: string, label: string) => Promise<string>;
|
|
||||||
popups: {
|
|
||||||
host: ReturnType<typeof createHostPopup>;
|
|
||||||
guest: ReturnType<typeof createGuestPopup>;
|
|
||||||
setting: ReturnType<typeof createSettingPopup>;
|
|
||||||
};
|
|
||||||
elements: {
|
|
||||||
setting: HTMLElement;
|
|
||||||
icon: SVGElement;
|
|
||||||
spinner: HTMLElement;
|
|
||||||
};
|
|
||||||
stateInterval?: number;
|
|
||||||
updateNext: boolean;
|
|
||||||
ignoreChange: boolean;
|
|
||||||
rollbackInjector?: () => void;
|
|
||||||
me?: Omit<Profile, 'id'>;
|
|
||||||
profiles: Record<string, Profile>;
|
|
||||||
permission: Permission;
|
|
||||||
videoChangeListener: (event: CustomEvent<VideoDataChanged>) => void;
|
|
||||||
videoStateChangeListener: () => void;
|
|
||||||
onHost: () => Promise<boolean>;
|
|
||||||
onJoin: () => Promise<boolean>;
|
|
||||||
onStop: () => void;
|
|
||||||
putProfile: (id: string, profile?: Profile) => void;
|
|
||||||
showSpinner: () => void;
|
|
||||||
hideSpinner: () => void;
|
|
||||||
initMyProfile: () => void;
|
|
||||||
}
|
|
||||||
>({
|
|
||||||
name: () => t('plugins.music-together.name'),
|
name: () => t('plugins.music-together.name'),
|
||||||
description: () => t('plugins.music-together.description'),
|
description: () => t('plugins.music-together.description'),
|
||||||
restartNeeded: false,
|
restartNeeded: false,
|
||||||
@ -86,757 +12,9 @@ export default createPlugin<
|
|||||||
config: {
|
config: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
},
|
},
|
||||||
stylesheets: [style],
|
stylesheets: [],
|
||||||
backend({ ipc }) {
|
backend: onMainLoad,
|
||||||
ipc.handle('music-together:prompt', async (title: string, label: string) =>
|
|
||||||
prompt({
|
|
||||||
title,
|
|
||||||
label,
|
|
||||||
type: 'input',
|
|
||||||
...promptOptions(),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
renderer: {
|
renderer: {
|
||||||
updateNext: false,
|
start: onRendererLoad,
|
||||||
ignoreChange: false,
|
|
||||||
permission: 'playlist',
|
|
||||||
popups: {} as {
|
|
||||||
host: ReturnType<typeof createHostPopup>;
|
|
||||||
guest: ReturnType<typeof createGuestPopup>;
|
|
||||||
setting: ReturnType<typeof createSettingPopup>;
|
|
||||||
},
|
|
||||||
elements: {} as {
|
|
||||||
setting: HTMLElement;
|
|
||||||
icon: SVGElement;
|
|
||||||
spinner: HTMLElement;
|
|
||||||
},
|
|
||||||
profiles: {},
|
|
||||||
showPrompt: () => Promise.resolve(''),
|
|
||||||
api: null,
|
|
||||||
|
|
||||||
/* events */
|
|
||||||
videoChangeListener(event: CustomEvent<VideoDataChanged>) {
|
|
||||||
if (event.detail.name === 'dataloaded' || this.updateNext) {
|
|
||||||
if (this.connection?.mode === 'host') {
|
|
||||||
const videoList: VideoData[] =
|
|
||||||
this.queue?.flatItems.map(
|
|
||||||
(it) =>
|
|
||||||
({
|
|
||||||
videoId: it!.videoId,
|
|
||||||
ownerId: this.connection!.id,
|
|
||||||
}) satisfies VideoData,
|
|
||||||
) ?? [];
|
|
||||||
|
|
||||||
this.queue?.setVideoList(videoList, false);
|
|
||||||
this.queue?.syncQueueOwner();
|
|
||||||
this.connection.broadcast('SYNC_QUEUE', {
|
|
||||||
videoList,
|
|
||||||
});
|
|
||||||
|
|
||||||
this.updateNext = event.detail.name === 'dataloaded';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
videoStateChangeListener() {
|
|
||||||
if (this.connection?.mode !== 'guest') return;
|
|
||||||
if (this.ignoreChange) return;
|
|
||||||
if (this.permission !== 'all') return;
|
|
||||||
|
|
||||||
const state = this.playerApi?.getPlayerState();
|
|
||||||
if (state !== 1 && state !== 2) return;
|
|
||||||
|
|
||||||
this.connection.broadcast('SYNC_PROGRESS', {
|
|
||||||
// progress: this.playerApi?.getCurrentTime(),
|
|
||||||
state: this.playerApi?.getPlayerState(),
|
|
||||||
// index: this.queue?.selectedIndex ?? 0,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
/* connection */
|
|
||||||
async onHost() {
|
|
||||||
this.connection = new Connection();
|
|
||||||
const wait = await this.connection.waitForReady().catch(() => null);
|
|
||||||
if (!wait) return false;
|
|
||||||
|
|
||||||
if (!this.me) this.me = getDefaultProfile(this.connection.id);
|
|
||||||
const rawItems =
|
|
||||||
this.queue?.flatItems?.map(
|
|
||||||
(it) =>
|
|
||||||
({
|
|
||||||
videoId: it!.videoId,
|
|
||||||
ownerId: this.connection!.id,
|
|
||||||
}) satisfies VideoData,
|
|
||||||
) ?? [];
|
|
||||||
this.queue?.setOwner({
|
|
||||||
id: this.connection.id,
|
|
||||||
...this.me,
|
|
||||||
});
|
|
||||||
this.queue?.setVideoList(rawItems, false);
|
|
||||||
this.queue?.syncQueueOwner();
|
|
||||||
this.queue?.initQueue();
|
|
||||||
this.queue?.injection();
|
|
||||||
|
|
||||||
this.profiles = {};
|
|
||||||
this.connection.onConnections((connection) => {
|
|
||||||
if (!connection) {
|
|
||||||
this.api?.toastService?.show(
|
|
||||||
t('plugins.music-together.toast.disconnected'),
|
|
||||||
);
|
|
||||||
this.onStop();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!connection.open) {
|
|
||||||
this.api?.toastService?.show(
|
|
||||||
t('plugins.music-together.toast.user-disconnected', {
|
|
||||||
name: this.profiles[connection.peer]?.name,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
this.putProfile(connection.peer, undefined);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
this.putProfile(this.connection.id, {
|
|
||||||
id: this.connection.id,
|
|
||||||
...this.me,
|
|
||||||
});
|
|
||||||
|
|
||||||
const listener = async (
|
|
||||||
event: ConnectionEventUnion,
|
|
||||||
conn?: DataConnection,
|
|
||||||
) => {
|
|
||||||
this.ignoreChange = true;
|
|
||||||
|
|
||||||
switch (event.type) {
|
|
||||||
case 'ADD_SONGS': {
|
|
||||||
if (conn && this.permission === 'host-only') return;
|
|
||||||
|
|
||||||
await this.queue?.addVideos(
|
|
||||||
event.payload.videoList,
|
|
||||||
event.payload.index,
|
|
||||||
);
|
|
||||||
await this.connection?.broadcast('ADD_SONGS', event.payload);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'REMOVE_SONG': {
|
|
||||||
if (conn && this.permission === 'host-only') return;
|
|
||||||
|
|
||||||
this.queue?.removeVideo(event.payload.index);
|
|
||||||
await this.connection?.broadcast('REMOVE_SONG', event.payload);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'MOVE_SONG': {
|
|
||||||
if (conn && this.permission === 'host-only') {
|
|
||||||
await this.connection?.broadcast('SYNC_QUEUE', {
|
|
||||||
videoList: this.queue?.videoList ?? [],
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.queue?.moveItem(
|
|
||||||
event.payload.fromIndex,
|
|
||||||
event.payload.toIndex,
|
|
||||||
);
|
|
||||||
await this.connection?.broadcast('MOVE_SONG', event.payload);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'IDENTIFY': {
|
|
||||||
if (!event.payload || !conn) {
|
|
||||||
console.warn(
|
|
||||||
'Music Together [Host]: Received "IDENTIFY" event without payload or connection',
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.api?.toastService?.show(
|
|
||||||
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?.toastService?.show(
|
|
||||||
t('plugins.music-together.toast.disconnected'),
|
|
||||||
);
|
|
||||||
this.onStop();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let resolveIgnore: number | null = null;
|
|
||||||
const listener = async (event: ConnectionEventUnion) => {
|
|
||||||
this.ignoreChange = true;
|
|
||||||
switch (event.type) {
|
|
||||||
case 'ADD_SONGS': {
|
|
||||||
await this.queue?.addVideos(
|
|
||||||
event.payload.videoList,
|
|
||||||
event.payload.index,
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'REMOVE_SONG': {
|
|
||||||
this.queue?.removeVideo(event.payload.index);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'MOVE_SONG': {
|
|
||||||
this.queue?.moveItem(
|
|
||||||
event.payload.fromIndex,
|
|
||||||
event.payload.toIndex,
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'IDENTIFY': {
|
|
||||||
console.warn(
|
|
||||||
'Music Together [Guest]: Received "IDENTIFY" event from guest',
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'SYNC_QUEUE': {
|
|
||||||
if (Array.isArray(event.payload?.videoList)) {
|
|
||||||
await this.queue?.setVideoList(event.payload.videoList);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'SYNC_PROFILE': {
|
|
||||||
if (!event.payload) {
|
|
||||||
console.warn(
|
|
||||||
'Music Together [Guest]: Received "SYNC_PROFILE" event without payload',
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
Object.entries(event.payload.profiles).forEach(([id, profile]) => {
|
|
||||||
this.putProfile(id, profile);
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'SYNC_PROGRESS': {
|
|
||||||
if (typeof event.payload?.progress === 'number') {
|
|
||||||
const currentTime = this.playerApi?.getCurrentTime() ?? 0;
|
|
||||||
if (Math.abs(event.payload.progress - currentTime) > 3)
|
|
||||||
this.playerApi?.seekTo(event.payload.progress);
|
|
||||||
}
|
|
||||||
if (this.playerApi?.getPlayerState() !== event.payload?.state) {
|
|
||||||
if (event.payload?.state === 2) this.playerApi?.pauseVideo();
|
|
||||||
if (event.payload?.state === 1) this.playerApi?.playVideo();
|
|
||||||
}
|
|
||||||
if (typeof event.payload?.index === 'number') {
|
|
||||||
const nowIndex = this.queue?.selectedIndex ?? 0;
|
|
||||||
|
|
||||||
if (nowIndex !== event.payload.index) {
|
|
||||||
this.queue?.setIndex(event.payload.index);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'PERMISSION': {
|
|
||||||
if (!event.payload) {
|
|
||||||
console.warn(
|
|
||||||
'Music Together [Guest]: Received "PERMISSION" event without payload',
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.permission = event.payload;
|
|
||||||
this.popups.guest.setPermission(this.permission);
|
|
||||||
this.popups.host.setPermission(this.permission);
|
|
||||||
this.popups.setting.setPermission(this.permission);
|
|
||||||
|
|
||||||
const permissionLabel = t(
|
|
||||||
`plugins.music-together.menu.permission.${this.permission}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
this.api?.toastService?.show(
|
|
||||||
t('plugins.music-together.toast.permission-changed', {
|
|
||||||
permission: permissionLabel,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
default: {
|
|
||||||
console.warn('Music Together [Guest]: Unknown Event', event);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof resolveIgnore === 'number') clearTimeout(resolveIgnore);
|
|
||||||
resolveIgnore = window.setTimeout(() => {
|
|
||||||
this.ignoreChange = false;
|
|
||||||
}, 16); // wait 1 frame
|
|
||||||
};
|
|
||||||
|
|
||||||
this.connection.on(listener);
|
|
||||||
this.queue?.on(async (event: ConnectionEventUnion) => {
|
|
||||||
this.ignoreChange = true;
|
|
||||||
switch (event.type) {
|
|
||||||
case 'ADD_SONGS': {
|
|
||||||
await this.connection?.broadcast('ADD_SONGS', event.payload);
|
|
||||||
await this.connection?.broadcast('SYNC_QUEUE', undefined);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'REMOVE_SONG': {
|
|
||||||
await this.connection?.broadcast('REMOVE_SONG', event.payload);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'MOVE_SONG': {
|
|
||||||
await this.connection?.broadcast('MOVE_SONG', event.payload);
|
|
||||||
await this.connection?.broadcast('SYNC_QUEUE', undefined);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'SYNC_PROGRESS': {
|
|
||||||
if (this.permission === 'host-only')
|
|
||||||
await this.connection?.broadcast('SYNC_QUEUE', undefined);
|
|
||||||
else
|
|
||||||
await this.connection?.broadcast('SYNC_PROGRESS', event.payload);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof resolveIgnore === 'number') clearTimeout(resolveIgnore);
|
|
||||||
resolveIgnore = window.setTimeout(() => {
|
|
||||||
this.ignoreChange = false;
|
|
||||||
}, 16); // wait 1 frame
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!this.me) this.me = getDefaultProfile(this.connection.id);
|
|
||||||
this.queue?.injection();
|
|
||||||
this.queue?.setOwner({
|
|
||||||
id: this.connection.id,
|
|
||||||
...this.me,
|
|
||||||
});
|
|
||||||
|
|
||||||
const progress = Array.from(
|
|
||||||
document.querySelectorAll<
|
|
||||||
HTMLElement & {
|
|
||||||
_update: (...args: unknown[]) => void;
|
|
||||||
}
|
|
||||||
>('tp-yt-paper-progress'),
|
|
||||||
);
|
|
||||||
const rollbackList = progress.map((progress) => {
|
|
||||||
const original = progress._update;
|
|
||||||
progress._update = (...args) => {
|
|
||||||
const now = args[0];
|
|
||||||
|
|
||||||
if (this.permission === 'all' && typeof now === 'number') {
|
|
||||||
const currentTime = this.playerApi?.getCurrentTime() ?? 0;
|
|
||||||
if (Math.abs(now - currentTime) > 3)
|
|
||||||
this.connection?.broadcast('SYNC_PROGRESS', {
|
|
||||||
progress: now,
|
|
||||||
state: this.playerApi?.getPlayerState(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
original.call(progress, ...args);
|
|
||||||
};
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
progress._update = original;
|
|
||||||
};
|
|
||||||
});
|
|
||||||
this.rollbackInjector = () => {
|
|
||||||
rollbackList.forEach((rollback) => rollback());
|
|
||||||
};
|
|
||||||
|
|
||||||
this.connection.broadcast('IDENTIFY', {
|
|
||||||
profile: {
|
|
||||||
id: this.connection.id,
|
|
||||||
handleId: this.me.handleId,
|
|
||||||
name: this.me.name,
|
|
||||||
thumbnail: this.me.thumbnail,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
this.connection.broadcast('SYNC_PROFILE', undefined);
|
|
||||||
this.connection.broadcast('PERMISSION', undefined);
|
|
||||||
|
|
||||||
this.queue?.clear();
|
|
||||||
this.queue?.syncQueueOwner();
|
|
||||||
this.queue?.initQueue();
|
|
||||||
|
|
||||||
this.connection.broadcast('SYNC_QUEUE', undefined);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
|
|
||||||
onStop() {
|
|
||||||
this.connection?.disconnect();
|
|
||||||
this.queue?.rollbackInjection();
|
|
||||||
this.queue?.removeQueueOwner();
|
|
||||||
if (this.rollbackInjector) {
|
|
||||||
this.rollbackInjector();
|
|
||||||
this.rollbackInjector = undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.profiles = {};
|
|
||||||
this.popups.host.setUsers(Object.values(this.profiles));
|
|
||||||
this.popups.guest.setUsers(Object.values(this.profiles));
|
|
||||||
|
|
||||||
this.popups.host.dismiss();
|
|
||||||
this.popups.guest.dismiss();
|
|
||||||
this.popups.setting.dismiss();
|
|
||||||
},
|
|
||||||
|
|
||||||
/* methods */
|
|
||||||
putProfile(id: string, profile?: Profile) {
|
|
||||||
if (profile === undefined) {
|
|
||||||
delete this.profiles[id];
|
|
||||||
} else {
|
|
||||||
this.profiles[id] = profile;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.popups.host.setUsers(Object.values(this.profiles));
|
|
||||||
this.popups.guest.setUsers(Object.values(this.profiles));
|
|
||||||
},
|
|
||||||
|
|
||||||
showSpinner() {
|
|
||||||
this.elements.icon.style.setProperty('display', 'none');
|
|
||||||
this.elements.spinner.removeAttribute('hidden');
|
|
||||||
this.elements.spinner.setAttribute('active', '');
|
|
||||||
},
|
|
||||||
|
|
||||||
hideSpinner() {
|
|
||||||
this.elements.icon.style.removeProperty('display');
|
|
||||||
this.elements.spinner.removeAttribute('active');
|
|
||||||
this.elements.spinner.setAttribute('hidden', '');
|
|
||||||
},
|
|
||||||
|
|
||||||
initMyProfile() {
|
|
||||||
const accountButton = document.querySelector<
|
|
||||||
HTMLElement & {
|
|
||||||
onButtonTap: () => void;
|
|
||||||
}
|
|
||||||
>('ytmusic-settings-button');
|
|
||||||
|
|
||||||
accountButton?.onButtonTap();
|
|
||||||
setTimeout(() => {
|
|
||||||
accountButton?.onButtonTap();
|
|
||||||
const renderer = document.querySelector<
|
|
||||||
HTMLElement & { data: unknown }
|
|
||||||
>('ytd-active-account-header-renderer');
|
|
||||||
if (!accountButton || !renderer) {
|
|
||||||
console.warn('Music Together: Cannot find account');
|
|
||||||
this.me = getDefaultProfile(this.connection?.id ?? '');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const accountData = renderer.data as RawAccountData;
|
|
||||||
this.me = {
|
|
||||||
handleId:
|
|
||||||
accountData.channelHandle.runs[0].text ??
|
|
||||||
accountData.accountName.runs[0].text,
|
|
||||||
name: accountData.accountName.runs[0].text,
|
|
||||||
thumbnail: accountData.accountPhoto.thumbnails[0].url,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (this.me.thumbnail) {
|
|
||||||
this.popups.host.setProfile(this.me.thumbnail);
|
|
||||||
this.popups.guest.setProfile(this.me.thumbnail);
|
|
||||||
this.popups.setting.setProfile(this.me.thumbnail);
|
|
||||||
}
|
|
||||||
}, 0);
|
|
||||||
},
|
|
||||||
/* hooks */
|
|
||||||
|
|
||||||
start({ ipc }) {
|
|
||||||
this.ipc = ipc;
|
|
||||||
this.showPrompt = async (title: string, label: string) =>
|
|
||||||
ipc.invoke('music-together:prompt', title, label) as Promise<string>;
|
|
||||||
this.api = document.querySelector<AppElement>('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?.toastService?.show(
|
|
||||||
t('plugins.music-together.toast.closed'),
|
|
||||||
);
|
|
||||||
hostPopup.dismiss();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (id === 'music-together-copy-id') {
|
|
||||||
navigator.clipboard
|
|
||||||
.writeText(this.connection?.id ?? '')
|
|
||||||
.then(() => {
|
|
||||||
this.api?.toastService?.show(
|
|
||||||
t('plugins.music-together.toast.id-copied'),
|
|
||||||
);
|
|
||||||
hostPopup.dismiss();
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
this.api?.toastService?.show(
|
|
||||||
t('plugins.music-together.toast.id-copy-failed'),
|
|
||||||
);
|
|
||||||
hostPopup.dismiss();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (id === 'music-together-permission') {
|
|
||||||
if (this.permission === 'all') this.permission = 'host-only';
|
|
||||||
else if (this.permission === 'playlist') this.permission = 'all';
|
|
||||||
else if (this.permission === 'host-only')
|
|
||||||
this.permission = 'playlist';
|
|
||||||
this.connection?.broadcast('PERMISSION', this.permission);
|
|
||||||
|
|
||||||
hostPopup.setPermission(this.permission);
|
|
||||||
guestPopup.setPermission(this.permission);
|
|
||||||
settingPopup.setPermission(this.permission);
|
|
||||||
|
|
||||||
const permissionLabel = t(
|
|
||||||
`plugins.music-together.menu.permission.${this.permission}`,
|
|
||||||
);
|
|
||||||
this.api?.toastService?.show(
|
|
||||||
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?.toastService?.show(
|
|
||||||
t('plugins.music-together.toast.disconnected'),
|
|
||||||
);
|
|
||||||
guestPopup.dismiss();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const settingPopup = createSettingPopup({
|
|
||||||
onItemClick: async (id) => {
|
|
||||||
if (id === 'music-together-host') {
|
|
||||||
settingPopup.dismiss();
|
|
||||||
this.showSpinner();
|
|
||||||
const result = await this.onHost();
|
|
||||||
this.hideSpinner();
|
|
||||||
|
|
||||||
if (result) {
|
|
||||||
navigator.clipboard
|
|
||||||
.writeText(this.connection?.id ?? '')
|
|
||||||
.then(() => {
|
|
||||||
this.api?.toastService?.show(
|
|
||||||
t('plugins.music-together.toast.id-copied'),
|
|
||||||
);
|
|
||||||
hostPopup.showAtAnchor(setting);
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
this.api?.toastService?.show(
|
|
||||||
t('plugins.music-together.toast.id-copy-failed'),
|
|
||||||
);
|
|
||||||
hostPopup.showAtAnchor(setting);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
this.api?.toastService?.show(
|
|
||||||
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?.toastService?.show(
|
|
||||||
t('plugins.music-together.toast.joined'),
|
|
||||||
);
|
|
||||||
guestPopup.showAtAnchor(setting);
|
|
||||||
} else {
|
|
||||||
this.api?.toastService?.show(
|
|
||||||
t('plugins.music-together.toast.join-failed'),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
this.popups = {
|
|
||||||
host: hostPopup,
|
|
||||||
guest: guestPopup,
|
|
||||||
setting: settingPopup,
|
|
||||||
};
|
|
||||||
setting.addEventListener('click', () => {
|
|
||||||
let popup = settingPopup;
|
|
||||||
if (this.connection?.mode === 'host') popup = hostPopup;
|
|
||||||
if (this.connection?.mode === 'guest') popup = guestPopup;
|
|
||||||
|
|
||||||
if (popup.isShowing()) popup.dismiss();
|
|
||||||
else popup.showAtAnchor(setting);
|
|
||||||
});
|
|
||||||
|
|
||||||
/* account data getter */
|
|
||||||
this.initMyProfile();
|
|
||||||
},
|
|
||||||
onPlayerApiReady(playerApi) {
|
|
||||||
this.queue = new Queue({
|
|
||||||
owner: {
|
|
||||||
id: this.connection?.id ?? '',
|
|
||||||
...this.me!,
|
|
||||||
},
|
|
||||||
getProfile: (id) => this.profiles[id],
|
|
||||||
});
|
|
||||||
this.playerApi = playerApi;
|
|
||||||
|
|
||||||
this.playerApi.addEventListener(
|
|
||||||
'onStateChange',
|
|
||||||
this.videoStateChangeListener,
|
|
||||||
);
|
|
||||||
document.addEventListener('videodatachange', this.videoChangeListener);
|
|
||||||
},
|
|
||||||
stop() {
|
|
||||||
const dividers = Array.from(
|
|
||||||
document.querySelectorAll('.music-together-divider'),
|
|
||||||
);
|
|
||||||
dividers.forEach((divider) => divider.remove());
|
|
||||||
|
|
||||||
this.elements.setting?.remove();
|
|
||||||
this.onStop();
|
|
||||||
if (typeof this.stateInterval === 'number')
|
|
||||||
clearInterval(this.stateInterval);
|
|
||||||
if (this.playerApi)
|
|
||||||
this.playerApi.removeEventListener(
|
|
||||||
'onStateChange',
|
|
||||||
this.videoStateChangeListener,
|
|
||||||
);
|
|
||||||
if (this.videoChangeListener)
|
|
||||||
document.removeEventListener(
|
|
||||||
'videodatachange',
|
|
||||||
this.videoChangeListener,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,48 +0,0 @@
|
|||||||
import { SHA1Hash } from './sha1hash';
|
|
||||||
|
|
||||||
export const extractToken = (cookie = document.cookie) =>
|
|
||||||
cookie.match(/SAPISID=([^;]+);/)?.[1] ??
|
|
||||||
cookie.match(/__Secure-3PAPISID=([^;]+);/)?.[1];
|
|
||||||
|
|
||||||
export const getHash = async (
|
|
||||||
papisid: string,
|
|
||||||
millis = Date.now(),
|
|
||||||
origin: string = 'https://music.youtube.com',
|
|
||||||
) => (await SHA1Hash(`${millis} ${papisid} ${origin}`)).toLowerCase();
|
|
||||||
|
|
||||||
export const getAuthorizationHeader = async (
|
|
||||||
papisid: string,
|
|
||||||
millis = Date.now(),
|
|
||||||
origin: string = 'https://music.youtube.com',
|
|
||||||
) => {
|
|
||||||
return `SAPISIDHASH ${millis}_${await getHash(papisid, millis, origin)}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getClient = () => {
|
|
||||||
return {
|
|
||||||
hl: navigator.language.split('-')[0] ?? 'en',
|
|
||||||
gl: navigator.language.split('-')[1] ?? 'US',
|
|
||||||
deviceMake: '',
|
|
||||||
deviceModel: '',
|
|
||||||
userAgent: navigator.userAgent,
|
|
||||||
clientName: 'WEB_REMIX',
|
|
||||||
clientVersion: '1.20231208.05.02',
|
|
||||||
osName: '',
|
|
||||||
osVersion: '',
|
|
||||||
platform: 'DESKTOP',
|
|
||||||
timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
||||||
locationInfo: {
|
|
||||||
locationPermissionAuthorizationStatus:
|
|
||||||
'LOCATION_PERMISSION_AUTHORIZATION_STATUS_UNSUPPORTED',
|
|
||||||
},
|
|
||||||
musicAppInfo: {
|
|
||||||
pwaInstallabilityStatus: 'PWA_INSTALLABILITY_STATUS_UNKNOWN',
|
|
||||||
webDisplayMode: 'WEB_DISPLAY_MODE_BROWSER',
|
|
||||||
storeDigitalGoodsApiSupportStatus: {
|
|
||||||
playStoreDigitalGoodsApiSupportStatus:
|
|
||||||
'DIGITAL_GOODS_API_SUPPORT_STATUS_UNSUPPORTED',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
utcOffsetMinutes: -1 * new Date().getTimezoneOffset(),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@ -1 +0,0 @@
|
|||||||
export * from './queue';
|
|
||||||
@ -1,544 +0,0 @@
|
|||||||
import { getMusicQueueRenderer } from './song';
|
|
||||||
import { mapQueueItem } from './utils';
|
|
||||||
|
|
||||||
import { t } from '@/i18n';
|
|
||||||
|
|
||||||
import type { ConnectionEventUnion } from '@/plugins/music-together/connection';
|
|
||||||
import type { Profile, VideoData } from '../types';
|
|
||||||
import type { QueueItem } from '@/types/datahost-get-state';
|
|
||||||
import type { QueueElement, Store } from '@/types/queue';
|
|
||||||
|
|
||||||
const getHeaderPayload = (() => {
|
|
||||||
let payload: {
|
|
||||||
items?: QueueItem[] | undefined;
|
|
||||||
title: {
|
|
||||||
runs: {
|
|
||||||
text: string;
|
|
||||||
}[];
|
|
||||||
};
|
|
||||||
subtitle: {
|
|
||||||
runs: {
|
|
||||||
text: string;
|
|
||||||
}[];
|
|
||||||
};
|
|
||||||
buttons: {
|
|
||||||
chipCloudChipRenderer: {
|
|
||||||
style: {
|
|
||||||
styleType: string;
|
|
||||||
};
|
|
||||||
text: {
|
|
||||||
runs: {
|
|
||||||
text: string;
|
|
||||||
}[];
|
|
||||||
};
|
|
||||||
navigationEndpoint: {
|
|
||||||
saveQueueToPlaylistCommand: unknown;
|
|
||||||
};
|
|
||||||
icon: {
|
|
||||||
iconType: string;
|
|
||||||
};
|
|
||||||
accessibilityData: {
|
|
||||||
accessibilityData: {
|
|
||||||
label: string;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
isSelected: boolean;
|
|
||||||
uniqueId: string;
|
|
||||||
};
|
|
||||||
}[];
|
|
||||||
} | null = null;
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
if (!payload) {
|
|
||||||
payload = {
|
|
||||||
title: {
|
|
||||||
runs: [
|
|
||||||
{
|
|
||||||
text: t('plugins.music-together.internal.track-source'),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
subtitle: {
|
|
||||||
runs: [
|
|
||||||
{
|
|
||||||
text: t('plugins.music-together.name'),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
buttons: [
|
|
||||||
{
|
|
||||||
chipCloudChipRenderer: {
|
|
||||||
style: {
|
|
||||||
styleType: 'STYLE_TRANSPARENT',
|
|
||||||
},
|
|
||||||
text: {
|
|
||||||
runs: [
|
|
||||||
{
|
|
||||||
text: t('plugins.music-together.internal.save'),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
navigationEndpoint: {
|
|
||||||
saveQueueToPlaylistCommand: {},
|
|
||||||
},
|
|
||||||
icon: {
|
|
||||||
iconType: 'ADD_TO_PLAYLIST',
|
|
||||||
},
|
|
||||||
accessibilityData: {
|
|
||||||
accessibilityData: {
|
|
||||||
label: t('plugins.music-together.internal.save'),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
isSelected: false,
|
|
||||||
uniqueId: t('plugins.music-together.internal.save'),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return payload;
|
|
||||||
};
|
|
||||||
})();
|
|
||||||
|
|
||||||
export type QueueOptions = {
|
|
||||||
videoList?: VideoData[];
|
|
||||||
owner?: Profile;
|
|
||||||
queue?: QueueElement;
|
|
||||||
getProfile: (id: string) => Profile | undefined;
|
|
||||||
};
|
|
||||||
export type QueueEventListener = (event: ConnectionEventUnion) => void;
|
|
||||||
|
|
||||||
export class Queue {
|
|
||||||
private readonly queue: QueueElement;
|
|
||||||
|
|
||||||
private originalDispatch?: (obj: {
|
|
||||||
type: string;
|
|
||||||
payload?: { items?: QueueItem[] | undefined };
|
|
||||||
}) => void;
|
|
||||||
|
|
||||||
private internalDispatch = false;
|
|
||||||
private ignoreFlag = false;
|
|
||||||
private listeners: QueueEventListener[] = [];
|
|
||||||
|
|
||||||
private owner: Profile | null;
|
|
||||||
private readonly getProfile: (id: string) => Profile | undefined;
|
|
||||||
|
|
||||||
constructor(options: QueueOptions) {
|
|
||||||
this.getProfile = options.getProfile;
|
|
||||||
this.queue =
|
|
||||||
options.queue ?? document.querySelector<QueueElement>('#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.queue.store.store.getState().queue.items,
|
|
||||||
).findIndex(Boolean) ?? 0
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
get rawItems() {
|
|
||||||
return this.queue?.queue.store.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.queue.store.store.getState().queue.nextQueueItemId,
|
|
||||||
index:
|
|
||||||
index ??
|
|
||||||
this.queue.queue.store.store.getState().queue.items.length ??
|
|
||||||
0,
|
|
||||||
items,
|
|
||||||
shuffleEnabled: false,
|
|
||||||
shouldAssignIds: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
this.internalDispatch = false;
|
|
||||||
setTimeout(() => {
|
|
||||||
this.initQueue();
|
|
||||||
this.syncQueueOwner();
|
|
||||||
}, 0);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
removeVideo(index: number) {
|
|
||||||
this.internalDispatch = true;
|
|
||||||
this._videoList.splice(index, 1);
|
|
||||||
this.queue?.dispatch({
|
|
||||||
type: 'REMOVE_ITEM',
|
|
||||||
payload: index,
|
|
||||||
});
|
|
||||||
this.internalDispatch = false;
|
|
||||||
setTimeout(() => {
|
|
||||||
this.initQueue();
|
|
||||||
this.syncQueueOwner();
|
|
||||||
}, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
setIndex(index: number) {
|
|
||||||
this.internalDispatch = true;
|
|
||||||
this.queue?.dispatch({
|
|
||||||
type: 'SET_INDEX',
|
|
||||||
payload: index,
|
|
||||||
});
|
|
||||||
this.internalDispatch = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
moveItem(fromIndex: number, toIndex: number) {
|
|
||||||
this.internalDispatch = true;
|
|
||||||
const data = this._videoList.splice(fromIndex, 1)[0];
|
|
||||||
this._videoList.splice(toIndex, 0, data);
|
|
||||||
this.queue?.dispatch({
|
|
||||||
type: 'MOVE_ITEM',
|
|
||||||
payload: {
|
|
||||||
fromIndex,
|
|
||||||
toIndex,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
this.internalDispatch = false;
|
|
||||||
setTimeout(() => {
|
|
||||||
this.initQueue();
|
|
||||||
this.syncQueueOwner();
|
|
||||||
}, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
clear() {
|
|
||||||
this.internalDispatch = true;
|
|
||||||
this._videoList = [];
|
|
||||||
this.queue?.dispatch({
|
|
||||||
type: 'CLEAR',
|
|
||||||
});
|
|
||||||
this.internalDispatch = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
on(listener: QueueEventListener) {
|
|
||||||
this.listeners.push(listener);
|
|
||||||
}
|
|
||||||
|
|
||||||
off(listener: QueueEventListener) {
|
|
||||||
this.listeners = this.listeners.filter((it) => it !== listener);
|
|
||||||
}
|
|
||||||
|
|
||||||
rollbackInjection() {
|
|
||||||
if (!this.queue) {
|
|
||||||
console.error('Queue is not initialized!');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.originalDispatch)
|
|
||||||
this.queue.queue.store.store.dispatch = this
|
|
||||||
.originalDispatch as Store['dispatch'];
|
|
||||||
}
|
|
||||||
|
|
||||||
injection() {
|
|
||||||
if (!this.queue) {
|
|
||||||
console.error('Queue is not initialized!');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.originalDispatch = this.queue.queue.store.store.dispatch;
|
|
||||||
this.queue.queue.store.store.dispatch = (event) => {
|
|
||||||
if (!this.queue || !this.owner) {
|
|
||||||
console.error('Queue is not initialized!');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.internalDispatch) {
|
|
||||||
if (event.type === 'CLEAR') {
|
|
||||||
this.ignoreFlag = true;
|
|
||||||
}
|
|
||||||
if (event.type === 'ADD_ITEMS') {
|
|
||||||
if (this.ignoreFlag) {
|
|
||||||
this.ignoreFlag = false;
|
|
||||||
const videoList = mapQueueItem(
|
|
||||||
(it) =>
|
|
||||||
({
|
|
||||||
videoId: it!.videoId,
|
|
||||||
ownerId: this.owner!.id,
|
|
||||||
}) satisfies VideoData,
|
|
||||||
(
|
|
||||||
event.payload! as {
|
|
||||||
items: QueueItem[];
|
|
||||||
}
|
|
||||||
).items,
|
|
||||||
);
|
|
||||||
const index = this._videoList.length + videoList.length - 1;
|
|
||||||
|
|
||||||
if (videoList.length > 0) {
|
|
||||||
this.broadcast({
|
|
||||||
// play
|
|
||||||
type: 'ADD_SONGS',
|
|
||||||
payload: {
|
|
||||||
videoList,
|
|
||||||
},
|
|
||||||
after: [
|
|
||||||
{
|
|
||||||
type: 'SYNC_PROGRESS',
|
|
||||||
payload: {
|
|
||||||
index,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else if (
|
|
||||||
(
|
|
||||||
event.payload as {
|
|
||||||
items: unknown[];
|
|
||||||
}
|
|
||||||
).items.length === 1
|
|
||||||
) {
|
|
||||||
this.broadcast({
|
|
||||||
// add playlist
|
|
||||||
type: 'ADD_SONGS',
|
|
||||||
payload: {
|
|
||||||
// index: (event.payload as any).index,
|
|
||||||
videoList: mapQueueItem(
|
|
||||||
(it) =>
|
|
||||||
({
|
|
||||||
videoId: it!.videoId,
|
|
||||||
ownerId: this.owner!.id,
|
|
||||||
}) satisfies VideoData,
|
|
||||||
(
|
|
||||||
event.payload! as {
|
|
||||||
items: QueueItem[];
|
|
||||||
}
|
|
||||||
).items,
|
|
||||||
),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.type === 'MOVE_ITEM') {
|
|
||||||
this.broadcast({
|
|
||||||
type: 'MOVE_SONG',
|
|
||||||
payload: {
|
|
||||||
fromIndex: (
|
|
||||||
event.payload as {
|
|
||||||
fromIndex: number;
|
|
||||||
}
|
|
||||||
).fromIndex,
|
|
||||||
toIndex: (
|
|
||||||
event.payload as {
|
|
||||||
toIndex: number;
|
|
||||||
}
|
|
||||||
).toIndex,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (event.type === 'REMOVE_ITEM') {
|
|
||||||
this.broadcast({
|
|
||||||
type: 'REMOVE_SONG',
|
|
||||||
payload: {
|
|
||||||
index: event.payload as number,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (event.type === 'SET_INDEX') {
|
|
||||||
this.broadcast({
|
|
||||||
type: 'SYNC_PROGRESS',
|
|
||||||
payload: {
|
|
||||||
index: event.payload as number,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.type === 'SET_HEADER') event.payload = getHeaderPayload();
|
|
||||||
if (event.type === 'ADD_STEERING_CHIPS') {
|
|
||||||
event.type = 'CLEAR_STEERING_CHIPS';
|
|
||||||
event.payload = undefined;
|
|
||||||
}
|
|
||||||
if (event.type === 'SET_PLAYER_UI_STATE') {
|
|
||||||
if (
|
|
||||||
(event.payload as string) === 'INACTIVE' &&
|
|
||||||
this.videoList.length > 0
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (event.type === 'HAS_SHOWN_AUTOPLAY') return;
|
|
||||||
if (event.type === 'ADD_AUTOMIX_ITEMS') return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const fakeContext = {
|
|
||||||
...this.queue,
|
|
||||||
queue: {
|
|
||||||
...this.queue.queue,
|
|
||||||
store: {
|
|
||||||
...this.queue.queue.store,
|
|
||||||
dispatch: this.originalDispatch,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
this.originalDispatch?.call(
|
|
||||||
fakeContext,
|
|
||||||
event as {
|
|
||||||
type: string;
|
|
||||||
payload?: { items?: QueueItem[] | undefined } | undefined;
|
|
||||||
},
|
|
||||||
);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/* sync */
|
|
||||||
initQueue() {
|
|
||||||
if (!this.queue) return;
|
|
||||||
|
|
||||||
this.internalDispatch = true;
|
|
||||||
this.queue.dispatch({
|
|
||||||
type: 'HAS_SHOWN_AUTOPLAY',
|
|
||||||
payload: false,
|
|
||||||
});
|
|
||||||
this.queue.dispatch({
|
|
||||||
type: 'SET_HEADER',
|
|
||||||
payload: getHeaderPayload(),
|
|
||||||
});
|
|
||||||
this.queue.dispatch({
|
|
||||||
type: 'CLEAR_STEERING_CHIPS',
|
|
||||||
});
|
|
||||||
this.internalDispatch = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
async syncVideo() {
|
|
||||||
const response = await getMusicQueueRenderer(
|
|
||||||
this._videoList.map((it) => it.videoId),
|
|
||||||
);
|
|
||||||
if (!response) return false;
|
|
||||||
|
|
||||||
const items = response.queueDatas.map((it) => it.content);
|
|
||||||
|
|
||||||
this.internalDispatch = true;
|
|
||||||
this.queue?.dispatch({
|
|
||||||
type: 'UPDATE_ITEMS',
|
|
||||||
payload: {
|
|
||||||
items: items,
|
|
||||||
nextQueueItemId:
|
|
||||||
this.queue.queue.store.store.getState().queue.nextQueueItemId,
|
|
||||||
shouldAssignIds: true,
|
|
||||||
currentIndex: -1,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
this.internalDispatch = false;
|
|
||||||
setTimeout(() => {
|
|
||||||
this.initQueue();
|
|
||||||
this.syncQueueOwner();
|
|
||||||
}, 0);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
syncQueueOwner() {
|
|
||||||
const allQueue = document.querySelectorAll('#queue');
|
|
||||||
|
|
||||||
allQueue.forEach((queue) => {
|
|
||||||
const list = Array.from(
|
|
||||||
queue?.querySelectorAll<HTMLElement>('ytmusic-player-queue-item') ?? [],
|
|
||||||
);
|
|
||||||
|
|
||||||
list.forEach((item, index: number | undefined) => {
|
|
||||||
if (typeof index !== 'number') return;
|
|
||||||
|
|
||||||
const id = this._videoList[index]?.ownerId;
|
|
||||||
const data = this.getProfile(id);
|
|
||||||
|
|
||||||
const profile =
|
|
||||||
item.querySelector<HTMLImageElement>('.music-together-owner') ??
|
|
||||||
document.createElement('img');
|
|
||||||
profile.classList.add('music-together-owner');
|
|
||||||
profile.dataset.id = id;
|
|
||||||
profile.dataset.index = index.toString();
|
|
||||||
|
|
||||||
const name =
|
|
||||||
item.querySelector<HTMLElement>('.music-together-name') ??
|
|
||||||
document.createElement('div');
|
|
||||||
name.classList.add('music-together-name');
|
|
||||||
name.textContent =
|
|
||||||
data?.name ?? t('plugins.music-together.internal.unknown-user');
|
|
||||||
|
|
||||||
if (data) {
|
|
||||||
profile.dataset.thumbnail = data.thumbnail ?? '';
|
|
||||||
profile.dataset.name = data.name ?? '';
|
|
||||||
profile.dataset.handleId = data.handleId ?? '';
|
|
||||||
profile.dataset.id = data.id ?? '';
|
|
||||||
|
|
||||||
profile.src = data.thumbnail ?? '';
|
|
||||||
profile.title = data.name ?? '';
|
|
||||||
profile.alt = data.handleId ?? '';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!profile.isConnected) item.append(profile);
|
|
||||||
if (!name.isConnected) item.append(name);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
removeQueueOwner() {
|
|
||||||
const allQueue = document.querySelectorAll('#queue');
|
|
||||||
|
|
||||||
allQueue.forEach((queue) => {
|
|
||||||
const list = Array.from(
|
|
||||||
queue?.querySelectorAll<HTMLElement>('ytmusic-player-queue-item') ?? [],
|
|
||||||
);
|
|
||||||
|
|
||||||
list.forEach((item) => {
|
|
||||||
const profile = item.querySelector<HTMLImageElement>(
|
|
||||||
'.music-together-owner',
|
|
||||||
);
|
|
||||||
const name = item.querySelector<HTMLElement>('.music-together-name');
|
|
||||||
profile?.remove();
|
|
||||||
name?.remove();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/* private */
|
|
||||||
private broadcast(event: ConnectionEventUnion) {
|
|
||||||
this.listeners.forEach((listener) => listener(event));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,7 +0,0 @@
|
|||||||
export const SHA1Hash = async (str: string) => {
|
|
||||||
const enc = new TextEncoder();
|
|
||||||
const hash = await crypto.subtle.digest('SHA-1', enc.encode(str));
|
|
||||||
return Array.from(new Uint8Array(hash))
|
|
||||||
.map((v) => v.toString(16).padStart(2, '0'))
|
|
||||||
.join('');
|
|
||||||
};
|
|
||||||
@ -1,50 +0,0 @@
|
|||||||
import { extractToken, getAuthorizationHeader, getClient } from './client';
|
|
||||||
|
|
||||||
type QueueRendererResponse = {
|
|
||||||
queueDatas: {
|
|
||||||
content: unknown;
|
|
||||||
}[];
|
|
||||||
responseContext: unknown;
|
|
||||||
trackingParams: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getMusicQueueRenderer = async (
|
|
||||||
videoIds: string[],
|
|
||||||
): Promise<QueueRendererResponse | null> => {
|
|
||||||
const token = extractToken();
|
|
||||||
if (!token) return null;
|
|
||||||
|
|
||||||
const response = await fetch(
|
|
||||||
'https://music.youtube.com/youtubei/v1/music/get_queue?key=AIzaSyC9XL3ZjWddXya6X74dJoCTL-WEYFDNX30&prettyPrint=false',
|
|
||||||
{
|
|
||||||
method: 'POST',
|
|
||||||
credentials: 'include',
|
|
||||||
body: JSON.stringify({
|
|
||||||
context: {
|
|
||||||
client: getClient(),
|
|
||||||
request: {
|
|
||||||
useSsl: true,
|
|
||||||
internalExperimentFlags: [],
|
|
||||||
consistencyTokenJars: [],
|
|
||||||
},
|
|
||||||
user: {
|
|
||||||
lockedSafetyMode: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
videoIds,
|
|
||||||
}),
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'Origin': 'https://music.youtube.com',
|
|
||||||
'Authorization': await getAuthorizationHeader(token),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const text = await response.text();
|
|
||||||
try {
|
|
||||||
return JSON.parse(text) as QueueRendererResponse;
|
|
||||||
} catch {}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
@ -1,26 +0,0 @@
|
|||||||
import {
|
|
||||||
ItemPlaylistPanelVideoRenderer,
|
|
||||||
PlaylistPanelVideoWrapperRenderer,
|
|
||||||
QueueItem,
|
|
||||||
} from '@/types/datahost-get-state';
|
|
||||||
|
|
||||||
export const mapQueueItem = <T>(
|
|
||||||
map: (item?: ItemPlaylistPanelVideoRenderer) => T,
|
|
||||||
array: QueueItem[],
|
|
||||||
): T[] =>
|
|
||||||
array
|
|
||||||
.map((item) => {
|
|
||||||
if ('playlistPanelVideoWrapperRenderer' in item) {
|
|
||||||
const keys = Object.keys(
|
|
||||||
item.playlistPanelVideoWrapperRenderer!.primaryRenderer,
|
|
||||||
) as (keyof PlaylistPanelVideoWrapperRenderer['primaryRenderer'])[];
|
|
||||||
return item.playlistPanelVideoWrapperRenderer!.primaryRenderer[keys[0]];
|
|
||||||
}
|
|
||||||
if ('playlistPanelVideoRenderer' in item) {
|
|
||||||
return item.playlistPanelVideoRenderer;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.error('Music Together: Unknown item', item);
|
|
||||||
return undefined;
|
|
||||||
})
|
|
||||||
.map(map);
|
|
||||||
93
src/plugins/music-together/src/Button.tsx
Normal file
93
src/plugins/music-together/src/Button.tsx
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
import { createSignal, Show } from 'solid-js';
|
||||||
|
import { css } from 'solid-styled-components';
|
||||||
|
|
||||||
|
import { useFloating } from 'solid-floating-ui';
|
||||||
|
|
||||||
|
import { autoUpdate, flip, offset } from '@floating-ui/dom';
|
||||||
|
|
||||||
|
import { Portal } from 'solid-js/web';
|
||||||
|
|
||||||
|
import { cacheNoArgs } from '@/providers/decorators';
|
||||||
|
import { MusicTogetherPanel } from '@/plugins/music-together/src/Panel';
|
||||||
|
|
||||||
|
const buttonStyle = cacheNoArgs(
|
||||||
|
() => css`
|
||||||
|
display: inline-flex;
|
||||||
|
|
||||||
|
cursor: pointer;
|
||||||
|
margin-left: 8px;
|
||||||
|
margin-right: 16px;
|
||||||
|
|
||||||
|
& svg {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
fill: rgba(255, 255, 255, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover svg:hover {
|
||||||
|
fill: #fff;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
);
|
||||||
|
|
||||||
|
const popupStyle = cacheNoArgs(
|
||||||
|
() => css`
|
||||||
|
position: fixed;
|
||||||
|
top: var(--offset-y, 0);
|
||||||
|
left: var(--offset-x, 0);
|
||||||
|
|
||||||
|
z-index: 1000;
|
||||||
|
`,
|
||||||
|
);
|
||||||
|
|
||||||
|
export const MusicTogetherButton = () => {
|
||||||
|
const [enabled, setEnabled] = createSignal(false);
|
||||||
|
|
||||||
|
const [anchor, setAnchor] = createSignal<HTMLElement | null>(null);
|
||||||
|
const [panel, setPanel] = createSignal<HTMLElement | null>(null);
|
||||||
|
|
||||||
|
const position = useFloating(anchor, panel, {
|
||||||
|
whileElementsMounted: autoUpdate,
|
||||||
|
strategy: 'fixed',
|
||||||
|
placement: 'bottom-end',
|
||||||
|
middleware: [
|
||||||
|
offset({
|
||||||
|
mainAxis: 4,
|
||||||
|
crossAxis: 0,
|
||||||
|
}),
|
||||||
|
flip({ fallbackStrategy: 'bestFit' }),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
id="music-together-setting-button"
|
||||||
|
class={`${buttonStyle()} style-scope ytmusic-nav-bar`}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
ref={setAnchor}
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 -960 960 960"
|
||||||
|
width="24"
|
||||||
|
onClick={() => setEnabled(!enabled())}
|
||||||
|
>
|
||||||
|
<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>
|
||||||
|
<Show when={enabled()}>
|
||||||
|
<Portal>
|
||||||
|
<div
|
||||||
|
ref={setPanel}
|
||||||
|
class={popupStyle()}
|
||||||
|
style={{
|
||||||
|
'--offset-x': `${position.x}px`,
|
||||||
|
'--offset-y': `${position.y}px`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<MusicTogetherPanel />
|
||||||
|
</div>
|
||||||
|
</Portal>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
154
src/plugins/music-together/src/Panel.tsx
Normal file
154
src/plugins/music-together/src/Panel.tsx
Normal file
@ -0,0 +1,154 @@
|
|||||||
|
import { css } from 'solid-styled-components';
|
||||||
|
import { createEffect, Match, Switch } from 'solid-js';
|
||||||
|
|
||||||
|
import { PanelItem } from './PanelItem';
|
||||||
|
import { MusicTogetherStatus } from './Status';
|
||||||
|
import {
|
||||||
|
IconConnect,
|
||||||
|
IconKey,
|
||||||
|
IconMusicCast,
|
||||||
|
IconOff,
|
||||||
|
IconTune,
|
||||||
|
} from './icons';
|
||||||
|
|
||||||
|
import { cacheNoArgs } from '@/providers/decorators';
|
||||||
|
import { t } from '@/i18n';
|
||||||
|
|
||||||
|
import { AppElement } from '@/types/queue';
|
||||||
|
|
||||||
|
import { Host } from '../api/host';
|
||||||
|
import { Guest } from '../api/guest';
|
||||||
|
import { Connection } from '../connection';
|
||||||
|
import { useToast } from '../context/ToastContext';
|
||||||
|
import { setStatus, status } from '../store/status';
|
||||||
|
import { connection, setConnection } from '../store/connection';
|
||||||
|
import { useRendererContext } from '../context/RendererContext';
|
||||||
|
|
||||||
|
const panelStyle = cacheNoArgs(
|
||||||
|
() => css`
|
||||||
|
border-radius: 10px !important;
|
||||||
|
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
|
||||||
|
`,
|
||||||
|
);
|
||||||
|
const horizontalDividerStyle = cacheNoArgs(
|
||||||
|
() => css`
|
||||||
|
background-color: rgba(255, 255, 255, 0.15);
|
||||||
|
width: 100%;
|
||||||
|
height: 1px;
|
||||||
|
`,
|
||||||
|
);
|
||||||
|
|
||||||
|
export const MusicTogetherPanel = () => {
|
||||||
|
const show = useToast();
|
||||||
|
const { ipc } = useRendererContext();
|
||||||
|
|
||||||
|
const onHost = async () => {
|
||||||
|
setStatus('mode', 'connecting');
|
||||||
|
const result = new Connection();
|
||||||
|
await result.waitForReady();
|
||||||
|
setStatus('mode', 'host');
|
||||||
|
setConnection(result);
|
||||||
|
|
||||||
|
await onHostCopy();
|
||||||
|
};
|
||||||
|
const onHostCopy = async () => {
|
||||||
|
const id = connection()?.id;
|
||||||
|
|
||||||
|
if (!id) {
|
||||||
|
show(t('plugins.music-together.toast.id-copy-failed'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const success = await navigator.clipboard
|
||||||
|
.writeText(id)
|
||||||
|
.then(() => true)
|
||||||
|
.catch(() => false);
|
||||||
|
|
||||||
|
if (!success) {
|
||||||
|
show(t('plugins.music-together.toast.id-copy-failed'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
show(t('plugins.music-together.toast.id-copied', { id }));
|
||||||
|
};
|
||||||
|
|
||||||
|
const onClose = () => {
|
||||||
|
setStatus('mode', 'disconnected');
|
||||||
|
connection()?.disconnect();
|
||||||
|
setConnection(null);
|
||||||
|
|
||||||
|
show(t('plugins.music-together.toast.closed'));
|
||||||
|
};
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
const conn = connection();
|
||||||
|
const mode = status.mode;
|
||||||
|
const app = document.querySelector<AppElement>('ytmusic-app');
|
||||||
|
|
||||||
|
if (conn && app) {
|
||||||
|
if (mode === 'host') {
|
||||||
|
const listener = Host.buildListener(conn, {
|
||||||
|
ipc,
|
||||||
|
app,
|
||||||
|
});
|
||||||
|
conn.on(listener);
|
||||||
|
}
|
||||||
|
if (mode === 'guest') {
|
||||||
|
const listener = Guest.buildListener(conn, {
|
||||||
|
ipc,
|
||||||
|
app,
|
||||||
|
});
|
||||||
|
conn.on(listener);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<tp-yt-paper-listbox
|
||||||
|
class={`style-scope ytmusic-menu-popup-renderer ${panelStyle()}`}
|
||||||
|
>
|
||||||
|
<MusicTogetherStatus />
|
||||||
|
<Switch>
|
||||||
|
<Match when={status.mode === 'disconnected'}>
|
||||||
|
<div class={horizontalDividerStyle()} />
|
||||||
|
<PanelItem
|
||||||
|
text={t('plugins.music-together.menu.host')}
|
||||||
|
icon={<IconMusicCast width={24} height={24} />}
|
||||||
|
onClick={onHost}
|
||||||
|
/>
|
||||||
|
<PanelItem
|
||||||
|
text={t('plugins.music-together.menu.join')}
|
||||||
|
icon={<IconConnect width={24} height={24} />}
|
||||||
|
/>
|
||||||
|
</Match>
|
||||||
|
<Match when={status.mode === 'host'}>
|
||||||
|
<div class={horizontalDividerStyle()} />
|
||||||
|
<PanelItem
|
||||||
|
text={t('plugins.music-together.menu.click-to-copy-id')}
|
||||||
|
icon={<IconKey width={24} height={24} />}
|
||||||
|
onClick={onHostCopy}
|
||||||
|
/>
|
||||||
|
<PanelItem
|
||||||
|
text={t('plugins.music-together.menu.set-permission', {
|
||||||
|
permission: t('plugins.music-together.menu.permission.host-only'),
|
||||||
|
})}
|
||||||
|
icon={<IconTune width={24} height={24} />}
|
||||||
|
/>
|
||||||
|
<div class={horizontalDividerStyle()} />
|
||||||
|
<PanelItem
|
||||||
|
text={t('plugins.music-together.menu.close')}
|
||||||
|
icon={<IconOff width={24} height={24} />}
|
||||||
|
onClick={onClose}
|
||||||
|
/>
|
||||||
|
</Match>
|
||||||
|
<Match when={status.mode === 'guest'}>
|
||||||
|
<div class={horizontalDividerStyle()} />
|
||||||
|
<PanelItem
|
||||||
|
text={t('plugins.music-together.menu.close')}
|
||||||
|
icon={<IconOff width={24} height={24} />}
|
||||||
|
onClick={onClose}
|
||||||
|
/>
|
||||||
|
</Match>
|
||||||
|
</Switch>
|
||||||
|
</tp-yt-paper-listbox>
|
||||||
|
);
|
||||||
|
};
|
||||||
43
src/plugins/music-together/src/PanelItem.tsx
Normal file
43
src/plugins/music-together/src/PanelItem.tsx
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import { JSX } from 'solid-js';
|
||||||
|
|
||||||
|
import { css } from 'solid-styled-components';
|
||||||
|
|
||||||
|
import { cacheNoArgs } from '@/providers/decorators';
|
||||||
|
|
||||||
|
const itemStyle = cacheNoArgs(
|
||||||
|
() => css`
|
||||||
|
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)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
);
|
||||||
|
|
||||||
|
export type PanelItemProps = {
|
||||||
|
icon: JSX.Element;
|
||||||
|
text: string;
|
||||||
|
onClick?: () => void;
|
||||||
|
};
|
||||||
|
export const PanelItem = (props: PanelItemProps) => {
|
||||||
|
return (
|
||||||
|
<div class={`style-scope ${itemStyle()}`} onClick={props.onClick}>
|
||||||
|
<div class="icon style-scope ytmusic-menu-service-item-renderer">
|
||||||
|
{props.icon}
|
||||||
|
</div>
|
||||||
|
<div class="text style-scope ytmusic-menu-service-item-renderer">
|
||||||
|
{props.text}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
211
src/plugins/music-together/src/Status.tsx
Normal file
211
src/plugins/music-together/src/Status.tsx
Normal file
@ -0,0 +1,211 @@
|
|||||||
|
import { For, Match, Show, Switch } from 'solid-js';
|
||||||
|
import { css } from 'solid-styled-components';
|
||||||
|
|
||||||
|
import { status } from '../store/status';
|
||||||
|
|
||||||
|
import { cacheNoArgs } from '@/providers/decorators';
|
||||||
|
import { user } from '@/plugins/music-together/store/user';
|
||||||
|
|
||||||
|
const panelStyle = cacheNoArgs(
|
||||||
|
() => css`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
|
||||||
|
padding: 16px;
|
||||||
|
`,
|
||||||
|
);
|
||||||
|
const containerStyle = cacheNoArgs(
|
||||||
|
() => css`
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 16px;
|
||||||
|
`,
|
||||||
|
);
|
||||||
|
const profileStyle = cacheNoArgs(
|
||||||
|
() => css`
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
border-radius: 50%;
|
||||||
|
overflow: hidden;
|
||||||
|
flex-shrink: 0;
|
||||||
|
`,
|
||||||
|
);
|
||||||
|
const itemStyle = cacheNoArgs(
|
||||||
|
() => css`
|
||||||
|
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;
|
||||||
|
`,
|
||||||
|
);
|
||||||
|
const userContainerStyle = cacheNoArgs(
|
||||||
|
() => css`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
overflow: auto;
|
||||||
|
|
||||||
|
gap: 8px;
|
||||||
|
padding-top: 16px;
|
||||||
|
font-size: 14px;
|
||||||
|
`,
|
||||||
|
);
|
||||||
|
const emptyStyle = cacheNoArgs(
|
||||||
|
() => css`
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
font-size: 14px;
|
||||||
|
color: rgba(255, 255, 255, 0.5);
|
||||||
|
text-align: center;
|
||||||
|
`,
|
||||||
|
);
|
||||||
|
const spinnerContainerStyle = cacheNoArgs(
|
||||||
|
() => css`
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
`,
|
||||||
|
);
|
||||||
|
const horizontalDividerStyle = cacheNoArgs(
|
||||||
|
() => css`
|
||||||
|
background-color: rgba(255, 255, 255, 0.15);
|
||||||
|
width: 100%;
|
||||||
|
height: 1px;
|
||||||
|
`,
|
||||||
|
);
|
||||||
|
|
||||||
|
export const MusicTogetherStatus = () => {
|
||||||
|
return (
|
||||||
|
<div class={panelStyle()}>
|
||||||
|
<div class={containerStyle()}>
|
||||||
|
<img
|
||||||
|
class={profileStyle()}
|
||||||
|
style={{
|
||||||
|
width: '24px',
|
||||||
|
height: '24px',
|
||||||
|
}}
|
||||||
|
src={user.thumbnail}
|
||||||
|
alt="Profile Image"
|
||||||
|
/>
|
||||||
|
<div class={itemStyle()}>
|
||||||
|
<ytmd-trans key="plugins.music-together.name" />
|
||||||
|
<span id="music-together-status-label">
|
||||||
|
<Switch>
|
||||||
|
<Match when={status.mode === 'disconnected'}>
|
||||||
|
<ytmd-trans
|
||||||
|
key="plugins.music-together.menu.status.disconnected"
|
||||||
|
style={{ color: 'rgba(255, 255, 255, 0.5)' }}
|
||||||
|
/>
|
||||||
|
</Match>
|
||||||
|
<Match when={status.mode === 'host'}>
|
||||||
|
<ytmd-trans
|
||||||
|
key="plugins.music-together.menu.status.host"
|
||||||
|
style={{ color: 'rgba(255, 0, 0, 1)' }}
|
||||||
|
/>
|
||||||
|
</Match>
|
||||||
|
<Match when={status.mode === 'guest'}>
|
||||||
|
<ytmd-trans
|
||||||
|
key="plugins.music-together.menu.status.guest"
|
||||||
|
style={{ color: 'rgba(255, 255, 255, 1)' }}
|
||||||
|
/>
|
||||||
|
</Match>
|
||||||
|
<Match when={status.mode === 'connecting'}>
|
||||||
|
<ytmd-trans
|
||||||
|
key="plugins.music-together.menu.status.connecting"
|
||||||
|
style={{ color: 'rgba(255, 255, 255, 0.5)' }}
|
||||||
|
/>
|
||||||
|
</Match>
|
||||||
|
</Switch>
|
||||||
|
</span>
|
||||||
|
<Show
|
||||||
|
when={
|
||||||
|
status.mode !== 'connecting' && status.mode !== 'disconnected'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<marquee id="music-together-permission-label">
|
||||||
|
<Switch>
|
||||||
|
<Match when={status.permission === 'all'}>
|
||||||
|
<ytmd-trans
|
||||||
|
key="plugins.music-together.menu.permission.all"
|
||||||
|
style={{ color: 'rgba(255, 255, 255, 1)' }}
|
||||||
|
/>
|
||||||
|
</Match>
|
||||||
|
<Match when={status.permission === 'playlist'}>
|
||||||
|
<ytmd-trans
|
||||||
|
key="plugins.music-together.menu.permission.playlist"
|
||||||
|
style={{ color: 'rgba(255, 255, 255, 0.75)' }}
|
||||||
|
/>
|
||||||
|
</Match>
|
||||||
|
<Match when={status.permission === 'host-only'}>
|
||||||
|
<ytmd-trans
|
||||||
|
key="plugins.music-together.menu.permission.host-only"
|
||||||
|
style={{ color: 'rgba(255, 255, 255, 0.5)' }}
|
||||||
|
/>
|
||||||
|
</Match>
|
||||||
|
</Switch>
|
||||||
|
</marquee>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Show
|
||||||
|
when={status.mode !== 'connecting' && status.mode !== 'disconnected'}
|
||||||
|
fallback={
|
||||||
|
<Show when={status.mode === 'connecting'}>
|
||||||
|
<div
|
||||||
|
class={horizontalDividerStyle()}
|
||||||
|
style={{
|
||||||
|
'margin-top': '16px',
|
||||||
|
'margin-bottom': '32px',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div class={spinnerContainerStyle()}>
|
||||||
|
<tp-yt-paper-spinner-lite
|
||||||
|
active
|
||||||
|
id="music-together-host-spinner"
|
||||||
|
class="loading-indicator style-scope music-together-spinner"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div class={horizontalDividerStyle()} style="margin: 16px 0;" />
|
||||||
|
<div class={itemStyle()}>
|
||||||
|
<ytmd-trans
|
||||||
|
key="plugins.music-together.menu.connected-users"
|
||||||
|
attr:count={status.users.length}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class={userContainerStyle()}>
|
||||||
|
<For
|
||||||
|
each={status.users}
|
||||||
|
fallback={
|
||||||
|
<span class={emptyStyle()}>
|
||||||
|
<ytmd-trans key="plugins.music-together.menu.empty-user" />
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{(user) => (
|
||||||
|
<img
|
||||||
|
class={profileStyle()}
|
||||||
|
src={user.thumbnail}
|
||||||
|
title={user.name}
|
||||||
|
alt={`${user.name} (${user.id})`}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
7
src/plugins/music-together/src/icons/IconConnect.tsx
Normal file
7
src/plugins/music-together/src/icons/IconConnect.tsx
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { IconProps } from './types';
|
||||||
|
|
||||||
|
export const IconConnect = (props: IconProps) => (
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" {...props}>
|
||||||
|
<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>
|
||||||
|
);
|
||||||
7
src/plugins/music-together/src/icons/IconKey.tsx
Normal file
7
src/plugins/music-together/src/icons/IconKey.tsx
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { IconProps } from './types';
|
||||||
|
|
||||||
|
export const IconKey = (props: IconProps) => (
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" {...props}>
|
||||||
|
<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>
|
||||||
|
);
|
||||||
7
src/plugins/music-together/src/icons/IconMusicCast.tsx
Normal file
7
src/plugins/music-together/src/icons/IconMusicCast.tsx
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { IconProps } from './types';
|
||||||
|
|
||||||
|
export const IconMusicCast = (props: IconProps) => (
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" {...props}>
|
||||||
|
<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>
|
||||||
|
);
|
||||||
7
src/plugins/music-together/src/icons/IconOff.tsx
Normal file
7
src/plugins/music-together/src/icons/IconOff.tsx
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { IconProps } from './types';
|
||||||
|
|
||||||
|
export const IconOff = (props: IconProps) => (
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" {...props}>
|
||||||
|
<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>
|
||||||
|
);
|
||||||
7
src/plugins/music-together/src/icons/IconTune.tsx
Normal file
7
src/plugins/music-together/src/icons/IconTune.tsx
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { IconProps } from './types';
|
||||||
|
|
||||||
|
export const IconTune = (props: IconProps) => (
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" {...props}>
|
||||||
|
<path d="M440-120v-240h80v80h320v80H520v80h-80Zm-320-80v-80h240v80H120Zm160-160v-80H120v-80h160v-80h80v240h-80Zm160-80v-80h400v80H440Zm160-160v-240h80v80h160v80H680v80h-80Zm-480-80v-80h400v80H120Z"/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
5
src/plugins/music-together/src/icons/index.ts
Normal file
5
src/plugins/music-together/src/icons/index.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export * from './IconConnect';
|
||||||
|
export * from './IconKey';
|
||||||
|
export * from './IconMusicCast';
|
||||||
|
export * from './IconOff';
|
||||||
|
export * from './IconTune';
|
||||||
5
src/plugins/music-together/src/icons/types.ts
Normal file
5
src/plugins/music-together/src/icons/types.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export type IconProps = {
|
||||||
|
width?: number;
|
||||||
|
height?: number;
|
||||||
|
fill?: string;
|
||||||
|
};
|
||||||
56
src/plugins/music-together/src/index.tsx
Normal file
56
src/plugins/music-together/src/index.tsx
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
import { render } from 'solid-js/web';
|
||||||
|
|
||||||
|
import { MusicTogetherButton } from './Button';
|
||||||
|
|
||||||
|
import { AppElement } from '@/types/queue';
|
||||||
|
import { RendererContext } from '@/types/contexts';
|
||||||
|
import { MusicTogetherConfig } from '@/plugins/music-together/types';
|
||||||
|
|
||||||
|
import { ToastProvider } from '../context/ToastContext';
|
||||||
|
import { RendererContextProvider } from '../context/RendererContext';
|
||||||
|
|
||||||
|
import { setUser } from '../store/user';
|
||||||
|
|
||||||
|
export const onRendererLoad = (
|
||||||
|
context: RendererContext<MusicTogetherConfig>,
|
||||||
|
) => {
|
||||||
|
const container = document.createElement('div');
|
||||||
|
const target = document.querySelector<HTMLElement>(
|
||||||
|
'#right-content > ytmusic-settings-button',
|
||||||
|
);
|
||||||
|
const api = document.querySelector<AppElement>('ytmusic-app');
|
||||||
|
|
||||||
|
if (!target) {
|
||||||
|
console.warn('Music Together [renderer]: Cannot inject a button');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const button = target.querySelector<HTMLElement>('tp-yt-paper-icon-button');
|
||||||
|
button?.click();
|
||||||
|
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
const thumbnail = target?.querySelector<HTMLImageElement>('img')?.src;
|
||||||
|
const name = document.querySelector('#account-name')?.textContent;
|
||||||
|
|
||||||
|
if (name) {
|
||||||
|
setUser({ name, thumbnail });
|
||||||
|
|
||||||
|
clearInterval(interval);
|
||||||
|
setTimeout(() => {
|
||||||
|
button?.click();
|
||||||
|
|
||||||
|
target?.insertAdjacentElement('beforebegin', container);
|
||||||
|
render(
|
||||||
|
() => (
|
||||||
|
<RendererContextProvider context={context}>
|
||||||
|
<ToastProvider service={api!.toastService}>
|
||||||
|
<MusicTogetherButton />
|
||||||
|
</ToastProvider>
|
||||||
|
</RendererContextProvider>
|
||||||
|
),
|
||||||
|
container,
|
||||||
|
);
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
}, 1);
|
||||||
|
};
|
||||||
7
src/plugins/music-together/store/connection.ts
Normal file
7
src/plugins/music-together/store/connection.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { createSignal } from 'solid-js';
|
||||||
|
|
||||||
|
import { Connection } from '../connection';
|
||||||
|
|
||||||
|
export const [connection, setConnection] = createSignal<Connection | null>(
|
||||||
|
null,
|
||||||
|
);
|
||||||
12
src/plugins/music-together/store/queue.ts
Normal file
12
src/plugins/music-together/store/queue.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { createStore } from 'solid-js/store';
|
||||||
|
|
||||||
|
import { VideoData } from '../types';
|
||||||
|
|
||||||
|
export type QueueStoreType = {
|
||||||
|
queue: VideoData[];
|
||||||
|
title: string;
|
||||||
|
};
|
||||||
|
export const [queue, setQueue] = createStore<QueueStoreType>({
|
||||||
|
queue: [],
|
||||||
|
title: '',
|
||||||
|
});
|
||||||
28
src/plugins/music-together/store/status.ts
Normal file
28
src/plugins/music-together/store/status.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import { createStore } from 'solid-js/store';
|
||||||
|
|
||||||
|
import { ConnectedState, Permission, User } from '../types';
|
||||||
|
|
||||||
|
// export const getDefaultProfile = (
|
||||||
|
// connectionID: string,
|
||||||
|
// id: string = Date.now().toString(36),
|
||||||
|
// ): User => {
|
||||||
|
// const name = `Guest ${id.slice(-6)}`;
|
||||||
|
//
|
||||||
|
// return {
|
||||||
|
// id: connectionID,
|
||||||
|
// handleId: `#music-together:${id}`,
|
||||||
|
// name,
|
||||||
|
// thumbnail: `https://ui-avatars.com/api/?name=${name}&background=random`,
|
||||||
|
// };
|
||||||
|
// };
|
||||||
|
|
||||||
|
export type StatusStoreType = {
|
||||||
|
mode: ConnectedState;
|
||||||
|
permission: Permission;
|
||||||
|
users: User[];
|
||||||
|
};
|
||||||
|
export const [status, setStatus] = createStore<StatusStoreType>({
|
||||||
|
mode: 'disconnected',
|
||||||
|
permission: 'all',
|
||||||
|
users: [],
|
||||||
|
});
|
||||||
14
src/plugins/music-together/store/user.ts
Normal file
14
src/plugins/music-together/store/user.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { createStore } from 'solid-js/store';
|
||||||
|
|
||||||
|
type ClinetUser = {
|
||||||
|
name: string;
|
||||||
|
thumbnail: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const id = Date.now().toString(36);
|
||||||
|
const name = `Guest ${id.slice(0, 4)}`;
|
||||||
|
const thumbnail = `https://ui-avatars.com/api/?name=${name}&background=random`;
|
||||||
|
export const [user, setUser] = createStore<ClinetUser>({
|
||||||
|
name,
|
||||||
|
thumbnail,
|
||||||
|
});
|
||||||
@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
margin-left: 8px;
|
margin-left: 8px;
|
||||||
|
margin-right: 16px;
|
||||||
|
|
||||||
& svg {
|
& svg {
|
||||||
width: 24px;
|
width: 24px;
|
||||||
@ -83,42 +84,15 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.music-together-status {
|
.music-together-status {
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: stretch;
|
|
||||||
|
|
||||||
padding: 16px;
|
|
||||||
}
|
}
|
||||||
.music-together-profile {
|
.music-together-profile {
|
||||||
width: 24px;
|
|
||||||
height: 24px;
|
|
||||||
border-radius: 50%;
|
|
||||||
overflow: hidden;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
}
|
||||||
.music-together-profile.big {
|
.music-together-profile.big {
|
||||||
width: 32px;
|
|
||||||
height: 32px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.music-together-status-container {
|
.music-together-status-container {
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: flex-start;
|
|
||||||
gap: 16px;
|
|
||||||
}
|
}
|
||||||
.music-together-status-item {
|
.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 {
|
.music-together-user-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -131,11 +105,6 @@
|
|||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
.music-together-empty {
|
.music-together-empty {
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
font-size: 14px;
|
|
||||||
color: rgba(255, 255, 255, .5);
|
|
||||||
text-align: center;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.music-together-owner {
|
.music-together-owner {
|
||||||
|
|||||||
@ -1,8 +0,0 @@
|
|||||||
<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>
|
|
||||||
@ -1,5 +0,0 @@
|
|||||||
<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>
|
|
||||||
@ -1,7 +0,0 @@
|
|||||||
<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>
|
|
||||||
@ -1,23 +0,0 @@
|
|||||||
<div class="music-together-status">
|
|
||||||
<div class="music-together-status-container">
|
|
||||||
<img class="music-together-profile big" alt="Profile Image">
|
|
||||||
<div class="music-together-status-item">
|
|
||||||
<ytmd-trans key="plugins.music-together.name"></ytmd-trans>
|
|
||||||
<span id="music-together-status-label">
|
|
||||||
<ytmd-trans key="plugins.music-together.menu.status.disconnected"></ytmd-trans>
|
|
||||||
</span>
|
|
||||||
<marquee id="music-together-permission-label">
|
|
||||||
<ytmd-trans key="plugins.music-together.menu.permission.playlist" style="color: rgba(255, 255, 255, 0.75)"></ytmd-trans>
|
|
||||||
</marquee>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="music-together-divider horizontal" style="margin: 16px 0;"></div>
|
|
||||||
<div class="music-together-status-item">
|
|
||||||
<ytmd-trans key="plugins.music-together.menu.connected-users"></ytmd-trans>
|
|
||||||
</div>
|
|
||||||
<div class="music-together-user-container">
|
|
||||||
<span class="music-together-empty">
|
|
||||||
<ytmd-trans key="plugins.music-together.menu.empty-user"></ytmd-trans>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@ -1,4 +1,7 @@
|
|||||||
export type Profile = {
|
export type MusicTogetherConfig = {
|
||||||
|
enabled: boolean;
|
||||||
|
};
|
||||||
|
export type User = {
|
||||||
id: string;
|
id: string;
|
||||||
handleId: string;
|
handleId: string;
|
||||||
name: string;
|
name: string;
|
||||||
@ -8,18 +11,25 @@ export type VideoData = {
|
|||||||
videoId: string;
|
videoId: string;
|
||||||
ownerId: string;
|
ownerId: string;
|
||||||
};
|
};
|
||||||
|
export type ConnectedState = 'disconnected' | 'host' | 'guest' | 'connecting';
|
||||||
export type Permission = 'host-only' | 'playlist' | 'all';
|
export type Permission = 'host-only' | 'playlist' | 'all';
|
||||||
|
|
||||||
export const getDefaultProfile = (
|
export type ConnectionEventMap = {
|
||||||
connectionID: string,
|
ADD_SONGS: { videoList: VideoData[]; index?: number };
|
||||||
id: string = Date.now().toString(),
|
REMOVE_SONG: { index: number };
|
||||||
): Profile => {
|
MOVE_SONG: { fromIndex: number; toIndex: number };
|
||||||
const name = `Guest ${id.slice(0, 4)}`;
|
IDENTIFY: { user: User } | undefined;
|
||||||
|
SYNC_USER: { users: User[] } | undefined;
|
||||||
return {
|
SYNC_QUEUE: { videoList: VideoData[] } | undefined;
|
||||||
id: connectionID,
|
SYNC_PROGRESS:
|
||||||
handleId: `#music-together:${id}`,
|
| { progress?: number; state?: number; index?: number }
|
||||||
name,
|
| undefined;
|
||||||
thumbnail: `https://ui-avatars.com/api/?name=${name}&background=random`,
|
PERMISSION: Permission | undefined;
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
export type ConnectionEventUnion = {
|
||||||
|
[Event in keyof ConnectionEventMap]: {
|
||||||
|
type: Event;
|
||||||
|
payload: ConnectionEventMap[Event];
|
||||||
|
after?: ConnectionEventUnion[];
|
||||||
|
};
|
||||||
|
}[keyof ConnectionEventMap];
|
||||||
|
|||||||
@ -1,42 +0,0 @@
|
|||||||
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,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@ -1,62 +0,0 @@
|
|||||||
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,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@ -1,49 +0,0 @@
|
|||||||
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,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@ -1,102 +0,0 @@
|
|||||||
import { ElementFromHtml } from '@/plugins/utils/renderer';
|
|
||||||
import { t } from '@/i18n';
|
|
||||||
|
|
||||||
import statusHTML from '../templates/status.html?raw';
|
|
||||||
|
|
||||||
import type { Permission, Profile } from '../types';
|
|
||||||
|
|
||||||
export const createStatus = () => {
|
|
||||||
const element = ElementFromHtml(statusHTML);
|
|
||||||
const icon = document.querySelector<HTMLImageElement>(
|
|
||||||
'ytmusic-settings-button > tp-yt-paper-icon-button > tp-yt-iron-icon#icon img',
|
|
||||||
);
|
|
||||||
|
|
||||||
const profile = element.querySelector<HTMLImageElement>(
|
|
||||||
'.music-together-profile',
|
|
||||||
)!;
|
|
||||||
const statusLabel = element.querySelector<HTMLSpanElement>(
|
|
||||||
'#music-together-status-label',
|
|
||||||
)!;
|
|
||||||
const permissionLabel = element.querySelector<HTMLMarqueeElement>(
|
|
||||||
'#music-together-permission-label',
|
|
||||||
)!;
|
|
||||||
|
|
||||||
profile.src = icon?.src ?? '';
|
|
||||||
|
|
||||||
const setStatus = (status: 'disconnected' | 'host' | 'guest') => {
|
|
||||||
if (status === 'disconnected') {
|
|
||||||
statusLabel.textContent = t(
|
|
||||||
'plugins.music-together.menu.status.disconnected',
|
|
||||||
);
|
|
||||||
statusLabel.style.color = 'rgba(255, 255, 255, 0.5)';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (status === 'host') {
|
|
||||||
statusLabel.textContent = t('plugins.music-together.menu.status.host');
|
|
||||||
statusLabel.style.color = 'rgba(255, 0, 0, 1)';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (status === 'guest') {
|
|
||||||
statusLabel.textContent = t('plugins.music-together.menu.status.guest');
|
|
||||||
statusLabel.style.color = 'rgba(255, 255, 255, 1)';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const setPermission = (permission: Permission) => {
|
|
||||||
if (permission === 'host-only') {
|
|
||||||
permissionLabel.textContent = t(
|
|
||||||
'plugins.music-together.menu.permission.host-only',
|
|
||||||
);
|
|
||||||
permissionLabel.style.color = 'rgba(255, 255, 255, 0.5)';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (permission === 'playlist') {
|
|
||||||
permissionLabel.textContent = t(
|
|
||||||
'plugins.music-together.menu.permission.playlist',
|
|
||||||
);
|
|
||||||
permissionLabel.style.color = 'rgba(255, 255, 255, 0.75)';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (permission === 'all') {
|
|
||||||
permissionLabel.textContent = t(
|
|
||||||
'plugins.music-together.menu.permission.all',
|
|
||||||
);
|
|
||||||
permissionLabel.style.color = 'rgba(255, 255, 255, 1)';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const setProfile = (src: string) => {
|
|
||||||
profile.src = src;
|
|
||||||
};
|
|
||||||
|
|
||||||
const setUsers = (users: Profile[]) => {
|
|
||||||
const container = element.querySelector<HTMLDivElement>(
|
|
||||||
'.music-together-user-container',
|
|
||||||
)!;
|
|
||||||
const empty = element.querySelector<HTMLElement>('.music-together-empty')!;
|
|
||||||
for (const child of Array.from(container.children)) {
|
|
||||||
if (child !== empty) child.remove();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (users.length === 0) empty.style.display = 'block';
|
|
||||||
else empty.style.display = 'none';
|
|
||||||
|
|
||||||
for (const user of users) {
|
|
||||||
const img = document.createElement('img');
|
|
||||||
img.classList.add('music-together-profile');
|
|
||||||
img.src = user.thumbnail ?? '';
|
|
||||||
img.title = user.name;
|
|
||||||
img.alt = `${user.name} (${user.id})`;
|
|
||||||
|
|
||||||
container.append(img);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
element,
|
|
||||||
setStatus,
|
|
||||||
setUsers,
|
|
||||||
setProfile,
|
|
||||||
setPermission,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@ -14,7 +14,7 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
color: rgba(255, 255, 255, 0.5);
|
color: rgba(255, 255, 255, 0.5);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
margin: 0 var(--ytd-rich-grid-item-margin);
|
margin: 0 var(--ytd-margin-2x, 8px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.navigation-item:hover {
|
.navigation-item:hover {
|
||||||
@ -32,4 +32,5 @@
|
|||||||
width: var(--iron-icon-width, 24px);
|
width: var(--iron-icon-width, 24px);
|
||||||
height: var(--iron-icon-height, 24px);
|
height: var(--iron-icon-height, 24px);
|
||||||
animation: var(--iron-icon_-_animation);
|
animation: var(--iron-icon_-_animation);
|
||||||
|
padding: var(--ytd-margin-base, 4px) var(--ytd-margin-2x, 8px);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -50,7 +50,7 @@ export const SyncedLine = ({ line }: SyncedLineProps) => {
|
|||||||
_ytAPI?.seekTo(line.timeInMs / 1000);
|
_ytAPI?.seekTo(line.timeInMs / 1000);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div class="text-lyrics description ytmusic-description-shelf-renderer">
|
<div dir="auto" class="text-lyrics description ytmusic-description-shelf-renderer">
|
||||||
<yt-formatted-string
|
<yt-formatted-string
|
||||||
text={{
|
text={{
|
||||||
runs: [{ text: config()?.showTimeCodes ? `[${line.time}] ` : '' }],
|
runs: [{ text: config()?.showTimeCodes ? `[${line.time}] ` : '' }],
|
||||||
|
|||||||
@ -71,7 +71,7 @@ export function throttle<T extends (...params: unknown[]) => unknown>(
|
|||||||
}) as T;
|
}) as T;
|
||||||
}
|
}
|
||||||
|
|
||||||
function memoize<T extends (...params: unknown[]) => unknown>(fn: T): T {
|
export function memoize<T extends (...params: unknown[]) => unknown>(fn: T): T {
|
||||||
const cache = new Map();
|
const cache = new Map();
|
||||||
|
|
||||||
return ((...args) => {
|
return ((...args) => {
|
||||||
@ -84,7 +84,7 @@ function memoize<T extends (...params: unknown[]) => unknown>(fn: T): T {
|
|||||||
}) as T;
|
}) as T;
|
||||||
}
|
}
|
||||||
|
|
||||||
function retry<T extends (...params: unknown[]) => Promise<unknown>>(
|
export function retry<T extends (...params: unknown[]) => Promise<unknown>>(
|
||||||
fn: T,
|
fn: T,
|
||||||
{ retries = 3, delay = 1000 } = {},
|
{ retries = 3, delay = 1000 } = {},
|
||||||
) {
|
) {
|
||||||
@ -102,12 +102,3 @@ function retry<T extends (...params: unknown[]) => Promise<unknown>>(
|
|||||||
throw latestError;
|
throw latestError;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default {
|
|
||||||
singleton,
|
|
||||||
debounce,
|
|
||||||
cache,
|
|
||||||
throttle,
|
|
||||||
memoize,
|
|
||||||
retry,
|
|
||||||
};
|
|
||||||
|
|||||||
@ -24,16 +24,6 @@ const parseBooleanFromArgsType = (args: ArgsType<boolean>) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const parseStringFromArgsType = (args: ArgsType<string>) => {
|
|
||||||
if (typeof args === 'string') {
|
|
||||||
return args;
|
|
||||||
} else if (Array.isArray(args)) {
|
|
||||||
return args[0];
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export default (win: BrowserWindow) => {
|
export default (win: BrowserWindow) => {
|
||||||
return {
|
return {
|
||||||
// Playback
|
// Playback
|
||||||
@ -100,15 +90,14 @@ export default (win: BrowserWindow) => {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
// Queue
|
// Queue
|
||||||
addSongToQueue: (videoId: string, queueInsertPosition: string) => {
|
addSongToQueue: (
|
||||||
const videoIdValue = parseStringFromArgsType(videoId);
|
videoIds: string | string[],
|
||||||
if (videoIdValue === null) return;
|
options: {
|
||||||
|
queueInsertPosition?: 'INSERT_AT_END' | 'INSERT_AFTER_CURRENT_VIDEO';
|
||||||
win.webContents.send(
|
index?: number;
|
||||||
'ytmd:add-to-queue',
|
},
|
||||||
videoIdValue,
|
) => {
|
||||||
queueInsertPosition,
|
win.webContents.send('ytmd:add-to-queue', videoIds, options);
|
||||||
);
|
|
||||||
},
|
},
|
||||||
moveSongInQueue: (
|
moveSongInQueue: (
|
||||||
fromIndex: ArgsType<number>,
|
fromIndex: ArgsType<number>,
|
||||||
|
|||||||
@ -171,21 +171,27 @@ async function onApiLoaded() {
|
|||||||
} satisfies QueueResponse);
|
} satisfies QueueResponse);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
type AddToQueueOptions = {
|
||||||
|
queueInsertPosition?: 'INSERT_AT_END' | 'INSERT_AFTER_CURRENT_VIDEO';
|
||||||
|
index?: number;
|
||||||
|
};
|
||||||
window.ipcRenderer.on(
|
window.ipcRenderer.on(
|
||||||
'ytmd:add-to-queue',
|
'ytmd:add-to-queue',
|
||||||
(_, videoId: string, queueInsertPosition: string) => {
|
(_, videoIds: string | string[], { queueInsertPosition = 'INSERT_AT_END', index }: AddToQueueOptions) => {
|
||||||
|
const ids = Array.isArray(videoIds) ? videoIds : [videoIds];
|
||||||
const queue = document.querySelector<QueueElement>('#queue');
|
const queue = document.querySelector<QueueElement>('#queue');
|
||||||
const app = document.querySelector<YouTubeMusicAppElement>('ytmusic-app');
|
const app = document.querySelector<YouTubeMusicAppElement>('ytmusic-app');
|
||||||
if (!app) return;
|
if (!app) return;
|
||||||
|
|
||||||
const store = queue?.queue.store.store;
|
const store = queue?.queue.store.store;
|
||||||
|
console.log('add-to-queue!', ids, queue, app, store);
|
||||||
if (!store) return;
|
if (!store) return;
|
||||||
|
|
||||||
app.networkManager
|
app.networkManager
|
||||||
.fetch('/music/get_queue', {
|
.fetch('/music/get_queue', {
|
||||||
queueContextParams: store.getState().queue.queueContextParams,
|
queueContextParams: store.getState().queue.queueContextParams,
|
||||||
queueInsertPosition,
|
queueInsertPosition,
|
||||||
videoIds: [videoId],
|
videoIds: ids,
|
||||||
})
|
})
|
||||||
.then((result) => {
|
.then((result) => {
|
||||||
if (
|
if (
|
||||||
@ -201,7 +207,8 @@ async function onApiLoaded() {
|
|||||||
payload: {
|
payload: {
|
||||||
nextQueueItemId: store.getState().queue.nextQueueItemId,
|
nextQueueItemId: store.getState().queue.nextQueueItemId,
|
||||||
index:
|
index:
|
||||||
queueInsertPosition === 'INSERT_AFTER_CURRENT_VIDEO'
|
(index ??
|
||||||
|
queueInsertPosition === 'INSERT_AFTER_CURRENT_VIDEO')
|
||||||
? queueItems.findIndex(
|
? queueItems.findIndex(
|
||||||
(it) =>
|
(it) =>
|
||||||
(
|
(
|
||||||
@ -383,8 +390,14 @@ const defineYTMDTransElements = () => {
|
|||||||
YTMDTrans.prototype.connectedCallback = function () {
|
YTMDTrans.prototype.connectedCallback = function () {
|
||||||
const that = this as HTMLElement;
|
const that = this as HTMLElement;
|
||||||
const key = that.getAttribute('key');
|
const key = that.getAttribute('key');
|
||||||
|
const options: Record<string, unknown> = {};
|
||||||
|
that.getAttributeNames().forEach((attr) => {
|
||||||
|
if (attr === 'key') return;
|
||||||
|
|
||||||
|
options[attr] = that.getAttribute(attr);
|
||||||
|
});
|
||||||
if (key) {
|
if (key) {
|
||||||
const targetHtml = i18t(key);
|
const targetHtml = i18t(key, options);
|
||||||
(that.innerHTML as string | TrustedHTML) = defaultTrustedTypePolicy
|
(that.innerHTML as string | TrustedHTML) = defaultTrustedTypePolicy
|
||||||
? defaultTrustedTypePolicy.createHTML(targetHtml)
|
? defaultTrustedTypePolicy.createHTML(targetHtml)
|
||||||
: targetHtml;
|
: targetHtml;
|
||||||
|
|||||||
11
src/yt-web-components.d.ts
vendored
11
src/yt-web-components.d.ts
vendored
@ -42,6 +42,17 @@ declare module 'solid-js' {
|
|||||||
YpYtPaperSpinnerLiteProps;
|
YpYtPaperSpinnerLiteProps;
|
||||||
'tp-yt-paper-icon-button': ComponentProps<'div'> &
|
'tp-yt-paper-icon-button': ComponentProps<'div'> &
|
||||||
TpYtPaperIconButtonProps;
|
TpYtPaperIconButtonProps;
|
||||||
|
'tp-yt-paper-listbox': ComponentProps<'div'>;
|
||||||
|
|
||||||
|
// Non-ytmusic elements
|
||||||
|
'ytmd-trans': ComponentProps<'span'> & {
|
||||||
|
key: string;
|
||||||
|
} & {
|
||||||
|
[key: `attr:${strig}`]: unknown;
|
||||||
|
};
|
||||||
|
|
||||||
|
// fallback
|
||||||
|
'marquee': ComponentProps<'marquee'>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,15 +7,15 @@ import { Project } from 'ts-morph';
|
|||||||
const snakeToCamel = (text: string) =>
|
const snakeToCamel = (text: string) =>
|
||||||
text.replace(/-(\w)/g, (_, letter: string) => letter.toUpperCase());
|
text.replace(/-(\w)/g, (_, letter: string) => letter.toUpperCase());
|
||||||
|
|
||||||
export const i18nImporter = () => {
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
const globalProject = new Project({
|
||||||
const project = new Project({
|
tsConfigFilePath: resolve(__dirname, '..', 'tsconfig.json'),
|
||||||
tsConfigFilePath: resolve(__dirname, '..', 'tsconfig.json'),
|
skipAddingFilesFromTsConfig: true,
|
||||||
skipAddingFilesFromTsConfig: true,
|
skipLoadingLibFiles: true,
|
||||||
skipLoadingLibFiles: true,
|
skipFileDependencyResolution: true,
|
||||||
skipFileDependencyResolution: true,
|
});
|
||||||
});
|
|
||||||
|
|
||||||
|
export const i18nImporter = () => {
|
||||||
const srcPath = resolve(__dirname, '..', 'src');
|
const srcPath = resolve(__dirname, '..', 'src');
|
||||||
const plugins = globSync(['src/i18n/resources/*.json']).map((path) => {
|
const plugins = globSync(['src/i18n/resources/*.json']).map((path) => {
|
||||||
const nameWithExt = basename(path);
|
const nameWithExt = basename(path);
|
||||||
@ -24,24 +24,28 @@ export const i18nImporter = () => {
|
|||||||
return { name, path };
|
return { name, path };
|
||||||
});
|
});
|
||||||
|
|
||||||
const src = project.createSourceFile('vm:i18n', (writer) => {
|
const src = globalProject.createSourceFile(
|
||||||
// prettier-ignore
|
'vm:i18n',
|
||||||
for (const { name, path } of plugins) {
|
(writer) => {
|
||||||
|
// prettier-ignore
|
||||||
|
for (const { name, path } of plugins) {
|
||||||
const relativePath = relative(resolve(srcPath, '..'), path).replace(/\\/g, '/');
|
const relativePath = relative(resolve(srcPath, '..'), path).replace(/\\/g, '/');
|
||||||
writer.writeLine(`import ${snakeToCamel(name)}Json from "./${relativePath}";`);
|
writer.writeLine(`import ${snakeToCamel(name)}Json from "./${relativePath}";`);
|
||||||
}
|
}
|
||||||
|
|
||||||
writer.blankLine();
|
writer.blankLine();
|
||||||
|
|
||||||
writer.writeLine('export const languageResources = {');
|
writer.writeLine('export const languageResources = {');
|
||||||
for (const { name } of plugins) {
|
for (const { name } of plugins) {
|
||||||
writer.writeLine(` "${name}": {`);
|
writer.writeLine(` "${name}": {`);
|
||||||
writer.writeLine(` translation: ${snakeToCamel(name)}Json,`);
|
writer.writeLine(` translation: ${snakeToCamel(name)}Json,`);
|
||||||
writer.writeLine(' },');
|
writer.writeLine(' },');
|
||||||
}
|
}
|
||||||
writer.writeLine('};');
|
writer.writeLine('};');
|
||||||
writer.blankLine();
|
writer.blankLine();
|
||||||
});
|
},
|
||||||
|
{ overwrite: true },
|
||||||
|
);
|
||||||
|
|
||||||
return src.getText();
|
return src.getText();
|
||||||
};
|
};
|
||||||
|
|||||||
@ -7,17 +7,17 @@ import { Project } from 'ts-morph';
|
|||||||
const snakeToCamel = (text: string) =>
|
const snakeToCamel = (text: string) =>
|
||||||
text.replace(/-(\w)/g, (_, letter: string) => letter.toUpperCase());
|
text.replace(/-(\w)/g, (_, letter: string) => letter.toUpperCase());
|
||||||
|
|
||||||
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||||
|
const globalProject = new Project({
|
||||||
|
tsConfigFilePath: resolve(__dirname, '..', 'tsconfig.json'),
|
||||||
|
skipAddingFilesFromTsConfig: true,
|
||||||
|
skipLoadingLibFiles: true,
|
||||||
|
skipFileDependencyResolution: true,
|
||||||
|
});
|
||||||
|
|
||||||
export const pluginVirtualModuleGenerator = (
|
export const pluginVirtualModuleGenerator = (
|
||||||
mode: 'main' | 'preload' | 'renderer',
|
mode: 'main' | 'preload' | 'renderer',
|
||||||
) => {
|
) => {
|
||||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
||||||
const project = new Project({
|
|
||||||
tsConfigFilePath: resolve(__dirname, '..', 'tsconfig.json'),
|
|
||||||
skipAddingFilesFromTsConfig: true,
|
|
||||||
skipLoadingLibFiles: true,
|
|
||||||
skipFileDependencyResolution: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
const srcPath = resolve(__dirname, '..', 'src');
|
const srcPath = resolve(__dirname, '..', 'src');
|
||||||
const plugins = globSync([
|
const plugins = globSync([
|
||||||
'src/plugins/*/index.{js,ts}',
|
'src/plugins/*/index.{js,ts}',
|
||||||
@ -35,35 +35,39 @@ export const pluginVirtualModuleGenerator = (
|
|||||||
return { name, path };
|
return { name, path };
|
||||||
});
|
});
|
||||||
|
|
||||||
const src = project.createSourceFile('vm:pluginIndexes', (writer) => {
|
const src = globalProject.createSourceFile(
|
||||||
// prettier-ignore
|
'vm:pluginIndexes',
|
||||||
for (const { name, path } of plugins) {
|
(writer) => {
|
||||||
|
// prettier-ignore
|
||||||
|
for (const { name, path } of plugins) {
|
||||||
const relativePath = relative(resolve(srcPath, '..'), path).replace(/\\/g, '/');
|
const relativePath = relative(resolve(srcPath, '..'), path).replace(/\\/g, '/');
|
||||||
writer.writeLine(`import ${snakeToCamel(name)}Plugin, { pluginStub as ${snakeToCamel(name)}PluginStub } from "./${relativePath}";`);
|
writer.writeLine(`import ${snakeToCamel(name)}Plugin, { pluginStub as ${snakeToCamel(name)}PluginStub } from "./${relativePath}";`);
|
||||||
}
|
}
|
||||||
|
|
||||||
writer.blankLine();
|
writer.blankLine();
|
||||||
|
|
||||||
// Context-specific exports
|
// Context-specific exports
|
||||||
writer.writeLine(`export const ${mode}Plugins = {`);
|
writer.writeLine(`export const ${mode}Plugins = {`);
|
||||||
for (const { name } of plugins) {
|
for (const { name } of plugins) {
|
||||||
const checkMode = mode === 'main' ? 'backend' : mode;
|
const checkMode = mode === 'main' ? 'backend' : mode;
|
||||||
// HACK: To avoid situation like importing renderer plugins in main
|
// HACK: To avoid situation like importing renderer plugins in main
|
||||||
writer.writeLine(
|
writer.writeLine(
|
||||||
` ...(${snakeToCamel(name)}Plugin['${checkMode}'] ? { "${name}": ${snakeToCamel(name)}Plugin } : {}),`,
|
` ...(${snakeToCamel(name)}Plugin['${checkMode}'] ? { "${name}": ${snakeToCamel(name)}Plugin } : {}),`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
writer.writeLine('};');
|
writer.writeLine('};');
|
||||||
writer.blankLine();
|
writer.blankLine();
|
||||||
|
|
||||||
// All plugins export (stub only) // Omit<Plugin, 'backend' | 'preload' | 'renderer'>
|
// All plugins export (stub only) // Omit<Plugin, 'backend' | 'preload' | 'renderer'>
|
||||||
writer.writeLine('export const allPlugins = {');
|
writer.writeLine('export const allPlugins = {');
|
||||||
for (const { name } of plugins) {
|
for (const { name } of plugins) {
|
||||||
writer.writeLine(` "${name}": ${snakeToCamel(name)}PluginStub,`);
|
writer.writeLine(` "${name}": ${snakeToCamel(name)}PluginStub,`);
|
||||||
}
|
}
|
||||||
writer.writeLine('};');
|
writer.writeLine('};');
|
||||||
writer.blankLine();
|
writer.blankLine();
|
||||||
});
|
},
|
||||||
|
{ overwrite: true },
|
||||||
|
);
|
||||||
|
|
||||||
return src.getText();
|
return src.getText();
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { readFile } from 'node:fs/promises';
|
import { readFileSync } from 'node:fs';
|
||||||
import { resolve, basename, dirname } from 'node:path';
|
import { resolve, basename, dirname } from 'node:path';
|
||||||
import { fileURLToPath } from 'node:url';
|
import { fileURLToPath } from 'node:url';
|
||||||
|
|
||||||
@ -8,10 +8,34 @@ import {
|
|||||||
ts,
|
ts,
|
||||||
ObjectLiteralExpression,
|
ObjectLiteralExpression,
|
||||||
VariableDeclarationKind,
|
VariableDeclarationKind,
|
||||||
|
Node,
|
||||||
|
type ObjectLiteralElementLike,
|
||||||
} from 'ts-morph';
|
} from 'ts-morph';
|
||||||
|
|
||||||
import type { PluginOption } from 'vite';
|
import type { PluginOption } from 'vite';
|
||||||
|
|
||||||
|
// Initialize a global project instance to reuse across load calls
|
||||||
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||||
|
const globalProject = new Project({
|
||||||
|
tsConfigFilePath: resolve(__dirname, '..', 'tsconfig.json'),
|
||||||
|
skipAddingFilesFromTsConfig: true,
|
||||||
|
skipLoadingLibFiles: true,
|
||||||
|
skipFileDependencyResolution: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Helper to extract a property’s name from its node
|
||||||
|
const getPropertyName = (prop: Node): string | null => {
|
||||||
|
const kind = prop.getKind();
|
||||||
|
if (
|
||||||
|
kind === ts.SyntaxKind.PropertyAssignment ||
|
||||||
|
kind === ts.SyntaxKind.ShorthandPropertyAssignment ||
|
||||||
|
kind === ts.SyntaxKind.MethodDeclaration
|
||||||
|
) {
|
||||||
|
return prop.getFirstChildByKindOrThrow(ts.SyntaxKind.Identifier).getText();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
export default function (
|
export default function (
|
||||||
mode: 'backend' | 'preload' | 'renderer' | 'none',
|
mode: 'backend' | 'preload' | 'renderer' | 'none',
|
||||||
): PluginOption {
|
): PluginOption {
|
||||||
@ -22,131 +46,96 @@ export default function (
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
name: 'ytm-plugin-loader',
|
name: 'ytm-plugin-loader',
|
||||||
async load(id) {
|
load(id) {
|
||||||
if (!pluginFilter(id)) return null;
|
if (!pluginFilter(id)) return null;
|
||||||
|
|
||||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
// Read file asynchronously
|
||||||
|
const fileContent = readFileSync(id, 'utf8');
|
||||||
const project = new Project({
|
// Create or update source file in the global project instance
|
||||||
tsConfigFilePath: resolve(__dirname, '..', 'tsconfig.json'),
|
const src = globalProject.createSourceFile(
|
||||||
skipAddingFilesFromTsConfig: true,
|
|
||||||
skipLoadingLibFiles: true,
|
|
||||||
skipFileDependencyResolution: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
const src = project.createSourceFile(
|
|
||||||
'_pf' + basename(id),
|
'_pf' + basename(id),
|
||||||
await readFile(id, 'utf8'),
|
fileContent,
|
||||||
|
{ overwrite: true },
|
||||||
);
|
);
|
||||||
|
|
||||||
const exports = src.getExportedDeclarations();
|
const exports = src.getExportedDeclarations();
|
||||||
let objExpr: ObjectLiteralExpression | undefined = undefined;
|
let objExpr: ObjectLiteralExpression | undefined;
|
||||||
|
|
||||||
for (const [name, [expr]] of exports) {
|
// Identify the default export as an object literal, or via a 'createPlugin' call
|
||||||
if (name !== 'default') continue;
|
for (const [exportName, declarations] of exports) {
|
||||||
|
if (exportName !== 'default') continue;
|
||||||
switch (expr.getKind()) {
|
const expr = declarations[0];
|
||||||
case ts.SyntaxKind.ObjectLiteralExpression: {
|
|
||||||
objExpr = expr.asKindOrThrow(ts.SyntaxKind.ObjectLiteralExpression);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case ts.SyntaxKind.CallExpression: {
|
|
||||||
const callExpr = expr.asKindOrThrow(ts.SyntaxKind.CallExpression);
|
|
||||||
if (callExpr.getArguments().length !== 1) continue;
|
|
||||||
|
|
||||||
const name = callExpr.getExpression().getText();
|
|
||||||
if (name !== 'createPlugin') continue;
|
|
||||||
|
|
||||||
|
const exprKind = expr.getKind();
|
||||||
|
if (exprKind === ts.SyntaxKind.ObjectLiteralExpression) {
|
||||||
|
objExpr = expr.asKindOrThrow(ts.SyntaxKind.ObjectLiteralExpression);
|
||||||
|
break;
|
||||||
|
} else if (exprKind === ts.SyntaxKind.CallExpression) {
|
||||||
|
const callExpr = expr.asKindOrThrow(ts.SyntaxKind.CallExpression);
|
||||||
|
if (
|
||||||
|
callExpr.getArguments().length === 1 &&
|
||||||
|
callExpr.getExpression().getText() === 'createPlugin'
|
||||||
|
) {
|
||||||
const arg = callExpr.getArguments()[0];
|
const arg = callExpr.getArguments()[0];
|
||||||
if (arg.getKind() !== ts.SyntaxKind.ObjectLiteralExpression)
|
if (arg.getKind() === ts.SyntaxKind.ObjectLiteralExpression) {
|
||||||
continue;
|
objExpr = arg.asKindOrThrow(
|
||||||
|
ts.SyntaxKind.ObjectLiteralExpression,
|
||||||
objExpr = arg.asKindOrThrow(ts.SyntaxKind.ObjectLiteralExpression);
|
);
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!objExpr) return null;
|
if (!objExpr) return null;
|
||||||
|
|
||||||
const properties = objExpr.getProperties();
|
// Build a map of property names to their AST nodes for fast lookup
|
||||||
const propertyNames = properties.map((prop) => {
|
const propMap = new Map<string, ObjectLiteralElementLike>();
|
||||||
switch (prop.getKind()) {
|
for (const prop of objExpr.getProperties()) {
|
||||||
case ts.SyntaxKind.PropertyAssignment:
|
const name = getPropertyName(prop);
|
||||||
return prop
|
if (name) propMap.set(name, prop);
|
||||||
.asKindOrThrow(ts.SyntaxKind.PropertyAssignment)
|
}
|
||||||
.getName();
|
|
||||||
case ts.SyntaxKind.ShorthandPropertyAssignment:
|
|
||||||
return prop
|
|
||||||
.asKindOrThrow(ts.SyntaxKind.ShorthandPropertyAssignment)
|
|
||||||
.getName();
|
|
||||||
case ts.SyntaxKind.MethodDeclaration:
|
|
||||||
return prop
|
|
||||||
.asKindOrThrow(ts.SyntaxKind.MethodDeclaration)
|
|
||||||
.getName();
|
|
||||||
default:
|
|
||||||
throw new Error('Not implemented');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const contexts = ['backend', 'preload', 'renderer', 'menu'];
|
const contexts = ['backend', 'preload', 'renderer', 'menu'];
|
||||||
for (const ctx of contexts) {
|
for (const ctx of contexts) {
|
||||||
if (mode === 'none') {
|
if (mode === 'none' && propMap.has(ctx)) {
|
||||||
const index = propertyNames.indexOf(ctx);
|
propMap.get(ctx)?.remove();
|
||||||
if (index === -1) continue;
|
|
||||||
|
|
||||||
objExpr.getProperty(propertyNames[index])?.remove();
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if (ctx === mode || (ctx === 'menu' && mode === 'backend')) continue;
|
||||||
if (ctx === mode) continue;
|
if (propMap.has(ctx)) propMap.get(ctx)?.remove();
|
||||||
if (ctx === 'menu' && mode === 'backend') continue;
|
|
||||||
|
|
||||||
const index = propertyNames.indexOf(ctx);
|
|
||||||
if (index === -1) continue;
|
|
||||||
|
|
||||||
objExpr.getProperty(propertyNames[index])?.remove();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const stubObjExpr = src
|
// Add an exported variable 'pluginStub' with the modified object literal's text
|
||||||
.addVariableStatement({
|
const varStmt = src.addVariableStatement({
|
||||||
isExported: true,
|
isExported: true,
|
||||||
declarationKind: VariableDeclarationKind.Const,
|
declarationKind: VariableDeclarationKind.Const,
|
||||||
declarations: [
|
declarations: [
|
||||||
{
|
{
|
||||||
name: 'pluginStub',
|
name: 'pluginStub',
|
||||||
initializer: (writer) => writer.write(objExpr.getText()),
|
initializer: (writer) => writer.write(objExpr.getText()),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
})
|
|
||||||
.getDeclarations()[0]
|
|
||||||
.getInitializer() as ObjectLiteralExpression;
|
|
||||||
|
|
||||||
const stubProperties = stubObjExpr.getProperties();
|
|
||||||
const stubPropertyNames = stubProperties.map((prop) => {
|
|
||||||
switch (prop.getKind()) {
|
|
||||||
case ts.SyntaxKind.PropertyAssignment:
|
|
||||||
return prop
|
|
||||||
.asKindOrThrow(ts.SyntaxKind.PropertyAssignment)
|
|
||||||
.getName();
|
|
||||||
case ts.SyntaxKind.ShorthandPropertyAssignment:
|
|
||||||
return prop
|
|
||||||
.asKindOrThrow(ts.SyntaxKind.ShorthandPropertyAssignment)
|
|
||||||
.getName();
|
|
||||||
case ts.SyntaxKind.MethodDeclaration:
|
|
||||||
return prop
|
|
||||||
.asKindOrThrow(ts.SyntaxKind.MethodDeclaration)
|
|
||||||
.getName();
|
|
||||||
default:
|
|
||||||
throw new Error('Not implemented');
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
const stubObjExpr = varStmt
|
||||||
|
.getDeclarations()[0]
|
||||||
|
.getInitializerIfKindOrThrow(ts.SyntaxKind.ObjectLiteralExpression);
|
||||||
|
|
||||||
if (mode === 'backend') contexts.pop();
|
// Similarly build a map for the stub properties
|
||||||
for (const ctx of contexts) {
|
const stubMap = new Map<string, ObjectLiteralElementLike>();
|
||||||
const index = stubPropertyNames.indexOf(ctx);
|
for (const prop of stubObjExpr.getProperties()) {
|
||||||
if (index === -1) continue;
|
const name = getPropertyName(prop);
|
||||||
|
if (name) stubMap.set(name, prop);
|
||||||
|
}
|
||||||
|
|
||||||
stubObjExpr.getProperty(stubPropertyNames[index])?.remove();
|
const stubContexts =
|
||||||
|
mode === 'backend'
|
||||||
|
? contexts.filter((ctx) => ctx !== 'backend')
|
||||||
|
: contexts;
|
||||||
|
for (const ctx of stubContexts) {
|
||||||
|
if (stubMap.has(ctx)) {
|
||||||
|
stubMap.get(ctx)?.remove();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
Reference in New Issue
Block a user