mirror of
https://github.com/th-ch/youtube-music.git
synced 2026-01-12 19:01:47 +00:00
Compare commits
92 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8020d61715 | |||
| cb1381bbb3 | |||
| f42f20f770 | |||
| 9fb1dbfde0 | |||
| 4f1ebab45d | |||
| 95644ea513 | |||
| 2fcddc8d2d | |||
| 6505a93645 | |||
| 11c25efd47 | |||
| a97bc8da5a | |||
| 782116b31b | |||
| 708d4b5480 | |||
| 9ba8913da7 | |||
| d07dae2542 | |||
| e7f213c4dc | |||
| 4cbf6c015b | |||
| 8acb93225b | |||
| 8153955ccf | |||
| 1de1cbac65 | |||
| 825aac1dab | |||
| d48aa7ad39 | |||
| 65ad09a02e | |||
| 08aae09446 | |||
| 88f54a389f | |||
| 4c23b1f970 | |||
| a979f1c8ea | |||
| ead448ed98 | |||
| fade340e80 | |||
| 8cc8160f70 | |||
| ee354ff678 | |||
| ca7ccc7b3e | |||
| f6b2766ec2 | |||
| 4a5f811485 | |||
| a092da7ba5 | |||
| b587a16419 | |||
| 4cf4f19ccc | |||
| 5b84c9efce | |||
| 98bbbfd851 | |||
| da2dbcacc4 | |||
| 7b6235694b | |||
| 4ad8e7b9dc | |||
| 51ecfff86b | |||
| 2c21a03201 | |||
| 4c2cb8dac9 | |||
| 9edcd2c32e | |||
| 0829fd2167 | |||
| 494e58296b | |||
| 151da2d786 | |||
| d7f5f28091 | |||
| 358fb3b084 | |||
| af79ba266d | |||
| 45f419f41a | |||
| 5189d6cfee | |||
| 102034b58c | |||
| 50d92bc004 | |||
| f2716e1dc8 | |||
| 9d436ed0a8 | |||
| 31d472e289 | |||
| 7f9a2b3011 | |||
| f54b346dce | |||
| dbff62bc5b | |||
| 87f43e3237 | |||
| 49cdcbdcc2 | |||
| bcff26c85b | |||
| bcfb33b4b1 | |||
| b172d8e509 | |||
| f10d146272 | |||
| 6105821a94 | |||
| 7c983df6f4 | |||
| cbbddefcf8 | |||
| 8d49c67fcb | |||
| c94b22b82c | |||
| cab3cb49f0 | |||
| e42084f008 | |||
| adca273ec3 | |||
| 91dceb3c22 | |||
| 216e76f4a1 | |||
| 178bfa483f | |||
| 10ecf5d2fe | |||
| 7099b81296 | |||
| 1f15376b00 | |||
| 02b7a39753 | |||
| 6edc84a8bd | |||
| 11a0d39064 | |||
| d5a5ed35b6 | |||
| dbb9e95b32 | |||
| d4c8a4320d | |||
| 68d4f38e41 | |||
| 2204784e89 | |||
| c3b995b0a8 | |||
| ed0a344077 | |||
| 199d912823 |
@ -1 +0,0 @@
|
|||||||
.eslintrc.js
|
|
||||||
80
.eslintrc.js
80
.eslintrc.js
@ -1,80 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
extends: [
|
|
||||||
'eslint:recommended',
|
|
||||||
'plugin:import/recommended',
|
|
||||||
'plugin:import/typescript',
|
|
||||||
'plugin:@typescript-eslint/eslint-recommended',
|
|
||||||
'plugin:@typescript-eslint/recommended',
|
|
||||||
'plugin:@typescript-eslint/recommended-requiring-type-checking',
|
|
||||||
],
|
|
||||||
plugins: ['prettier', '@typescript-eslint', 'import'],
|
|
||||||
parser: '@typescript-eslint/parser',
|
|
||||||
parserOptions: {
|
|
||||||
project: './tsconfig.json',
|
|
||||||
tsconfigRootDir: __dirname,
|
|
||||||
sourceType: 'module',
|
|
||||||
ecmaVersion: 'latest'
|
|
||||||
},
|
|
||||||
rules: {
|
|
||||||
'arrow-parens': ['error', 'always'],
|
|
||||||
'object-curly-spacing': ['error', 'always'],
|
|
||||||
'@typescript-eslint/no-floating-promises': 'off',
|
|
||||||
'@typescript-eslint/no-misused-promises': ['off', { checksVoidReturn: false }],
|
|
||||||
'@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }],
|
|
||||||
"@typescript-eslint/no-non-null-assertion": "off",
|
|
||||||
'import/first': 'error',
|
|
||||||
'import/newline-after-import': 'error',
|
|
||||||
'import/no-default-export': 'off',
|
|
||||||
'import/no-duplicates': 'error',
|
|
||||||
'import/no-unresolved': ['error', { ignore: ['^virtual:', '\\?inline$', '\\?raw$', '\\?asset&asarUnpack'] }],
|
|
||||||
'import/order': [
|
|
||||||
'error',
|
|
||||||
{
|
|
||||||
'groups': ['builtin', 'external', ['internal', 'index', 'sibling'], 'parent', 'type'],
|
|
||||||
'newlines-between': 'always-and-inside-groups',
|
|
||||||
'alphabetize': {order: 'ignore', caseInsensitive: false}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
'import/prefer-default-export': 'off',
|
|
||||||
'camelcase': ['error', {properties: 'never'}],
|
|
||||||
'class-methods-use-this': 'off',
|
|
||||||
'lines-around-comment': [
|
|
||||||
'error',
|
|
||||||
{
|
|
||||||
beforeBlockComment: false,
|
|
||||||
afterBlockComment: false,
|
|
||||||
beforeLineComment: false,
|
|
||||||
afterLineComment: false,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
'max-len': 'off',
|
|
||||||
'no-mixed-operators': 'error',
|
|
||||||
'no-multi-spaces': ['error', {ignoreEOLComments: true}],
|
|
||||||
'no-tabs': 'error',
|
|
||||||
'no-void': 'error',
|
|
||||||
'no-empty': 'off',
|
|
||||||
'prefer-promise-reject-errors': 'off',
|
|
||||||
'quotes': ['error', 'single', {
|
|
||||||
avoidEscape: true,
|
|
||||||
allowTemplateLiterals: false,
|
|
||||||
}],
|
|
||||||
'quote-props': ['error', 'consistent'],
|
|
||||||
'semi': ['error', 'always'],
|
|
||||||
},
|
|
||||||
env: {
|
|
||||||
browser: true,
|
|
||||||
node: true,
|
|
||||||
es6: true,
|
|
||||||
},
|
|
||||||
ignorePatterns: ['dist', 'node_modules'],
|
|
||||||
root: true,
|
|
||||||
settings: {
|
|
||||||
'import/parsers': {
|
|
||||||
'@typescript-eslint/parser': ['.ts']
|
|
||||||
},
|
|
||||||
'import/resolver': {
|
|
||||||
typescript: {},
|
|
||||||
exports: {},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
4
.github/workflows/build.yml
vendored
4
.github/workflows/build.yml
vendored
@ -21,7 +21,7 @@ jobs:
|
|||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Install pnpm
|
- name: Install pnpm
|
||||||
uses: pnpm/action-setup@v2
|
uses: pnpm/action-setup@v4
|
||||||
with:
|
with:
|
||||||
version: 9
|
version: 9
|
||||||
run_install: false
|
run_install: false
|
||||||
@ -90,7 +90,7 @@ jobs:
|
|||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Install pnpm
|
- name: Install pnpm
|
||||||
uses: pnpm/action-setup@v2
|
uses: pnpm/action-setup@v4
|
||||||
with:
|
with:
|
||||||
version: 9
|
version: 9
|
||||||
run_install: false
|
run_install: false
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
[](https://github.com/th-ch/youtube-music/releases/)
|
[](https://github.com/th-ch/youtube-music/releases/)
|
||||||
[](https://github.com/th-ch/youtube-music/blob/master/LICENSE)
|
[](https://github.com/th-ch/youtube-music/blob/master/LICENSE)
|
||||||
[](https://github.com/th-ch/youtube-music/blob/master/.eslintrc.js)
|
[](https://github.com/th-ch/youtube-music/blob/master/eslint.config.mjs)
|
||||||
[](https://GitHub.com/th-ch/youtube-music/releases/)
|
[](https://GitHub.com/th-ch/youtube-music/releases/)
|
||||||
[](https://GitHub.com/th-ch/youtube-music/releases/)
|
[](https://GitHub.com/th-ch/youtube-music/releases/)
|
||||||
[](https://aur.archlinux.org/packages/youtube-music-bin)
|
[](https://aur.archlinux.org/packages/youtube-music-bin)
|
||||||
|
|||||||
50
changelog.md
50
changelog.md
@ -2,8 +2,58 @@
|
|||||||
|
|
||||||
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.5.3](https://github.com/th-ch/youtube-music/compare/v3.5.2...v3.5.3)
|
||||||
|
|
||||||
|
- fix: fix `trustedHTML` issue [`#2339`](https://github.com/th-ch/youtube-music/issues/2339)
|
||||||
|
- chore(deps): update dependency rollup to v4.21.3 [`6edc84a`](https://github.com/th-ch/youtube-music/commit/6edc84a8bd6c7e009041117ba0d2004783eb3a47)
|
||||||
|
- chore(deps): update typescript-eslint monorepo to v8.6.0 [`d4c8a43`](https://github.com/th-ch/youtube-music/commit/d4c8a4320d733f7bddc4dcd1de93644790e71d66)
|
||||||
|
- chore(deps): update dependency eslint to v8.57.1 [`02b7a39`](https://github.com/th-ch/youtube-music/commit/02b7a39753528cfd8c0d107d6d2ec6ef78c5afe7)
|
||||||
|
|
||||||
|
#### [v3.5.2](https://github.com/th-ch/youtube-music/compare/v3.5.1...v3.5.2)
|
||||||
|
|
||||||
|
> 7 September 2024
|
||||||
|
|
||||||
|
- chore(deps): update typescript-eslint monorepo to v8.4.0 [`#2401`](https://github.com/th-ch/youtube-music/pull/2401)
|
||||||
|
- chore(deps): update dependency @total-typescript/ts-reset to v0.6.1 [`#2396`](https://github.com/th-ch/youtube-music/pull/2396)
|
||||||
|
- chore(deps): update dependency electron to v31.5.0 [`#2397`](https://github.com/th-ch/youtube-music/pull/2397)
|
||||||
|
- chore(deps): update dependency eslint-import-resolver-typescript to v3.6.3 [`#2376`](https://github.com/th-ch/youtube-music/pull/2376)
|
||||||
|
- chore(deps): update dependency discord-api-types to v0.37.100 [`#2394`](https://github.com/th-ch/youtube-music/pull/2394)
|
||||||
|
- fix(deps): update dependency electron-updater to v6.3.4 [`#2395`](https://github.com/th-ch/youtube-music/pull/2395)
|
||||||
|
- chore(deps): update dependency @babel/runtime to v7.25.6 [`#2388`](https://github.com/th-ch/youtube-music/pull/2388)
|
||||||
|
- chore(deps): update dependency vite-plugin-inspect to v0.8.7 [`#2389`](https://github.com/th-ch/youtube-music/pull/2389)
|
||||||
|
- chore(deps): update dependency discord-api-types to v0.37.99 [`#2374`](https://github.com/th-ch/youtube-music/pull/2374)
|
||||||
|
- chore(deps): update dependency vite to v5.4.3 [`#2377`](https://github.com/th-ch/youtube-music/pull/2377)
|
||||||
|
- fix: incorrect regex when splitting artistName [`#2378`](https://github.com/th-ch/youtube-music/pull/2378)
|
||||||
|
- chore(deps): update dependency @babel/runtime to v7.25.4 [`#2373`](https://github.com/th-ch/youtube-music/pull/2373)
|
||||||
|
- synced-lyrics: make the lyrics search more reliable [`#2343`](https://github.com/th-ch/youtube-music/pull/2343)
|
||||||
|
- fix(deps): update dependency solid-js to v1.8.22 [`#2354`](https://github.com/th-ch/youtube-music/pull/2354)
|
||||||
|
- chore(deps): update typescript-eslint monorepo to v8.3.0 [`#2350`](https://github.com/th-ch/youtube-music/pull/2350)
|
||||||
|
- fix(deps): update dependency electron-debug to v4.0.1 [`#2349`](https://github.com/th-ch/youtube-music/pull/2349)
|
||||||
|
- chore(deps): update dependency electron to v31.4.0 [`#2356`](https://github.com/th-ch/youtube-music/pull/2356)
|
||||||
|
- fix: hide native-controls on linux when in-app-menu is used [`#2366`](https://github.com/th-ch/youtube-music/pull/2366)
|
||||||
|
- fix: detect the upgrade btn using the icon [`#2364`](https://github.com/th-ch/youtube-music/pull/2364)
|
||||||
|
- fix: exclude build-id files from rpm [`#2361`](https://github.com/th-ch/youtube-music/pull/2361)
|
||||||
|
- fix(deps): update dependency i18next to v23.12.3 [`#2352`](https://github.com/th-ch/youtube-music/pull/2352)
|
||||||
|
- fix(deps): update dependency @floating-ui/dom to v1.6.10 [`#2340`](https://github.com/th-ch/youtube-music/pull/2340)
|
||||||
|
- fix(deps): update dependency electron-updater to v6.3.3 [`#2347`](https://github.com/th-ch/youtube-music/pull/2347)
|
||||||
|
- fix(deps): update dependency solid-js to v1.8.20 [`#2345`](https://github.com/th-ch/youtube-music/pull/2345)
|
||||||
|
- chore(deps): update dependency vite to v5.4.0 [`#2342`](https://github.com/th-ch/youtube-music/pull/2342)
|
||||||
|
- chore(deps): update typescript-eslint monorepo to v8.0.1 [`#2335`](https://github.com/th-ch/youtube-music/pull/2335)
|
||||||
|
- fix(deps): update dependency @floating-ui/dom to v1.6.9 [`#2337`](https://github.com/th-ch/youtube-music/pull/2337)
|
||||||
|
- chore(deps): update playwright monorepo to v1.46.0 [`#2336`](https://github.com/th-ch/youtube-music/pull/2336)
|
||||||
|
- chore(README): Translation README to Russian and adding Synced Lyrics to main README [`#2338`](https://github.com/th-ch/youtube-music/pull/2338)
|
||||||
|
- chore(deps): update dependency rollup to v4.20.0 [`#2326`](https://github.com/th-ch/youtube-music/pull/2326)
|
||||||
|
- fix(synced-lyric): fix timestamp [`#2323`](https://github.com/th-ch/youtube-music/issues/2323) [`#2379`](https://github.com/th-ch/youtube-music/issues/2379)
|
||||||
|
- Revert "fix(MPRIS): Prevents player to start with invalid MPRIS interface (#1996)" [`#2225`](https://github.com/th-ch/youtube-music/issues/2225)
|
||||||
|
- fix(adblocker/inplayer): fix Response.prototype.json [`#2310`](https://github.com/th-ch/youtube-music/issues/2310)
|
||||||
|
- chore(deps): update dependency eslint-plugin-import to v2.30.0 [`f48e46d`](https://github.com/th-ch/youtube-music/commit/f48e46d29cf09c76c5172fd56d2d0f705616e4e3)
|
||||||
|
- Revert "chore(deps): update dependency electron-builder to v25" [`089eff3`](https://github.com/th-ch/youtube-music/commit/089eff3152903c8b55ad3e5571b944062a647e27)
|
||||||
|
- chore(deps): update dependency electron-builder to v25 [`fe4c89c`](https://github.com/th-ch/youtube-music/commit/fe4c89c349bb9f4f54d95c2018943095ccfdab0c)
|
||||||
|
|
||||||
#### [v3.5.1](https://github.com/th-ch/youtube-music/compare/v3.5.0...v3.5.1)
|
#### [v3.5.1](https://github.com/th-ch/youtube-music/compare/v3.5.0...v3.5.1)
|
||||||
|
|
||||||
|
> 1 August 2024
|
||||||
|
|
||||||
- fix(deps): update dependency youtubei.js to v10.3.0 [`#2306`](https://github.com/th-ch/youtube-music/pull/2306)
|
- fix(deps): update dependency youtubei.js to v10.3.0 [`#2306`](https://github.com/th-ch/youtube-music/pull/2306)
|
||||||
- fix: Window gets stuck offscreen in some instances [`#2303`](https://github.com/th-ch/youtube-music/pull/2303)
|
- fix: Window gets stuck offscreen in some instances [`#2303`](https://github.com/th-ch/youtube-music/pull/2303)
|
||||||
- fix: Incorrect window size on multi-monitor scaled displays [`#2302`](https://github.com/th-ch/youtube-music/pull/2302)
|
- fix: Incorrect window size on multi-monitor scaled displays [`#2302`](https://github.com/th-ch/youtube-music/pull/2302)
|
||||||
|
|||||||
80
eslint.config.mjs
Normal file
80
eslint.config.mjs
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
//@ts-check
|
||||||
|
|
||||||
|
import eslint from '@eslint/js';
|
||||||
|
import prettier from 'eslint-plugin-prettier/recommended';
|
||||||
|
import stylistic from '@stylistic/eslint-plugin-js';
|
||||||
|
import tsEslint from 'typescript-eslint';
|
||||||
|
|
||||||
|
import * as importPlugin from 'eslint-plugin-import';
|
||||||
|
|
||||||
|
export default tsEslint.config(
|
||||||
|
eslint.configs.recommended,
|
||||||
|
tsEslint.configs.eslintRecommended,
|
||||||
|
...tsEslint.configs.recommendedTypeChecked,
|
||||||
|
prettier,
|
||||||
|
{ ignores: ['dist', 'node_modules', '*.config.*js', '*.test.*js'] },
|
||||||
|
{
|
||||||
|
plugins: {
|
||||||
|
stylistic,
|
||||||
|
importPlugin
|
||||||
|
},
|
||||||
|
languageOptions: {
|
||||||
|
parser: tsEslint.parser,
|
||||||
|
parserOptions: {
|
||||||
|
project: true,
|
||||||
|
sourceType: 'module',
|
||||||
|
ecmaVersion: 'latest'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
'stylistic/arrow-parens': ['error', 'always'],
|
||||||
|
'stylistic/object-curly-spacing': ['error', 'always'],
|
||||||
|
'prettier/prettier': ['error', { singleQuote: true, semi: true, tabWidth: 2, trailingComma: 'all', quoteProps: 'preserve' }],
|
||||||
|
'@typescript-eslint/no-floating-promises': 'off',
|
||||||
|
'@typescript-eslint/no-misused-promises': ['off', { checksVoidReturn: false }],
|
||||||
|
'@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }],
|
||||||
|
'@typescript-eslint/no-non-null-assertion': 'off',
|
||||||
|
'importPlugin/first': 'error',
|
||||||
|
'importPlugin/newline-after-import': 'off',
|
||||||
|
'importPlugin/no-default-export': 'off',
|
||||||
|
'importPlugin/no-duplicates': 'error',
|
||||||
|
'importPlugin/no-unresolved': ['error', { ignore: ['^virtual:', '\\?inline$', '\\?raw$', '\\?asset&asarUnpack'] }],
|
||||||
|
'importPlugin/order': ['error', {
|
||||||
|
'groups': ['builtin', 'external', ['internal', 'index', 'sibling'], 'parent', 'type'],
|
||||||
|
'newlines-between': 'always-and-inside-groups',
|
||||||
|
'alphabetize': { order: 'ignore', caseInsensitive: false }
|
||||||
|
}],
|
||||||
|
'importPlugin/prefer-default-export': 'off',
|
||||||
|
'camelcase': ['error', { properties: 'never' }],
|
||||||
|
'class-methods-use-this': 'off',
|
||||||
|
'stylistic/lines-around-comment': ['error', {
|
||||||
|
beforeBlockComment: false,
|
||||||
|
afterBlockComment: false,
|
||||||
|
beforeLineComment: false,
|
||||||
|
afterLineComment: false,
|
||||||
|
}],
|
||||||
|
'stylistic/max-len': 'off',
|
||||||
|
'stylistic/no-mixed-operators': 'warn', // prettier does not support no-mixed-operators
|
||||||
|
'stylistic/no-multi-spaces': ['error', { ignoreEOLComments: true }],
|
||||||
|
'stylistic/no-tabs': 'error',
|
||||||
|
'no-void': 'error',
|
||||||
|
'no-empty': 'off',
|
||||||
|
'prefer-promise-reject-errors': 'off',
|
||||||
|
'stylistic/quotes': ['error', 'single', {
|
||||||
|
avoidEscape: true,
|
||||||
|
allowTemplateLiterals: false,
|
||||||
|
}],
|
||||||
|
'stylistic/quote-props': ['error', 'consistent'],
|
||||||
|
'stylistic/semi': ['error', 'always'],
|
||||||
|
},
|
||||||
|
settings: {
|
||||||
|
'import/parsers': {
|
||||||
|
'@typescript-eslint/parser': ['.ts']
|
||||||
|
},
|
||||||
|
'import/resolver': {
|
||||||
|
typescript: {},
|
||||||
|
exports: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
60
package.json
60
package.json
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "youtube-music",
|
"name": "youtube-music",
|
||||||
"productName": "YouTube Music",
|
"productName": "YouTube Music",
|
||||||
"version": "3.5.2",
|
"version": "3.6.0",
|
||||||
"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",
|
||||||
@ -149,7 +149,7 @@
|
|||||||
"xml2js": "0.6.2",
|
"xml2js": "0.6.2",
|
||||||
"node-fetch": "3.3.2",
|
"node-fetch": "3.3.2",
|
||||||
"@electron/universal": "2.0.1",
|
"@electron/universal": "2.0.1",
|
||||||
"@babel/runtime": "7.25.6"
|
"@babel/runtime": "7.25.7"
|
||||||
},
|
},
|
||||||
"patchedDependencies": {
|
"patchedDependencies": {
|
||||||
"vudio@2.1.1": "patches/vudio@2.1.1.patch",
|
"vudio@2.1.1": "patches/vudio@2.1.1.patch",
|
||||||
@ -163,11 +163,16 @@
|
|||||||
"@electron/remote": "2.1.2",
|
"@electron/remote": "2.1.2",
|
||||||
"@ffmpeg.wasm/core-mt": "0.12.0",
|
"@ffmpeg.wasm/core-mt": "0.12.0",
|
||||||
"@ffmpeg.wasm/main": "0.12.0",
|
"@ffmpeg.wasm/main": "0.12.0",
|
||||||
"@floating-ui/dom": "1.6.10",
|
"@floating-ui/dom": "1.6.11",
|
||||||
"@foobar404/wave": "2.0.5",
|
"@foobar404/wave": "2.0.5",
|
||||||
|
"@hono/node-server": "1.13.2",
|
||||||
|
"@hono/swagger-ui": "0.4.1",
|
||||||
|
"@hono/zod-openapi": "0.16.4",
|
||||||
|
"@hono/zod-validator": "0.4.1",
|
||||||
"@jellybrick/electron-better-web-request": "1.0.4",
|
"@jellybrick/electron-better-web-request": "1.0.4",
|
||||||
"@jellybrick/mpris-service": "2.1.4",
|
"@jellybrick/mpris-service": "2.1.4",
|
||||||
"@skyra/jaro-winkler": "^1.1.1",
|
"@jimp/plugin-invert": "0.22.12",
|
||||||
|
"@skyra/jaro-winkler": "1.1.1",
|
||||||
"@xhayper/discord-rpc": "1.2.0",
|
"@xhayper/discord-rpc": "1.2.0",
|
||||||
"async-mutex": "0.5.0",
|
"async-mutex": "0.5.0",
|
||||||
"butterchurn": "3.0.0-beta.4",
|
"butterchurn": "3.0.0-beta.4",
|
||||||
@ -176,19 +181,21 @@
|
|||||||
"conf": "13.0.1",
|
"conf": "13.0.1",
|
||||||
"custom-electron-prompt": "1.5.8",
|
"custom-electron-prompt": "1.5.8",
|
||||||
"dbus-next": "0.10.2",
|
"dbus-next": "0.10.2",
|
||||||
"deepmerge-ts": "7.1.0",
|
"deepmerge-ts": "7.1.3",
|
||||||
"electron-debug": "4.0.1",
|
"electron-debug": "4.0.1",
|
||||||
"electron-is": "3.0.0",
|
"electron-is": "3.0.0",
|
||||||
"electron-localshortcut": "3.2.1",
|
"electron-localshortcut": "3.2.1",
|
||||||
"electron-store": "10.0.0",
|
"electron-store": "10.0.0",
|
||||||
"electron-unhandled": "4.0.1",
|
"electron-unhandled": "4.0.1",
|
||||||
"electron-updater": "6.3.4",
|
"electron-updater": "6.3.9",
|
||||||
"fast-average-color": "9.4.0",
|
"fast-average-color": "9.4.0",
|
||||||
"fast-equals": "5.0.1",
|
"fast-equals": "5.0.1",
|
||||||
"filenamify": "6.0.0",
|
"filenamify": "6.0.0",
|
||||||
|
"hono": "4.6.4",
|
||||||
"howler": "2.2.4",
|
"howler": "2.2.4",
|
||||||
"html-to-text": "9.0.5",
|
"html-to-text": "9.0.5",
|
||||||
"i18next": "23.14.0",
|
"i18next": "23.15.2",
|
||||||
|
"jimp": "1.6.0",
|
||||||
"keyboardevent-from-electron-accelerator": "2.0.0",
|
"keyboardevent-from-electron-accelerator": "2.0.0",
|
||||||
"keyboardevents-areequal": "0.2.2",
|
"keyboardevents-areequal": "0.2.2",
|
||||||
"node-html-parser": "6.1.13",
|
"node-html-parser": "6.1.13",
|
||||||
@ -198,46 +205,51 @@
|
|||||||
"serve": "14.2.3",
|
"serve": "14.2.3",
|
||||||
"simple-youtube-age-restriction-bypass": "github:organization/Simple-YouTube-Age-Restriction-Bypass#v2.5.9",
|
"simple-youtube-age-restriction-bypass": "github:organization/Simple-YouTube-Age-Restriction-Bypass#v2.5.9",
|
||||||
"solid-floating-ui": "0.3.1",
|
"solid-floating-ui": "0.3.1",
|
||||||
"solid-js": "1.8.22",
|
"solid-js": "1.9.2",
|
||||||
"solid-styled-components": "0.28.5",
|
"solid-styled-components": "0.28.5",
|
||||||
"solid-transition-group": "0.2.3",
|
"solid-transition-group": "0.2.3",
|
||||||
"ts-morph": "23.0.0",
|
"ts-morph": "24.0.0",
|
||||||
"vudio": "2.1.1",
|
"vudio": "2.1.1",
|
||||||
"x11": "2.3.0",
|
"x11": "2.3.0",
|
||||||
"youtubei.js": "10.4.0"
|
"youtubei.js": "10.5.0",
|
||||||
|
"zod": "3.23.8"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@playwright/test": "1.47.0",
|
"@eslint/js": "9.12.0",
|
||||||
|
"@playwright/test": "1.48.0",
|
||||||
|
"@stylistic/eslint-plugin-js": "2.9.0",
|
||||||
"@total-typescript/ts-reset": "0.6.1",
|
"@total-typescript/ts-reset": "0.6.1",
|
||||||
"@types/color": "3.0.6",
|
"@types/color": "3.0.6",
|
||||||
"@types/electron-localshortcut": "3.1.3",
|
"@types/electron-localshortcut": "3.1.3",
|
||||||
"@types/howler": "2.2.11",
|
"@types/eslint__js": "8.42.3",
|
||||||
|
"@types/howler": "2.2.12",
|
||||||
"@types/html-to-text": "9.0.4",
|
"@types/html-to-text": "9.0.4",
|
||||||
"@types/semver": "7.5.8",
|
"@types/semver": "7.5.8",
|
||||||
"@typescript-eslint/eslint-plugin": "8.4.0",
|
"@types/trusted-types": "2.0.7",
|
||||||
"@typescript-eslint/parser": "8.4.0",
|
|
||||||
"bufferutil": "4.0.8",
|
"bufferutil": "4.0.8",
|
||||||
"builtin-modules": "4.0.0",
|
"builtin-modules": "4.0.0",
|
||||||
"cross-env": "7.0.3",
|
"cross-env": "7.0.3",
|
||||||
"del-cli": "5.1.0",
|
"del-cli": "6.0.0",
|
||||||
"discord-api-types": "0.37.100",
|
"discord-api-types": "0.37.101",
|
||||||
"electron": "32.0.2",
|
"electron": "32.2.0",
|
||||||
"electron-builder": "24.13.3",
|
"electron-builder": "24.13.3",
|
||||||
"electron-devtools-installer": "3.2.0",
|
"electron-devtools-installer": "3.2.0",
|
||||||
"electron-vite": "2.3.0",
|
"electron-vite": "2.3.0",
|
||||||
"esbuild": "0.23.1",
|
"esbuild": "0.24.0",
|
||||||
"eslint": "8.57.0",
|
"eslint": "9.12.0",
|
||||||
|
"eslint-config-prettier": "9.1.0",
|
||||||
"eslint-import-resolver-exports": "1.0.0-beta.5",
|
"eslint-import-resolver-exports": "1.0.0-beta.5",
|
||||||
"eslint-import-resolver-typescript": "3.6.3",
|
"eslint-import-resolver-typescript": "3.6.3",
|
||||||
"eslint-plugin-import": "2.30.0",
|
"eslint-plugin-import": "2.31.0",
|
||||||
"eslint-plugin-prettier": "5.2.1",
|
"eslint-plugin-prettier": "5.2.1",
|
||||||
"glob": "11.0.0",
|
"glob": "11.0.0",
|
||||||
"node-gyp": "10.2.0",
|
"node-gyp": "10.2.0",
|
||||||
"playwright": "1.47.0",
|
"playwright": "1.48.0",
|
||||||
"rollup": "4.21.2",
|
"rollup": "4.24.0",
|
||||||
"typescript": "5.5.4",
|
"typescript": "5.6.3",
|
||||||
|
"typescript-eslint": "8.8.1",
|
||||||
"utf-8-validate": "6.0.4",
|
"utf-8-validate": "6.0.4",
|
||||||
"vite": "5.4.3",
|
"vite": "5.4.8",
|
||||||
"vite-plugin-inspect": "0.8.7",
|
"vite-plugin-inspect": "0.8.7",
|
||||||
"vite-plugin-resolve": "2.5.2",
|
"vite-plugin-resolve": "2.5.2",
|
||||||
"vite-plugin-solid": "2.10.2",
|
"vite-plugin-solid": "2.10.2",
|
||||||
|
|||||||
2411
pnpm-lock.yaml
generated
2411
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
38
src/custom-electron-prompt.d.ts
vendored
38
src/custom-electron-prompt.d.ts
vendored
@ -64,29 +64,29 @@ declare module 'custom-electron-prompt' {
|
|||||||
export type PromptOptions<T extends string> = T extends 'input'
|
export type PromptOptions<T extends string> = T extends 'input'
|
||||||
? InputPromptOptions
|
? InputPromptOptions
|
||||||
: T extends 'select'
|
: T extends 'select'
|
||||||
? SelectPromptOptions
|
? SelectPromptOptions
|
||||||
: T extends 'counter'
|
: T extends 'counter'
|
||||||
? CounterPromptOptions
|
? CounterPromptOptions
|
||||||
: T extends 'keybind'
|
: T extends 'keybind'
|
||||||
? KeybindPromptOptions
|
? KeybindPromptOptions
|
||||||
: T extends 'multiInput'
|
: T extends 'multiInput'
|
||||||
? MultiInputPromptOptions
|
? MultiInputPromptOptions
|
||||||
: never;
|
: never;
|
||||||
|
|
||||||
type PromptResult<T extends string> = T extends 'input'
|
type PromptResult<T extends string> = T extends 'input'
|
||||||
? string
|
? string
|
||||||
: T extends 'select'
|
: T extends 'select'
|
||||||
? string
|
? string
|
||||||
: T extends 'counter'
|
: T extends 'counter'
|
||||||
? number
|
? number
|
||||||
: T extends 'keybind'
|
: T extends 'keybind'
|
||||||
? {
|
? {
|
||||||
value: string;
|
value: string;
|
||||||
accelerator: string;
|
accelerator: string;
|
||||||
}[]
|
}[]
|
||||||
: T extends 'multiInput'
|
: T extends 'multiInput'
|
||||||
? string[]
|
? string[]
|
||||||
: never;
|
: never;
|
||||||
|
|
||||||
const prompt: <T extends Type>(
|
const prompt: <T extends Type>(
|
||||||
options?: PromptOptions<T> & { type: T },
|
options?: PromptOptions<T> & { type: T },
|
||||||
|
|||||||
@ -14,9 +14,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"language": {
|
"language": {
|
||||||
"code": "إنجليزي",
|
"code": "ar",
|
||||||
"local-name": "الإنجليزي",
|
"local-name": "العربية",
|
||||||
"name": "الإنجليزية"
|
"name": "Arabic"
|
||||||
},
|
},
|
||||||
"main": {
|
"main": {
|
||||||
"console": {
|
"console": {
|
||||||
@ -194,7 +194,21 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"tray": {
|
"tray": {
|
||||||
"next": "التالي"
|
"next": "التالي",
|
||||||
|
"previous": "السابق",
|
||||||
|
"quit": "خروج",
|
||||||
|
"restart": "إعادة تشغيل التطبيق",
|
||||||
|
"show": "عرض النافدة",
|
||||||
|
"tooltip": {
|
||||||
|
"default": "يوتيوب اغاني",
|
||||||
|
"with-song-info": "يوتيوب أغاني: {{الفنان}}-{{العنوان}}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"plugins": {
|
||||||
|
"adblocker": {
|
||||||
|
"description": "حجب جميع الإعلانات والمسارات خارج الصندوق",
|
||||||
|
"name": "حاجب الإعلانات"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -279,6 +279,49 @@
|
|||||||
},
|
},
|
||||||
"name": "Mode ambient"
|
"name": "Mode ambient"
|
||||||
},
|
},
|
||||||
|
"api-server": {
|
||||||
|
"description": "Afegeix un servidor API per controlar el reproductor",
|
||||||
|
"dialog": {
|
||||||
|
"request": {
|
||||||
|
"buttons": {
|
||||||
|
"allow": "Permet",
|
||||||
|
"deny": "Denegar"
|
||||||
|
},
|
||||||
|
"message": "Permetre que {{ID}} ({{origin}}) accedeixi a l'API?",
|
||||||
|
"title": "Petició d'autorització API"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"menu": {
|
||||||
|
"auth-strategy": {
|
||||||
|
"label": "Estratègia d'autorització",
|
||||||
|
"submenu": {
|
||||||
|
"auth-at-first": {
|
||||||
|
"label": "Autoritza a la primera petició"
|
||||||
|
},
|
||||||
|
"none": {
|
||||||
|
"label": "Sense autorització"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"hostname": {
|
||||||
|
"label": "Nom del host"
|
||||||
|
},
|
||||||
|
"port": {
|
||||||
|
"label": "Port"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"name": "Servidor API [Beta]",
|
||||||
|
"prompt": {
|
||||||
|
"hostname": {
|
||||||
|
"label": "Introdueix el nom del host (per exemple 0.0.0.0) pel servidor API:",
|
||||||
|
"title": "Nom del host"
|
||||||
|
},
|
||||||
|
"port": {
|
||||||
|
"label": "Introdueix el port pel servidor API:",
|
||||||
|
"title": "Port"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"audio-compressor": {
|
"audio-compressor": {
|
||||||
"description": "Aplica compressió a l'àudio (baixa el volum de les parts més sorolloses de la senyal d'àudio i puja el volum de les parts més fluixes)",
|
"description": "Aplica compressió a l'àudio (baixa el volum de les parts més sorolloses de la senyal d'àudio i puja el volum de les parts més fluixes)",
|
||||||
"name": "Compressió d'àudio"
|
"name": "Compressió d'àudio"
|
||||||
|
|||||||
@ -158,18 +158,18 @@
|
|||||||
},
|
},
|
||||||
"remove-upgrade-button": "Remove upgrade button",
|
"remove-upgrade-button": "Remove upgrade button",
|
||||||
"theme": {
|
"theme": {
|
||||||
|
"dialog": {
|
||||||
|
"button": {
|
||||||
|
"cancel": "Cancel",
|
||||||
|
"remove": "Remove"
|
||||||
|
},
|
||||||
|
"remove-theme": "Are you sure you want to remove the custom theme?",
|
||||||
|
"remove-theme-message": "This will remove the custom theme"
|
||||||
|
},
|
||||||
"label": "Theme",
|
"label": "Theme",
|
||||||
"submenu": {
|
"submenu": {
|
||||||
"import-css-file": "Import custom CSS file",
|
"import-css-file": "Import custom CSS file",
|
||||||
"no-theme": "No theme"
|
"no-theme": "No theme"
|
||||||
},
|
|
||||||
"dialog": {
|
|
||||||
"remove-theme": "Are you sure you want to remove the custom theme?",
|
|
||||||
"remove-theme-message": "This will remove the custom theme",
|
|
||||||
"button": {
|
|
||||||
"cancel": "Cancel",
|
|
||||||
"remove": "Remove"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -207,6 +207,10 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"plugins": {
|
"plugins": {
|
||||||
|
"ad-speedup": {
|
||||||
|
"description": "If an ad play it mutes the audio and sets playback speed to 16x",
|
||||||
|
"name": "Ad Speedup"
|
||||||
|
},
|
||||||
"adblocker": {
|
"adblocker": {
|
||||||
"description": "Block all ads and tracking out of the box",
|
"description": "Block all ads and tracking out of the box",
|
||||||
"menu": {
|
"menu": {
|
||||||
@ -214,17 +218,12 @@
|
|||||||
},
|
},
|
||||||
"name": "Ad Blocker"
|
"name": "Ad Blocker"
|
||||||
},
|
},
|
||||||
"ad-speedup": {
|
|
||||||
"name": "Ad Speedup",
|
|
||||||
"description": "If an ad play it mutes the audio and sets playback speed to 16x"
|
|
||||||
},
|
|
||||||
"album-actions": {
|
"album-actions": {
|
||||||
"description": "Adds Undislike, Dislike, Like, and Unlike buttons to apply this to all songs in a playlist or album",
|
"description": "Adds Undislike, Dislike, Like, and Unlike buttons to apply this to all songs in a playlist or album",
|
||||||
"name": "Album Actions"
|
"name": "Album Actions"
|
||||||
},
|
},
|
||||||
"album-color-theme": {
|
"album-color-theme": {
|
||||||
"description": "Applies a dynamic theme and visual effects based on the album color palette",
|
"description": "Applies a dynamic theme and visual effects based on the album color palette",
|
||||||
"name": "Album Color Theme",
|
|
||||||
"menu": {
|
"menu": {
|
||||||
"color-mix-ratio": {
|
"color-mix-ratio": {
|
||||||
"label": "Color mix ratio",
|
"label": "Color mix ratio",
|
||||||
@ -232,7 +231,8 @@
|
|||||||
"percent": "{{ratio}}%"
|
"percent": "{{ratio}}%"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"name": "Album Color Theme"
|
||||||
},
|
},
|
||||||
"ambient-mode": {
|
"ambient-mode": {
|
||||||
"description": "Applies a lighting effect by casting gentle colors from the video, into your screen’s background",
|
"description": "Applies a lighting effect by casting gentle colors from the video, into your screen’s background",
|
||||||
@ -279,6 +279,49 @@
|
|||||||
},
|
},
|
||||||
"name": "Ambient Mode"
|
"name": "Ambient Mode"
|
||||||
},
|
},
|
||||||
|
"api-server": {
|
||||||
|
"description": "Adds an API server to control the player",
|
||||||
|
"name": "API Server [Beta]",
|
||||||
|
"dialog": {
|
||||||
|
"request": {
|
||||||
|
"title": "API authorization request",
|
||||||
|
"message": "Allow {{ID}} ({{origin}}) to access the API?",
|
||||||
|
"buttons": {
|
||||||
|
"allow": "Allow",
|
||||||
|
"deny": "Deny"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"menu": {
|
||||||
|
"hostname": {
|
||||||
|
"label": "Hostname"
|
||||||
|
},
|
||||||
|
"port": {
|
||||||
|
"label": "Port"
|
||||||
|
},
|
||||||
|
"auth-strategy": {
|
||||||
|
"label": "Authorization strategy",
|
||||||
|
"submenu": {
|
||||||
|
"auth-at-first": {
|
||||||
|
"label": "Authorize at first request"
|
||||||
|
},
|
||||||
|
"none": {
|
||||||
|
"label": "No authorization"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"prompt": {
|
||||||
|
"hostname": {
|
||||||
|
"title": "Hostname",
|
||||||
|
"label": "Enter the hostname (like 0.0.0.0) for the API server:"
|
||||||
|
},
|
||||||
|
"port": {
|
||||||
|
"title": "Port",
|
||||||
|
"label": "Enter the port for the API server:"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"audio-compressor": {
|
"audio-compressor": {
|
||||||
"description": "Apply compression to audio (lowers the volume of the loudest parts of the signal and raises the volume of the softest parts)",
|
"description": "Apply compression to audio (lowers the volume of the loudest parts of the signal and raises the volume of the softest parts)",
|
||||||
"name": "Audio Compressor"
|
"name": "Audio Compressor"
|
||||||
@ -414,24 +457,24 @@
|
|||||||
"description": "Downloads MP3 / source audio directly from the interface",
|
"description": "Downloads MP3 / source audio directly from the interface",
|
||||||
"menu": {
|
"menu": {
|
||||||
"choose-download-folder": "Choose download folder",
|
"choose-download-folder": "Choose download folder",
|
||||||
"download-playlist": "Download playlist",
|
|
||||||
"presets": "Presets",
|
|
||||||
"skip-existing": "Skip existing files",
|
|
||||||
"download-finish-settings": {
|
"download-finish-settings": {
|
||||||
"label": "Download on finish",
|
"label": "Download on finish",
|
||||||
|
"prompt": {
|
||||||
|
"last-percent": "After x percent",
|
||||||
|
"last-seconds": "Last x seconds",
|
||||||
|
"title": "Configure when to download"
|
||||||
|
},
|
||||||
"submenu": {
|
"submenu": {
|
||||||
|
"advanced": "Advanced",
|
||||||
"enabled": "Enabled",
|
"enabled": "Enabled",
|
||||||
"mode": "Time mode",
|
"mode": "Time mode",
|
||||||
"seconds": "Seconds",
|
|
||||||
"percent": "Percent",
|
"percent": "Percent",
|
||||||
"advanced": "Advanced"
|
"seconds": "Seconds"
|
||||||
},
|
|
||||||
"prompt": {
|
|
||||||
"title": "Configure when to download",
|
|
||||||
"last-seconds": "Last x seconds",
|
|
||||||
"last-percent": "After x percent"
|
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"download-playlist": "Download playlist",
|
||||||
|
"presets": "Presets",
|
||||||
|
"skip-existing": "Skip existing files"
|
||||||
},
|
},
|
||||||
"name": "Downloader",
|
"name": "Downloader",
|
||||||
"renderer": {
|
"renderer": {
|
||||||
@ -609,19 +652,19 @@
|
|||||||
"dialog": {
|
"dialog": {
|
||||||
"lastfm": {
|
"lastfm": {
|
||||||
"auth-failed": {
|
"auth-failed": {
|
||||||
"title": "Authentication Failed",
|
"message": "Failed to authenticate with Last.fm\nHide the popup until the next restart.",
|
||||||
"message": "Failed to authenticate with Last.fm\nHide the popup until the next restart."
|
"title": "Authentication Failed"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"menu": {
|
"menu": {
|
||||||
"scrobble-other-media": "Scrobble other media",
|
|
||||||
"lastfm": {
|
"lastfm": {
|
||||||
"api-settings": "Last.fm API Settings"
|
"api-settings": "Last.fm API Settings"
|
||||||
},
|
},
|
||||||
"listenbrainz": {
|
"listenbrainz": {
|
||||||
"token": "Enter ListenBrainz user token"
|
"token": "Enter ListenBrainz user token"
|
||||||
}
|
},
|
||||||
|
"scrobble-other-media": "Scrobble other media"
|
||||||
},
|
},
|
||||||
"name": "Scrobbler",
|
"name": "Scrobbler",
|
||||||
"prompt": {
|
"prompt": {
|
||||||
@ -670,55 +713,55 @@
|
|||||||
},
|
},
|
||||||
"synced-lyrics": {
|
"synced-lyrics": {
|
||||||
"description": "Provides synced lyrics to songs, using providers like LRClib.",
|
"description": "Provides synced lyrics to songs, using providers like LRClib.",
|
||||||
"name": "Synced Lyrics",
|
|
||||||
"errors": {
|
"errors": {
|
||||||
"fetch": "⚠️ - An error occurred while fetching the lyrics. Please try again later.",
|
"fetch": "⚠️ - An error occurred while fetching the lyrics. Please try again later.",
|
||||||
"not-found": "⚠️ - No lyrics found for this song."
|
"not-found": "⚠️ - No lyrics found for this song."
|
||||||
},
|
},
|
||||||
"warnings": {
|
|
||||||
"instrumental": "⚠️ - This is an instrumental song",
|
|
||||||
"inexact": "⚠️ - The lyrics for this song may not be exact",
|
|
||||||
"duration-mismatch": "⚠️ - The lyrics may be out of sync due to a duration mismatch."
|
|
||||||
},
|
|
||||||
"refetch-btn": {
|
|
||||||
"normal": "Refetch lyrics",
|
|
||||||
"fetching": "Fetching..."
|
|
||||||
},
|
|
||||||
"menu": {
|
"menu": {
|
||||||
"precise-timing": {
|
"default-text-string": {
|
||||||
"label": "Make the lyrics perfectly synced",
|
"label": "Default character between lyrics",
|
||||||
"tooltip": "Calculate to the milisecond the display of the next line (can have a small impact on performance)"
|
"tooltip": "Choose the default character to use for the gap between lyrics"
|
||||||
},
|
},
|
||||||
"line-effect": {
|
"line-effect": {
|
||||||
"label": "Line effect",
|
"label": "Line effect",
|
||||||
"tooltip": "Choose the effect to apply to the current line",
|
|
||||||
"submenu": {
|
"submenu": {
|
||||||
"scale": {
|
"focus": {
|
||||||
"label": "Scale",
|
"label": "Focus",
|
||||||
"tooltip": "Scale the current line"
|
"tooltip": "Make only the current line white"
|
||||||
},
|
},
|
||||||
"offset": {
|
"offset": {
|
||||||
"label": "Offset",
|
"label": "Offset",
|
||||||
"tooltip": "Offset on the right the current line"
|
"tooltip": "Offset on the right the current line"
|
||||||
},
|
},
|
||||||
"focus": {
|
"scale": {
|
||||||
"label": "Focus",
|
"label": "Scale",
|
||||||
"tooltip": "Make only the current line white"
|
"tooltip": "Scale the current line"
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"tooltip": "Choose the effect to apply to the current line"
|
||||||
},
|
},
|
||||||
"default-text-string": {
|
"precise-timing": {
|
||||||
"label": "Default character between lyrics",
|
"label": "Make the lyrics perfectly synced",
|
||||||
"tooltip": "Choose the default character to use for the gap between lyrics"
|
"tooltip": "Calculate to the milisecond the display of the next line (can have a small impact on performance)"
|
||||||
},
|
|
||||||
"show-time-codes": {
|
|
||||||
"label": "Show time codes",
|
|
||||||
"tooltip": "Show the time codes next to the lyrics"
|
|
||||||
},
|
},
|
||||||
"show-lyrics-even-if-inexact": {
|
"show-lyrics-even-if-inexact": {
|
||||||
"label": "Show lyrics even if inexact",
|
"label": "Show lyrics even if inexact",
|
||||||
"tooltip": "If the song is not found, the plugin tries again with a different search query.\nThe result from the second attempt may not be exact."
|
"tooltip": "If the song is not found, the plugin tries again with a different search query.\nThe result from the second attempt may not be exact."
|
||||||
|
},
|
||||||
|
"show-time-codes": {
|
||||||
|
"label": "Show time codes",
|
||||||
|
"tooltip": "Show the time codes next to the lyrics"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"name": "Synced Lyrics",
|
||||||
|
"refetch-btn": {
|
||||||
|
"fetching": "Fetching...",
|
||||||
|
"normal": "Refetch lyrics"
|
||||||
|
},
|
||||||
|
"warnings": {
|
||||||
|
"duration-mismatch": "⚠️ - The lyrics may be out of sync due to a duration mismatch.",
|
||||||
|
"inexact": "⚠️ - The lyrics for this song may not be exact",
|
||||||
|
"instrumental": "⚠️ - This is an instrumental song"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"taskbar-mediacontrol": {
|
"taskbar-mediacontrol": {
|
||||||
|
|||||||
@ -279,6 +279,48 @@
|
|||||||
},
|
},
|
||||||
"name": "Modo ambiente"
|
"name": "Modo ambiente"
|
||||||
},
|
},
|
||||||
|
"api-server": {
|
||||||
|
"description": "Añade un servidor API para controlar el reproductor",
|
||||||
|
"dialog": {
|
||||||
|
"request": {
|
||||||
|
"buttons": {
|
||||||
|
"allow": "Permitir",
|
||||||
|
"deny": "Denegar"
|
||||||
|
},
|
||||||
|
"message": "¿Permitir {{ID}} ({{origin}}) acceder a la API?",
|
||||||
|
"title": "Petición de autorización API"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"menu": {
|
||||||
|
"auth-strategy": {
|
||||||
|
"submenu": {
|
||||||
|
"auth-at-first": {
|
||||||
|
"label": "Autorizar la primera solicitud"
|
||||||
|
},
|
||||||
|
"none": {
|
||||||
|
"label": "Sin autorización"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"hostname": {
|
||||||
|
"label": "Nombre del host"
|
||||||
|
},
|
||||||
|
"port": {
|
||||||
|
"label": "Puerto"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"name": "Servidor API [Beta]",
|
||||||
|
"prompt": {
|
||||||
|
"hostname": {
|
||||||
|
"label": "Introduzca el nombre de host (como 0.0.0.0) para el servidor API:",
|
||||||
|
"title": "Nombre de host"
|
||||||
|
},
|
||||||
|
"port": {
|
||||||
|
"label": "Introduzca el puerto para el servidor API:",
|
||||||
|
"title": "Puerto"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"audio-compressor": {
|
"audio-compressor": {
|
||||||
"description": "Aplicar compresión al audio (reduce la diferencia entre las partes más fuertes y más suaves de una pista para que tenga un nivel más consistente)",
|
"description": "Aplicar compresión al audio (reduce la diferencia entre las partes más fuertes y más suaves de una pista para que tenga un nivel más consistente)",
|
||||||
"name": "Compresor de audio"
|
"name": "Compresor de audio"
|
||||||
|
|||||||
397
src/i18n/resources/fa.json
Normal file
397
src/i18n/resources/fa.json
Normal file
@ -0,0 +1,397 @@
|
|||||||
|
{
|
||||||
|
"common": {
|
||||||
|
"console": {
|
||||||
|
"plugins": {
|
||||||
|
"execute-failed": "اجرای افزونه {{pluginName}}::{{contextName}} با خطا مواجه شد",
|
||||||
|
"executed-at-ms": "افزونه {{pluginName}}::{{contextName}} در {{ms}} میلیثانیه اجرا شد",
|
||||||
|
"initialize-failed": "افزونه \"{{pluginName}}\" با خطا در حین مقداردهی اولیه مواجه شد",
|
||||||
|
"load-all": "در حال بارگذاری تمامی افزونهها",
|
||||||
|
"load-failed": "افزونه \"{{pluginName}}\" بارگیری نشد",
|
||||||
|
"loaded": "افزونه \"{{pluginName}}\" بارگیری شد",
|
||||||
|
"unload-failed": "افزونه \"{{pluginName}}\" بارگذاری نشد",
|
||||||
|
"unloaded": "افزونه \"{{pluginName}}\" بارگذاری شد"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"language": {
|
||||||
|
"code": "fa",
|
||||||
|
"local-name": "فارسی",
|
||||||
|
"name": "Persian"
|
||||||
|
},
|
||||||
|
"main": {
|
||||||
|
"console": {
|
||||||
|
"did-finish-load": {
|
||||||
|
"dev-tools": "بارگذاری کامل شد. DevTools باز شد"
|
||||||
|
},
|
||||||
|
"i18n": {
|
||||||
|
"loaded": "i18n بارگذاری شد"
|
||||||
|
},
|
||||||
|
"second-instance": {
|
||||||
|
"receive-command": "دریافت فرمان از طریق پروتکل: \"{{command}}\""
|
||||||
|
},
|
||||||
|
"theme": {
|
||||||
|
"css-file-not-found": "فایل CSS \"{{cssFile}}\" وجود ندارد، نادیده گرفته شد"
|
||||||
|
},
|
||||||
|
"unresponsive": {
|
||||||
|
"details": "خطای عدم پاسخگویی!\n{{error}}"
|
||||||
|
},
|
||||||
|
"when-ready": {
|
||||||
|
"clearing-cache-after-20s": "پاکسازی حافظه کش برنامه"
|
||||||
|
},
|
||||||
|
"window": {
|
||||||
|
"tried-to-render-offscreen": "پنجره تلاش کرد خارج از صفحه نمایش داده شود، اندازه پنجره={{windowSize}}، اندازه نمایشگر={{displaySize}}، موقعیت={{position}}"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"dialog": {
|
||||||
|
"hide-menu-enabled": {
|
||||||
|
"detail": "منو مخفی است، از 'Alt' برای نمایش آن استفاده کنید (یا 'Escape' اگر از منوی داخل برنامه استفاده میکنید)",
|
||||||
|
"message": "پنهانسازی منو فعال است",
|
||||||
|
"title": "پنهان کردن منو فعال شد"
|
||||||
|
},
|
||||||
|
"need-to-restart": {
|
||||||
|
"buttons": {
|
||||||
|
"later": "بعداً",
|
||||||
|
"restart-now": "هماکنون راهاندازی مجدد کنید"
|
||||||
|
},
|
||||||
|
"detail": "افزونه \"{{pluginName}}\" برای اعمال تغییرات نیاز به راهاندازی مجدد دارد",
|
||||||
|
"message": "\"{{pluginName}}\" نیاز به راهاندازی مجدد دارد",
|
||||||
|
"title": "نیاز به راهاندازی مجدد"
|
||||||
|
},
|
||||||
|
"unresponsive": {
|
||||||
|
"buttons": {
|
||||||
|
"quit": "خروج",
|
||||||
|
"relaunch": "راهاندازی مجدد",
|
||||||
|
"wait": "منتظر بمانید"
|
||||||
|
},
|
||||||
|
"detail": "از بابت این مشکل متأسفیم! لطفاً انتخاب کنید که چه کاری انجام دهید:",
|
||||||
|
"message": "برنامه پاسخی نمیدهد",
|
||||||
|
"title": "پنجره بدون پاسخ"
|
||||||
|
},
|
||||||
|
"update-available": {
|
||||||
|
"buttons": {
|
||||||
|
"disable": "غیرفعال کردن بهروزرسانیها",
|
||||||
|
"download": "دانلود",
|
||||||
|
"ok": "تأیید"
|
||||||
|
},
|
||||||
|
"detail": "نسخه جدیدی در دسترس است و میتوان آن را از {{downloadLink}} دانلود کرد",
|
||||||
|
"message": "نسخه جدیدی در دسترس است",
|
||||||
|
"title": "بهروزرسانی موجود است"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"menu": {
|
||||||
|
"about": "درباره",
|
||||||
|
"navigation": {
|
||||||
|
"label": "ناوبری",
|
||||||
|
"submenu": {
|
||||||
|
"copy-current-url": "کپی کردن URL فعلی",
|
||||||
|
"go-back": "بازگشت",
|
||||||
|
"go-forward": "حرکت به جلو",
|
||||||
|
"quit": "خروجی",
|
||||||
|
"restart": "راهاندازی مجدد برنامه"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"label": "گزینهها",
|
||||||
|
"submenu": {
|
||||||
|
"advanced-options": {
|
||||||
|
"label": "گزینههای پیشرفته",
|
||||||
|
"submenu": {
|
||||||
|
"auto-reset-app-cache": "ریست کردن حافظه کش برنامه هنگام شروع",
|
||||||
|
"disable-hardware-acceleration": "غیرفعال کردن شتاب سختافزاری",
|
||||||
|
"edit-config-json": "ویرایش config.json",
|
||||||
|
"override-user-agent": "تغییر User-Agent",
|
||||||
|
"restart-on-config-changes": "راهاندازی مجدد در صورت تغییرات در پیکربندی",
|
||||||
|
"set-proxy": {
|
||||||
|
"label": "تنظیم پراکسی",
|
||||||
|
"prompt": {
|
||||||
|
"label": "آدرس پراکسی را وارد کنید: (برای غیرفعال کردن، خالی بگذارید)",
|
||||||
|
"placeholder": "مثال: SOCKS5://127.0.0.1:9999",
|
||||||
|
"title": "تنظیم پراکسی"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"toggle-dev-tools": "باز کردن DevTools"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"always-on-top": "همیشه در بالا",
|
||||||
|
"auto-update": "بهروزرسانی خودکار",
|
||||||
|
"hide-menu": {
|
||||||
|
"dialog": {
|
||||||
|
"message": "منو در اجرای بعدی مخفی خواهد بود، از [Alt] برای نمایش استفاده کنید (یا [`] اگر از منوی داخل برنامه استفاده میکنید)",
|
||||||
|
"title": "پنهانسازی منو فعال شد"
|
||||||
|
},
|
||||||
|
"label": "پنهان کردن منو"
|
||||||
|
},
|
||||||
|
"language": {
|
||||||
|
"dialog": {
|
||||||
|
"message": "زبان پس از راهاندازی مجدد تغییر خواهد کرد",
|
||||||
|
"title": "زبان تغییر کرد"
|
||||||
|
},
|
||||||
|
"label": "زبان",
|
||||||
|
"submenu": {
|
||||||
|
"to-help-translate": "میخواهید به ترجمه کمک کنید؟ اینجا کلیک کنید"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"resume-on-start": "ادامه آخرین آهنگ هنگام شروع برنامه",
|
||||||
|
"single-instance-lock": "قفل تنها یک نمونه",
|
||||||
|
"start-at-login": "شروع هنگام ورود",
|
||||||
|
"starting-page": {
|
||||||
|
"label": "صفحه شروع",
|
||||||
|
"unset": "لغو تنظیم"
|
||||||
|
},
|
||||||
|
"tray": {
|
||||||
|
"label": "نوار",
|
||||||
|
"submenu": {
|
||||||
|
"disabled": "غیرفعال",
|
||||||
|
"enabled-and-hide-app": "فعال و پنهان کردن برنامه",
|
||||||
|
"enabled-and-show-app": "فعال و نمایش برنامه",
|
||||||
|
"play-pause-on-click": "پخش/توقف با کلیک"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"visual-tweaks": {
|
||||||
|
"label": "تغییرات ظاهری",
|
||||||
|
"submenu": {
|
||||||
|
"like-buttons": {
|
||||||
|
"default": "پیشفرض",
|
||||||
|
"force-show": "اجبار به نمایش",
|
||||||
|
"hide": "پنهان کردن",
|
||||||
|
"label": "دکمههای پسندیدن"
|
||||||
|
},
|
||||||
|
"remove-upgrade-button": "حذف دکمه ارتقا",
|
||||||
|
"theme": {
|
||||||
|
"dialog": {
|
||||||
|
"button": {
|
||||||
|
"cancel": "لغو",
|
||||||
|
"remove": "حذف"
|
||||||
|
},
|
||||||
|
"remove-theme": "آیا مطمئن هستید که میخواهید تم سفارشی را حذف کنید؟",
|
||||||
|
"remove-theme-message": "این کار تم سفارشی را حذف خواهد کرد"
|
||||||
|
},
|
||||||
|
"label": "تم",
|
||||||
|
"submenu": {
|
||||||
|
"import-css-file": "وارد کردن فایل CSS سفارشی",
|
||||||
|
"no-theme": "بدون تم"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"plugins": {
|
||||||
|
"enabled": "فعال",
|
||||||
|
"label": "افزونهها",
|
||||||
|
"new": "جدید"
|
||||||
|
},
|
||||||
|
"view": {
|
||||||
|
"label": "مشاهده",
|
||||||
|
"submenu": {
|
||||||
|
"force-reload": "اجبار به بارگذاری مجدد",
|
||||||
|
"reload": "بارگذاری مجدد",
|
||||||
|
"reset-zoom": "اندازه واقعی",
|
||||||
|
"toggle-fullscreen": "تغییر به تمامصفحه",
|
||||||
|
"zoom-in": "بزرگنمایی",
|
||||||
|
"zoom-out": "کوچکنمایی"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tray": {
|
||||||
|
"next": "بعدی",
|
||||||
|
"play-pause": "پخش/توقف",
|
||||||
|
"previous": "قبلی",
|
||||||
|
"quit": "خروجی",
|
||||||
|
"restart": "راهاندازی مجدد برنامه",
|
||||||
|
"show": "نمایش پنجره",
|
||||||
|
"tooltip": {
|
||||||
|
"default": "یوتیوب موسیقی",
|
||||||
|
"with-song-info": "یوتیوب موسیقی: {{artist}} - {{title}}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"plugins": {
|
||||||
|
"ad-speedup": {
|
||||||
|
"description": "اگر تبلیغ پخش شود، صدا را بیصدا کرده و سرعت پخش را به 16 برابر افزایش میدهد",
|
||||||
|
"name": "سرعتدهی به تبلیغ"
|
||||||
|
},
|
||||||
|
"adblocker": {
|
||||||
|
"description": "مسدود کردن تمامی تبلیغات و ردیابیها از ابتدا",
|
||||||
|
"menu": {
|
||||||
|
"blocker": "مسدودکننده"
|
||||||
|
},
|
||||||
|
"name": "مسدودکننده تبلیغات"
|
||||||
|
},
|
||||||
|
"album-actions": {
|
||||||
|
"description": "افزودن دکمههای \"برگرفتن ناپسند\"، \"ناپسند\"، \"پسند\"، و \"حذف پسند\" برای اعمال آنها روی همه آهنگها در یک فهرست پخش یا آلبوم",
|
||||||
|
"name": "عملیات آلبوم"
|
||||||
|
},
|
||||||
|
"album-color-theme": {
|
||||||
|
"description": "اعمال یک تم پویا و جلوههای بصری بر اساس پالت رنگ آلبوم",
|
||||||
|
"menu": {
|
||||||
|
"color-mix-ratio": {
|
||||||
|
"label": "نسبت ترکیب رنگ"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"name": "تم رنگ آلبوم"
|
||||||
|
},
|
||||||
|
"ambient-mode": {
|
||||||
|
"description": "اعمال یک اثر نوری با پخش رنگهای ملایم از ویدئو به پسزمینه صفحه نمایش شما",
|
||||||
|
"menu": {
|
||||||
|
"blur-amount": {
|
||||||
|
"label": "میزان تاری",
|
||||||
|
"submenu": {
|
||||||
|
"pixels": "{{blurAmount}} پیکسل"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"buffer": {
|
||||||
|
"label": "بافر"
|
||||||
|
},
|
||||||
|
"opacity": {
|
||||||
|
"label": "شفافیت"
|
||||||
|
},
|
||||||
|
"quality": {
|
||||||
|
"label": "کیفیت",
|
||||||
|
"submenu": {
|
||||||
|
"pixels": "{{quality}} پیکسل"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"size": {
|
||||||
|
"label": "اندازه"
|
||||||
|
},
|
||||||
|
"smoothness-transition": {
|
||||||
|
"label": "انتقال نرمی",
|
||||||
|
"submenu": {
|
||||||
|
"during": "در طول {{interpolationTime}} ثانیه"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"use-fullscreen": {
|
||||||
|
"label": "استفاده از تمامصفحه"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"name": "حالت محیطی"
|
||||||
|
},
|
||||||
|
"audio-compressor": {
|
||||||
|
"description": "اعمال فشردهسازی به صدا (کاهش حجم بلندترین بخشهای سیگنال و افزایش حجم بخشهای نرمتر)",
|
||||||
|
"name": "فشردهساز صدا"
|
||||||
|
},
|
||||||
|
"blur-nav-bar": {
|
||||||
|
"description": "شفاف و محو کردن نوار ناوبری",
|
||||||
|
"name": "محو کردن نوار ناوبری"
|
||||||
|
},
|
||||||
|
"bypass-age-restrictions": {
|
||||||
|
"description": "دور زدن تأیید سن یوتیوب",
|
||||||
|
"name": "دور زدن محدودیتهای سنی"
|
||||||
|
},
|
||||||
|
"captions-selector": {
|
||||||
|
"description": "انتخاب زیرنویس برای آهنگهای یوتیوب موسیقی",
|
||||||
|
"menu": {
|
||||||
|
"autoload": "به طور خودکار انتخاب آخرین زیرنویس استفاده شده",
|
||||||
|
"disable-captions": "بدون زیرنویس به صورت پیشفرض"
|
||||||
|
},
|
||||||
|
"name": "انتخابکننده زیرنویس",
|
||||||
|
"prompt": {
|
||||||
|
"selector": {
|
||||||
|
"label": "زبان زیرنویس فعلی: {{language}}",
|
||||||
|
"none": "هیچکدام",
|
||||||
|
"title": "انتخاب زبان زیرنویس"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"templates": {
|
||||||
|
"title": "باز کردن انتخابکننده زیرنویس"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compact-sidebar": {
|
||||||
|
"description": "همیشه نوار کناری را در حالت فشرده تنظیم کن",
|
||||||
|
"name": "نوار کناری فشرده"
|
||||||
|
},
|
||||||
|
"crossfade": {
|
||||||
|
"description": "تداخل بین آهنگها",
|
||||||
|
"menu": {
|
||||||
|
"advanced": "پیشرفته"
|
||||||
|
},
|
||||||
|
"name": "تداخل [بتا]",
|
||||||
|
"prompt": {
|
||||||
|
"options": {
|
||||||
|
"multi-input": {
|
||||||
|
"fade-in-duration": "مدت زمان ورود تدریجی (میلیثانیه)",
|
||||||
|
"fade-out-duration": "مدت زمان خروج تدریجی (میلیثانیه)",
|
||||||
|
"fade-scaling": {
|
||||||
|
"label": "مقیاسبندی ورود تدریجی",
|
||||||
|
"linear": "خطی",
|
||||||
|
"logarithmic": "لگاریتمی"
|
||||||
|
},
|
||||||
|
"seconds-before-end": "تداخل N ثانیه قبل از پایان"
|
||||||
|
},
|
||||||
|
"title": "گزینههای تداخل"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"disable-autoplay": {
|
||||||
|
"description": "شروع آهنگ در حالت \"توقف\"",
|
||||||
|
"menu": {
|
||||||
|
"apply-once": "فقط در شروع اعمال میشود"
|
||||||
|
},
|
||||||
|
"name": "غیرفعال کردن پخش خودکار"
|
||||||
|
},
|
||||||
|
"discord": {
|
||||||
|
"backend": {
|
||||||
|
"already-connected": "تلاش برای اتصال با اتصال فعال",
|
||||||
|
"connected": "متصل به Discord",
|
||||||
|
"disconnected": "قطع اتصال از Discord"
|
||||||
|
},
|
||||||
|
"description": "نمایش آنچه گوش میدهید به دوستان با Rich Presence",
|
||||||
|
"menu": {
|
||||||
|
"auto-reconnect": "اتصال خودکار مجدد",
|
||||||
|
"clear-activity": "پاک کردن فعالیت",
|
||||||
|
"clear-activity-after-timeout": "پاک کردن فعالیت پس از تایماوت",
|
||||||
|
"connected": "متصل",
|
||||||
|
"disconnected": "قطع شده",
|
||||||
|
"hide-duration-left": "مخفی کردن مدت زمان باقیمانده",
|
||||||
|
"hide-github-button": "مخفی کردن دکمه لینک GitHub",
|
||||||
|
"play-on-youtube-music": "پخش در یوتیوب موسیقی",
|
||||||
|
"set-inactivity-timeout": "تنظیم تایماوت عدم فعالیت"
|
||||||
|
},
|
||||||
|
"name": "Rich Presence در Discord",
|
||||||
|
"prompt": {
|
||||||
|
"set-inactivity-timeout": {
|
||||||
|
"label": "ورود تایماوت عدم فعالیت به ثانیه:",
|
||||||
|
"title": "تنظیم تایماوت عدم فعالیت"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"downloader": {
|
||||||
|
"backend": {
|
||||||
|
"dialog": {
|
||||||
|
"error": {
|
||||||
|
"buttons": {
|
||||||
|
"ok": "تأیید"
|
||||||
|
},
|
||||||
|
"message": "اوه! متاسفیم، دانلود شکست خورد…",
|
||||||
|
"title": "خطا در دانلود!"
|
||||||
|
},
|
||||||
|
"start-download-playlist": {
|
||||||
|
"buttons": {
|
||||||
|
"ok": "تأیید"
|
||||||
|
},
|
||||||
|
"detail": "({{playlistSize}} آهنگ)",
|
||||||
|
"message": "دانلود فهرست پخش {{playlistTitle}}",
|
||||||
|
"title": "دانلود شروع شد"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"feedback": {
|
||||||
|
"conversion-progress": "تبدیل: {{percent}}%",
|
||||||
|
"converting": "در حال تبدیل…",
|
||||||
|
"done": "انجام شد: {{filePath}}",
|
||||||
|
"download-info": "در حال دانلود {{artist}} - {{title}} [{{videoId}}",
|
||||||
|
"download-progress": "دانلود: {{percent}}%",
|
||||||
|
"downloading": "در حال دانلود…",
|
||||||
|
"downloading-counter": "در حال دانلود {{current}}/{{total}}…",
|
||||||
|
"downloading-playlist": "در حال دانلود فهرست پخش \"{{playlistTitle}}\" - {{playlistSize}} آهنگ ({{playlistId}})",
|
||||||
|
"error-while-downloading": "خطا در دانلود \"{{author}} - {{title}}\": {{error}}",
|
||||||
|
"folder-already-exists": "پوشه {{playlistFolder}} از قبل وجود دارد",
|
||||||
|
"getting-playlist-info": "در حال دریافت اطلاعات فهرست پخش…",
|
||||||
|
"loading": "در حال بارگذاری…",
|
||||||
|
"playlist-has-only-one-song": "فهرست پخش فقط یک آیتم دارد، به طور مستقیم دانلود میشود",
|
||||||
|
"playlist-id-not-found": "شناسه فهرست پخش یافت نشد"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -498,6 +498,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"priority": "Prioridad ng Notification",
|
"priority": "Prioridad ng Notification",
|
||||||
|
"toast-style": "Estilo ng toast",
|
||||||
"unpause-notification": "Ipakita ang notification sa pag-unpause"
|
"unpause-notification": "Ipakita ang notification sa pag-unpause"
|
||||||
},
|
},
|
||||||
"name": "Mga Abiso"
|
"name": "Mga Abiso"
|
||||||
|
|||||||
@ -2,13 +2,14 @@
|
|||||||
"common": {
|
"common": {
|
||||||
"console": {
|
"console": {
|
||||||
"plugins": {
|
"plugins": {
|
||||||
"execute-failed": "נכשל ביצוע תוסף {{pluginName}}::{{contextName}}",
|
"execute-failed": "שגיאה בהרצת התוסף {{pluginName}}::{{contextName}}",
|
||||||
"executed-at-ms": "התוסף {{pluginName}}:{{contextName}} בוצע ב {{ms}}ms",
|
"executed-at-ms": "התוסף {{pluginName}}:{{contextName}} בוצע ב {{ms}}ms",
|
||||||
"initialize-failed": "טעינת התוסף \"{{pluginName}}\" נכשלה",
|
"initialize-failed": "טעינת התוסף \"{{pluginName}}\" נכשלה",
|
||||||
"load-all": "טוען את כל התוספים",
|
"load-all": "טוען את כל התוספים",
|
||||||
"load-failed": "טעינת התוסף \"{{pluginName}}\" נכשלה",
|
"load-failed": "לא ניתן לטעון את התוסף {{pluginName}}",
|
||||||
"loaded": "התוסף \"{{pluginName}}\" נטען",
|
"loaded": "התוסף \"{{pluginName}}\" נטען",
|
||||||
"unload-failed": "הסרת התוסף \"{{pluginName}} נכשלה"
|
"unload-failed": "הסרת התוסף \"{{pluginName}} נכשלה",
|
||||||
|
"unloaded": "תוסף {{pluginName}} הורד"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -25,14 +26,28 @@
|
|||||||
"i18n": {
|
"i18n": {
|
||||||
"loaded": "i18n נטען"
|
"loaded": "i18n נטען"
|
||||||
},
|
},
|
||||||
|
"second-instance": {
|
||||||
|
"receive-command": "התקבלה פקודה מעבר פרוטוקל: {{command}}"
|
||||||
|
},
|
||||||
"theme": {
|
"theme": {
|
||||||
"css-file-not-found": "קובץ ה-CSS \"{{cssFile}}\" לא קיים. מדלג"
|
"css-file-not-found": "קובץ ה-CSS \"{{cssFile}}\" לא קיים. מדלג"
|
||||||
},
|
},
|
||||||
|
"unresponsive": {
|
||||||
|
"details": "שגיאה ללא תגובה\n{{error}}"
|
||||||
|
},
|
||||||
"when-ready": {
|
"when-ready": {
|
||||||
"clearing-cache-after-20s": "מוחק קבצי מתמון"
|
"clearing-cache-after-20s": "מוחק קבצי מתמון"
|
||||||
|
},
|
||||||
|
"window": {
|
||||||
|
"tried-to-render-offscreen": "ווינדוס ניסה להציג תוכן מחוץ למסך, גודל חלון={{windowSize}}, גודל מסך={{displaySize}}, מיקום={{position}}"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dialog": {
|
"dialog": {
|
||||||
|
"hide-menu-enabled": {
|
||||||
|
"detail": "התפריט מוחבא, השתמש \"Alt\" על להציג אותו (או \"Esacpe\" אם משתמשים בתפריט בתוך האפליקציה)",
|
||||||
|
"message": "הסתרת התפריט מופעלת",
|
||||||
|
"title": "הסתרת התפריט הופעלה"
|
||||||
|
},
|
||||||
"need-to-restart": {
|
"need-to-restart": {
|
||||||
"buttons": {
|
"buttons": {
|
||||||
"later": "אחר כך",
|
"later": "אחר כך",
|
||||||
|
|||||||
@ -372,6 +372,9 @@
|
|||||||
"backend": {
|
"backend": {
|
||||||
"dialog": {
|
"dialog": {
|
||||||
"error": {
|
"error": {
|
||||||
|
"buttons": {
|
||||||
|
"ok": "Rendben"
|
||||||
|
},
|
||||||
"message": "Hoppá! Elnézést, a letöltés sikertelen volt…",
|
"message": "Hoppá! Elnézést, a letöltés sikertelen volt…",
|
||||||
"title": "A letöltés során hiba történt!"
|
"title": "A letöltés során hiba történt!"
|
||||||
},
|
},
|
||||||
@ -412,6 +415,7 @@
|
|||||||
"menu": {
|
"menu": {
|
||||||
"choose-download-folder": "Letöltési mappa kiválasztása",
|
"choose-download-folder": "Letöltési mappa kiválasztása",
|
||||||
"download-finish-settings": {
|
"download-finish-settings": {
|
||||||
|
"label": "Letöltés befejezéskor",
|
||||||
"prompt": {
|
"prompt": {
|
||||||
"last-percent": "x százalék után",
|
"last-percent": "x százalék után",
|
||||||
"last-seconds": "Utolsó x másodperc"
|
"last-seconds": "Utolsó x másodperc"
|
||||||
@ -425,7 +429,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"download-playlist": "Lejátszási lista letöltése",
|
"download-playlist": "Lejátszási lista letöltése",
|
||||||
"presets": "Előbeállítások",
|
"presets": "Sablonok",
|
||||||
"skip-existing": "Meglévő fájlok kihagyása"
|
"skip-existing": "Meglévő fájlok kihagyása"
|
||||||
},
|
},
|
||||||
"name": "Letöltő",
|
"name": "Letöltő",
|
||||||
@ -666,6 +670,18 @@
|
|||||||
"fetch": "⚠️ - Hiba történt a dalszövegek lekérése közben. Kérlek, próbáld újra később.",
|
"fetch": "⚠️ - Hiba történt a dalszövegek lekérése közben. Kérlek, próbáld újra később.",
|
||||||
"not-found": "⚠️ - Nem található dalszöveg ehhez a zenéhez."
|
"not-found": "⚠️ - Nem található dalszöveg ehhez a zenéhez."
|
||||||
},
|
},
|
||||||
|
"menu": {
|
||||||
|
"line-effect": {
|
||||||
|
"submenu": {
|
||||||
|
"scale": {
|
||||||
|
"label": "Mérték"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"precise-timing": {
|
||||||
|
"label": "Dalszöveg tökéletes szinkronizálása"
|
||||||
|
}
|
||||||
|
},
|
||||||
"name": "Szinkronizált dalszövegek",
|
"name": "Szinkronizált dalszövegek",
|
||||||
"refetch-btn": {
|
"refetch-btn": {
|
||||||
"fetching": "Lekérés folyamatban...",
|
"fetching": "Lekérés folyamatban...",
|
||||||
|
|||||||
@ -158,6 +158,14 @@
|
|||||||
},
|
},
|
||||||
"remove-upgrade-button": "Fjarlægja uppgræðartakkan",
|
"remove-upgrade-button": "Fjarlægja uppgræðartakkan",
|
||||||
"theme": {
|
"theme": {
|
||||||
|
"dialog": {
|
||||||
|
"button": {
|
||||||
|
"cancel": "Hætta við",
|
||||||
|
"remove": "Fjarlægja"
|
||||||
|
},
|
||||||
|
"remove-theme": "Ertu viss um að þú viljir fjarlægja þetta sérsniðna þema?",
|
||||||
|
"remove-theme-message": "Þetta mun fjarlægja sérsniðna þema"
|
||||||
|
},
|
||||||
"label": "Þema",
|
"label": "Þema",
|
||||||
"submenu": {
|
"submenu": {
|
||||||
"import-css-file": "Flytja inn sérsniðna CSS skrá",
|
"import-css-file": "Flytja inn sérsniðna CSS skrá",
|
||||||
@ -199,6 +207,10 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"plugins": {
|
"plugins": {
|
||||||
|
"ad-speedup": {
|
||||||
|
"description": "Ef auglýsing spilar slökknar hún á hljóðinu og stillir spilunarhraðann á 16x",
|
||||||
|
"name": "Auglýsingahraða"
|
||||||
|
},
|
||||||
"adblocker": {
|
"adblocker": {
|
||||||
"description": "Lokaðu fyrir allar auglýsingar og rakningar úr kassanum",
|
"description": "Lokaðu fyrir allar auglýsingar og rakningar úr kassanum",
|
||||||
"menu": {
|
"menu": {
|
||||||
@ -402,6 +414,21 @@
|
|||||||
"description": "Niðurhalar MP3 / upprunahljóði beint úr viðmótinu",
|
"description": "Niðurhalar MP3 / upprunahljóði beint úr viðmótinu",
|
||||||
"menu": {
|
"menu": {
|
||||||
"choose-download-folder": "Veldu niðurhalsmöppu",
|
"choose-download-folder": "Veldu niðurhalsmöppu",
|
||||||
|
"download-finish-settings": {
|
||||||
|
"label": "Sækja þegar lokið",
|
||||||
|
"prompt": {
|
||||||
|
"last-percent": "Eftir x sekúndur",
|
||||||
|
"last-seconds": "Síðustu x sekúndur",
|
||||||
|
"title": "Stilla hvenær á að hlaða niður"
|
||||||
|
},
|
||||||
|
"submenu": {
|
||||||
|
"advanced": "Ítarlegri",
|
||||||
|
"enabled": "Virkt",
|
||||||
|
"mode": "Tímastilling",
|
||||||
|
"percent": "Hlutfall",
|
||||||
|
"seconds": "Sekúndur"
|
||||||
|
}
|
||||||
|
},
|
||||||
"download-playlist": "Sækja spilunarlista",
|
"download-playlist": "Sækja spilunarlista",
|
||||||
"presets": "Forstillingar",
|
"presets": "Forstillingar",
|
||||||
"skip-existing": "Slepptu núverandi skrám"
|
"skip-existing": "Slepptu núverandi skrám"
|
||||||
@ -641,6 +668,10 @@
|
|||||||
"description": "Sleppur sjálfkrafa hlutum sem ekki eru tónlist, eins og inngangur/lok eða hlutar af tónlistarmyndböndum þar sem lag er ekki að spila",
|
"description": "Sleppur sjálfkrafa hlutum sem ekki eru tónlist, eins og inngangur/lok eða hlutar af tónlistarmyndböndum þar sem lag er ekki að spila",
|
||||||
"name": "Styrktarblokk"
|
"name": "Styrktarblokk"
|
||||||
},
|
},
|
||||||
|
"synced-lyrics": {
|
||||||
|
"description": "Veitir samstillta texta við lög, með því að nota veitur eins og LRClib.",
|
||||||
|
"name": "Samstilltur texti"
|
||||||
|
},
|
||||||
"taskbar-mediacontrol": {
|
"taskbar-mediacontrol": {
|
||||||
"description": "Stjórnaðu spilun frá Windows verkefnastikunni þinni",
|
"description": "Stjórnaðu spilun frá Windows verkefnastikunni þinni",
|
||||||
"name": "Miðlunarstýringarverkefnastikunnar"
|
"name": "Miðlunarstýringarverkefnastikunnar"
|
||||||
|
|||||||
@ -674,6 +674,42 @@
|
|||||||
"fetch": "⚠️ - Si è verificato un errore nel recuperare il testo. Per favore riprova più tardi.",
|
"fetch": "⚠️ - Si è verificato un errore nel recuperare il testo. Per favore riprova più tardi.",
|
||||||
"not-found": "⚠️ - Nessun testo trovato per questa canzone."
|
"not-found": "⚠️ - Nessun testo trovato per questa canzone."
|
||||||
},
|
},
|
||||||
|
"menu": {
|
||||||
|
"default-text-string": {
|
||||||
|
"label": "Carattere predefinito tra i testi",
|
||||||
|
"tooltip": "Scegliere il carattere predefinito da utilizzare per l'intervallo tra i testi"
|
||||||
|
},
|
||||||
|
"line-effect": {
|
||||||
|
"label": "Effetto linea",
|
||||||
|
"submenu": {
|
||||||
|
"focus": {
|
||||||
|
"label": "Focus",
|
||||||
|
"tooltip": "Rendi bianca solo la riga corrente"
|
||||||
|
},
|
||||||
|
"offset": {
|
||||||
|
"label": "Offset",
|
||||||
|
"tooltip": "Offset a destra della riga corrente"
|
||||||
|
},
|
||||||
|
"scale": {
|
||||||
|
"label": "Ingrandimento",
|
||||||
|
"tooltip": "Ingrandisci la linea corrente"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tooltip": "Scegli l'effetto da applicare alla linea corrente"
|
||||||
|
},
|
||||||
|
"precise-timing": {
|
||||||
|
"label": "Rendi i testi perfettamente sincronizzati",
|
||||||
|
"tooltip": "Calcola al millisecondo la visualizzazione della riga successiva (può avere un piccolo impatto sulle prestazioni)"
|
||||||
|
},
|
||||||
|
"show-lyrics-even-if-inexact": {
|
||||||
|
"label": "Mostra le lyric anche se incorrette",
|
||||||
|
"tooltip": "Se il brano non viene trovato, il plugin riprova con un'altra query di ricerca.\nIl risultato del secondo tentativo potrebbe non essere esatto."
|
||||||
|
},
|
||||||
|
"show-time-codes": {
|
||||||
|
"label": "Mostra time code",
|
||||||
|
"tooltip": "Mostra i codici temporali accanto ai testi"
|
||||||
|
}
|
||||||
|
},
|
||||||
"name": "Testi sincronizzati",
|
"name": "Testi sincronizzati",
|
||||||
"refetch-btn": {
|
"refetch-btn": {
|
||||||
"fetching": "Sto recuperando...",
|
"fetching": "Sto recuperando...",
|
||||||
|
|||||||
@ -279,6 +279,49 @@
|
|||||||
},
|
},
|
||||||
"name": "앰비언트 모드"
|
"name": "앰비언트 모드"
|
||||||
},
|
},
|
||||||
|
"api-server": {
|
||||||
|
"description": "플레이어를 제어하기 위한 API 서버를 추가합니다",
|
||||||
|
"dialog": {
|
||||||
|
"request": {
|
||||||
|
"buttons": {
|
||||||
|
"allow": "허용",
|
||||||
|
"deny": "거부"
|
||||||
|
},
|
||||||
|
"message": "{{ID}} ({{origin}})이(가) API에 액세스하도록 허용하시겠습니까?",
|
||||||
|
"title": "API 권한 요청"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"menu": {
|
||||||
|
"auth-strategy": {
|
||||||
|
"label": "인증 정책",
|
||||||
|
"submenu": {
|
||||||
|
"auth-at-first": {
|
||||||
|
"label": "첫 번째 요청 시 인증"
|
||||||
|
},
|
||||||
|
"none": {
|
||||||
|
"label": "인증 없음"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"hostname": {
|
||||||
|
"label": "호스트 명"
|
||||||
|
},
|
||||||
|
"port": {
|
||||||
|
"label": "포트"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"name": "API 서버 [베타]",
|
||||||
|
"prompt": {
|
||||||
|
"hostname": {
|
||||||
|
"label": "API 서버가 사용할 호스트 명(예: 0.0.0.0)을 입력하세요:",
|
||||||
|
"title": "호스트 명"
|
||||||
|
},
|
||||||
|
"port": {
|
||||||
|
"label": "API 서버가 사용할 포트를 입력하세요:",
|
||||||
|
"title": "포트"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"audio-compressor": {
|
"audio-compressor": {
|
||||||
"description": "오디오에 컴프레서를 적용합니다 (신호에서 가장 시끄러운 부분의 음량을 낮추고 가장 조용한 부분의 음량을 높임)",
|
"description": "오디오에 컴프레서를 적용합니다 (신호에서 가장 시끄러운 부분의 음량을 낮추고 가장 조용한 부분의 음량을 높임)",
|
||||||
"name": "오디오 컴프레서"
|
"name": "오디오 컴프레서"
|
||||||
|
|||||||
@ -668,6 +668,59 @@
|
|||||||
"description": "Automatycznie pomija fragmenty niebędące muzyką, takie jak wstęp/zakończenie lub fragmenty teledysków, w których utwór nie jest odtwarzany",
|
"description": "Automatycznie pomija fragmenty niebędące muzyką, takie jak wstęp/zakończenie lub fragmenty teledysków, w których utwór nie jest odtwarzany",
|
||||||
"name": "Pomiń nieistotne fragmenty"
|
"name": "Pomiń nieistotne fragmenty"
|
||||||
},
|
},
|
||||||
|
"synced-lyrics": {
|
||||||
|
"description": "Dodaje zsynchronizowane napisy do utworów używając między innymi LRClib.",
|
||||||
|
"errors": {
|
||||||
|
"fetch": "⚠️ - Wystąpił błąd podczas pobierania tekstu utworu. Spróbuj ponownie później.",
|
||||||
|
"not-found": "⚠️ - Nie znaleziono napisów dla tego utworu."
|
||||||
|
},
|
||||||
|
"menu": {
|
||||||
|
"default-text-string": {
|
||||||
|
"label": "Standardowy znak luki",
|
||||||
|
"tooltip": "Wybierz domyślny znak, który ma być wyświetlany jako pauza między słowami"
|
||||||
|
},
|
||||||
|
"line-effect": {
|
||||||
|
"label": "Efekty linijki",
|
||||||
|
"submenu": {
|
||||||
|
"focus": {
|
||||||
|
"label": "Fokus",
|
||||||
|
"tooltip": "Spraw, aby tylko obecna linijka była biała"
|
||||||
|
},
|
||||||
|
"offset": {
|
||||||
|
"label": "Przesunięcie",
|
||||||
|
"tooltip": "Przesuń w prawo obecną linijkę"
|
||||||
|
},
|
||||||
|
"scale": {
|
||||||
|
"label": "Skala",
|
||||||
|
"tooltip": "Zmień skalę aktualnej linijki"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tooltip": "Wybierz efekt, by zastosować go do aktualnej linijki"
|
||||||
|
},
|
||||||
|
"precise-timing": {
|
||||||
|
"label": "Zsynchronizuj tekst utworu do perfekcji",
|
||||||
|
"tooltip": "Wylicz czas wyświetlania następnej linijki co do milisekundy (może mieć mały wpływ na wydajność systemu)"
|
||||||
|
},
|
||||||
|
"show-lyrics-even-if-inexact": {
|
||||||
|
"label": "Pokaż teksty, mimo niezgodności",
|
||||||
|
"tooltip": "Jeżeli nie znaleziono tekstu piosenki z bazy danych, wtyczka spróbuje ponownie przez wyszukanie przybliżonej frazy.\nNależy jednak pamiętać, że następne próby mogą nie być trafne co do oryginału."
|
||||||
|
},
|
||||||
|
"show-time-codes": {
|
||||||
|
"label": "Pokaż znaczniki czasu",
|
||||||
|
"tooltip": "Pokaż znaczniki czasu obok linijek"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"name": "Napisy zsynchronizowane",
|
||||||
|
"refetch-btn": {
|
||||||
|
"fetching": "Pobieranie napisów...",
|
||||||
|
"normal": "Odśwież napisy"
|
||||||
|
},
|
||||||
|
"warnings": {
|
||||||
|
"duration-mismatch": "⚠️ - Napisy mogą nie być zsynchronizowane z powodu różnicy w czasie trwania utworu.",
|
||||||
|
"inexact": "⚠️ - Tekst utworu może się różnić od oryginału",
|
||||||
|
"instrumental": "⚠️ - To jest utwór instrumentalny"
|
||||||
|
}
|
||||||
|
},
|
||||||
"taskbar-mediacontrol": {
|
"taskbar-mediacontrol": {
|
||||||
"description": "Steruj odtwarzaniem z paska zadań systemu Windows",
|
"description": "Steruj odtwarzaniem z paska zadań systemu Windows",
|
||||||
"name": "Kontroler odtwarzania z paska zadań"
|
"name": "Kontroler odtwarzania z paska zadań"
|
||||||
|
|||||||
770
src/i18n/resources/pt-BR.json
Normal file
770
src/i18n/resources/pt-BR.json
Normal file
@ -0,0 +1,770 @@
|
|||||||
|
{
|
||||||
|
"common": {
|
||||||
|
"console": {
|
||||||
|
"plugins": {
|
||||||
|
"execute-failed": "Falha ao executar plugin {{pluginName}}::{{contextName}}",
|
||||||
|
"executed-at-ms": "Plugin {{pluginName}}::{{contextName}} executado em {{ms}} ms",
|
||||||
|
"initialize-failed": "Falha ao inicializar o plugin \"{{pluginName}}\"",
|
||||||
|
"load-all": "Carregando todos os plugins",
|
||||||
|
"load-failed": "Falha ao carregar o plugin \"{{pluginName}}\"",
|
||||||
|
"loaded": "Plugin \"{{pluginName}}\" carregado",
|
||||||
|
"unload-failed": "Falha ao descarregar o plugin \"{{pluginName}}\"",
|
||||||
|
"unloaded": "Plugin \"{{pluginName}}\" descarregado"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"language": {
|
||||||
|
"code": "pt-BR",
|
||||||
|
"local-name": "Português (Brasil)",
|
||||||
|
"name": "Portuguese (Brazil)"
|
||||||
|
},
|
||||||
|
"main": {
|
||||||
|
"console": {
|
||||||
|
"did-finish-load": {
|
||||||
|
"dev-tools": "Carregamento concluído. DevTools aberto"
|
||||||
|
},
|
||||||
|
"i18n": {
|
||||||
|
"loaded": "i18n carregado"
|
||||||
|
},
|
||||||
|
"second-instance": {
|
||||||
|
"receive-command": "Comando recebido pelo protocolo: \"{{command}}\""
|
||||||
|
},
|
||||||
|
"theme": {
|
||||||
|
"css-file-not-found": "Arquivo CSS \"{{cssFile}}\" não existe, ignorando"
|
||||||
|
},
|
||||||
|
"unresponsive": {
|
||||||
|
"details": "Erro de falta de resposta!\n{{error}}"
|
||||||
|
},
|
||||||
|
"when-ready": {
|
||||||
|
"clearing-cache-after-20s": "Limpando cache do aplicativo"
|
||||||
|
},
|
||||||
|
"window": {
|
||||||
|
"tried-to-render-offscreen": "A janela tentou renderizar fora da tela, tamanho da janela={{windowSize}}, tamanho da tela={{displaySize}}, posição={{position}}"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"dialog": {
|
||||||
|
"hide-menu-enabled": {
|
||||||
|
"detail": "O menu está oculto, use 'Alt' para mostrá-lo (ou 'Esc' ao usar o menu dentro do aplicativo)",
|
||||||
|
"message": "Ocultar menu está ativado",
|
||||||
|
"title": "Ocultar menu ativado"
|
||||||
|
},
|
||||||
|
"need-to-restart": {
|
||||||
|
"buttons": {
|
||||||
|
"later": "Depois",
|
||||||
|
"restart-now": "Reiniciar agora"
|
||||||
|
},
|
||||||
|
"detail": "O plugin \"{{pluginName}}\" requer uma reinicialização para entrar em vigor",
|
||||||
|
"message": "\"{{pluginName}}\" precisa reiniciar",
|
||||||
|
"title": "Necessário reiniciar"
|
||||||
|
},
|
||||||
|
"unresponsive": {
|
||||||
|
"buttons": {
|
||||||
|
"quit": "Fechar",
|
||||||
|
"relaunch": "Reiniciar",
|
||||||
|
"wait": "Aguardar"
|
||||||
|
},
|
||||||
|
"detail": "Lamentamos o inconveniente! Por favor, escolha o que fazer:",
|
||||||
|
"message": "O aplicativo não está respondendo",
|
||||||
|
"title": "Janela não responde"
|
||||||
|
},
|
||||||
|
"update-available": {
|
||||||
|
"buttons": {
|
||||||
|
"disable": "Desativar atualizações",
|
||||||
|
"download": "Baixar",
|
||||||
|
"ok": "OK"
|
||||||
|
},
|
||||||
|
"detail": "Uma versão mais recente está disponível em {{downloadLink}}",
|
||||||
|
"message": "Nova versão disponível",
|
||||||
|
"title": "Atualização disponível"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"menu": {
|
||||||
|
"about": "Sobre",
|
||||||
|
"navigation": {
|
||||||
|
"label": "Navegação",
|
||||||
|
"submenu": {
|
||||||
|
"copy-current-url": "Copiar URL atual",
|
||||||
|
"go-back": "Voltar",
|
||||||
|
"go-forward": "Avançar",
|
||||||
|
"quit": "Sair",
|
||||||
|
"restart": "Reiniciar aplicativo"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"label": "Opções",
|
||||||
|
"submenu": {
|
||||||
|
"advanced-options": {
|
||||||
|
"label": "Opções avançadas",
|
||||||
|
"submenu": {
|
||||||
|
"auto-reset-app-cache": "Limpar cache ao iniciar aplicativo",
|
||||||
|
"disable-hardware-acceleration": "Desativar aceleração de hardware",
|
||||||
|
"edit-config-json": "Editar config.json",
|
||||||
|
"override-user-agent": "Substituir User-Agent",
|
||||||
|
"restart-on-config-changes": "Reiniciar ao alterar configurações",
|
||||||
|
"set-proxy": {
|
||||||
|
"label": "Definir proxy",
|
||||||
|
"prompt": {
|
||||||
|
"label": "Digite o endereço do proxy: (deixe em branco para desativar)",
|
||||||
|
"placeholder": "Exemplo: SOCKS5://127.0.0.1:9999",
|
||||||
|
"title": "Definir proxy"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"toggle-dev-tools": "Alternar DevTools"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"always-on-top": "Sempre no topo",
|
||||||
|
"auto-update": "Atualização automática",
|
||||||
|
"hide-menu": {
|
||||||
|
"dialog": {
|
||||||
|
"message": "O menu ficará oculto na próxima inicialização, use [Alt] para exibi-lo (ou a tecla de crase [`] se estiver usando o menu do aplicativo)",
|
||||||
|
"title": "Ocultar menu ativado"
|
||||||
|
},
|
||||||
|
"label": "Ocultar menu"
|
||||||
|
},
|
||||||
|
"language": {
|
||||||
|
"dialog": {
|
||||||
|
"message": "O idioma será alterado depois de reiniciar",
|
||||||
|
"title": "Idioma alterado"
|
||||||
|
},
|
||||||
|
"label": "Idioma",
|
||||||
|
"submenu": {
|
||||||
|
"to-help-translate": "Quer ajudar a traduzir? Clique aqui"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"resume-on-start": "Continuar última música ao iniciar o aplicativo",
|
||||||
|
"single-instance-lock": "Bloqueio de instância única",
|
||||||
|
"start-at-login": "Iniciar com o sistema",
|
||||||
|
"starting-page": {
|
||||||
|
"label": "Página inicial",
|
||||||
|
"unset": "Limpar"
|
||||||
|
},
|
||||||
|
"tray": {
|
||||||
|
"label": "Área de Notificação",
|
||||||
|
"submenu": {
|
||||||
|
"disabled": "Desativado",
|
||||||
|
"enabled-and-hide-app": "Ativado e aplicativo oculto",
|
||||||
|
"enabled-and-show-app": "Ativado e mostrar aplicativo",
|
||||||
|
"play-pause-on-click": "Reproduzir/Pausar ao clicar"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"visual-tweaks": {
|
||||||
|
"label": "Ajustes visuais",
|
||||||
|
"submenu": {
|
||||||
|
"like-buttons": {
|
||||||
|
"default": "Padrão",
|
||||||
|
"force-show": "Forçar exibir",
|
||||||
|
"hide": "Ocultar",
|
||||||
|
"label": "Botões de 'Curtir'"
|
||||||
|
},
|
||||||
|
"remove-upgrade-button": "Remover botão de atualização",
|
||||||
|
"theme": {
|
||||||
|
"dialog": {
|
||||||
|
"button": {
|
||||||
|
"cancel": "Cancelar",
|
||||||
|
"remove": "Remover"
|
||||||
|
},
|
||||||
|
"remove-theme": "Deseja realmente remover o tema personalizado?",
|
||||||
|
"remove-theme-message": "Isto removerá o tema personalizado"
|
||||||
|
},
|
||||||
|
"label": "Tema",
|
||||||
|
"submenu": {
|
||||||
|
"import-css-file": "Importar arquivo CSS personalizado",
|
||||||
|
"no-theme": "Sem tema"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"plugins": {
|
||||||
|
"enabled": "Ativado",
|
||||||
|
"label": "Plugins",
|
||||||
|
"new": "NOVO"
|
||||||
|
},
|
||||||
|
"view": {
|
||||||
|
"label": "Visualização",
|
||||||
|
"submenu": {
|
||||||
|
"force-reload": "Forçar recarregar",
|
||||||
|
"reload": "Recarregar",
|
||||||
|
"reset-zoom": "Tamanho atual",
|
||||||
|
"toggle-fullscreen": "Alternar tela cheia",
|
||||||
|
"zoom-in": "Ampliar",
|
||||||
|
"zoom-out": "Reduzir"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tray": {
|
||||||
|
"next": "Próximo",
|
||||||
|
"play-pause": "Reproduzir/Pausar",
|
||||||
|
"previous": "Anterior",
|
||||||
|
"quit": "Sair",
|
||||||
|
"restart": "Reiniciar aplicativo",
|
||||||
|
"show": "Mostrar janela",
|
||||||
|
"tooltip": {
|
||||||
|
"default": "YouTube Music",
|
||||||
|
"with-song-info": "YouTube Music: {{artist}} - {{title}}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"plugins": {
|
||||||
|
"ad-speedup": {
|
||||||
|
"description": "Se um anúncio for reproduzido, ele silencia o áudio e define a velocidade de reprodução para 16x",
|
||||||
|
"name": "Acelerador de Anúncios"
|
||||||
|
},
|
||||||
|
"adblocker": {
|
||||||
|
"description": "Bloqueio de todos os anúncios e rastreamentos imediatamente",
|
||||||
|
"menu": {
|
||||||
|
"blocker": "Bloqueador"
|
||||||
|
},
|
||||||
|
"name": "Bloqueador de Anúncios"
|
||||||
|
},
|
||||||
|
"album-actions": {
|
||||||
|
"description": "Adiciona botões Remover Não curtir, Não curtir, Curtir e Remover Curtir para aplicar em todas as músicas em uma lista ou álbum",
|
||||||
|
"name": "Ações do álbum"
|
||||||
|
},
|
||||||
|
"album-color-theme": {
|
||||||
|
"description": "Aplica um tema dinâmico e efeitos visuais com base na paleta de cores do álbum",
|
||||||
|
"menu": {
|
||||||
|
"color-mix-ratio": {
|
||||||
|
"label": "Proporção de mistura de cores",
|
||||||
|
"submenu": {
|
||||||
|
"percent": "{{ratio}}%"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"name": "Tema da cor do álbum"
|
||||||
|
},
|
||||||
|
"ambient-mode": {
|
||||||
|
"description": "Aplica um efeito de iluminação projetando cores suaves do vídeo no fundo da tela",
|
||||||
|
"menu": {
|
||||||
|
"blur-amount": {
|
||||||
|
"label": "Quantidade de desfoque",
|
||||||
|
"submenu": {
|
||||||
|
"pixels": "{{blurAmount}} pixels"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"buffer": {
|
||||||
|
"label": "Buffer",
|
||||||
|
"submenu": {
|
||||||
|
"buffer": "{{buffer}}"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"opacity": {
|
||||||
|
"label": "Opacidade",
|
||||||
|
"submenu": {
|
||||||
|
"percent": "{{opacity}}%"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"quality": {
|
||||||
|
"label": "Qualidade",
|
||||||
|
"submenu": {
|
||||||
|
"pixels": "{{quality}} pixels"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"size": {
|
||||||
|
"label": "Tamanho",
|
||||||
|
"submenu": {
|
||||||
|
"percent": "{{size}}%"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"smoothness-transition": {
|
||||||
|
"label": "Transição suave",
|
||||||
|
"submenu": {
|
||||||
|
"during": "Durante {{interpolationTime}} s"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"use-fullscreen": {
|
||||||
|
"label": "Usando tela cheia"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"name": "Modo ambiente"
|
||||||
|
},
|
||||||
|
"audio-compressor": {
|
||||||
|
"description": "Aplicar compressão ao áudio (reduz o volume das partes mais altas e aumenta o volume das partes mais baixas)",
|
||||||
|
"name": "Compressor de áudio"
|
||||||
|
},
|
||||||
|
"blur-nav-bar": {
|
||||||
|
"description": "Torna a barra de navegação transparente e desfocada",
|
||||||
|
"name": "Desfocar barra de navegação"
|
||||||
|
},
|
||||||
|
"bypass-age-restrictions": {
|
||||||
|
"description": "Pular a verificação de idade do YouTube",
|
||||||
|
"name": "Ignorar restrições de idade"
|
||||||
|
},
|
||||||
|
"captions-selector": {
|
||||||
|
"description": "Seletor de legendas para faixas de áudio do YouTube Music",
|
||||||
|
"menu": {
|
||||||
|
"autoload": "Selecionar automaticamente a última legenda usada",
|
||||||
|
"disable-captions": "Sem legendas por padrão"
|
||||||
|
},
|
||||||
|
"name": "Seletor de legendas",
|
||||||
|
"prompt": {
|
||||||
|
"selector": {
|
||||||
|
"label": "Idioma atual da legenda: {{language}}",
|
||||||
|
"none": "Nenhum",
|
||||||
|
"title": "Selecionar idioma da legenda"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"templates": {
|
||||||
|
"title": "Abrir seletor de legendas"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compact-sidebar": {
|
||||||
|
"description": "Sempre definir a barra lateral no modo compacto",
|
||||||
|
"name": "Barra lateral compacta"
|
||||||
|
},
|
||||||
|
"crossfade": {
|
||||||
|
"description": "Crossfade entre músicas",
|
||||||
|
"menu": {
|
||||||
|
"advanced": "Avançado"
|
||||||
|
},
|
||||||
|
"name": "Crossfade [Beta]",
|
||||||
|
"prompt": {
|
||||||
|
"options": {
|
||||||
|
"multi-input": {
|
||||||
|
"fade-in-duration": "Duração do fade (ms)",
|
||||||
|
"fade-out-duration": "Duração do fade out (ms)",
|
||||||
|
"fade-scaling": {
|
||||||
|
"label": "Escala do fade",
|
||||||
|
"linear": "Linear",
|
||||||
|
"logarithmic": "Logarítmico"
|
||||||
|
},
|
||||||
|
"seconds-before-end": "Crossfade N segundos antes do fim"
|
||||||
|
},
|
||||||
|
"title": "Opções de crossfade"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"disable-autoplay": {
|
||||||
|
"description": "Faz a música começar no modo \"pausado\"",
|
||||||
|
"menu": {
|
||||||
|
"apply-once": "Aplicar somente ao iniciar"
|
||||||
|
},
|
||||||
|
"name": "Desativar reprodução automática"
|
||||||
|
},
|
||||||
|
"discord": {
|
||||||
|
"backend": {
|
||||||
|
"already-connected": "Tentativa de conectar-se com conexão ativa",
|
||||||
|
"connected": "Conectado no Discord",
|
||||||
|
"disconnected": "Desconectado do Discord"
|
||||||
|
},
|
||||||
|
"description": "Mostre aos seus amigos o que você ouve com Rich Presence",
|
||||||
|
"menu": {
|
||||||
|
"auto-reconnect": "Reconexão automática",
|
||||||
|
"clear-activity": "Limpar atividades",
|
||||||
|
"clear-activity-after-timeout": "Limpar atividades após tempo limite",
|
||||||
|
"connected": "Conectado",
|
||||||
|
"disconnected": "Desconectado",
|
||||||
|
"hide-duration-left": "Ocultar duração restante",
|
||||||
|
"hide-github-button": "Ocultar botão do GitHub",
|
||||||
|
"play-on-youtube-music": "Reproduzir no YouTube Music",
|
||||||
|
"set-inactivity-timeout": "Definir tempo limite de inatividade"
|
||||||
|
},
|
||||||
|
"name": "Rich Presence do Discord",
|
||||||
|
"prompt": {
|
||||||
|
"set-inactivity-timeout": {
|
||||||
|
"label": "Digite o tempo de inatividade em segundos:",
|
||||||
|
"title": "Definir tempo limite de inatividade"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"downloader": {
|
||||||
|
"backend": {
|
||||||
|
"dialog": {
|
||||||
|
"error": {
|
||||||
|
"buttons": {
|
||||||
|
"ok": "OK"
|
||||||
|
},
|
||||||
|
"message": "Ah! Desculpe, o download falhou…",
|
||||||
|
"title": "Erro no download!"
|
||||||
|
},
|
||||||
|
"start-download-playlist": {
|
||||||
|
"buttons": {
|
||||||
|
"ok": "OK"
|
||||||
|
},
|
||||||
|
"detail": "({{playlistSize}} músicas)",
|
||||||
|
"message": "Baixando lista de reprodução {{playlistTitle}}",
|
||||||
|
"title": "Download iniciado"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"feedback": {
|
||||||
|
"conversion-progress": "Convertendo: {{percent}}%",
|
||||||
|
"converting": "Convertendo…",
|
||||||
|
"done": "Concluído: {{filePath}}",
|
||||||
|
"download-info": "Baixando {{artist}} - {{title}} [{{videoId}}",
|
||||||
|
"download-progress": "Download: {{percent}}%",
|
||||||
|
"downloading": "Baixando…",
|
||||||
|
"downloading-counter": "Baixando {{current}}/{{total}}…",
|
||||||
|
"downloading-playlist": "Baixando lista de reprodução \"{{playlistTitle}}\" - {{playlistSize}} músicas ({{playlistId}})",
|
||||||
|
"error-while-downloading": "Erro ao baixar \"{{author}} - {{title}}\": {{error}}",
|
||||||
|
"folder-already-exists": "A pasta {{playlistFolder}} já existe",
|
||||||
|
"getting-playlist-info": "Obtendo informações da playlist…",
|
||||||
|
"loading": "Carregando…",
|
||||||
|
"playlist-has-only-one-song": "Playlist possui apenas um item, baixando diretamente",
|
||||||
|
"playlist-id-not-found": "Nenhum playlist ID encontrado",
|
||||||
|
"playlist-is-empty": "Playlist está vazia",
|
||||||
|
"playlist-is-mix-or-private": "Erro ao obter informações da playlist: verifique se não é uma playlist privada ou “”Mixada para você”\n\n{{error}}",
|
||||||
|
"preparing-file": "Preparando arquivo…",
|
||||||
|
"saving": "Salvando…",
|
||||||
|
"trying-to-get-playlist-id": "Tentando obter playlist ID: {{playlistId}}",
|
||||||
|
"video-id-not-found": "Vídeo não encontrado",
|
||||||
|
"writing-id3": "Salvando tags ID3…"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"description": "Faça download do MP3 / fonte de áudio diretamente da interface",
|
||||||
|
"menu": {
|
||||||
|
"choose-download-folder": "Escolha a pasta de download",
|
||||||
|
"download-finish-settings": {
|
||||||
|
"label": "Baixar ao finalizar",
|
||||||
|
"prompt": {
|
||||||
|
"last-percent": "Após x %",
|
||||||
|
"last-seconds": "Últimos x segundos",
|
||||||
|
"title": "Configurar quando baixar"
|
||||||
|
},
|
||||||
|
"submenu": {
|
||||||
|
"advanced": "Avançado",
|
||||||
|
"enabled": "Ativado",
|
||||||
|
"mode": "Modo de tempo",
|
||||||
|
"percent": "Porcento",
|
||||||
|
"seconds": "Segundos"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"download-playlist": "Baixar playlist",
|
||||||
|
"presets": "Predefinições",
|
||||||
|
"skip-existing": "Pular arquivos existentes"
|
||||||
|
},
|
||||||
|
"name": "Downloader",
|
||||||
|
"renderer": {
|
||||||
|
"can-not-update-progress": "Não é possível atualizar o progresso"
|
||||||
|
},
|
||||||
|
"templates": {
|
||||||
|
"button": "Baixar"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"exponential-volume": {
|
||||||
|
"description": "Torna o controle deslizante de volume exponencial para que seja mais fácil selecionar volumes mais baixos.",
|
||||||
|
"name": "Volume Exponencial"
|
||||||
|
},
|
||||||
|
"in-app-menu": {
|
||||||
|
"description": "Dá às barras de menu uma aparência elegante, escura ou com a cor do álbum",
|
||||||
|
"menu": {
|
||||||
|
"hide-dom-window-controls": "Ocultar controles da janela DOM"
|
||||||
|
},
|
||||||
|
"name": "Menu no aplicativo"
|
||||||
|
},
|
||||||
|
"lumiastream": {
|
||||||
|
"description": "Adiciona suporte ao Lumia Stream",
|
||||||
|
"name": "Lumia Stream [Beta]"
|
||||||
|
},
|
||||||
|
"lyrics-genius": {
|
||||||
|
"description": "Adiciona suporte a letras para a maioria das músicas",
|
||||||
|
"menu": {
|
||||||
|
"romanized-lyrics": "Letras Romanizadas"
|
||||||
|
},
|
||||||
|
"name": "Letras Genius",
|
||||||
|
"renderer": {
|
||||||
|
"fetched-lyrics": "Letras buscadas por Genius"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"music-together": {
|
||||||
|
"description": "Compartilhe uma playlist com outras pessoas. Quando o anfitrião toca uma música, todos os outros ouvirão",
|
||||||
|
"dialog": {
|
||||||
|
"enter-host": "Insira o ID do host"
|
||||||
|
},
|
||||||
|
"internal": {
|
||||||
|
"save": "Salvar",
|
||||||
|
"track-source": "Fonte da Faixa",
|
||||||
|
"unknown-user": "Usuário Desconhecido"
|
||||||
|
},
|
||||||
|
"menu": {
|
||||||
|
"click-to-copy-id": "Copiar ID do host",
|
||||||
|
"close": "Fechar Music Together",
|
||||||
|
"connected-users": "Usuários Conectados",
|
||||||
|
"disconnect": "Desconectar Music Together",
|
||||||
|
"empty-user": "Nenhum usuário conectado",
|
||||||
|
"host": "Anfitrião do Music Together",
|
||||||
|
"join": "Entrar no Music Together",
|
||||||
|
"permission": {
|
||||||
|
"all": "Permitir que os convidados controlem a lista de reprodução e o player",
|
||||||
|
"host-only": "Somente o host pode controlar a lista de reprodução e o player",
|
||||||
|
"playlist": "Permitir que os convidados controlem a lista de reprodução"
|
||||||
|
},
|
||||||
|
"set-permission": "Mudar Permissões de Controle",
|
||||||
|
"status": {
|
||||||
|
"disconnected": "Desconectado",
|
||||||
|
"guest": "Conectado como convidado",
|
||||||
|
"host": "Conectado como Anfitrião"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"name": "Music Together [Beta]",
|
||||||
|
"toast": {
|
||||||
|
"add-song-failed": "Falha ao adicionar música",
|
||||||
|
"closed": "Music Together fechado",
|
||||||
|
"disconnected": "Music Together desconectado",
|
||||||
|
"host-failed": "Falha ao hospedar o Music Together",
|
||||||
|
"id-copied": "ID do anfitrião copiado para a área de transferência",
|
||||||
|
"id-copy-failed": "Falha ao copiar o ID do anfitrião para a área de transferência",
|
||||||
|
"join-failed": "Falha ao ingressar no Music Together",
|
||||||
|
"joined": "Entrou no Music Together",
|
||||||
|
"permission-changed": "A permissão do Music Together foi alterada para \"{{permission}}\"",
|
||||||
|
"remove-song-failed": "Falha ao remover música",
|
||||||
|
"user-connected": "{{name}} juntou-se ao Music Together",
|
||||||
|
"user-disconnected": "{{name}} saiu do Music Together"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"navigation": {
|
||||||
|
"description": "Setas de navegação para avançar/retornar diretamente integradas na interface, como no seu navegador favorito",
|
||||||
|
"name": "Navegação"
|
||||||
|
},
|
||||||
|
"no-google-login": {
|
||||||
|
"description": "Remova os botões e links de login do Google da interface",
|
||||||
|
"name": "Sem login do Google"
|
||||||
|
},
|
||||||
|
"notifications": {
|
||||||
|
"description": "Exibir uma notificação quando uma música começar a tocar (notificações interativas estão disponíveis no Windows)",
|
||||||
|
"menu": {
|
||||||
|
"interactive": "Notificações interativas",
|
||||||
|
"interactive-settings": {
|
||||||
|
"label": "Configurações interativas",
|
||||||
|
"submenu": {
|
||||||
|
"hide-button-text": "Ocultar texto do botão",
|
||||||
|
"refresh-on-play-pause": "Atualizar ao Reproduzir/Pausar",
|
||||||
|
"tray-controls": "Abrir/Fechar ao clicar na área de notificação"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"priority": "Prioridade da notificação",
|
||||||
|
"toast-style": "Estilo de alerta",
|
||||||
|
"unpause-notification": "Mostrar notificação ao despausar"
|
||||||
|
},
|
||||||
|
"name": "Notificações"
|
||||||
|
},
|
||||||
|
"picture-in-picture": {
|
||||||
|
"description": "Permite alternar o aplicativo para o modo picture-in-picture",
|
||||||
|
"menu": {
|
||||||
|
"always-on-top": "Sempre no topo",
|
||||||
|
"hotkey": {
|
||||||
|
"label": "Tecla de atalho",
|
||||||
|
"prompt": {
|
||||||
|
"keybind-options": {
|
||||||
|
"hotkey": "Tecla de atalho"
|
||||||
|
},
|
||||||
|
"label": "Escolha uma tecla de atalho para alternar entre picture-in-picture",
|
||||||
|
"title": "Atalho do picture-in-picture"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"save-window-position": "Salvar posição da janela",
|
||||||
|
"save-window-size": "Salvar tamanho da janela",
|
||||||
|
"use-native-pip": "Usar PiP nativo do navegador"
|
||||||
|
},
|
||||||
|
"name": "Picture-in-picture",
|
||||||
|
"templates": {
|
||||||
|
"button": "Picture-in-picture"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"playback-speed": {
|
||||||
|
"description": "Ouça rápido, ouça devagar! Adiciona um controle deslizante que controla a velocidade da música",
|
||||||
|
"name": "Velocidade de reprodução",
|
||||||
|
"templates": {
|
||||||
|
"button": "Velocidade"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"precise-volume": {
|
||||||
|
"description": "Controle o volume com precisão usando a roda do mouse/teclas de atalho, com um HUD personalizado e etapas de volume personalizáveis",
|
||||||
|
"menu": {
|
||||||
|
"arrows-shortcuts": "Controles de teclas de seta locais",
|
||||||
|
"custom-volume-steps": "Definir etapas de volume personalizadas",
|
||||||
|
"global-shortcuts": "Teclas de atalho globais"
|
||||||
|
},
|
||||||
|
"name": "Volume preciso",
|
||||||
|
"prompt": {
|
||||||
|
"global-shortcuts": {
|
||||||
|
"keybind-options": {
|
||||||
|
"decrease": "Diminuir volume",
|
||||||
|
"increase": "Aumentar volume"
|
||||||
|
},
|
||||||
|
"label": "Selecione as teclas de atalho global do volume:",
|
||||||
|
"title": "Teclas de atalho global de volume"
|
||||||
|
},
|
||||||
|
"volume-steps": {
|
||||||
|
"label": "Escolha as etapas de aumento/diminuição do volume",
|
||||||
|
"title": "Fases de volume"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"quality-changer": {
|
||||||
|
"backend": {
|
||||||
|
"dialog": {
|
||||||
|
"quality-changer": {
|
||||||
|
"detail": "Qualidade atual: {{quality}}",
|
||||||
|
"message": "Escolher qualidade do vídeo:",
|
||||||
|
"title": "Escolher qualidade do vídeo"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"description": "Permite alterar a qualidade do vídeo com um botão na sobreposição de vídeo",
|
||||||
|
"name": "Alterador de qualidade do vídeo"
|
||||||
|
},
|
||||||
|
"scrobbler": {
|
||||||
|
"description": "Adicionar suporte para scrobbling (last.fm, Listenbrainz, etc.)",
|
||||||
|
"dialog": {
|
||||||
|
"lastfm": {
|
||||||
|
"auth-failed": {
|
||||||
|
"message": "Falha ao autenticar com Last.fm\nOcultar o pop-up até a próxima reinicialização.",
|
||||||
|
"title": "Falha na autenticação"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"menu": {
|
||||||
|
"lastfm": {
|
||||||
|
"api-settings": "Configurações da API do Last.fm"
|
||||||
|
},
|
||||||
|
"listenbrainz": {
|
||||||
|
"token": "Insira o token de usuário ListenBrainz"
|
||||||
|
},
|
||||||
|
"scrobble-other-media": "Scrobble outras mídias"
|
||||||
|
},
|
||||||
|
"name": "Scrobbler",
|
||||||
|
"prompt": {
|
||||||
|
"lastfm": {
|
||||||
|
"api-key": "Chave de API do Last.fm",
|
||||||
|
"api-secret": "Chave secreta da API do Last.fm"
|
||||||
|
},
|
||||||
|
"listenbrainz": {
|
||||||
|
"token": {
|
||||||
|
"label": "Insira seu token de usuário do ListenBrainz:",
|
||||||
|
"title": "ListenBrainz token"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"shortcuts": {
|
||||||
|
"description": "Permite definir teclas de atalho globais para reprodução (reproduzir/pausar/próximo/anterior) e desativar o OSD de mídia substituindo as teclas de mídia, ativando Ctrl/CMD + F para pesquisar, ativando o suporte Linux MPRIS para teclas de mídia e teclas de atalho personalizadas para usuários avançados",
|
||||||
|
"menu": {
|
||||||
|
"override-media-keys": "Substituir chaves de multimédia",
|
||||||
|
"set-keybinds": "Definir controles globais de música"
|
||||||
|
},
|
||||||
|
"name": "Atalhos (& MPRIS)",
|
||||||
|
"prompt": {
|
||||||
|
"keybind": {
|
||||||
|
"keybind-options": {
|
||||||
|
"next": "Próximo",
|
||||||
|
"play-pause": "Reproduzir / Pausar",
|
||||||
|
"previous": "Anterior"
|
||||||
|
},
|
||||||
|
"label": "Escolha atalhos de teclado globais para controle de músicas:",
|
||||||
|
"title": "Atalhos de teclado global"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"skip-disliked-songs": {
|
||||||
|
"description": "Ignora músicas marcadas com \"não gostei\"",
|
||||||
|
"name": "Pular músicas marcadas com \"não gostei\""
|
||||||
|
},
|
||||||
|
"skip-silences": {
|
||||||
|
"description": "Pular automaticamente seções de silêncio em músicas",
|
||||||
|
"name": "Pular silêncios"
|
||||||
|
},
|
||||||
|
"sponsorblock": {
|
||||||
|
"description": "Pula automaticamente partes não musicais, como introdução/finalização ou partes de videoclipes onde a música não está tocando",
|
||||||
|
"name": "SponsorBlock [Bloquear patrocínios]"
|
||||||
|
},
|
||||||
|
"synced-lyrics": {
|
||||||
|
"description": "Fornece letras sincronizadas para músicas, usando provedores como LRClib.",
|
||||||
|
"errors": {
|
||||||
|
"fetch": "⚠️ - Ocorreu um erro ao buscar a letra. Tente novamente mais tarde.",
|
||||||
|
"not-found": "⚠️ - Nenhuma letra encontrada para esta música."
|
||||||
|
},
|
||||||
|
"menu": {
|
||||||
|
"default-text-string": {
|
||||||
|
"label": "Caractere padrão entre letras",
|
||||||
|
"tooltip": "Escolha o caractere padrão a ser usado para o intervalo entre as letras"
|
||||||
|
},
|
||||||
|
"line-effect": {
|
||||||
|
"label": "Efeito de linha",
|
||||||
|
"submenu": {
|
||||||
|
"focus": {
|
||||||
|
"label": "Foco",
|
||||||
|
"tooltip": "Deixe apenas a linha atual branca"
|
||||||
|
},
|
||||||
|
"offset": {
|
||||||
|
"label": "Deslocar",
|
||||||
|
"tooltip": "Deslocamento à direita da linha atual"
|
||||||
|
},
|
||||||
|
"scale": {
|
||||||
|
"label": "Aumentar",
|
||||||
|
"tooltip": "Aumentar a linha atual"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tooltip": "Escolha o efeito a ser aplicado à linha atual"
|
||||||
|
},
|
||||||
|
"precise-timing": {
|
||||||
|
"label": "Deixa as letras perfeitamente sincronizadas",
|
||||||
|
"tooltip": "Calcular até o milissegundo a exibição da próxima linha (pode ter um pequeno impacto no desempenho)"
|
||||||
|
},
|
||||||
|
"show-lyrics-even-if-inexact": {
|
||||||
|
"label": "Mostrar letras mesmo que não sejam exatas",
|
||||||
|
"tooltip": "Se a música não for encontrada, o plugin tenta novamente com uma consulta de pesquisa diferente.\nO resultado da segunda tentativa pode não ser exato."
|
||||||
|
},
|
||||||
|
"show-time-codes": {
|
||||||
|
"label": "Mostrar códigos de tempo",
|
||||||
|
"tooltip": "Mostrar os códigos de tempo ao lado das letras"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"name": "Letras sincronizadas",
|
||||||
|
"refetch-btn": {
|
||||||
|
"fetching": "Buscando...",
|
||||||
|
"normal": "Buscar letras novamente"
|
||||||
|
},
|
||||||
|
"warnings": {
|
||||||
|
"duration-mismatch": "⚠️ - A letra pode estar dessincronizada devido a uma incompatibilidade de duração.",
|
||||||
|
"inexact": "⚠️ - A letra desta música pode não ser exata",
|
||||||
|
"instrumental": "⚠️ - Esta é uma música instrumental"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"taskbar-mediacontrol": {
|
||||||
|
"description": "Controle a reprodução na barra de tarefas do Windows",
|
||||||
|
"name": "Controle de mídia da barra de tarefas"
|
||||||
|
},
|
||||||
|
"touchbar": {
|
||||||
|
"description": "Adiciona um widget TouchBar para usuários do macOS",
|
||||||
|
"name": "TouchBar"
|
||||||
|
},
|
||||||
|
"tuna-obs": {
|
||||||
|
"description": "Integração com o plugin Tuna do OBS",
|
||||||
|
"name": "Tuna OBS"
|
||||||
|
},
|
||||||
|
"video-toggle": {
|
||||||
|
"description": "Adiciona um botão para alternar entre o modo Vídeo/Música. Também é possível remover opcionalmente toda a aba de vídeo",
|
||||||
|
"menu": {
|
||||||
|
"align": {
|
||||||
|
"label": "Alinhamento",
|
||||||
|
"submenu": {
|
||||||
|
"left": "Esquerda",
|
||||||
|
"middle": "Meio",
|
||||||
|
"right": "Direita"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"force-hide": "Forçar remoção da aba de vídeo",
|
||||||
|
"mode": {
|
||||||
|
"label": "Modo",
|
||||||
|
"submenu": {
|
||||||
|
"custom": "Alternância personalizada",
|
||||||
|
"disabled": "Desativado",
|
||||||
|
"native": "Alternância nativa"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"name": "Alternar vídeo",
|
||||||
|
"templates": {
|
||||||
|
"button": "Música"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"visualizer": {
|
||||||
|
"description": "Adiciona um visualizador ao player",
|
||||||
|
"menu": {
|
||||||
|
"visualizer-type": "Tipo de visualizador"
|
||||||
|
},
|
||||||
|
"name": "Visualizador"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -410,6 +410,17 @@
|
|||||||
"description": "Завантажує MP3 / джерело аудіо безпосередньо з інтерфейсу",
|
"description": "Завантажує MP3 / джерело аудіо безпосередньо з інтерфейсу",
|
||||||
"menu": {
|
"menu": {
|
||||||
"choose-download-folder": "Оберіть папку для завантаження",
|
"choose-download-folder": "Оберіть папку для завантаження",
|
||||||
|
"download-finish-settings": {
|
||||||
|
"prompt": {
|
||||||
|
"last-percent": "Після Х відсотків",
|
||||||
|
"last-seconds": "Останні Х секунд"
|
||||||
|
},
|
||||||
|
"submenu": {
|
||||||
|
"enabled": "Увімкнено",
|
||||||
|
"percent": "Відсотків",
|
||||||
|
"seconds": "Секунди"
|
||||||
|
}
|
||||||
|
},
|
||||||
"download-playlist": "Завантажити плейлист",
|
"download-playlist": "Завантажити плейлист",
|
||||||
"presets": "Попередні налаштування",
|
"presets": "Попередні налаштування",
|
||||||
"skip-existing": "Пропустити наявні файли"
|
"skip-existing": "Пропустити наявні файли"
|
||||||
@ -649,6 +660,53 @@
|
|||||||
"description": "Автоматично пропускати немузичні частини, такі як вступ/закінчення або частини музичних відеороликів, де не відтворюється музика",
|
"description": "Автоматично пропускати немузичні частини, такі як вступ/закінчення або частини музичних відеороликів, де не відтворюється музика",
|
||||||
"name": "SponsorBlock"
|
"name": "SponsorBlock"
|
||||||
},
|
},
|
||||||
|
"synced-lyrics": {
|
||||||
|
"errors": {
|
||||||
|
"fetch": "⚠️ - При завантаженні тексту сталась помилка. Спробуйте ще раз пізніше.",
|
||||||
|
"not-found": "⚠️ - До цієї пісні текст не знайдено."
|
||||||
|
},
|
||||||
|
"menu": {
|
||||||
|
"default-text-string": {
|
||||||
|
"label": "Символ за замовчуванням між текстами пісень",
|
||||||
|
"tooltip": "Виберіть символ за замовчуванням, який буде використовуватися для проміжку між текстами пісень"
|
||||||
|
},
|
||||||
|
"line-effect": {
|
||||||
|
"label": "Лінійний ефект",
|
||||||
|
"submenu": {
|
||||||
|
"focus": {
|
||||||
|
"label": "Зосереджитись",
|
||||||
|
"tooltip": "Зробити білим лише поточний рядок"
|
||||||
|
},
|
||||||
|
"scale": {
|
||||||
|
"label": "Масштабувати",
|
||||||
|
"tooltip": "Масштабуваты поточну лінію"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tooltip": "Виберіть ефект, який потрібно застосувати до поточної лінії"
|
||||||
|
},
|
||||||
|
"precise-timing": {
|
||||||
|
"label": "Зробити текст пісні ідеально синхронізованим"
|
||||||
|
},
|
||||||
|
"show-lyrics-even-if-inexact": {
|
||||||
|
"label": "Показувати текст пісні, навіть якщо він неточний",
|
||||||
|
"tooltip": "Якщо пісня не знайдена, плагін повторює спробу з іншим пошуковим запитом.\nРезультат з другої спроби може бути не точним."
|
||||||
|
},
|
||||||
|
"show-time-codes": {
|
||||||
|
"label": "Показувати часові марки",
|
||||||
|
"tooltip": "Показує часові маркы поруч із текстом пісні"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"name": "Синхронізовані тексти",
|
||||||
|
"refetch-btn": {
|
||||||
|
"fetching": "Отримання...",
|
||||||
|
"normal": "Перезавантажити текст"
|
||||||
|
},
|
||||||
|
"warnings": {
|
||||||
|
"duration-mismatch": "⚠️ - Тексти цієї пісні можуть бути не синхронізовані через не співпадіння довжини пісні.",
|
||||||
|
"inexact": "⚠️ - Текст цієї пісні може не співпадати",
|
||||||
|
"instrumental": "⚠️ - Це інструментал"
|
||||||
|
}
|
||||||
|
},
|
||||||
"taskbar-mediacontrol": {
|
"taskbar-mediacontrol": {
|
||||||
"description": "Керування відтворенням з панелі завдань Windows",
|
"description": "Керування відтворенням з панелі завдань Windows",
|
||||||
"name": "Керування медіа на панелі завдань"
|
"name": "Керування медіа на панелі завдань"
|
||||||
|
|||||||
33
src/index.ts
33
src/index.ts
@ -11,6 +11,7 @@ import {
|
|||||||
shell,
|
shell,
|
||||||
dialog,
|
dialog,
|
||||||
ipcMain,
|
ipcMain,
|
||||||
|
type BrowserWindowConstructorOptions,
|
||||||
} from 'electron';
|
} from 'electron';
|
||||||
import enhanceWebRequest, {
|
import enhanceWebRequest, {
|
||||||
BetterSession,
|
BetterSession,
|
||||||
@ -56,7 +57,6 @@ import { loadI18n, setLanguage, t } from '@/i18n';
|
|||||||
import ErrorHtmlAsset from '@assets/error.html?asset';
|
import ErrorHtmlAsset from '@assets/error.html?asset';
|
||||||
|
|
||||||
import type { PluginConfig } from '@/types/plugins';
|
import type { PluginConfig } from '@/types/plugins';
|
||||||
import BrowserWindowConstructorOptions = Electron.BrowserWindowConstructorOptions;
|
|
||||||
|
|
||||||
if (!is.macOS()) {
|
if (!is.macOS()) {
|
||||||
delete allPlugins['touchbar'];
|
delete allPlugins['touchbar'];
|
||||||
@ -334,7 +334,9 @@ async function createMainWindow() {
|
|||||||
const display = screen.getDisplayNearestPoint(windowPosition);
|
const display = screen.getDisplayNearestPoint(windowPosition);
|
||||||
const primaryDisplay = screen.getPrimaryDisplay();
|
const primaryDisplay = screen.getPrimaryDisplay();
|
||||||
|
|
||||||
const scaleFactor = is.windows() ? primaryDisplay.scaleFactor / display.scaleFactor : 1;
|
const scaleFactor = is.windows()
|
||||||
|
? primaryDisplay.scaleFactor / display.scaleFactor
|
||||||
|
: 1;
|
||||||
const scaledWidth = Math.floor(windowSize.width * scaleFactor);
|
const scaledWidth = Math.floor(windowSize.width * scaleFactor);
|
||||||
const scaledHeight = Math.floor(windowSize.height * scaleFactor);
|
const scaledHeight = Math.floor(windowSize.height * scaleFactor);
|
||||||
|
|
||||||
@ -342,10 +344,10 @@ async function createMainWindow() {
|
|||||||
const scaledY = windowY;
|
const scaledY = windowY;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
scaledX + (scaledWidth / 2) < display.bounds.x - 8 || // Left
|
scaledX + scaledWidth / 2 < display.bounds.x - 8 || // Left
|
||||||
scaledX + (scaledWidth / 2) > display.bounds.x + display.bounds.width || // Right
|
scaledX + scaledWidth / 2 > display.bounds.x + display.bounds.width || // Right
|
||||||
scaledY < display.bounds.y - 8 || // Top
|
scaledY < display.bounds.y - 8 || // Top
|
||||||
scaledY + (scaledHeight / 2) > display.bounds.y + display.bounds.height // Bottom
|
scaledY + scaledHeight / 2 > display.bounds.y + display.bounds.height // Bottom
|
||||||
) {
|
) {
|
||||||
// Window is offscreen
|
// Window is offscreen
|
||||||
if (is.dev()) {
|
if (is.dev()) {
|
||||||
@ -442,7 +444,7 @@ async function createMainWindow() {
|
|||||||
...defaultTitleBarOverlayOptions,
|
...defaultTitleBarOverlayOptions,
|
||||||
height: Math.floor(
|
height: Math.floor(
|
||||||
defaultTitleBarOverlayOptions.height! *
|
defaultTitleBarOverlayOptions.height! *
|
||||||
win.webContents.getZoomFactor(),
|
win.webContents.getZoomFactor(),
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -455,7 +457,7 @@ async function createMainWindow() {
|
|||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
win.webContents.loadURL(
|
win.webContents.loadURL(
|
||||||
'https://accounts.google.com/ServiceLogin?ltmpl=music&service=youtube&continue=https%3A%2F%2Fwww.youtube.com%2Fsignin%3Faction_handle_signin%3Dtrue%26next%3Dhttps%253A%252F%252Fmusic.youtube.com%252F'
|
'https://accounts.google.com/ServiceLogin?ltmpl=music&service=youtube&continue=https%3A%2F%2Fwww.youtube.com%2Fsignin%3Faction_handle_signin%3Dtrue%26next%3Dhttps%253A%252F%252Fmusic.youtube.com%252F',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -479,8 +481,8 @@ app.once('browser-window-created', (_event, win) => {
|
|||||||
const updatedUserAgent = is.macOS()
|
const updatedUserAgent = is.macOS()
|
||||||
? userAgents.mac
|
? userAgents.mac
|
||||||
: is.windows()
|
: is.windows()
|
||||||
? userAgents.windows
|
? userAgents.windows
|
||||||
: userAgents.linux;
|
: userAgents.linux;
|
||||||
|
|
||||||
win.webContents.userAgent = updatedUserAgent;
|
win.webContents.userAgent = updatedUserAgent;
|
||||||
app.userAgentFallback = updatedUserAgent;
|
app.userAgentFallback = updatedUserAgent;
|
||||||
@ -618,6 +620,7 @@ app.whenReady().then(async () => {
|
|||||||
shortcutDetails.target !== appLocation ||
|
shortcutDetails.target !== appLocation ||
|
||||||
shortcutDetails.appUserModelId !== appID
|
shortcutDetails.appUserModelId !== appID
|
||||||
) {
|
) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/only-throw-error
|
||||||
throw 'needUpdate';
|
throw 'needUpdate';
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -641,7 +644,9 @@ app.whenReady().then(async () => {
|
|||||||
// In dev mode, get string from process.env.VITE_DEV_SERVER_URL, else use fs.readFileSync
|
// In dev mode, get string from process.env.VITE_DEV_SERVER_URL, else use fs.readFileSync
|
||||||
if (is.dev() && process.env.ELECTRON_RENDERER_URL) {
|
if (is.dev() && process.env.ELECTRON_RENDERER_URL) {
|
||||||
// HACK: to make vite work with electron renderer (supports hot reload)
|
// HACK: to make vite work with electron renderer (supports hot reload)
|
||||||
event.returnValue = [null, `
|
event.returnValue = [
|
||||||
|
null,
|
||||||
|
`
|
||||||
console.log('${LoggerPrefix}', 'Loading vite from dev server');
|
console.log('${LoggerPrefix}', 'Loading vite from dev server');
|
||||||
(async () => {
|
(async () => {
|
||||||
await new Promise((resolve) => {
|
await new Promise((resolve) => {
|
||||||
@ -662,7 +667,8 @@ app.whenReady().then(async () => {
|
|||||||
document.body.appendChild(rendererScript);
|
document.body.appendChild(rendererScript);
|
||||||
})();
|
})();
|
||||||
0
|
0
|
||||||
`];
|
`,
|
||||||
|
];
|
||||||
} else {
|
} else {
|
||||||
const rendererPath = path.join(__dirname, '..', 'renderer');
|
const rendererPath = path.join(__dirname, '..', 'renderer');
|
||||||
const indexHTML = parse(
|
const indexHTML = parse(
|
||||||
@ -674,7 +680,10 @@ app.whenReady().then(async () => {
|
|||||||
scriptSrc.getAttribute('src')!,
|
scriptSrc.getAttribute('src')!,
|
||||||
);
|
);
|
||||||
const scriptString = fs.readFileSync(scriptPath, 'utf-8');
|
const scriptString = fs.readFileSync(scriptPath, 'utf-8');
|
||||||
event.returnValue = [url.pathToFileURL(scriptPath).toString(), scriptString + ';0'];
|
event.returnValue = [
|
||||||
|
url.pathToFileURL(scriptPath).toString(),
|
||||||
|
scriptString + ';0',
|
||||||
|
];
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -34,11 +34,12 @@ const createContext = (
|
|||||||
win.webContents.send(event, ...args);
|
win.webContents.send(event, ...args);
|
||||||
},
|
},
|
||||||
handle: (event: string, listener: CallableFunction) => {
|
handle: (event: string, listener: CallableFunction) => {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-return,@typescript-eslint/no-unsafe-call
|
||||||
ipcMain.handle(event, (_, ...args: unknown[]) => listener(...args));
|
ipcMain.handle(event, (_, ...args: unknown[]) => listener(...args));
|
||||||
},
|
},
|
||||||
on: (event: string, listener: CallableFunction) => {
|
on: (event: string, listener: CallableFunction) => {
|
||||||
ipcMain.on(event, (_, ...args: unknown[]) => {
|
ipcMain.on(event, (_, ...args: unknown[]) => {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
||||||
listener(...args);
|
listener(...args);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@ -75,11 +76,11 @@ export const forceUnloadMainPlugin = async (
|
|||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
console.log(
|
const message = t('common.console.plugins.unload-failed', {
|
||||||
LoggerPrefix,
|
pluginName: id,
|
||||||
t('common.console.plugins.unload-failed', { pluginName: id }),
|
});
|
||||||
);
|
console.log(LoggerPrefix, message);
|
||||||
return Promise.reject();
|
return Promise.reject(new Error(message));
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(
|
console.error(
|
||||||
@ -87,7 +88,7 @@ export const forceUnloadMainPlugin = async (
|
|||||||
t('common.console.plugins.unload-failed', { pluginName: id }),
|
t('common.console.plugins.unload-failed', { pluginName: id }),
|
||||||
);
|
);
|
||||||
console.trace(err);
|
console.trace(err);
|
||||||
return Promise.reject(err);
|
return Promise.reject(err as Error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -111,11 +112,11 @@ export const forceLoadMainPlugin = async (
|
|||||||
) {
|
) {
|
||||||
loadedPluginMap[id] = plugin;
|
loadedPluginMap[id] = plugin;
|
||||||
} else {
|
} else {
|
||||||
console.log(
|
const message = t('common.console.plugins.load-failed', {
|
||||||
LoggerPrefix,
|
pluginName: id,
|
||||||
t('common.console.plugins.load-failed', { pluginName: id }),
|
});
|
||||||
);
|
console.log(LoggerPrefix, message);
|
||||||
return Promise.reject();
|
return Promise.reject(new Error(message));
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(
|
console.error(
|
||||||
@ -123,7 +124,7 @@ export const forceLoadMainPlugin = async (
|
|||||||
t('common.console.plugins.initialize-failed', { pluginName: id }),
|
t('common.console.plugins.initialize-failed', { pluginName: id }),
|
||||||
);
|
);
|
||||||
console.trace(err);
|
console.trace(err);
|
||||||
return Promise.reject(err);
|
return Promise.reject(err as Error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -18,7 +18,8 @@ const loadedPluginMap: Record<
|
|||||||
export const createContext = <Config extends PluginConfig>(
|
export const createContext = <Config extends PluginConfig>(
|
||||||
id: string,
|
id: string,
|
||||||
): RendererContext<Config> => ({
|
): RendererContext<Config> => ({
|
||||||
getConfig: async () => window.ipcRenderer.invoke('ytmd:get-config', id),
|
getConfig: async () =>
|
||||||
|
window.ipcRenderer.invoke('ytmd:get-config', id) as Promise<Config>,
|
||||||
setConfig: async (newConfig) => {
|
setConfig: async (newConfig) => {
|
||||||
await window.ipcRenderer.invoke('ytmd:set-config', id, newConfig);
|
await window.ipcRenderer.invoke('ytmd:set-config', id, newConfig);
|
||||||
},
|
},
|
||||||
@ -30,6 +31,7 @@ export const createContext = <Config extends PluginConfig>(
|
|||||||
window.ipcRenderer.invoke(event, ...args),
|
window.ipcRenderer.invoke(event, ...args),
|
||||||
on: (event: string, listener: CallableFunction) => {
|
on: (event: string, listener: CallableFunction) => {
|
||||||
window.ipcRenderer.on(event, (_, ...args: unknown[]) => {
|
window.ipcRenderer.on(event, (_, ...args: unknown[]) => {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
||||||
listener(...args);
|
listener(...args);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|||||||
157
src/menu.ts
157
src/menu.ts
@ -1,5 +1,13 @@
|
|||||||
import is from 'electron-is';
|
import is from 'electron-is';
|
||||||
import { app, BrowserWindow, clipboard, dialog, Menu, MenuItem, shell, } from 'electron';
|
import {
|
||||||
|
app,
|
||||||
|
BrowserWindow,
|
||||||
|
clipboard,
|
||||||
|
dialog,
|
||||||
|
Menu,
|
||||||
|
MenuItem,
|
||||||
|
shell,
|
||||||
|
} from 'electron';
|
||||||
import prompt from 'custom-electron-prompt';
|
import prompt from 'custom-electron-prompt';
|
||||||
import { satisfies } from 'semver';
|
import { satisfies } from 'semver';
|
||||||
|
|
||||||
@ -68,12 +76,21 @@ export const mainMenuTemplate = async (
|
|||||||
const plugin = allPlugins[id];
|
const plugin = allPlugins[id];
|
||||||
const pluginLabel = plugin?.name?.() ?? id;
|
const pluginLabel = plugin?.name?.() ?? id;
|
||||||
const pluginDescription = plugin?.description?.() ?? undefined;
|
const pluginDescription = plugin?.description?.() ?? undefined;
|
||||||
const isNew = plugin?.addedVersion ? satisfies(packageJson.version, plugin.addedVersion) : false;
|
const isNew = plugin?.addedVersion
|
||||||
|
? satisfies(packageJson.version, plugin.addedVersion)
|
||||||
|
: false;
|
||||||
|
|
||||||
if (!config.plugins.isEnabled(id)) {
|
if (!config.plugins.isEnabled(id)) {
|
||||||
return [
|
return [
|
||||||
id,
|
id,
|
||||||
pluginEnabledMenu(id, pluginLabel, pluginDescription, isNew, true, innerRefreshMenu),
|
pluginEnabledMenu(
|
||||||
|
id,
|
||||||
|
pluginLabel,
|
||||||
|
pluginDescription,
|
||||||
|
isNew,
|
||||||
|
true,
|
||||||
|
innerRefreshMenu,
|
||||||
|
),
|
||||||
] as const;
|
] as const;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -115,9 +132,18 @@ export const mainMenuTemplate = async (
|
|||||||
const plugin = allPlugins[id];
|
const plugin = allPlugins[id];
|
||||||
const pluginLabel = plugin?.name?.() ?? id;
|
const pluginLabel = plugin?.name?.() ?? id;
|
||||||
const pluginDescription = plugin?.description?.() ?? undefined;
|
const pluginDescription = plugin?.description?.() ?? undefined;
|
||||||
const isNew = plugin?.addedVersion ? satisfies(packageJson.version, plugin.addedVersion) : false;
|
const isNew = plugin?.addedVersion
|
||||||
|
? satisfies(packageJson.version, plugin.addedVersion)
|
||||||
|
: false;
|
||||||
|
|
||||||
return pluginEnabledMenu(id, pluginLabel, pluginDescription, isNew, true, innerRefreshMenu);
|
return pluginEnabledMenu(
|
||||||
|
id,
|
||||||
|
pluginLabel,
|
||||||
|
pluginDescription,
|
||||||
|
isNew,
|
||||||
|
true,
|
||||||
|
innerRefreshMenu,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
const availableLanguages = Object.keys(languageResources);
|
const availableLanguages = Object.keys(languageResources);
|
||||||
@ -229,12 +255,12 @@ export const mainMenuTemplate = async (
|
|||||||
submenu: [
|
submenu: [
|
||||||
...((config.get('options.themes')?.length ?? 0) === 0
|
...((config.get('options.themes')?.length ?? 0) === 0
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
label: t(
|
label: t(
|
||||||
'main.menu.options.submenu.visual-tweaks.submenu.theme.submenu.no-theme',
|
'main.menu.options.submenu.visual-tweaks.submenu.theme.submenu.no-theme',
|
||||||
),
|
),
|
||||||
}
|
},
|
||||||
]
|
]
|
||||||
: []),
|
: []),
|
||||||
...(config.get('options.themes')?.map((theme: string) => ({
|
...(config.get('options.themes')?.map((theme: string) => ({
|
||||||
type: 'normal' as const,
|
type: 'normal' as const,
|
||||||
@ -251,16 +277,25 @@ export const mainMenuTemplate = async (
|
|||||||
{ theme },
|
{ theme },
|
||||||
),
|
),
|
||||||
buttons: [
|
buttons: [
|
||||||
t('main.menu.options.submenu.visual-tweaks.submenu.theme.dialog.button.cancel'),
|
t(
|
||||||
t('main.menu.options.submenu.visual-tweaks.submenu.theme.dialog.button.remove'),
|
'main.menu.options.submenu.visual-tweaks.submenu.theme.dialog.button.cancel',
|
||||||
|
),
|
||||||
|
t(
|
||||||
|
'main.menu.options.submenu.visual-tweaks.submenu.theme.dialog.button.remove',
|
||||||
|
),
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response === 1) {
|
if (response === 1) {
|
||||||
config.set('options.themes', config.get('options.themes')?.filter((t) => t !== theme) ?? []);
|
config.set(
|
||||||
|
'options.themes',
|
||||||
|
config
|
||||||
|
.get('options.themes')
|
||||||
|
?.filter((t) => t !== theme) ?? [],
|
||||||
|
);
|
||||||
innerRefreshMenu();
|
innerRefreshMenu();
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
})) ?? []),
|
})) ?? []),
|
||||||
{ type: 'separator' },
|
{ type: 'separator' },
|
||||||
{
|
{
|
||||||
@ -306,40 +341,40 @@ export const mainMenuTemplate = async (
|
|||||||
},
|
},
|
||||||
...((is.windows() || is.linux()
|
...((is.windows() || is.linux()
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
label: t('main.menu.options.submenu.hide-menu.label'),
|
label: t('main.menu.options.submenu.hide-menu.label'),
|
||||||
type: 'checkbox',
|
type: 'checkbox',
|
||||||
checked: config.get('options.hideMenu'),
|
checked: config.get('options.hideMenu'),
|
||||||
click(item) {
|
click(item) {
|
||||||
config.setMenuOption('options.hideMenu', item.checked);
|
config.setMenuOption('options.hideMenu', item.checked);
|
||||||
if (item.checked && !config.get('options.hideMenuWarned')) {
|
if (item.checked && !config.get('options.hideMenuWarned')) {
|
||||||
dialog.showMessageBox(win, {
|
dialog.showMessageBox(win, {
|
||||||
type: 'info',
|
type: 'info',
|
||||||
title: t(
|
title: t(
|
||||||
'main.menu.options.submenu.hide-menu.dialog.title',
|
'main.menu.options.submenu.hide-menu.dialog.title',
|
||||||
),
|
),
|
||||||
message: t(
|
message: t(
|
||||||
'main.menu.options.submenu.hide-menu.dialog.message',
|
'main.menu.options.submenu.hide-menu.dialog.message',
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
]
|
||||||
]
|
|
||||||
: []) satisfies Electron.MenuItemConstructorOptions[]),
|
: []) satisfies Electron.MenuItemConstructorOptions[]),
|
||||||
...((is.windows() || is.macOS()
|
...((is.windows() || is.macOS()
|
||||||
? // Only works on Win/Mac
|
? // Only works on Win/Mac
|
||||||
// https://www.electronjs.org/docs/api/app#appsetloginitemsettingssettings-macos-windows
|
// https://www.electronjs.org/docs/api/app#appsetloginitemsettingssettings-macos-windows
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
label: t('main.menu.options.submenu.start-at-login'),
|
label: t('main.menu.options.submenu.start-at-login'),
|
||||||
type: 'checkbox',
|
type: 'checkbox',
|
||||||
checked: config.get('options.startAtLogin'),
|
checked: config.get('options.startAtLogin'),
|
||||||
click(item) {
|
click(item) {
|
||||||
config.setMenuOption('options.startAtLogin', item.checked);
|
config.setMenuOption('options.startAtLogin', item.checked);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
]
|
||||||
]
|
|
||||||
: []) satisfies Electron.MenuItemConstructorOptions[]),
|
: []) satisfies Electron.MenuItemConstructorOptions[]),
|
||||||
{
|
{
|
||||||
label: t('main.menu.options.submenu.tray.label'),
|
label: t('main.menu.options.submenu.tray.label'),
|
||||||
@ -493,25 +528,25 @@ export const mainMenuTemplate = async (
|
|||||||
{ type: 'separator' },
|
{ type: 'separator' },
|
||||||
is.macOS()
|
is.macOS()
|
||||||
? {
|
? {
|
||||||
label: t(
|
label: t(
|
||||||
'main.menu.options.submenu.advanced-options.submenu.toggle-dev-tools',
|
'main.menu.options.submenu.advanced-options.submenu.toggle-dev-tools',
|
||||||
),
|
),
|
||||||
// Cannot use "toggleDevTools" role in macOS
|
// Cannot use "toggleDevTools" role in macOS
|
||||||
click() {
|
click() {
|
||||||
const { webContents } = win;
|
const { webContents } = win;
|
||||||
if (webContents.isDevToolsOpened()) {
|
if (webContents.isDevToolsOpened()) {
|
||||||
webContents.closeDevTools();
|
webContents.closeDevTools();
|
||||||
} else {
|
} else {
|
||||||
webContents.openDevTools();
|
webContents.openDevTools();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
label: t(
|
label: t(
|
||||||
'main.menu.options.submenu.advanced-options.submenu.toggle-dev-tools',
|
'main.menu.options.submenu.advanced-options.submenu.toggle-dev-tools',
|
||||||
),
|
),
|
||||||
role: 'toggleDevTools',
|
role: 'toggleDevTools',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: t(
|
label: t(
|
||||||
'main.menu.options.submenu.advanced-options.submenu.edit-config-json',
|
'main.menu.options.submenu.advanced-options.submenu.edit-config-json',
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
function skipAd(target: Element) {
|
function skipAd(target: Element) {
|
||||||
const skipButton = target.querySelector<HTMLButtonElement>('button.ytp-ad-skip-button-modern');
|
const skipButton = target.querySelector<HTMLButtonElement>(
|
||||||
|
'button.ytp-ad-skip-button-modern',
|
||||||
|
);
|
||||||
if (skipButton) {
|
if (skipButton) {
|
||||||
skipButton.click();
|
skipButton.click();
|
||||||
}
|
}
|
||||||
@ -17,7 +19,7 @@ function speedUpAndMute(player: Element, isAdShowing: boolean) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const loadAdSpeedup = async () => {
|
export const loadAdSpeedup = () => {
|
||||||
const player = document.querySelector<HTMLVideoElement>('#movie_player');
|
const player = document.querySelector<HTMLVideoElement>('#movie_player');
|
||||||
if (!player) return;
|
if (!player) return;
|
||||||
|
|
||||||
@ -53,4 +55,4 @@ export const loadAdSpeedup = async () => {
|
|||||||
player.classList.contains('ad-interrupting');
|
player.classList.contains('ad-interrupting');
|
||||||
speedUpAndMute(player, isAdShowing);
|
speedUpAndMute(player, isAdShowing);
|
||||||
skipAd(player);
|
skipAd(player);
|
||||||
}
|
};
|
||||||
|
|||||||
@ -79,7 +79,7 @@ export default createPlugin({
|
|||||||
if (config.blocker === blockers.AdSpeedup) {
|
if (config.blocker === blockers.AdSpeedup) {
|
||||||
await loadAdSpeedup();
|
await loadAdSpeedup();
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
backend: {
|
backend: {
|
||||||
mainWindow: null as BrowserWindow | null,
|
mainWindow: null as BrowserWindow | null,
|
||||||
|
|||||||
@ -104,21 +104,28 @@ export default createPlugin<
|
|||||||
buttons.splice(i, 1);
|
buttons.splice(i, 1);
|
||||||
i--;
|
i--;
|
||||||
} else {
|
} else {
|
||||||
(buttons[i].children[0].children[0] as HTMLElement).style.setProperty(
|
(
|
||||||
|
buttons[i].children[0].children[0] as HTMLElement
|
||||||
|
).style.setProperty(
|
||||||
'-webkit-mask-size',
|
'-webkit-mask-size',
|
||||||
`100% ${100 - ((count / listsLength) * 100)}%`,
|
`100% ${100 - (count / listsLength) * 100}%`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
i++;
|
i++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const menuParent = document.querySelector('#action-buttons')?.parentElement;
|
const menuParent =
|
||||||
|
document.querySelector('#action-buttons')?.parentElement;
|
||||||
if (menuParent && !document.querySelector('.like-menu')) {
|
if (menuParent && !document.querySelector('.like-menu')) {
|
||||||
const menu = document.createElement('div');
|
const menu = document.createElement('div');
|
||||||
menu.id = 'ytmd-album-action-buttons';
|
menu.id = 'ytmd-album-action-buttons';
|
||||||
menu.className = 'action-buttons style-scope ytmusic-responsive-header-renderer';
|
menu.className =
|
||||||
|
'action-buttons style-scope ytmusic-responsive-header-renderer';
|
||||||
|
|
||||||
menuParent.insertBefore(menu, menuParent.children[menuParent.children.length - 1]);
|
menuParent.insertBefore(
|
||||||
|
menu,
|
||||||
|
menuParent.children[menuParent.children.length - 1],
|
||||||
|
);
|
||||||
for (const button of buttons) {
|
for (const button of buttons) {
|
||||||
menu.appendChild(button);
|
menu.appendChild(button);
|
||||||
button.addEventListener('click', this.loadFullList);
|
button.addEventListener('click', this.loadFullList);
|
||||||
|
|||||||
@ -25,7 +25,12 @@ export default createPlugin<
|
|||||||
sidebarSmall: HTMLElement | null;
|
sidebarSmall: HTMLElement | null;
|
||||||
ytmusicAppLayout: HTMLElement | null;
|
ytmusicAppLayout: HTMLElement | null;
|
||||||
|
|
||||||
getMixedColor(color: string, key: string, alpha?: number, ratioMultiply?: number): string;
|
getMixedColor(
|
||||||
|
color: string,
|
||||||
|
key: string,
|
||||||
|
alpha?: number,
|
||||||
|
ratioMultiply?: number,
|
||||||
|
): string;
|
||||||
updateColor(): void;
|
updateColor(): void;
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -91,7 +96,10 @@ export default createPlugin<
|
|||||||
this.ytmusicAppLayout = document.querySelector<HTMLElement>('#layout');
|
this.ytmusicAppLayout = document.querySelector<HTMLElement>('#layout');
|
||||||
|
|
||||||
const config = await getConfig();
|
const config = await getConfig();
|
||||||
document.documentElement.style.setProperty(RATIO_KEY, `${~~(config.ratio * 100)}%`);
|
document.documentElement.style.setProperty(
|
||||||
|
RATIO_KEY,
|
||||||
|
`${~~(config.ratio * 100)}%`,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
onPlayerApiReady(playerApi) {
|
onPlayerApiReady(playerApi) {
|
||||||
const fastAverageColor = new FastAverageColor();
|
const fastAverageColor = new FastAverageColor();
|
||||||
@ -100,10 +108,12 @@ export default createPlugin<
|
|||||||
if (event.detail.name !== 'dataloaded') return;
|
if (event.detail.name !== 'dataloaded') return;
|
||||||
|
|
||||||
const playerResponse = playerApi.getPlayerResponse();
|
const playerResponse = playerApi.getPlayerResponse();
|
||||||
const thumbnail = playerResponse?.videoDetails?.thumbnail?.thumbnails?.at(0);
|
const thumbnail =
|
||||||
|
playerResponse?.videoDetails?.thumbnail?.thumbnails?.at(0);
|
||||||
if (!thumbnail) return;
|
if (!thumbnail) return;
|
||||||
|
|
||||||
const albumColor = await fastAverageColor.getColorAsync(thumbnail.url)
|
const albumColor = await fastAverageColor
|
||||||
|
.getColorAsync(thumbnail.url)
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
return null;
|
return null;
|
||||||
@ -120,8 +130,14 @@ export default createPlugin<
|
|||||||
this.darkColor = this.darkColor?.darken(0.05);
|
this.darkColor = this.darkColor?.darken(0.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
document.documentElement.style.setProperty(COLOR_KEY, `${~~this.color.red()}, ${~~this.color.green()}, ${~~this.color.blue()}`);
|
document.documentElement.style.setProperty(
|
||||||
document.documentElement.style.setProperty(DARK_COLOR_KEY, `${~~this.darkColor.red()}, ${~~this.darkColor.green()}, ${~~this.darkColor.blue()}`);
|
COLOR_KEY,
|
||||||
|
`${~~this.color.red()}, ${~~this.color.green()}, ${~~this.color.blue()}`,
|
||||||
|
);
|
||||||
|
document.documentElement.style.setProperty(
|
||||||
|
DARK_COLOR_KEY,
|
||||||
|
`${~~this.darkColor.red()}, ${~~this.darkColor.green()}, ${~~this.darkColor.blue()}`,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
document.documentElement.style.setProperty(COLOR_KEY, '0, 0, 0');
|
document.documentElement.style.setProperty(COLOR_KEY, '0, 0, 0');
|
||||||
document.documentElement.style.setProperty(DARK_COLOR_KEY, '0, 0, 0');
|
document.documentElement.style.setProperty(DARK_COLOR_KEY, '0, 0, 0');
|
||||||
@ -131,7 +147,10 @@ export default createPlugin<
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
onConfigChange(config) {
|
onConfigChange(config) {
|
||||||
document.documentElement.style.setProperty(RATIO_KEY, `${~~(config.ratio * 100)}%`);
|
document.documentElement.style.setProperty(
|
||||||
|
RATIO_KEY,
|
||||||
|
`${~~(config.ratio * 100)}%`,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
getMixedColor(color: string, key: string, alpha = 1, ratioMultiply) {
|
getMixedColor(color: string, key: string, alpha = 1, ratioMultiply) {
|
||||||
const keyColor = `rgba(var(${key}), ${alpha})`;
|
const keyColor = `rgba(var(${key}), ${alpha})`;
|
||||||
@ -181,11 +200,23 @@ export default createPlugin<
|
|||||||
'--yt-spec-black-1-alpha-95': 'rgba(40,40,40,0.95)',
|
'--yt-spec-black-1-alpha-95': 'rgba(40,40,40,0.95)',
|
||||||
};
|
};
|
||||||
Object.entries(variableMap).map(([variable, color]) => {
|
Object.entries(variableMap).map(([variable, color]) => {
|
||||||
document.documentElement.style.setProperty(variable, this.getMixedColor(color, COLOR_KEY), 'important');
|
document.documentElement.style.setProperty(
|
||||||
|
variable,
|
||||||
|
this.getMixedColor(color, COLOR_KEY),
|
||||||
|
'important',
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
document.body.style.setProperty('background', this.getMixedColor('#030303', COLOR_KEY), 'important');
|
document.body.style.setProperty(
|
||||||
document.documentElement.style.setProperty('--ytmusic-background', this.getMixedColor('#030303', DARK_COLOR_KEY), 'important');
|
'background',
|
||||||
|
this.getMixedColor('#030303', COLOR_KEY),
|
||||||
|
'important',
|
||||||
|
);
|
||||||
|
document.documentElement.style.setProperty(
|
||||||
|
'--ytmusic-background',
|
||||||
|
this.getMixedColor('#030303', DARK_COLOR_KEY),
|
||||||
|
'important',
|
||||||
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@ -53,10 +53,16 @@ export default createPlugin({
|
|||||||
|
|
||||||
const songImage = document.querySelector<HTMLImageElement>('#song-image');
|
const songImage = document.querySelector<HTMLImageElement>('#song-image');
|
||||||
const songVideo = document.querySelector<HTMLDivElement>('#song-video');
|
const songVideo = document.querySelector<HTMLDivElement>('#song-video');
|
||||||
const image = songImage?.querySelector<HTMLImageElement>('yt-img-shadow > img');
|
const image = songImage?.querySelector<HTMLImageElement>(
|
||||||
const video = await waitForElement<HTMLVideoElement>('.html5-video-container > video');
|
'yt-img-shadow > img',
|
||||||
|
);
|
||||||
|
const video = await waitForElement<HTMLVideoElement>(
|
||||||
|
'.html5-video-container > video',
|
||||||
|
);
|
||||||
|
|
||||||
const videoWrapper = document.querySelector('#song-video > .player-wrapper');
|
const videoWrapper = document.querySelector(
|
||||||
|
'#song-video > .player-wrapper',
|
||||||
|
);
|
||||||
|
|
||||||
const injectBlurImage = () => {
|
const injectBlurImage = () => {
|
||||||
if (!songImage || !image) return null;
|
if (!songImage || !image) return null;
|
||||||
@ -95,7 +101,9 @@ export default createPlugin({
|
|||||||
const blurCanvas = document.createElement('canvas');
|
const blurCanvas = document.createElement('canvas');
|
||||||
blurCanvas.classList.add('html5-blur-canvas');
|
blurCanvas.classList.add('html5-blur-canvas');
|
||||||
|
|
||||||
const context = blurCanvas.getContext('2d', { willReadFrequently: true });
|
const context = blurCanvas.getContext('2d', {
|
||||||
|
willReadFrequently: true,
|
||||||
|
});
|
||||||
|
|
||||||
/* effect */
|
/* effect */
|
||||||
let lastEffectWorkId: number | null = null;
|
let lastEffectWorkId: number | null = null;
|
||||||
@ -109,14 +117,18 @@ export default createPlugin({
|
|||||||
if (!context) return;
|
if (!context) return;
|
||||||
|
|
||||||
const width = this.qualityRatio;
|
const width = this.qualityRatio;
|
||||||
let height = Math.max(Math.floor((blurCanvas.height / blurCanvas.width) * width), 1,);
|
let height = Math.max(
|
||||||
|
Math.floor((blurCanvas.height / blurCanvas.width) * width),
|
||||||
|
1,
|
||||||
|
);
|
||||||
if (!Number.isFinite(height)) height = width;
|
if (!Number.isFinite(height)) height = width;
|
||||||
if (!height) return;
|
if (!height) return;
|
||||||
|
|
||||||
context.globalAlpha = 1;
|
context.globalAlpha = 1;
|
||||||
if (lastImageData) {
|
if (lastImageData) {
|
||||||
const frameOffset = (1 / this.buffer) * (1000 / this.interpolationTime);
|
const frameOffset =
|
||||||
context.globalAlpha = 1 - (frameOffset * 2); // because of alpha value must be < 1
|
(1 / this.buffer) * (1000 / this.interpolationTime);
|
||||||
|
context.globalAlpha = 1 - frameOffset * 2; // because of alpha value must be < 1
|
||||||
context.putImageData(lastImageData, 0, 0);
|
context.putImageData(lastImageData, 0, 0);
|
||||||
context.globalAlpha = frameOffset;
|
context.globalAlpha = frameOffset;
|
||||||
}
|
}
|
||||||
@ -137,7 +149,9 @@ export default createPlugin({
|
|||||||
if (newWidth === 0 || newHeight === 0) return;
|
if (newWidth === 0 || newHeight === 0) return;
|
||||||
|
|
||||||
blurCanvas.width = this.qualityRatio;
|
blurCanvas.width = this.qualityRatio;
|
||||||
blurCanvas.height = Math.floor((newHeight / newWidth) * this.qualityRatio);
|
blurCanvas.height = Math.floor(
|
||||||
|
(newHeight / newWidth) * this.qualityRatio,
|
||||||
|
);
|
||||||
|
|
||||||
if (this.isFullscreen) blurCanvas.classList.add('fullscreen');
|
if (this.isFullscreen) blurCanvas.classList.add('fullscreen');
|
||||||
else blurCanvas.classList.remove('fullscreen');
|
else blurCanvas.classList.remove('fullscreen');
|
||||||
@ -151,7 +165,10 @@ export default createPlugin({
|
|||||||
|
|
||||||
/* hooking */
|
/* hooking */
|
||||||
let canvasInterval: NodeJS.Timeout | null = null;
|
let canvasInterval: NodeJS.Timeout | null = null;
|
||||||
canvasInterval = setInterval(onSync, Math.max(1, Math.ceil(1000 / this.buffer)));
|
canvasInterval = setInterval(
|
||||||
|
onSync,
|
||||||
|
Math.max(1, Math.ceil(1000 / this.buffer)),
|
||||||
|
);
|
||||||
|
|
||||||
const onPause = () => {
|
const onPause = () => {
|
||||||
if (canvasInterval) clearInterval(canvasInterval);
|
if (canvasInterval) clearInterval(canvasInterval);
|
||||||
@ -159,7 +176,10 @@ export default createPlugin({
|
|||||||
};
|
};
|
||||||
const onPlay = () => {
|
const onPlay = () => {
|
||||||
if (canvasInterval) clearInterval(canvasInterval);
|
if (canvasInterval) clearInterval(canvasInterval);
|
||||||
canvasInterval = setInterval(onSync, Math.max(1, Math.ceil(1000 / this.buffer)));
|
canvasInterval = setInterval(
|
||||||
|
onSync,
|
||||||
|
Math.max(1, Math.ceil(1000 / this.buffer)),
|
||||||
|
);
|
||||||
};
|
};
|
||||||
songVideo.addEventListener('pause', onPause);
|
songVideo.addEventListener('pause', onPause);
|
||||||
songVideo.addEventListener('play', onPlay);
|
songVideo.addEventListener('play', onPlay);
|
||||||
@ -198,11 +218,20 @@ export default createPlugin({
|
|||||||
if (isPageOpen) {
|
if (isPageOpen) {
|
||||||
const isVideo = isVideoMode();
|
const isVideo = isVideoMode();
|
||||||
if (!force) {
|
if (!force) {
|
||||||
if (this.lastMediaType === 'video' && this.lastVideoSource === video?.src) return false;
|
if (
|
||||||
if (this.lastMediaType === 'image' && this.lastImageSource === image?.src) return false;
|
this.lastMediaType === 'video' &&
|
||||||
|
this.lastVideoSource === video?.src
|
||||||
|
)
|
||||||
|
return false;
|
||||||
|
if (
|
||||||
|
this.lastMediaType === 'image' &&
|
||||||
|
this.lastImageSource === image?.src
|
||||||
|
)
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
this.unregister?.();
|
this.unregister?.();
|
||||||
this.unregister = (isVideo ? injectBlurVideo() : injectBlurImage()) ?? null;
|
this.unregister =
|
||||||
|
(isVideo ? injectBlurVideo() : injectBlurImage()) ?? null;
|
||||||
} else {
|
} else {
|
||||||
this.unregister?.();
|
this.unregister?.();
|
||||||
this.unregister = null;
|
this.unregister = null;
|
||||||
|
|||||||
@ -1,14 +1,24 @@
|
|||||||
import { t } from "@/i18n";
|
import { MenuItemConstructorOptions } from 'electron';
|
||||||
import { MenuContext } from "@/types/contexts";
|
|
||||||
import { MenuItemConstructorOptions } from "electron";
|
import { t } from '@/i18n';
|
||||||
import { AmbientModePluginConfig } from "./types";
|
import { MenuContext } from '@/types/contexts';
|
||||||
|
import { AmbientModePluginConfig } from './types';
|
||||||
|
|
||||||
export interface menuParameters {
|
export interface menuParameters {
|
||||||
getConfig: () => AmbientModePluginConfig | Promise<AmbientModePluginConfig>;
|
getConfig: () => AmbientModePluginConfig | Promise<AmbientModePluginConfig>;
|
||||||
setConfig: (conf: Partial<Omit<AmbientModePluginConfig, "enabled">>) => void | Promise<void>;
|
setConfig: (
|
||||||
|
conf: Partial<Omit<AmbientModePluginConfig, 'enabled'>>,
|
||||||
|
) => void | Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const menu: (ctx: MenuContext<AmbientModePluginConfig>) => MenuItemConstructorOptions[] | Promise<MenuItemConstructorOptions[]> = async ({ getConfig, setConfig }: menuParameters) => {
|
export const menu: (
|
||||||
|
ctx: MenuContext<AmbientModePluginConfig>,
|
||||||
|
) =>
|
||||||
|
| MenuItemConstructorOptions[]
|
||||||
|
| Promise<MenuItemConstructorOptions[]> = async ({
|
||||||
|
getConfig,
|
||||||
|
setConfig,
|
||||||
|
}: menuParameters) => {
|
||||||
const interpolationTimeList = [0, 500, 1000, 1500, 2000, 3000, 4000, 5000];
|
const interpolationTimeList = [0, 500, 1000, 1500, 2000, 3000, 4000, 5000];
|
||||||
const qualityList = [10, 25, 50, 100, 200, 500, 1000];
|
const qualityList = [10, 25, 50, 100, 200, 500, 1000];
|
||||||
const sizeList = [100, 110, 125, 150, 175, 200, 300];
|
const sizeList = [100, 110, 125, 150, 175, 200, 300];
|
||||||
@ -107,4 +117,4 @@ export const menu: (ctx: MenuContext<AmbientModePluginConfig>) => MenuItemConstr
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}
|
};
|
||||||
|
|||||||
@ -7,4 +7,4 @@ export type AmbientModePluginConfig = {
|
|||||||
size: number;
|
size: number;
|
||||||
opacity: number;
|
opacity: number;
|
||||||
fullscreen: boolean;
|
fullscreen: boolean;
|
||||||
};
|
};
|
||||||
|
|||||||
1
src/plugins/api-server/backend/index.ts
Normal file
1
src/plugins/api-server/backend/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './main';
|
||||||
103
src/plugins/api-server/backend/main.ts
Normal file
103
src/plugins/api-server/backend/main.ts
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
import { jwt } from 'hono/jwt';
|
||||||
|
import { OpenAPIHono as Hono } from '@hono/zod-openapi';
|
||||||
|
import { cors } from 'hono/cors';
|
||||||
|
import { swaggerUI } from '@hono/swagger-ui';
|
||||||
|
import { serve } from '@hono/node-server';
|
||||||
|
|
||||||
|
import registerCallback from '@/providers/song-info';
|
||||||
|
import { createBackend } from '@/utils';
|
||||||
|
|
||||||
|
import { JWTPayloadSchema } from './scheme';
|
||||||
|
import { registerAuth, registerControl } from './routes';
|
||||||
|
|
||||||
|
import type { APIServerConfig } from '../config';
|
||||||
|
import type { BackendType } from './types';
|
||||||
|
|
||||||
|
export const backend = createBackend<BackendType, APIServerConfig>({
|
||||||
|
async start(ctx) {
|
||||||
|
const config = await ctx.getConfig();
|
||||||
|
|
||||||
|
this.init(ctx);
|
||||||
|
registerCallback((songInfo) => {
|
||||||
|
this.songInfo = songInfo;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.run(config.hostname, config.port);
|
||||||
|
},
|
||||||
|
stop() {
|
||||||
|
this.end();
|
||||||
|
},
|
||||||
|
onConfigChange(config) {
|
||||||
|
if (
|
||||||
|
this.oldConfig?.hostname === config.hostname &&
|
||||||
|
this.oldConfig?.port === config.port
|
||||||
|
) {
|
||||||
|
this.oldConfig = config;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.end();
|
||||||
|
this.run(config.hostname, config.port);
|
||||||
|
this.oldConfig = config;
|
||||||
|
},
|
||||||
|
|
||||||
|
// Custom
|
||||||
|
async init(ctx) {
|
||||||
|
const config = await ctx.getConfig();
|
||||||
|
this.app = new Hono();
|
||||||
|
|
||||||
|
this.app.use('*', cors());
|
||||||
|
|
||||||
|
// middlewares
|
||||||
|
this.app.use(
|
||||||
|
'/api/*',
|
||||||
|
jwt({
|
||||||
|
secret: config.secret,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
this.app.use('/api/*', async (ctx, next) => {
|
||||||
|
const result = await JWTPayloadSchema.spa(await ctx.get('jwtPayload'));
|
||||||
|
|
||||||
|
const isAuthorized =
|
||||||
|
result.success && config.authorizedClients.includes(result.data.id);
|
||||||
|
if (!isAuthorized) {
|
||||||
|
ctx.status(401);
|
||||||
|
return ctx.body('Unauthorized');
|
||||||
|
}
|
||||||
|
|
||||||
|
return await next();
|
||||||
|
});
|
||||||
|
|
||||||
|
// routes
|
||||||
|
registerControl(this.app, ctx, () => this.songInfo);
|
||||||
|
registerAuth(this.app, ctx);
|
||||||
|
|
||||||
|
// swagger
|
||||||
|
this.app.doc('/doc', {
|
||||||
|
openapi: '3.1.0',
|
||||||
|
info: {
|
||||||
|
version: '1.0.0',
|
||||||
|
title: 'Youtube Music API Server',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
this.app.get('/swagger', swaggerUI({ url: '/doc' }));
|
||||||
|
},
|
||||||
|
run(hostname, port) {
|
||||||
|
if (!this.app) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.server = serve({
|
||||||
|
fetch: this.app.fetch.bind(this.app),
|
||||||
|
port,
|
||||||
|
hostname,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
end() {
|
||||||
|
this.server?.close();
|
||||||
|
this.server = undefined;
|
||||||
|
},
|
||||||
|
});
|
||||||
94
src/plugins/api-server/backend/routes/auth.ts
Normal file
94
src/plugins/api-server/backend/routes/auth.ts
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
import { createRoute, z } from '@hono/zod-openapi';
|
||||||
|
import { dialog } from 'electron';
|
||||||
|
import { sign } from 'hono/jwt';
|
||||||
|
|
||||||
|
import { getConnInfo } from '@hono/node-server/conninfo';
|
||||||
|
|
||||||
|
import { t } from '@/i18n';
|
||||||
|
|
||||||
|
import { APIServerConfig } from '../../config';
|
||||||
|
import { JWTPayload } from '../scheme';
|
||||||
|
|
||||||
|
import type { HonoApp } from '../types';
|
||||||
|
import type { BackendContext } from '@/types/contexts';
|
||||||
|
|
||||||
|
const routes = {
|
||||||
|
request: createRoute({
|
||||||
|
method: 'post',
|
||||||
|
path: '/auth/{id}',
|
||||||
|
summary: '',
|
||||||
|
description: '',
|
||||||
|
request: {
|
||||||
|
params: z.object({
|
||||||
|
id: z.string(),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
responses: {
|
||||||
|
200: {
|
||||||
|
description: 'Success',
|
||||||
|
content: {
|
||||||
|
'application/json': {
|
||||||
|
schema: z.object({
|
||||||
|
accessToken: z.string(),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
403: {
|
||||||
|
description: 'Forbidden',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const register = (
|
||||||
|
app: HonoApp,
|
||||||
|
{ getConfig, setConfig }: BackendContext<APIServerConfig>,
|
||||||
|
) => {
|
||||||
|
app.openapi(routes.request, async (ctx) => {
|
||||||
|
const config = await getConfig();
|
||||||
|
const { id } = ctx.req.param();
|
||||||
|
|
||||||
|
if (config.authorizedClients.includes(id)) {
|
||||||
|
// SKIP CHECK
|
||||||
|
} else if (config.authStrategy === 'AUTH_AT_FIRST') {
|
||||||
|
const result = await dialog.showMessageBox({
|
||||||
|
title: t('plugins.api-server.dialog.request.title'),
|
||||||
|
message: t('plugins.api-server.dialog.request.message', {
|
||||||
|
origin: getConnInfo(ctx).remote.address,
|
||||||
|
id,
|
||||||
|
}),
|
||||||
|
buttons: [
|
||||||
|
t('plugins.api-server.dialog.request.buttons.allow'),
|
||||||
|
t('plugins.api-server.dialog.request.deny'),
|
||||||
|
],
|
||||||
|
defaultId: 1,
|
||||||
|
cancelId: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.response === 1) {
|
||||||
|
ctx.status(403);
|
||||||
|
return ctx.body(null);
|
||||||
|
}
|
||||||
|
} else if (config.authStrategy === 'NONE') {
|
||||||
|
// SKIP CHECK
|
||||||
|
}
|
||||||
|
|
||||||
|
setConfig({
|
||||||
|
authorizedClients: [...config.authorizedClients, id],
|
||||||
|
});
|
||||||
|
|
||||||
|
const token = await sign(
|
||||||
|
{
|
||||||
|
id,
|
||||||
|
iat: ~~(Date.now() / 1000),
|
||||||
|
} satisfies JWTPayload,
|
||||||
|
config.secret,
|
||||||
|
);
|
||||||
|
|
||||||
|
ctx.status(200);
|
||||||
|
return ctx.json({
|
||||||
|
accessToken: token,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
482
src/plugins/api-server/backend/routes/control.ts
Normal file
482
src/plugins/api-server/backend/routes/control.ts
Normal file
@ -0,0 +1,482 @@
|
|||||||
|
import { createRoute, z } from '@hono/zod-openapi';
|
||||||
|
|
||||||
|
import { ipcMain } from 'electron';
|
||||||
|
|
||||||
|
import getSongControls from '@/providers/song-controls';
|
||||||
|
|
||||||
|
import {
|
||||||
|
AuthHeadersSchema,
|
||||||
|
type ResponseSongInfo,
|
||||||
|
SongInfoSchema,
|
||||||
|
GoForwardScheme,
|
||||||
|
GoBackSchema,
|
||||||
|
SwitchRepeatSchema,
|
||||||
|
SetVolumeSchema,
|
||||||
|
SetFullscreenSchema,
|
||||||
|
} from '../scheme';
|
||||||
|
|
||||||
|
import type { SongInfo } from '@/providers/song-info';
|
||||||
|
import type { BackendContext } from '@/types/contexts';
|
||||||
|
import type { APIServerConfig } from '../../config';
|
||||||
|
import type { HonoApp } from '../types';
|
||||||
|
import type { QueueResponse } from '@/types/youtube-music-desktop-internal';
|
||||||
|
|
||||||
|
const API_VERSION = 'v1';
|
||||||
|
|
||||||
|
const routes = {
|
||||||
|
previous: createRoute({
|
||||||
|
method: 'post',
|
||||||
|
path: `/api/${API_VERSION}/previous`,
|
||||||
|
summary: 'play previous song',
|
||||||
|
description: 'Plays the previous song in the queue',
|
||||||
|
request: {
|
||||||
|
headers: AuthHeadersSchema,
|
||||||
|
},
|
||||||
|
responses: {
|
||||||
|
204: {
|
||||||
|
description: 'Success',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
next: createRoute({
|
||||||
|
method: 'post',
|
||||||
|
path: `/api/${API_VERSION}/next`,
|
||||||
|
summary: 'play next song',
|
||||||
|
description: 'Plays the next song in the queue',
|
||||||
|
request: {
|
||||||
|
headers: AuthHeadersSchema,
|
||||||
|
},
|
||||||
|
responses: {
|
||||||
|
204: {
|
||||||
|
description: 'Success',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
play: createRoute({
|
||||||
|
method: 'post',
|
||||||
|
path: `/api/${API_VERSION}/play`,
|
||||||
|
summary: 'Play',
|
||||||
|
description: 'Change the state of the player to play',
|
||||||
|
request: {
|
||||||
|
headers: AuthHeadersSchema,
|
||||||
|
},
|
||||||
|
responses: {
|
||||||
|
204: {
|
||||||
|
description: 'Success',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
pause: createRoute({
|
||||||
|
method: 'post',
|
||||||
|
path: `/api/${API_VERSION}/pause`,
|
||||||
|
summary: 'Pause',
|
||||||
|
description: 'Change the state of the player to pause',
|
||||||
|
request: {
|
||||||
|
headers: AuthHeadersSchema,
|
||||||
|
},
|
||||||
|
responses: {
|
||||||
|
204: {
|
||||||
|
description: 'Success',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
togglePlay: createRoute({
|
||||||
|
method: 'post',
|
||||||
|
path: `/api/${API_VERSION}/toggle-play`,
|
||||||
|
summary: 'Toggle play/pause',
|
||||||
|
description:
|
||||||
|
'Change the state of the player to play if paused, or pause if playing',
|
||||||
|
request: {
|
||||||
|
headers: AuthHeadersSchema,
|
||||||
|
},
|
||||||
|
responses: {
|
||||||
|
204: {
|
||||||
|
description: 'Success',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
like: createRoute({
|
||||||
|
method: 'post',
|
||||||
|
path: `/api/${API_VERSION}/like`,
|
||||||
|
summary: 'like song',
|
||||||
|
description: 'Set the current song as liked',
|
||||||
|
request: {
|
||||||
|
headers: AuthHeadersSchema,
|
||||||
|
},
|
||||||
|
responses: {
|
||||||
|
204: {
|
||||||
|
description: 'Success',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
dislike: createRoute({
|
||||||
|
method: 'post',
|
||||||
|
path: `/api/${API_VERSION}/dislike`,
|
||||||
|
summary: 'dislike song',
|
||||||
|
description: 'Set the current song as disliked',
|
||||||
|
request: {
|
||||||
|
headers: AuthHeadersSchema,
|
||||||
|
},
|
||||||
|
responses: {
|
||||||
|
204: {
|
||||||
|
description: 'Success',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
|
||||||
|
goBack: createRoute({
|
||||||
|
method: 'post',
|
||||||
|
path: `/api/${API_VERSION}/go-back`,
|
||||||
|
summary: 'go back',
|
||||||
|
description: 'Move the current song back by a number of seconds',
|
||||||
|
request: {
|
||||||
|
headers: AuthHeadersSchema,
|
||||||
|
body: {
|
||||||
|
description: 'seconds to go back',
|
||||||
|
content: {
|
||||||
|
'application/json': {
|
||||||
|
schema: GoBackSchema,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
responses: {
|
||||||
|
204: {
|
||||||
|
description: 'Success',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
|
||||||
|
goForward: createRoute({
|
||||||
|
method: 'post',
|
||||||
|
path: `/api/${API_VERSION}/go-forward`,
|
||||||
|
summary: 'go forward',
|
||||||
|
description: 'Move the current song forward by a number of seconds',
|
||||||
|
request: {
|
||||||
|
headers: AuthHeadersSchema,
|
||||||
|
body: {
|
||||||
|
description: 'seconds to go forward',
|
||||||
|
content: {
|
||||||
|
'application/json': {
|
||||||
|
schema: GoForwardScheme,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
responses: {
|
||||||
|
204: {
|
||||||
|
description: 'Success',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
|
||||||
|
shuffle: createRoute({
|
||||||
|
method: 'post',
|
||||||
|
path: `/api/${API_VERSION}/shuffle`,
|
||||||
|
summary: 'shuffle',
|
||||||
|
description: 'Shuffle the queue',
|
||||||
|
request: {
|
||||||
|
headers: AuthHeadersSchema,
|
||||||
|
},
|
||||||
|
responses: {
|
||||||
|
204: {
|
||||||
|
description: 'Success',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
switchRepeat: createRoute({
|
||||||
|
method: 'post',
|
||||||
|
path: `/api/${API_VERSION}/switch-repeat`,
|
||||||
|
summary: 'switch repeat',
|
||||||
|
description: 'Switch the repeat mode',
|
||||||
|
request: {
|
||||||
|
headers: AuthHeadersSchema,
|
||||||
|
body: {
|
||||||
|
description: 'number of times to click the repeat button',
|
||||||
|
content: {
|
||||||
|
'application/json': {
|
||||||
|
schema: SwitchRepeatSchema,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
responses: {
|
||||||
|
204: {
|
||||||
|
description: 'Success',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
setVolume: createRoute({
|
||||||
|
method: 'post',
|
||||||
|
path: `/api/${API_VERSION}/volume`,
|
||||||
|
summary: 'set volume',
|
||||||
|
description: 'Set the volume of the player',
|
||||||
|
request: {
|
||||||
|
headers: AuthHeadersSchema,
|
||||||
|
body: {
|
||||||
|
description: 'volume to set',
|
||||||
|
content: {
|
||||||
|
'application/json': {
|
||||||
|
schema: SetVolumeSchema,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
responses: {
|
||||||
|
204: {
|
||||||
|
description: 'Success',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
setFullscreen: createRoute({
|
||||||
|
method: 'post',
|
||||||
|
path: `/api/${API_VERSION}/fullscreen`,
|
||||||
|
summary: 'set fullscreen',
|
||||||
|
description: 'Set the fullscreen state of the player',
|
||||||
|
request: {
|
||||||
|
headers: AuthHeadersSchema,
|
||||||
|
body: {
|
||||||
|
description: 'fullscreen state',
|
||||||
|
content: {
|
||||||
|
'application/json': {
|
||||||
|
schema: SetFullscreenSchema,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
responses: {
|
||||||
|
204: {
|
||||||
|
description: 'Success',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
toggleMute: createRoute({
|
||||||
|
method: 'post',
|
||||||
|
path: `/api/${API_VERSION}/toggle-mute`,
|
||||||
|
summary: 'toggle mute',
|
||||||
|
description: 'Toggle the mute state of the player',
|
||||||
|
request: {
|
||||||
|
headers: AuthHeadersSchema,
|
||||||
|
},
|
||||||
|
responses: {
|
||||||
|
204: {
|
||||||
|
description: 'Success',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
|
||||||
|
getFullscreenState: createRoute({
|
||||||
|
method: 'get',
|
||||||
|
path: `/api/${API_VERSION}/fullscreen`,
|
||||||
|
summary: 'get fullscreen state',
|
||||||
|
description: 'Get the current fullscreen state',
|
||||||
|
request: {
|
||||||
|
headers: AuthHeadersSchema,
|
||||||
|
},
|
||||||
|
responses: {
|
||||||
|
200: {
|
||||||
|
description: 'Success',
|
||||||
|
content: {
|
||||||
|
'application/json': {
|
||||||
|
schema: z.object({
|
||||||
|
state: z.boolean(),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
queueInfo: createRoute({
|
||||||
|
method: 'get',
|
||||||
|
path: `/api/${API_VERSION}/queue-info`,
|
||||||
|
summary: 'get current queue info',
|
||||||
|
description: 'Get the current queue info',
|
||||||
|
request: {
|
||||||
|
headers: AuthHeadersSchema,
|
||||||
|
},
|
||||||
|
responses: {
|
||||||
|
200: {
|
||||||
|
description: 'Success',
|
||||||
|
content: {
|
||||||
|
'application/json': {
|
||||||
|
schema: z.object({}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
204: {
|
||||||
|
description: 'No queue info',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
songInfo: createRoute({
|
||||||
|
method: 'get',
|
||||||
|
path: `/api/${API_VERSION}/song-info`,
|
||||||
|
summary: 'get current song info',
|
||||||
|
description: 'Get the current song info',
|
||||||
|
request: {
|
||||||
|
headers: AuthHeadersSchema,
|
||||||
|
},
|
||||||
|
responses: {
|
||||||
|
200: {
|
||||||
|
description: 'Success',
|
||||||
|
content: {
|
||||||
|
'application/json': {
|
||||||
|
schema: SongInfoSchema,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
204: {
|
||||||
|
description: 'No song info',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const register = (
|
||||||
|
app: HonoApp,
|
||||||
|
{ window }: BackendContext<APIServerConfig>,
|
||||||
|
songInfoGetter: () => SongInfo | undefined,
|
||||||
|
) => {
|
||||||
|
const controller = getSongControls(window);
|
||||||
|
|
||||||
|
app.openapi(routes.previous, (ctx) => {
|
||||||
|
controller.previous();
|
||||||
|
|
||||||
|
ctx.status(204);
|
||||||
|
return ctx.body(null);
|
||||||
|
});
|
||||||
|
app.openapi(routes.previous, (ctx) => {
|
||||||
|
controller.previous();
|
||||||
|
|
||||||
|
ctx.status(204);
|
||||||
|
return ctx.body(null);
|
||||||
|
});
|
||||||
|
app.openapi(routes.play, (ctx) => {
|
||||||
|
controller.play();
|
||||||
|
|
||||||
|
ctx.status(204);
|
||||||
|
return ctx.body(null);
|
||||||
|
});
|
||||||
|
app.openapi(routes.pause, (ctx) => {
|
||||||
|
controller.pause();
|
||||||
|
|
||||||
|
ctx.status(204);
|
||||||
|
return ctx.body(null);
|
||||||
|
});
|
||||||
|
app.openapi(routes.togglePlay, (ctx) => {
|
||||||
|
controller.playPause();
|
||||||
|
|
||||||
|
ctx.status(204);
|
||||||
|
return ctx.body(null);
|
||||||
|
});
|
||||||
|
app.openapi(routes.like, (ctx) => {
|
||||||
|
controller.like();
|
||||||
|
|
||||||
|
ctx.status(204);
|
||||||
|
return ctx.body(null);
|
||||||
|
});
|
||||||
|
app.openapi(routes.dislike, (ctx) => {
|
||||||
|
controller.dislike();
|
||||||
|
|
||||||
|
ctx.status(204);
|
||||||
|
return ctx.body(null);
|
||||||
|
});
|
||||||
|
app.openapi(routes.goBack, (ctx) => {
|
||||||
|
const { seconds } = ctx.req.valid('json');
|
||||||
|
controller.goBack(seconds);
|
||||||
|
|
||||||
|
ctx.status(204);
|
||||||
|
return ctx.body(null);
|
||||||
|
});
|
||||||
|
app.openapi(routes.goForward, (ctx) => {
|
||||||
|
const { seconds } = ctx.req.valid('json');
|
||||||
|
controller.goForward(seconds);
|
||||||
|
|
||||||
|
ctx.status(204);
|
||||||
|
return ctx.body(null);
|
||||||
|
});
|
||||||
|
app.openapi(routes.shuffle, (ctx) => {
|
||||||
|
controller.shuffle();
|
||||||
|
|
||||||
|
ctx.status(204);
|
||||||
|
return ctx.body(null);
|
||||||
|
});
|
||||||
|
app.openapi(routes.switchRepeat, (ctx) => {
|
||||||
|
const { iteration } = ctx.req.valid('json');
|
||||||
|
controller.switchRepeat(iteration);
|
||||||
|
|
||||||
|
ctx.status(204);
|
||||||
|
return ctx.body(null);
|
||||||
|
});
|
||||||
|
app.openapi(routes.setVolume, (ctx) => {
|
||||||
|
const { volume } = ctx.req.valid('json');
|
||||||
|
controller.setVolume(volume);
|
||||||
|
|
||||||
|
ctx.status(204);
|
||||||
|
return ctx.body(null);
|
||||||
|
});
|
||||||
|
app.openapi(routes.setFullscreen, (ctx) => {
|
||||||
|
const { state } = ctx.req.valid('json');
|
||||||
|
controller.setFullscreen(state);
|
||||||
|
|
||||||
|
ctx.status(204);
|
||||||
|
return ctx.body(null);
|
||||||
|
});
|
||||||
|
app.openapi(routes.toggleMute, (ctx) => {
|
||||||
|
controller.muteUnmute();
|
||||||
|
|
||||||
|
ctx.status(204);
|
||||||
|
return ctx.body(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
app.openapi(routes.getFullscreenState, async (ctx) => {
|
||||||
|
const stateResponsePromise = new Promise<boolean>((resolve) => {
|
||||||
|
ipcMain.once(
|
||||||
|
'ytmd:set-fullscreen',
|
||||||
|
(_, isFullscreen: boolean | undefined) => {
|
||||||
|
return resolve(!!isFullscreen);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
controller.requestFullscreenInformation();
|
||||||
|
});
|
||||||
|
|
||||||
|
const fullscreen = await stateResponsePromise;
|
||||||
|
|
||||||
|
ctx.status(200);
|
||||||
|
return ctx.json({ state: fullscreen });
|
||||||
|
});
|
||||||
|
app.openapi(routes.queueInfo, async (ctx) => {
|
||||||
|
const queueResponsePromise = new Promise<QueueResponse>((resolve) => {
|
||||||
|
ipcMain.once('ytmd:get-queue-response', (_, queue: QueueResponse) => {
|
||||||
|
return resolve(queue);
|
||||||
|
});
|
||||||
|
|
||||||
|
controller.requestQueueInformation();
|
||||||
|
});
|
||||||
|
|
||||||
|
const info = await queueResponsePromise;
|
||||||
|
|
||||||
|
if (!info) {
|
||||||
|
ctx.status(204);
|
||||||
|
return ctx.body(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.status(200);
|
||||||
|
return ctx.json(info);
|
||||||
|
});
|
||||||
|
app.openapi(routes.songInfo, (ctx) => {
|
||||||
|
const info = songInfoGetter();
|
||||||
|
|
||||||
|
if (!info) {
|
||||||
|
ctx.status(204);
|
||||||
|
return ctx.body(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = { ...info };
|
||||||
|
delete body.image;
|
||||||
|
|
||||||
|
ctx.status(200);
|
||||||
|
return ctx.json(body satisfies ResponseSongInfo);
|
||||||
|
});
|
||||||
|
};
|
||||||
2
src/plugins/api-server/backend/routes/index.ts
Normal file
2
src/plugins/api-server/backend/routes/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export { register as registerControl } from './control';
|
||||||
|
export { register as registerAuth } from './auth';
|
||||||
13
src/plugins/api-server/backend/scheme/auth.ts
Normal file
13
src/plugins/api-server/backend/scheme/auth.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { z } from '@hono/zod-openapi';
|
||||||
|
|
||||||
|
export const AuthHeadersSchema = z.object({
|
||||||
|
authorization: z.string().openapi({
|
||||||
|
example: 'Bearer token',
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type JWTPayload = z.infer<typeof JWTPayloadSchema>;
|
||||||
|
export const JWTPayloadSchema = z.object({
|
||||||
|
id: z.string(),
|
||||||
|
iat: z.number(),
|
||||||
|
});
|
||||||
5
src/plugins/api-server/backend/scheme/go-back.ts
Normal file
5
src/plugins/api-server/backend/scheme/go-back.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import { z } from '@hono/zod-openapi';
|
||||||
|
|
||||||
|
export const GoBackSchema = z.object({
|
||||||
|
seconds: z.number(),
|
||||||
|
});
|
||||||
5
src/plugins/api-server/backend/scheme/go-forward.ts
Normal file
5
src/plugins/api-server/backend/scheme/go-forward.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import { z } from '@hono/zod-openapi';
|
||||||
|
|
||||||
|
export const GoForwardScheme = z.object({
|
||||||
|
seconds: z.number(),
|
||||||
|
});
|
||||||
7
src/plugins/api-server/backend/scheme/index.ts
Normal file
7
src/plugins/api-server/backend/scheme/index.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
export * from './auth';
|
||||||
|
export * from './song-info';
|
||||||
|
export * from './go-back';
|
||||||
|
export * from './go-forward';
|
||||||
|
export * from './switch-repeat';
|
||||||
|
export * from './set-volume';
|
||||||
|
export * from './set-fullscreen';
|
||||||
5
src/plugins/api-server/backend/scheme/set-fullscreen.ts
Normal file
5
src/plugins/api-server/backend/scheme/set-fullscreen.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import { z } from '@hono/zod-openapi';
|
||||||
|
|
||||||
|
export const SetFullscreenSchema = z.object({
|
||||||
|
state: z.boolean(),
|
||||||
|
});
|
||||||
5
src/plugins/api-server/backend/scheme/set-volume.ts
Normal file
5
src/plugins/api-server/backend/scheme/set-volume.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import { z } from '@hono/zod-openapi';
|
||||||
|
|
||||||
|
export const SetVolumeSchema = z.object({
|
||||||
|
volume: z.number(),
|
||||||
|
});
|
||||||
26
src/plugins/api-server/backend/scheme/song-info.ts
Normal file
26
src/plugins/api-server/backend/scheme/song-info.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import { z } from '@hono/zod-openapi';
|
||||||
|
|
||||||
|
import { MediaType } from '@/providers/song-info';
|
||||||
|
|
||||||
|
export type ResponseSongInfo = z.infer<typeof SongInfoSchema>;
|
||||||
|
export const SongInfoSchema = z.object({
|
||||||
|
title: z.string(),
|
||||||
|
artist: z.string(),
|
||||||
|
views: z.number(),
|
||||||
|
uploadDate: z.string().optional(),
|
||||||
|
imageSrc: z.string().nullable().optional(),
|
||||||
|
isPaused: z.boolean().optional(),
|
||||||
|
songDuration: z.number(),
|
||||||
|
elapsedSeconds: z.number().optional(),
|
||||||
|
url: z.string().optional(),
|
||||||
|
album: z.string().nullable().optional(),
|
||||||
|
videoId: z.string(),
|
||||||
|
playlistId: z.string().optional(),
|
||||||
|
mediaType: z.enum([
|
||||||
|
MediaType.Audio,
|
||||||
|
MediaType.OriginalMusicVideo,
|
||||||
|
MediaType.UserGeneratedContent,
|
||||||
|
MediaType.PodcastEpisode,
|
||||||
|
MediaType.OtherVideo,
|
||||||
|
]),
|
||||||
|
});
|
||||||
5
src/plugins/api-server/backend/scheme/switch-repeat.ts
Normal file
5
src/plugins/api-server/backend/scheme/switch-repeat.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import { z } from '@hono/zod-openapi';
|
||||||
|
|
||||||
|
export const SwitchRepeatSchema = z.object({
|
||||||
|
iteration: z.number(),
|
||||||
|
});
|
||||||
18
src/plugins/api-server/backend/types.ts
Normal file
18
src/plugins/api-server/backend/types.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { OpenAPIHono as Hono } from '@hono/zod-openapi';
|
||||||
|
import { serve } from '@hono/node-server';
|
||||||
|
|
||||||
|
import type { BackendContext } from '@/types/contexts';
|
||||||
|
import type { SongInfo } from '@/providers/song-info';
|
||||||
|
import type { APIServerConfig } from '../config';
|
||||||
|
|
||||||
|
export type HonoApp = Hono;
|
||||||
|
export type BackendType = {
|
||||||
|
app?: HonoApp;
|
||||||
|
server?: ReturnType<typeof serve>;
|
||||||
|
oldConfig?: APIServerConfig;
|
||||||
|
songInfo?: SongInfo;
|
||||||
|
|
||||||
|
init: (ctx: BackendContext<APIServerConfig>) => void;
|
||||||
|
run: (hostname: string, port: number) => void;
|
||||||
|
end: () => void;
|
||||||
|
};
|
||||||
19
src/plugins/api-server/config.ts
Normal file
19
src/plugins/api-server/config.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
export interface APIServerConfig {
|
||||||
|
enabled: boolean;
|
||||||
|
hostname: string;
|
||||||
|
port: number;
|
||||||
|
authStrategy: 'AUTH_AT_FIRST' | 'NONE';
|
||||||
|
secret: string;
|
||||||
|
|
||||||
|
authorizedClients: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const defaultAPIServerConfig: APIServerConfig = {
|
||||||
|
enabled: true,
|
||||||
|
hostname: '0.0.0.0',
|
||||||
|
port: 26538,
|
||||||
|
authStrategy: 'AUTH_AT_FIRST',
|
||||||
|
secret: Date.now().toString(36),
|
||||||
|
|
||||||
|
authorizedClients: [],
|
||||||
|
};
|
||||||
17
src/plugins/api-server/index.ts
Normal file
17
src/plugins/api-server/index.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { createPlugin } from '@/utils';
|
||||||
|
import { t } from '@/i18n';
|
||||||
|
|
||||||
|
import { defaultAPIServerConfig } from './config';
|
||||||
|
import { onMenu } from './menu';
|
||||||
|
import { backend } from './backend';
|
||||||
|
|
||||||
|
export default createPlugin({
|
||||||
|
name: () => t('plugins.api-server.name'),
|
||||||
|
description: () => t('plugins.api-server.description'),
|
||||||
|
restartNeeded: false,
|
||||||
|
config: defaultAPIServerConfig,
|
||||||
|
addedVersion: '3.6.X',
|
||||||
|
menu: onMenu,
|
||||||
|
|
||||||
|
backend,
|
||||||
|
});
|
||||||
93
src/plugins/api-server/menu.ts
Normal file
93
src/plugins/api-server/menu.ts
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
import prompt from 'custom-electron-prompt';
|
||||||
|
|
||||||
|
import { t } from '@/i18n';
|
||||||
|
import promptOptions from '@/providers/prompt-options';
|
||||||
|
|
||||||
|
import { APIServerConfig, defaultAPIServerConfig } from './config';
|
||||||
|
|
||||||
|
import type { MenuContext } from '@/types/contexts';
|
||||||
|
import type { MenuTemplate } from '@/menu';
|
||||||
|
|
||||||
|
export const onMenu = async ({
|
||||||
|
getConfig,
|
||||||
|
setConfig,
|
||||||
|
window,
|
||||||
|
}: MenuContext<APIServerConfig>): Promise<MenuTemplate> => {
|
||||||
|
const config = await getConfig();
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
label: t('plugins.api-server.menu.hostname.label'),
|
||||||
|
type: 'normal',
|
||||||
|
async click() {
|
||||||
|
const config = await getConfig();
|
||||||
|
|
||||||
|
const newHostname =
|
||||||
|
(await prompt(
|
||||||
|
{
|
||||||
|
title: t('plugins.api-server.prompt.hostname.title'),
|
||||||
|
label: t('plugins.api-server.prompt.hostname.label'),
|
||||||
|
value: config.hostname,
|
||||||
|
type: 'input',
|
||||||
|
width: 380,
|
||||||
|
...promptOptions(),
|
||||||
|
},
|
||||||
|
window,
|
||||||
|
)) ??
|
||||||
|
config.hostname ??
|
||||||
|
defaultAPIServerConfig.hostname;
|
||||||
|
|
||||||
|
setConfig({ ...config, hostname: newHostname });
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('plugins.api-server.menu.port.label'),
|
||||||
|
type: 'normal',
|
||||||
|
async click() {
|
||||||
|
const config = await getConfig();
|
||||||
|
|
||||||
|
const newPort =
|
||||||
|
(await prompt(
|
||||||
|
{
|
||||||
|
title: t('plugins.api-server.prompt.port.title'),
|
||||||
|
label: t('plugins.api-server.prompt.port.label'),
|
||||||
|
value: config.port,
|
||||||
|
type: 'counter',
|
||||||
|
counterOptions: { minimum: 0, maximum: 65565 },
|
||||||
|
width: 380,
|
||||||
|
...promptOptions(),
|
||||||
|
},
|
||||||
|
window,
|
||||||
|
)) ??
|
||||||
|
config.port ??
|
||||||
|
defaultAPIServerConfig.port;
|
||||||
|
|
||||||
|
setConfig({ ...config, port: newPort });
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('plugins.api-server.menu.auth-strategy.label'),
|
||||||
|
type: 'submenu',
|
||||||
|
submenu: [
|
||||||
|
{
|
||||||
|
label: t(
|
||||||
|
'plugins.api-server.menu.auth-strategy.submenu.auth-at-first.label',
|
||||||
|
),
|
||||||
|
type: 'radio',
|
||||||
|
checked: config.authStrategy === 'AUTH_AT_FIRST',
|
||||||
|
click() {
|
||||||
|
setConfig({ ...config, authStrategy: 'AUTH_AT_FIRST' });
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('plugins.api-server.menu.auth-strategy.submenu.none.label'),
|
||||||
|
type: 'radio',
|
||||||
|
checked: config.authStrategy === 'NONE',
|
||||||
|
click() {
|
||||||
|
setConfig({ ...config, authStrategy: 'NONE' });
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
};
|
||||||
@ -15,7 +15,10 @@ export default createPlugin({
|
|||||||
this.styleSheet = new CSSStyleSheet();
|
this.styleSheet = new CSSStyleSheet();
|
||||||
await this.styleSheet.replace(style);
|
await this.styleSheet.replace(style);
|
||||||
|
|
||||||
document.adoptedStyleSheets = [...document.adoptedStyleSheets, this.styleSheet];
|
document.adoptedStyleSheets = [
|
||||||
|
...document.adoptedStyleSheets,
|
||||||
|
this.styleSheet,
|
||||||
|
];
|
||||||
},
|
},
|
||||||
async stop() {
|
async stop() {
|
||||||
await this.styleSheet?.replace('');
|
await this.styleSheet?.replace('');
|
||||||
|
|||||||
@ -34,7 +34,7 @@ export default createPlugin<
|
|||||||
{
|
{
|
||||||
label: t('plugins.captions-selector.menu.autoload'),
|
label: t('plugins.captions-selector.menu.autoload'),
|
||||||
type: 'checkbox',
|
type: 'checkbox',
|
||||||
checked: config.autoload as boolean,
|
checked: config.autoload,
|
||||||
click(item) {
|
click(item) {
|
||||||
setConfig({ autoload: item.checked });
|
setConfig({ autoload: item.checked });
|
||||||
},
|
},
|
||||||
@ -42,7 +42,7 @@ export default createPlugin<
|
|||||||
{
|
{
|
||||||
label: t('plugins.captions-selector.menu.disable-captions'),
|
label: t('plugins.captions-selector.menu.disable-captions'),
|
||||||
type: 'checkbox',
|
type: 'checkbox',
|
||||||
checked: config.disableCaptions as boolean,
|
checked: config.disableCaptions,
|
||||||
click(item) {
|
click(item) {
|
||||||
setConfig({ disableCaptions: item.checked });
|
setConfig({ disableCaptions: item.checked });
|
||||||
},
|
},
|
||||||
|
|||||||
@ -64,7 +64,7 @@ interface VolumeFade {
|
|||||||
// Main class
|
// Main class
|
||||||
export class VolumeFader {
|
export class VolumeFader {
|
||||||
private readonly media: HTMLMediaElement;
|
private readonly media: HTMLMediaElement;
|
||||||
private readonly logger: VolumeLogger | false;
|
private readonly logger: VolumeLogger | null;
|
||||||
private scale: {
|
private scale: {
|
||||||
internalToVolume: (level: number) => number;
|
internalToVolume: (level: number) => number;
|
||||||
volumeToInternal: (level: number) => number;
|
volumeToInternal: (level: number) => number;
|
||||||
@ -100,7 +100,7 @@ export class VolumeFader {
|
|||||||
this.logger = options.logger;
|
this.logger = options.logger;
|
||||||
} else {
|
} else {
|
||||||
// Set log function explicitly to false
|
// Set log function explicitly to false
|
||||||
this.logger = false;
|
this.logger = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Linear volume fading?
|
// Linear volume fading?
|
||||||
@ -112,7 +112,7 @@ export class VolumeFader {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Log setting
|
// Log setting
|
||||||
this.logger && this.logger('Using linear fading.');
|
this.logger?.('Using linear fading.');
|
||||||
}
|
}
|
||||||
// No linear, but logarithmic fading…
|
// No linear, but logarithmic fading…
|
||||||
else {
|
else {
|
||||||
@ -152,9 +152,8 @@ export class VolumeFader {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Log setting if not default
|
// Log setting if not default
|
||||||
options.fadeScaling &&
|
if (options.fadeScaling)
|
||||||
this.logger &&
|
this.logger?.(
|
||||||
this.logger(
|
|
||||||
'Using logarithmic fading with ' +
|
'Using logarithmic fading with ' +
|
||||||
String(10 * dynamicRange) +
|
String(10 * dynamicRange) +
|
||||||
' dB dynamic range.',
|
' dB dynamic range.',
|
||||||
@ -170,8 +169,7 @@ export class VolumeFader {
|
|||||||
this.media.volume = options.initialVolume;
|
this.media.volume = options.initialVolume;
|
||||||
|
|
||||||
// Log setting
|
// Log setting
|
||||||
this.logger &&
|
this.logger?.('Set initial volume to ' + String(this.media.volume) + '.');
|
||||||
this.logger('Set initial volume to ' + String(this.media.volume) + '.');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fade duration given?
|
// Fade duration given?
|
||||||
@ -187,7 +185,7 @@ export class VolumeFader {
|
|||||||
this.active = false;
|
this.active = false;
|
||||||
|
|
||||||
// Initialization done
|
// Initialization done
|
||||||
this.logger && this.logger('Initialized for', this.media);
|
this.logger?.('Initialized for', this.media);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -236,8 +234,7 @@ export class VolumeFader {
|
|||||||
this.fadeDuration = fadeDuration;
|
this.fadeDuration = fadeDuration;
|
||||||
|
|
||||||
// Log setting
|
// Log setting
|
||||||
this.logger &&
|
this.logger?.('Set fade duration to ' + String(fadeDuration) + ' ms.');
|
||||||
this.logger('Set fade duration to ' + String(fadeDuration) + ' ms.');
|
|
||||||
} else {
|
} else {
|
||||||
// Abort and throw an exception
|
// Abort and throw an exception
|
||||||
throw new TypeError('Positive number expected as fade duration!');
|
throw new TypeError('Positive number expected as fade duration!');
|
||||||
@ -279,7 +276,7 @@ export class VolumeFader {
|
|||||||
this.start();
|
this.start();
|
||||||
|
|
||||||
// Log new fade
|
// Log new fade
|
||||||
this.logger && this.logger('New fade started:', this.fade);
|
this.logger?.('New fade started:', this.fade);
|
||||||
|
|
||||||
// Return instance for chaining
|
// Return instance for chaining
|
||||||
return this;
|
return this;
|
||||||
@ -313,7 +310,7 @@ export class VolumeFader {
|
|||||||
|
|
||||||
// Compute current level on internal scale
|
// Compute current level on internal scale
|
||||||
const level =
|
const level =
|
||||||
(progress * (this.fade.volume.end - this.fade.volume.start)) +
|
progress * (this.fade.volume.end - this.fade.volume.start) +
|
||||||
this.fade.volume.start;
|
this.fade.volume.start;
|
||||||
|
|
||||||
// Map fade level to volume level and apply it to media element
|
// Map fade level to volume level and apply it to media element
|
||||||
@ -323,8 +320,7 @@ export class VolumeFader {
|
|||||||
window.requestAnimationFrame(this.updateVolume.bind(this));
|
window.requestAnimationFrame(this.updateVolume.bind(this));
|
||||||
} else {
|
} else {
|
||||||
// Log end of fade
|
// Log end of fade
|
||||||
this.logger &&
|
this.logger?.('Fade to ' + String(this.fade.volume.end) + ' complete.');
|
||||||
this.logger('Fade to ' + String(this.fade.volume.end) + ' complete.');
|
|
||||||
|
|
||||||
// Time is up, jump to target volume
|
// Time is up, jump to target volume
|
||||||
this.media.volume = this.scale.internalToVolume(this.fade.volume.end);
|
this.media.volume = this.scale.internalToVolume(this.fade.volume.end);
|
||||||
@ -333,7 +329,7 @@ export class VolumeFader {
|
|||||||
this.active = false;
|
this.active = false;
|
||||||
|
|
||||||
// Done, call back (if callable)
|
// Done, call back (if callable)
|
||||||
typeof this.fade.callback === 'function' && this.fade.callback();
|
if (typeof this.fade.callback === 'function') this.fade.callback();
|
||||||
|
|
||||||
// Clear fade
|
// Clear fade
|
||||||
this.fade = undefined;
|
this.fade = undefined;
|
||||||
@ -382,7 +378,7 @@ export class VolumeFader {
|
|||||||
input = Math.log10(input);
|
input = Math.log10(input);
|
||||||
|
|
||||||
// Scale minus something × 10 dB to 0…1 (clipping at 0)
|
// Scale minus something × 10 dB to 0…1 (clipping at 0)
|
||||||
return Math.max(1 + (input / dynamicRange), 0);
|
return Math.max(1 + input / dynamicRange, 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -191,7 +191,7 @@ export default createPlugin<
|
|||||||
let waitForTransition: Promise<unknown>;
|
let waitForTransition: Promise<unknown>;
|
||||||
|
|
||||||
const getStreamURL = async (videoID: string): Promise<string> =>
|
const getStreamURL = async (videoID: string): Promise<string> =>
|
||||||
this.ipc?.invoke('audio-url', videoID);
|
this.ipc?.invoke('audio-url', videoID) as Promise<string>;
|
||||||
|
|
||||||
const getVideoIDFromURL = (url: string) =>
|
const getVideoIDFromURL = (url: string) =>
|
||||||
new URLSearchParams(url.split('?')?.at(-1)).get('v');
|
new URLSearchParams(url.split('?')?.at(-1)).get('v');
|
||||||
|
|||||||
@ -202,9 +202,9 @@ export const backend = createBackend<
|
|||||||
}
|
}
|
||||||
} else if (!config.hideDurationLeft) {
|
} else if (!config.hideDurationLeft) {
|
||||||
// Add the start and end time of the song
|
// Add the start and end time of the song
|
||||||
const songStartTime = Date.now() - ((songInfo.elapsedSeconds ?? 0) * 1000);
|
const songStartTime = Date.now() - (songInfo.elapsedSeconds ?? 0) * 1000;
|
||||||
activityInfo.startTimestamp = songStartTime;
|
activityInfo.startTimestamp = songStartTime;
|
||||||
activityInfo.endTimestamp = songStartTime + (songInfo.songDuration * 1000);
|
activityInfo.endTimestamp = songStartTime + songInfo.songDuration * 1000;
|
||||||
}
|
}
|
||||||
|
|
||||||
info.rpc.user?.setActivity(activityInfo).catch(console.error);
|
info.rpc.user?.setActivity(activityInfo).catch(console.error);
|
||||||
|
|||||||
@ -183,12 +183,18 @@ function downloadSongOnFinishSetup({
|
|||||||
config.downloadOnFinish.mode === 'seconds' &&
|
config.downloadOnFinish.mode === 'seconds' &&
|
||||||
duration - time <= config.downloadOnFinish.seconds
|
duration - time <= config.downloadOnFinish.seconds
|
||||||
) {
|
) {
|
||||||
downloadSong(currentUrl, config.downloadOnFinish.folder ?? config.downloadFolder);
|
downloadSong(
|
||||||
|
currentUrl,
|
||||||
|
config.downloadOnFinish.folder ?? config.downloadFolder,
|
||||||
|
);
|
||||||
} else if (
|
} else if (
|
||||||
config.downloadOnFinish.mode === 'percent' &&
|
config.downloadOnFinish.mode === 'percent' &&
|
||||||
time >= duration * (config.downloadOnFinish.percent / 100)
|
time >= duration * (config.downloadOnFinish.percent / 100)
|
||||||
) {
|
) {
|
||||||
downloadSong(currentUrl, config.downloadOnFinish.folder ?? config.downloadFolder);
|
downloadSong(
|
||||||
|
currentUrl,
|
||||||
|
config.downloadOnFinish.folder ?? config.downloadFolder,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -438,7 +444,7 @@ async function iterableStreamToProcessedUint8Array(
|
|||||||
}),
|
}),
|
||||||
ratio,
|
ratio,
|
||||||
);
|
);
|
||||||
increasePlaylistProgress(0.15 + (ratio * 0.85));
|
increasePlaylistProgress(0.15 + ratio * 0.85);
|
||||||
});
|
});
|
||||||
|
|
||||||
const safeVideoNameWithExtension = `${safeVideoName}.${extension}`;
|
const safeVideoNameWithExtension = `${safeVideoName}.${extension}`;
|
||||||
@ -566,7 +572,13 @@ export async function downloadPlaylist(givenUrl?: string | URL) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!playlist || !playlist.items || playlist.items.length === 0 || !playlist.header || !('title' in playlist.header)) {
|
if (
|
||||||
|
!playlist ||
|
||||||
|
!playlist.items ||
|
||||||
|
playlist.items.length === 0 ||
|
||||||
|
!playlist.header ||
|
||||||
|
!('title' in playlist.header)
|
||||||
|
) {
|
||||||
sendError(
|
sendError(
|
||||||
new Error(t('plugins.downloader.backend.feedback.playlist-is-empty')),
|
new Error(t('plugins.downloader.backend.feedback.playlist-is-empty')),
|
||||||
);
|
);
|
||||||
@ -660,7 +672,7 @@ export async function downloadPlaylist(givenUrl?: string | URL) {
|
|||||||
|
|
||||||
const increaseProgress = (itemPercentage: number) => {
|
const increaseProgress = (itemPercentage: number) => {
|
||||||
const currentProgress = (counter - 1) / (items.length ?? 1);
|
const currentProgress = (counter - 1) / (items.length ?? 1);
|
||||||
const newProgress = currentProgress + (progressStep * itemPercentage);
|
const newProgress = currentProgress + progressStep * itemPercentage;
|
||||||
win.setProgressBar(newProgress);
|
win.setProgressBar(newProgress);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -35,7 +35,10 @@ export const onMenu = async ({
|
|||||||
click(item) {
|
click(item) {
|
||||||
setConfig({
|
setConfig({
|
||||||
downloadOnFinish: {
|
downloadOnFinish: {
|
||||||
...deepmerge(defaultConfig.downloadOnFinish, config.downloadOnFinish)!,
|
...deepmerge(
|
||||||
|
defaultConfig.downloadOnFinish,
|
||||||
|
config.downloadOnFinish,
|
||||||
|
)!,
|
||||||
enabled: item.checked,
|
enabled: item.checked,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@ -49,14 +52,19 @@ export const onMenu = async ({
|
|||||||
click() {
|
click() {
|
||||||
const result = dialog.showOpenDialogSync({
|
const result = dialog.showOpenDialogSync({
|
||||||
properties: ['openDirectory', 'createDirectory'],
|
properties: ['openDirectory', 'createDirectory'],
|
||||||
defaultPath: getFolder(config.downloadOnFinish?.folder ?? config.downloadFolder),
|
defaultPath: getFolder(
|
||||||
|
config.downloadOnFinish?.folder ?? config.downloadFolder,
|
||||||
|
),
|
||||||
});
|
});
|
||||||
if (result) {
|
if (result) {
|
||||||
setConfig({
|
setConfig({
|
||||||
downloadOnFinish: {
|
downloadOnFinish: {
|
||||||
...deepmerge(defaultConfig.downloadOnFinish, config.downloadOnFinish)!,
|
...deepmerge(
|
||||||
|
defaultConfig.downloadOnFinish,
|
||||||
|
config.downloadOnFinish,
|
||||||
|
)!,
|
||||||
folder: result[0],
|
folder: result[0],
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -76,7 +84,10 @@ export const onMenu = async ({
|
|||||||
click() {
|
click() {
|
||||||
setConfig({
|
setConfig({
|
||||||
downloadOnFinish: {
|
downloadOnFinish: {
|
||||||
...deepmerge(defaultConfig.downloadOnFinish, config.downloadOnFinish)!,
|
...deepmerge(
|
||||||
|
defaultConfig.downloadOnFinish,
|
||||||
|
config.downloadOnFinish,
|
||||||
|
)!,
|
||||||
mode: 'seconds',
|
mode: 'seconds',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@ -91,7 +102,10 @@ export const onMenu = async ({
|
|||||||
click() {
|
click() {
|
||||||
setConfig({
|
setConfig({
|
||||||
downloadOnFinish: {
|
downloadOnFinish: {
|
||||||
...deepmerge(defaultConfig.downloadOnFinish, config.downloadOnFinish)!,
|
...deepmerge(
|
||||||
|
defaultConfig.downloadOnFinish,
|
||||||
|
config.downloadOnFinish,
|
||||||
|
)!,
|
||||||
mode: 'percent',
|
mode: 'percent',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@ -120,7 +134,9 @@ export const onMenu = async ({
|
|||||||
min: '0',
|
min: '0',
|
||||||
step: '1',
|
step: '1',
|
||||||
},
|
},
|
||||||
value: config.downloadOnFinish?.seconds ?? defaultConfig.downloadOnFinish!.seconds,
|
value:
|
||||||
|
config.downloadOnFinish?.seconds ??
|
||||||
|
defaultConfig.downloadOnFinish!.seconds,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: t(
|
label: t(
|
||||||
@ -133,7 +149,9 @@ export const onMenu = async ({
|
|||||||
max: '100',
|
max: '100',
|
||||||
step: '1',
|
step: '1',
|
||||||
},
|
},
|
||||||
value: config.downloadOnFinish?.percent ?? defaultConfig.downloadOnFinish!.percent,
|
value:
|
||||||
|
config.downloadOnFinish?.percent ??
|
||||||
|
defaultConfig.downloadOnFinish!.percent,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
...promptOptions(),
|
...promptOptions(),
|
||||||
@ -147,7 +165,10 @@ export const onMenu = async ({
|
|||||||
|
|
||||||
setConfig({
|
setConfig({
|
||||||
downloadOnFinish: {
|
downloadOnFinish: {
|
||||||
...deepmerge(defaultConfig.downloadOnFinish, config.downloadOnFinish)!,
|
...deepmerge(
|
||||||
|
defaultConfig.downloadOnFinish,
|
||||||
|
config.downloadOnFinish,
|
||||||
|
)!,
|
||||||
seconds: Number(res[0]),
|
seconds: Number(res[0]),
|
||||||
percent: Number(res[1]),
|
percent: Number(res[1]),
|
||||||
},
|
},
|
||||||
|
|||||||
@ -39,7 +39,9 @@ const menuObserver = new MutationObserver(() => {
|
|||||||
if (!menuUrl?.includes('watch?')) {
|
if (!menuUrl?.includes('watch?')) {
|
||||||
menuUrl = undefined;
|
menuUrl = undefined;
|
||||||
// check for podcast
|
// check for podcast
|
||||||
for (const it of document.querySelectorAll('tp-yt-paper-listbox [tabindex="-1"] #navigation-endpoint')) {
|
for (const it of document.querySelectorAll(
|
||||||
|
'tp-yt-paper-listbox [tabindex="-1"] #navigation-endpoint',
|
||||||
|
)) {
|
||||||
if (it.getAttribute('href')?.includes('podcast/')) {
|
if (it.getAttribute('href')?.includes('podcast/')) {
|
||||||
menuUrl = it.getAttribute('href')!;
|
menuUrl = it.getAttribute('href')!;
|
||||||
break;
|
break;
|
||||||
@ -72,7 +74,9 @@ export const onRendererLoad = ({
|
|||||||
?.getAttribute('href');
|
?.getAttribute('href');
|
||||||
|
|
||||||
if (!videoUrl && songMenu) {
|
if (!videoUrl && songMenu) {
|
||||||
for (const it of songMenu.querySelectorAll('ytmusic-menu-navigation-item-renderer[tabindex="-1"] #navigation-endpoint')) {
|
for (const it of songMenu.querySelectorAll(
|
||||||
|
'ytmusic-menu-navigation-item-renderer[tabindex="-1"] #navigation-endpoint',
|
||||||
|
)) {
|
||||||
if (it.getAttribute('href')?.includes('podcast/')) {
|
if (it.getAttribute('href')?.includes('podcast/')) {
|
||||||
videoUrl = it.getAttribute('href');
|
videoUrl = it.getAttribute('href');
|
||||||
break;
|
break;
|
||||||
@ -86,7 +90,8 @@ export const onRendererLoad = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (videoUrl.startsWith('podcast/')) {
|
if (videoUrl.startsWith('podcast/')) {
|
||||||
videoUrl = defaultConfig.url + '/watch?' + videoUrl.replace('podcast/', 'v=');
|
videoUrl =
|
||||||
|
defaultConfig.url + '/watch?' + videoUrl.replace('podcast/', 'v=');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (videoUrl.includes('?playlist=')) {
|
if (videoUrl.includes('?playlist=')) {
|
||||||
|
|||||||
@ -4,24 +4,12 @@ export interface InAppMenuConfig {
|
|||||||
}
|
}
|
||||||
export const defaultInAppMenuConfig: InAppMenuConfig = {
|
export const defaultInAppMenuConfig: InAppMenuConfig = {
|
||||||
enabled:
|
enabled:
|
||||||
(
|
((typeof window !== 'undefined' &&
|
||||||
(
|
!window.navigator?.userAgent?.toLowerCase().includes('mac')) ||
|
||||||
typeof window !== 'undefined' &&
|
(typeof global !== 'undefined' &&
|
||||||
!window.navigator?.userAgent?.toLowerCase().includes('mac')
|
global.process?.platform !== 'darwin')) &&
|
||||||
) ||
|
((typeof window !== 'undefined' &&
|
||||||
(
|
!window.navigator?.userAgent?.toLowerCase().includes('linux')) ||
|
||||||
typeof global !== 'undefined' &&
|
(typeof global !== 'undefined' && global.process?.platform !== 'linux')),
|
||||||
global.process?.platform !== 'darwin'
|
|
||||||
)
|
|
||||||
) && (
|
|
||||||
(
|
|
||||||
typeof window !== 'undefined' &&
|
|
||||||
!window.navigator?.userAgent?.toLowerCase().includes('linux')
|
|
||||||
) ||
|
|
||||||
(
|
|
||||||
typeof global !== 'undefined' &&
|
|
||||||
global.process?.platform !== 'linux'
|
|
||||||
)
|
|
||||||
),
|
|
||||||
hideDOMWindowControls: false,
|
hideDOMWindowControls: false,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,6 +1,13 @@
|
|||||||
import { register } from 'electron-localshortcut';
|
import { register } from 'electron-localshortcut';
|
||||||
|
|
||||||
import { BrowserWindow, Menu, MenuItem, ipcMain, nativeImage } from 'electron';
|
import {
|
||||||
|
BrowserWindow,
|
||||||
|
Menu,
|
||||||
|
MenuItem,
|
||||||
|
ipcMain,
|
||||||
|
nativeImage,
|
||||||
|
WebContents,
|
||||||
|
} from 'electron';
|
||||||
|
|
||||||
import type { BackendContext } from '@/types/contexts';
|
import type { BackendContext } from '@/types/contexts';
|
||||||
import type { InAppMenuConfig } from './constants';
|
import type { InAppMenuConfig } from './constants';
|
||||||
@ -50,11 +57,13 @@ export const onMainLoad = ({
|
|||||||
ipcMain.handle('ytmd:menu-event', (event, commandId: number) => {
|
ipcMain.handle('ytmd:menu-event', (event, commandId: number) => {
|
||||||
const target = getMenuItemById(commandId);
|
const target = getMenuItemById(commandId);
|
||||||
if (target)
|
if (target)
|
||||||
target.click(
|
(
|
||||||
undefined,
|
target.click as (
|
||||||
BrowserWindow.fromWebContents(event.sender),
|
args0: unknown,
|
||||||
event.sender,
|
args1: BrowserWindow | null,
|
||||||
);
|
args3: WebContents,
|
||||||
|
) => void
|
||||||
|
)(undefined, BrowserWindow.fromWebContents(event.sender), event.sender);
|
||||||
});
|
});
|
||||||
|
|
||||||
handle('get-menu-by-id', (commandId: number) => {
|
handle('get-menu-by-id', (commandId: number) => {
|
||||||
|
|||||||
@ -16,8 +16,9 @@ const isMacOS = navigator.userAgent.includes('Macintosh');
|
|||||||
const isNotWindowsOrMacOS =
|
const isNotWindowsOrMacOS =
|
||||||
!navigator.userAgent.includes('Windows') && !isMacOS;
|
!navigator.userAgent.includes('Windows') && !isMacOS;
|
||||||
|
|
||||||
|
const [config, setConfig] = createSignal<InAppMenuConfig>(
|
||||||
const [config, setConfig] = createSignal<InAppMenuConfig>(defaultInAppMenuConfig);
|
defaultInAppMenuConfig,
|
||||||
|
);
|
||||||
export const onRendererLoad = async ({
|
export const onRendererLoad = async ({
|
||||||
getConfig,
|
getConfig,
|
||||||
ipc,
|
ipc,
|
||||||
@ -29,14 +30,19 @@ export const onRendererLoad = async ({
|
|||||||
stylesheet.replaceSync(scrollStyle);
|
stylesheet.replaceSync(scrollStyle);
|
||||||
document.adoptedStyleSheets = [...document.adoptedStyleSheets, stylesheet];
|
document.adoptedStyleSheets = [...document.adoptedStyleSheets, stylesheet];
|
||||||
|
|
||||||
render(() => (
|
render(
|
||||||
<TitleBar
|
() => (
|
||||||
ipc={ipc}
|
<TitleBar
|
||||||
isMacOS={isMacOS}
|
ipc={ipc}
|
||||||
enableController={isNotWindowsOrMacOS && !config().hideDOMWindowControls}
|
isMacOS={isMacOS}
|
||||||
initialCollapsed={window.mainConfig.get('options.hideMenu')}
|
enableController={
|
||||||
/>
|
isNotWindowsOrMacOS && !config().hideDOMWindowControls
|
||||||
), document.body);
|
}
|
||||||
|
initialCollapsed={window.mainConfig.get('options.hideMenu')}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
document.body,
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const onPlayerApiReady = () => {
|
export const onPlayerApiReady = () => {
|
||||||
|
|||||||
@ -3,36 +3,38 @@ import { css } from 'solid-styled-components';
|
|||||||
|
|
||||||
import { cacheNoArgs } from '@/providers/decorators';
|
import { cacheNoArgs } from '@/providers/decorators';
|
||||||
|
|
||||||
const iconButton = cacheNoArgs(() => css`
|
const iconButton = cacheNoArgs(
|
||||||
-webkit-app-region: none;
|
() => css`
|
||||||
|
-webkit-app-region: none;
|
||||||
|
|
||||||
background: transparent;
|
background: transparent;
|
||||||
|
|
||||||
width: 24px;
|
width: 24px;
|
||||||
height: 24px;
|
height: 24px;
|
||||||
|
|
||||||
padding: 2px;
|
padding: 2px;
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
color: white;
|
color: white;
|
||||||
|
|
||||||
outline: none;
|
outline: none;
|
||||||
border: none;
|
border: none;
|
||||||
|
|
||||||
transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1);
|
transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1);
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background: rgba(255, 255, 255, 0.1);
|
background: rgba(255, 255, 255, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
&:active {
|
&:active {
|
||||||
scale: 0.9;
|
scale: 0.9;
|
||||||
}
|
}
|
||||||
`);
|
`,
|
||||||
|
);
|
||||||
|
|
||||||
type CollapseIconButtonProps = JSX.HTMLAttributes<HTMLButtonElement>;
|
type CollapseIconButtonProps = JSX.HTMLAttributes<HTMLButtonElement>;
|
||||||
export const IconButton = (props: CollapseIconButtonProps) => {
|
export const IconButton = (props: CollapseIconButtonProps) => {
|
||||||
|
|||||||
@ -3,31 +3,33 @@ import { css } from 'solid-styled-components';
|
|||||||
|
|
||||||
import { cacheNoArgs } from '@/providers/decorators';
|
import { cacheNoArgs } from '@/providers/decorators';
|
||||||
|
|
||||||
const menuStyle = cacheNoArgs(() => css`
|
const menuStyle = cacheNoArgs(
|
||||||
-webkit-app-region: none;
|
() => css`
|
||||||
|
-webkit-app-region: none;
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
align-self: stretch;
|
align-self: stretch;
|
||||||
|
|
||||||
padding: 2px 8px;
|
padding: 2px 8px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1);
|
transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1);
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: rgba(255, 255, 255, 0.1);
|
background-color: rgba(255, 255, 255, 0.1);
|
||||||
}
|
}
|
||||||
&:active {
|
&:active {
|
||||||
scale: 0.9;
|
scale: 0.9;
|
||||||
}
|
}
|
||||||
|
|
||||||
&[data-selected="true"] {
|
&[data-selected='true'] {
|
||||||
background-color: rgba(255, 255, 255, 0.2);
|
background-color: rgba(255, 255, 255, 0.2);
|
||||||
}
|
}
|
||||||
`);
|
`,
|
||||||
|
);
|
||||||
|
|
||||||
export type MenuButtonProps = JSX.HTMLAttributes<HTMLLIElement> & {
|
export type MenuButtonProps = JSX.HTMLAttributes<HTMLLIElement> & {
|
||||||
text?: string;
|
text?: string;
|
||||||
|
|||||||
@ -2,39 +2,48 @@ import { createSignal, JSX, Show, splitProps } from 'solid-js';
|
|||||||
import { mergeProps, Portal } from 'solid-js/web';
|
import { mergeProps, Portal } from 'solid-js/web';
|
||||||
import { css } from 'solid-styled-components';
|
import { css } from 'solid-styled-components';
|
||||||
import { Transition } from 'solid-transition-group';
|
import { Transition } from 'solid-transition-group';
|
||||||
import { autoUpdate, flip, offset, OffsetOptions, size } from '@floating-ui/dom';
|
import {
|
||||||
|
autoUpdate,
|
||||||
|
flip,
|
||||||
|
offset,
|
||||||
|
OffsetOptions,
|
||||||
|
size,
|
||||||
|
} from '@floating-ui/dom';
|
||||||
import { useFloating } from 'solid-floating-ui';
|
import { useFloating } from 'solid-floating-ui';
|
||||||
|
|
||||||
import { cacheNoArgs } from '@/providers/decorators';
|
import { cacheNoArgs } from '@/providers/decorators';
|
||||||
|
|
||||||
const panelStyle = cacheNoArgs(() => css`
|
const panelStyle = cacheNoArgs(
|
||||||
position: fixed;
|
() => css`
|
||||||
top: var(--offset-y, 0);
|
position: fixed;
|
||||||
left: var(--offset-x, 0);
|
top: var(--offset-y, 0);
|
||||||
|
left: var(--offset-x, 0);
|
||||||
|
|
||||||
max-width: var(--max-width, 100%);
|
max-width: var(--max-width, 100%);
|
||||||
max-height: var(--max-height, 100%);
|
max-height: var(--max-height, 100%);
|
||||||
|
|
||||||
z-index: 10000;
|
z-index: 10000;
|
||||||
width: fit-content;
|
width: fit-content;
|
||||||
height: fit-content;
|
height: fit-content;
|
||||||
|
|
||||||
padding: 4px;
|
padding: 4px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
|
|
||||||
background-color: color-mix(
|
background-color: color-mix(
|
||||||
in srgb,
|
in srgb,
|
||||||
var(--titlebar-background-color, #030303) 50%,
|
var(--titlebar-background-color, #030303) 50%,
|
||||||
rgba(0, 0, 0, 0.1)
|
rgba(0, 0, 0, 0.1)
|
||||||
);
|
);
|
||||||
backdrop-filter: blur(8px);
|
backdrop-filter: blur(8px);
|
||||||
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.05),
|
box-shadow:
|
||||||
0 2px 8px rgba(0, 0, 0, 0.2);
|
0 0 0 1px rgba(0, 0, 0, 0.05),
|
||||||
|
0 2px 8px rgba(0, 0, 0, 0.2);
|
||||||
|
|
||||||
transform-origin: var(--origin-x, 50%) var(--origin-y, 50%);
|
transform-origin: var(--origin-x, 50%) var(--origin-y, 50%);
|
||||||
`);
|
`,
|
||||||
|
);
|
||||||
|
|
||||||
const animationStyle = cacheNoArgs(() => ({
|
const animationStyle = cacheNoArgs(() => ({
|
||||||
enter: css`
|
enter: css`
|
||||||
@ -42,19 +51,23 @@ const animationStyle = cacheNoArgs(() => ({
|
|||||||
transform: scale(0.9);
|
transform: scale(0.9);
|
||||||
`,
|
`,
|
||||||
enterActive: css`
|
enterActive: css`
|
||||||
transition: opacity 0.225s cubic-bezier(0.33, 1, 0.68, 1), transform 0.225s cubic-bezier(0.33, 1, 0.68, 1);
|
transition:
|
||||||
|
opacity 0.225s cubic-bezier(0.33, 1, 0.68, 1),
|
||||||
|
transform 0.225s cubic-bezier(0.33, 1, 0.68, 1);
|
||||||
`,
|
`,
|
||||||
exitTo: css`
|
exitTo: css`
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: scale(0.9);
|
transform: scale(0.9);
|
||||||
`,
|
`,
|
||||||
exitActive: css`
|
exitActive: css`
|
||||||
transition: opacity 0.225s cubic-bezier(0.32, 0, 0.67, 0), transform 0.225s cubic-bezier(0.32, 0, 0.67, 0);
|
transition:
|
||||||
|
opacity 0.225s cubic-bezier(0.32, 0, 0.67, 0),
|
||||||
|
transform 0.225s cubic-bezier(0.32, 0, 0.67, 0);
|
||||||
`,
|
`,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export type Placement =
|
export type Placement =
|
||||||
'top'
|
| 'top'
|
||||||
| 'bottom'
|
| 'bottom'
|
||||||
| 'left'
|
| 'left'
|
||||||
| 'right'
|
| 'right'
|
||||||
@ -92,9 +105,15 @@ export const Panel = (props: PanelProps) => {
|
|||||||
size({
|
size({
|
||||||
padding: 8,
|
padding: 8,
|
||||||
apply({ elements, availableWidth, availableHeight }) {
|
apply({ elements, availableWidth, availableHeight }) {
|
||||||
elements.floating.style.setProperty('--max-width', `${Math.max(200, availableWidth)}px`);
|
elements.floating.style.setProperty(
|
||||||
elements.floating.style.setProperty('--max-height', `${Math.max(200, availableHeight)}px`);
|
'--max-width',
|
||||||
}
|
`${Math.max(200, availableWidth)}px`,
|
||||||
|
);
|
||||||
|
elements.floating.style.setProperty(
|
||||||
|
'--max-height',
|
||||||
|
`${Math.max(200, availableHeight)}px`,
|
||||||
|
);
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
flip({ fallbackStrategy: 'initialPlacement' }),
|
flip({ fallbackStrategy: 'initialPlacement' }),
|
||||||
],
|
],
|
||||||
@ -103,7 +122,10 @@ export const Panel = (props: PanelProps) => {
|
|||||||
const originX = () => {
|
const originX = () => {
|
||||||
if (position.placement.includes('left')) return '100%';
|
if (position.placement.includes('left')) return '100%';
|
||||||
if (position.placement.includes('right')) return '0';
|
if (position.placement.includes('right')) return '0';
|
||||||
if (position.placement.includes('top') || position.placement.includes('bottom')) {
|
if (
|
||||||
|
position.placement.includes('top') ||
|
||||||
|
position.placement.includes('bottom')
|
||||||
|
) {
|
||||||
if (position.placement.includes('start')) return '0';
|
if (position.placement.includes('start')) return '0';
|
||||||
if (position.placement.includes('end')) return '100%';
|
if (position.placement.includes('end')) return '100%';
|
||||||
}
|
}
|
||||||
@ -113,7 +135,10 @@ export const Panel = (props: PanelProps) => {
|
|||||||
const originY = () => {
|
const originY = () => {
|
||||||
if (position.placement.includes('top')) return '100%';
|
if (position.placement.includes('top')) return '100%';
|
||||||
if (position.placement.includes('bottom')) return '0';
|
if (position.placement.includes('bottom')) return '0';
|
||||||
if (position.placement.includes('left') || position.placement.includes('right')) {
|
if (
|
||||||
|
position.placement.includes('left') ||
|
||||||
|
position.placement.includes('right')
|
||||||
|
) {
|
||||||
if (position.placement.includes('start')) return '0';
|
if (position.placement.includes('start')) return '0';
|
||||||
if (position.placement.includes('end')) return '100%';
|
if (position.placement.includes('end')) return '100%';
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,100 +10,111 @@ import { autoUpdate, offset, size } from '@floating-ui/dom';
|
|||||||
import { Panel } from './Panel';
|
import { Panel } from './Panel';
|
||||||
import { cacheNoArgs } from '@/providers/decorators';
|
import { cacheNoArgs } from '@/providers/decorators';
|
||||||
|
|
||||||
const itemStyle = cacheNoArgs(() => css`
|
const itemStyle = cacheNoArgs(
|
||||||
position: relative;
|
() => css`
|
||||||
|
position: relative;
|
||||||
|
|
||||||
-webkit-app-region: none;
|
-webkit-app-region: none;
|
||||||
min-height: 32px;
|
min-height: 32px;
|
||||||
height: 32px;
|
height: 32px;
|
||||||
|
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 32px 1fr auto minmax(32px, auto);
|
grid-template-columns: 32px 1fr auto minmax(32px, auto);
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
box-sizing: border-box;
|
|
||||||
user-select: none;
|
|
||||||
-webkit-user-drag: none;
|
|
||||||
|
|
||||||
transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1);
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: rgba(255, 255, 255, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:active {
|
|
||||||
background-color: rgba(255, 255, 255, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
&[data-selected="true"] {
|
|
||||||
background-color: rgba(255, 255, 255, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
& * {
|
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
user-select: none;
|
||||||
`);
|
-webkit-user-drag: none;
|
||||||
|
|
||||||
const itemIconStyle = cacheNoArgs(() => css`
|
transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1);
|
||||||
height: 32px;
|
|
||||||
padding: 4px;
|
|
||||||
color: white;
|
|
||||||
`);
|
|
||||||
|
|
||||||
const itemLabelStyle = cacheNoArgs(() => css`
|
&:hover {
|
||||||
font-size: 12px;
|
background-color: rgba(255, 255, 255, 0.1);
|
||||||
color: white;
|
}
|
||||||
`);
|
|
||||||
|
|
||||||
const itemChipStyle = cacheNoArgs(() => css`
|
&:active {
|
||||||
display: flex;
|
background-color: rgba(255, 255, 255, 0.2);
|
||||||
justify-content: center;
|
}
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
min-width: 16px;
|
&[data-selected='true'] {
|
||||||
height: 16px;
|
background-color: rgba(255, 255, 255, 0.2);
|
||||||
padding: 0 4px;
|
}
|
||||||
margin-left: 8px;
|
|
||||||
|
|
||||||
border-radius: 4px;
|
& * {
|
||||||
background-color: rgba(255, 255, 255, 0.2);
|
box-sizing: border-box;
|
||||||
color: #f1f1f1;
|
}
|
||||||
font-size: 10px;
|
`,
|
||||||
font-weight: 500;
|
);
|
||||||
line-height: 1;
|
|
||||||
`);
|
|
||||||
|
|
||||||
const toolTipStyle = cacheNoArgs(() => css`
|
const itemIconStyle = cacheNoArgs(
|
||||||
min-width: 32px;
|
() => css`
|
||||||
width: 100%;
|
height: 32px;
|
||||||
height: 100%;
|
padding: 4px;
|
||||||
|
color: white;
|
||||||
|
`,
|
||||||
|
);
|
||||||
|
|
||||||
padding: 4px;
|
const itemLabelStyle = cacheNoArgs(
|
||||||
|
() => css`
|
||||||
|
font-size: 12px;
|
||||||
|
color: white;
|
||||||
|
`,
|
||||||
|
);
|
||||||
|
|
||||||
max-width: calc(var(--max-width, 100%) - 8px);
|
const itemChipStyle = cacheNoArgs(
|
||||||
max-height: calc(var(--max-height, 100%) - 8px);
|
() => css`
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
border-radius: 4px;
|
min-width: 16px;
|
||||||
background-color: rgba(25, 25, 25, 0.8);
|
height: 16px;
|
||||||
color: #f1f1f1;
|
padding: 0 4px;
|
||||||
font-size: 10px;
|
margin-left: 8px;
|
||||||
`);
|
|
||||||
|
|
||||||
const popupStyle = cacheNoArgs(() => css`
|
border-radius: 4px;
|
||||||
position: fixed;
|
background-color: rgba(255, 255, 255, 0.2);
|
||||||
top: var(--offset-y, 0);
|
color: #f1f1f1;
|
||||||
left: var(--offset-x, 0);
|
font-size: 10px;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 1;
|
||||||
|
`,
|
||||||
|
);
|
||||||
|
|
||||||
max-width: var(--max-width, 100%);
|
const toolTipStyle = cacheNoArgs(
|
||||||
max-height: var(--max-height, 100%);
|
() => css`
|
||||||
|
min-width: 32px;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
z-index: 100000000;
|
padding: 4px;
|
||||||
pointer-events: none;
|
|
||||||
|
|
||||||
`);
|
max-width: calc(var(--max-width, 100%) - 8px);
|
||||||
|
max-height: calc(var(--max-height, 100%) - 8px);
|
||||||
|
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: rgba(25, 25, 25, 0.8);
|
||||||
|
color: #f1f1f1;
|
||||||
|
font-size: 10px;
|
||||||
|
`,
|
||||||
|
);
|
||||||
|
|
||||||
|
const popupStyle = cacheNoArgs(
|
||||||
|
() => css`
|
||||||
|
position: fixed;
|
||||||
|
top: var(--offset-y, 0);
|
||||||
|
left: var(--offset-x, 0);
|
||||||
|
|
||||||
|
max-width: var(--max-width, 100%);
|
||||||
|
max-height: var(--max-height, 100%);
|
||||||
|
|
||||||
|
z-index: 100000000;
|
||||||
|
pointer-events: none;
|
||||||
|
`,
|
||||||
|
);
|
||||||
|
|
||||||
const animationStyle = cacheNoArgs(() => ({
|
const animationStyle = cacheNoArgs(() => ({
|
||||||
enter: css`
|
enter: css`
|
||||||
@ -111,14 +122,18 @@ const animationStyle = cacheNoArgs(() => ({
|
|||||||
transform: scale(0.9);
|
transform: scale(0.9);
|
||||||
`,
|
`,
|
||||||
enterActive: css`
|
enterActive: css`
|
||||||
transition: opacity 0.225s cubic-bezier(0.33, 1, 0.68, 1), transform 0.225s cubic-bezier(0.33, 1, 0.68, 1);
|
transition:
|
||||||
|
opacity 0.225s cubic-bezier(0.33, 1, 0.68, 1),
|
||||||
|
transform 0.225s cubic-bezier(0.33, 1, 0.68, 1);
|
||||||
`,
|
`,
|
||||||
exitTo: css`
|
exitTo: css`
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: scale(0.9);
|
transform: scale(0.9);
|
||||||
`,
|
`,
|
||||||
exitActive: css`
|
exitActive: css`
|
||||||
transition: opacity 0.225s cubic-bezier(0.32, 0, 0.67, 0), transform 0.225s cubic-bezier(0.32, 0, 0.67, 0);
|
transition:
|
||||||
|
opacity 0.225s cubic-bezier(0.32, 0, 0.67, 0),
|
||||||
|
transform 0.225s cubic-bezier(0.32, 0, 0.67, 0);
|
||||||
`,
|
`,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@ -160,7 +175,11 @@ type CheckboxPanelItemProps = BasePanelItemProps & {
|
|||||||
checked: boolean;
|
checked: boolean;
|
||||||
onChange?: (checked: boolean) => void;
|
onChange?: (checked: boolean) => void;
|
||||||
};
|
};
|
||||||
export type PanelItemProps = NormalPanelItemProps | SubmenuItemProps | RadioPanelItemProps | CheckboxPanelItemProps;
|
export type PanelItemProps =
|
||||||
|
| NormalPanelItemProps
|
||||||
|
| SubmenuItemProps
|
||||||
|
| RadioPanelItemProps
|
||||||
|
| CheckboxPanelItemProps;
|
||||||
export const PanelItem = (props: PanelItemProps) => {
|
export const PanelItem = (props: PanelItemProps) => {
|
||||||
const [open, setOpen] = createSignal(false);
|
const [open, setOpen] = createSignal(false);
|
||||||
const [toolTipOpen, setToolTipOpen] = createSignal(false);
|
const [toolTipOpen, setToolTipOpen] = createSignal(false);
|
||||||
@ -176,17 +195,24 @@ export const PanelItem = (props: PanelItemProps) => {
|
|||||||
offset({ mainAxis: 8 }),
|
offset({ mainAxis: 8 }),
|
||||||
size({
|
size({
|
||||||
apply({ rects, elements }) {
|
apply({ rects, elements }) {
|
||||||
elements.floating.style.setProperty('--max-width', `${rects.reference.width}px`);
|
elements.floating.style.setProperty(
|
||||||
}
|
'--max-width',
|
||||||
|
`${rects.reference.width}px`,
|
||||||
|
);
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleHover = (event: MouseEvent) => {
|
const handleHover = (event: MouseEvent) => {
|
||||||
setToolTipOpen(true);
|
setToolTipOpen(true);
|
||||||
event.target?.addEventListener('mouseleave', () => {
|
event.target?.addEventListener(
|
||||||
setToolTipOpen(false);
|
'mouseleave',
|
||||||
}, { once: true });
|
() => {
|
||||||
|
setToolTipOpen(false);
|
||||||
|
},
|
||||||
|
{ once: true },
|
||||||
|
);
|
||||||
|
|
||||||
if (props.type === 'submenu') {
|
if (props.type === 'submenu') {
|
||||||
const timer = setTimeout(() => {
|
const timer = setTimeout(() => {
|
||||||
@ -200,36 +226,54 @@ export const PanelItem = (props: PanelItemProps) => {
|
|||||||
};
|
};
|
||||||
document.addEventListener('mousemove', onMouseMove);
|
document.addEventListener('mousemove', onMouseMove);
|
||||||
|
|
||||||
event.target?.addEventListener('mouseleave', () => {
|
event.target?.addEventListener(
|
||||||
setTimeout(() => {
|
'mouseleave',
|
||||||
document.removeEventListener('mousemove', onMouseMove);
|
() => {
|
||||||
const parents = getParents(document.elementFromPoint(mouseX, mouseY));
|
setTimeout(() => {
|
||||||
|
document.removeEventListener('mousemove', onMouseMove);
|
||||||
|
const parents = getParents(
|
||||||
|
document.elementFromPoint(mouseX, mouseY),
|
||||||
|
);
|
||||||
|
|
||||||
if (!parents.includes(child())) {
|
if (!parents.includes(child())) {
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
} else {
|
} else {
|
||||||
const onOtherHover = (event: MouseEvent) => {
|
const onOtherHover = (event: MouseEvent) => {
|
||||||
const parents = getParents(event.target as HTMLElement);
|
const parents = getParents(event.target as HTMLElement);
|
||||||
const closestLevel = parents.find((it) => it?.dataset?.level)?.dataset.level ?? '';
|
const closestLevel =
|
||||||
const path = event.composedPath();
|
parents.find((it) => it?.dataset?.level)?.dataset.level ??
|
||||||
|
'';
|
||||||
|
const path = event.composedPath();
|
||||||
|
|
||||||
const isOtherItem = path.some((it) => it instanceof HTMLElement && it.classList.contains(itemStyle()));
|
const isOtherItem = path.some(
|
||||||
const isChild = closestLevel.startsWith(props.level.join('/'));
|
(it) =>
|
||||||
|
it instanceof HTMLElement &&
|
||||||
|
it.classList.contains(itemStyle()),
|
||||||
|
);
|
||||||
|
const isChild = closestLevel.startsWith(
|
||||||
|
props.level.join('/'),
|
||||||
|
);
|
||||||
|
|
||||||
if (isOtherItem && !isChild) {
|
if (isOtherItem && !isChild) {
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
document.removeEventListener('mousemove', onOtherHover);
|
document.removeEventListener('mousemove', onOtherHover);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
document.addEventListener('mousemove', onOtherHover);
|
document.addEventListener('mousemove', onOtherHover);
|
||||||
}
|
}
|
||||||
}, 225);
|
}, 225);
|
||||||
}, { once: true });
|
},
|
||||||
|
{ once: true },
|
||||||
|
);
|
||||||
}, 225);
|
}, 225);
|
||||||
|
|
||||||
event.target?.addEventListener('mouseleave', () => {
|
event.target?.addEventListener(
|
||||||
clearTimeout(timer);
|
'mouseleave',
|
||||||
}, { once: true });
|
() => {
|
||||||
|
clearTimeout(timer);
|
||||||
|
},
|
||||||
|
{ once: true },
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -244,7 +288,6 @@ export const PanelItem = (props: PanelItemProps) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li
|
<li
|
||||||
ref={setAnchor}
|
ref={setAnchor}
|
||||||
@ -253,45 +296,66 @@ export const PanelItem = (props: PanelItemProps) => {
|
|||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
data-selected={open()}
|
data-selected={open()}
|
||||||
>
|
>
|
||||||
<Switch fallback={<div class={itemIconStyle()}/>}>
|
<Switch fallback={<div class={itemIconStyle()} />}>
|
||||||
<Match when={props.type === 'checkbox' && props.checked}>
|
<Match when={props.type === 'checkbox' && props.checked}>
|
||||||
<svg class={itemIconStyle()} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="1.5"
|
<svg
|
||||||
stroke="currentColor" fill="none"
|
class={itemIconStyle()}
|
||||||
stroke-linecap="round" stroke-linejoin="round">
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
viewBox="0 0 24 24"
|
||||||
<path d="M5 12l5 5l10 -10"/>
|
stroke-width="1.5"
|
||||||
|
stroke="currentColor"
|
||||||
|
fill="none"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||||
|
<path d="M5 12l5 5l10 -10" />
|
||||||
</svg>
|
</svg>
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={props.type === 'radio' && props.checked}>
|
<Match when={props.type === 'radio' && props.checked}>
|
||||||
<svg class={itemIconStyle()} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"
|
<svg
|
||||||
style={{ padding: '6px' }}>
|
class={itemIconStyle()}
|
||||||
<path fill="currentColor"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
d="M10,5 C7.2,5 5,7.2 5,10 C5,12.8 7.2,15 10,15 C12.8,15 15,12.8 15,10 C15,7.2 12.8,5 10,5 L10,5 Z M10,0 C4.5,0 0,4.5 0,10 C0,15.5 4.5,20 10,20 C15.5,20 20,15.5 20,10 C20,4.5 15.5,0 10,0 L10,0 Z M10,18 C5.6,18 2,14.4 2,10 C2,5.6 5.6,2 10,2 C14.4,2 18,5.6 18,10 C18,14.4 14.4,18 10,18 L10,18 Z"/>
|
viewBox="0 0 20 20"
|
||||||
|
style={{ padding: '6px' }}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill="currentColor"
|
||||||
|
d="M10,5 C7.2,5 5,7.2 5,10 C5,12.8 7.2,15 10,15 C12.8,15 15,12.8 15,10 C15,7.2 12.8,5 10,5 L10,5 Z M10,0 C4.5,0 0,4.5 0,10 C0,15.5 4.5,20 10,20 C15.5,20 20,15.5 20,10 C20,4.5 15.5,0 10,0 L10,0 Z M10,18 C5.6,18 2,14.4 2,10 C2,5.6 5.6,2 10,2 C14.4,2 18,5.6 18,10 C18,14.4 14.4,18 10,18 L10,18 Z"
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={props.type === 'radio' && !props.checked}>
|
<Match when={props.type === 'radio' && !props.checked}>
|
||||||
<svg class={itemIconStyle()} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"
|
<svg
|
||||||
style={{ padding: '6px' }}>
|
class={itemIconStyle()}
|
||||||
<path fill="currentColor"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
d="M10,0 C4.5,0 0,4.5 0,10 C0,15.5 4.5,20 10,20 C15.5,20 20,15.5 20,10 C20,4.5 15.5,0 10,0 L10,0 Z M10,18 C5.6,18 2,14.4 2,10 C2,5.6 5.6,2 10,2 C14.4,2 18,5.6 18,10 C18,14.4 14.4,18 10,18 L10,18 Z"/>
|
viewBox="0 0 20 20"
|
||||||
|
style={{ padding: '6px' }}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill="currentColor"
|
||||||
|
d="M10,0 C4.5,0 0,4.5 0,10 C0,15.5 4.5,20 10,20 C15.5,20 20,15.5 20,10 C20,4.5 15.5,0 10,0 L10,0 Z M10,18 C5.6,18 2,14.4 2,10 C2,5.6 5.6,2 10,2 C14.4,2 18,5.6 18,10 C18,14.4 14.4,18 10,18 L10,18 Z"
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</Match>
|
</Match>
|
||||||
</Switch>
|
</Switch>
|
||||||
<span class={itemLabelStyle()}>
|
<span class={itemLabelStyle()}>{props.name}</span>
|
||||||
{props.name}
|
<Show when={props.chip} fallback={<div />}>
|
||||||
</span>
|
<span class={itemChipStyle()}>{props.chip}</span>
|
||||||
<Show when={props.chip} fallback={<div/>}>
|
|
||||||
<span class={itemChipStyle()}>
|
|
||||||
{props.chip}
|
|
||||||
</span>
|
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={props.type === 'submenu'}>
|
<Show when={props.type === 'submenu'}>
|
||||||
<svg class={itemIconStyle()} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="1.5"
|
<svg
|
||||||
stroke="currentColor"
|
class={itemIconStyle()}
|
||||||
fill="none"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
stroke-linecap="round" stroke-linejoin="round">
|
viewBox="0 0 24 24"
|
||||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
stroke-width="1.5"
|
||||||
<polyline points="9 6 15 12 9 18"/>
|
stroke="currentColor"
|
||||||
|
fill="none"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||||
|
<polyline points="9 6 15 12 9 18" />
|
||||||
</svg>
|
</svg>
|
||||||
<Panel
|
<Panel
|
||||||
ref={setChild}
|
ref={setChild}
|
||||||
@ -322,9 +386,7 @@ export const PanelItem = (props: PanelItemProps) => {
|
|||||||
exitActiveClass={animationStyle().exitActive}
|
exitActiveClass={animationStyle().exitActive}
|
||||||
>
|
>
|
||||||
<Show when={toolTipOpen()}>
|
<Show when={toolTipOpen()}>
|
||||||
<div class={toolTipStyle()}>
|
<div class={toolTipStyle()}>{props.toolTip}</div>
|
||||||
{props.toolTip}
|
|
||||||
</div>
|
|
||||||
</Show>
|
</Show>
|
||||||
</Transition>
|
</Transition>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,5 +1,15 @@
|
|||||||
import { Menu, MenuItem } from 'electron';
|
import { Menu, MenuItem } from 'electron';
|
||||||
import { createEffect, createResource, createSignal, Index, Match, onCleanup, onMount, Show, Switch } from 'solid-js';
|
import {
|
||||||
|
createEffect,
|
||||||
|
createResource,
|
||||||
|
createSignal,
|
||||||
|
Index,
|
||||||
|
Match,
|
||||||
|
onCleanup,
|
||||||
|
onMount,
|
||||||
|
Show,
|
||||||
|
Switch,
|
||||||
|
} from 'solid-js';
|
||||||
import { css } from 'solid-styled-components';
|
import { css } from 'solid-styled-components';
|
||||||
import { TransitionGroup } from 'solid-transition-group';
|
import { TransitionGroup } from 'solid-transition-group';
|
||||||
|
|
||||||
@ -14,49 +24,55 @@ import { cacheNoArgs } from '@/providers/decorators';
|
|||||||
import type { RendererContext } from '@/types/contexts';
|
import type { RendererContext } from '@/types/contexts';
|
||||||
import type { InAppMenuConfig } from '../constants';
|
import type { InAppMenuConfig } from '../constants';
|
||||||
|
|
||||||
const titleStyle = cacheNoArgs(() => css`
|
const titleStyle = cacheNoArgs(
|
||||||
-webkit-app-region: drag;
|
() => css`
|
||||||
box-sizing: border-box;
|
-webkit-app-region: drag;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
z-index: 10000000;
|
z-index: 10000000;
|
||||||
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: var(--menu-bar-height, 32px);
|
height: var(--menu-bar-height, 32px);
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-flow: row;
|
flex-flow: row;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
|
|
||||||
color: #f1f1f1;
|
color: #f1f1f1;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
padding: 4px 4px 4px var(--offset-left, 4px);
|
padding: 4px 4px 4px var(--offset-left, 4px);
|
||||||
background-color: var(--titlebar-background-color, #030303);
|
background-color: var(--titlebar-background-color, #030303);
|
||||||
user-select: none;
|
user-select: none;
|
||||||
|
|
||||||
transition: opacity 200ms ease 0s,
|
transition:
|
||||||
transform 300ms cubic-bezier(0.2, 0, 0.6, 1) 0s,
|
opacity 200ms ease 0s,
|
||||||
background-color 300ms cubic-bezier(0.2, 0, 0.6, 1) 0s;
|
transform 300ms cubic-bezier(0.2, 0, 0.6, 1) 0s,
|
||||||
|
background-color 300ms cubic-bezier(0.2, 0, 0.6, 1) 0s;
|
||||||
|
|
||||||
&[data-macos="true"] {
|
&[data-macos='true'] {
|
||||||
padding: 4px 4px 4px 74px;
|
padding: 4px 4px 4px 74px;
|
||||||
}
|
}
|
||||||
|
|
||||||
ytmusic-app:has(ytmusic-player[player-ui-state=FULLSCREEN]) ~ &:not([data-show="true"]) {
|
ytmusic-app:has(ytmusic-player[player-ui-state='FULLSCREEN'])
|
||||||
transform: translateY(calc(-1 * var(--menu-bar-height, 32px)));
|
~ &:not([data-show='true']) {
|
||||||
}
|
transform: translateY(calc(-1 * var(--menu-bar-height, 32px)));
|
||||||
`);
|
}
|
||||||
|
`,
|
||||||
|
);
|
||||||
|
|
||||||
const separatorStyle = cacheNoArgs(() => css`
|
const separatorStyle = cacheNoArgs(
|
||||||
min-height: 1px;
|
() => css`
|
||||||
height: 1px;
|
min-height: 1px;
|
||||||
margin: 4px 0;
|
height: 1px;
|
||||||
|
margin: 4px 0;
|
||||||
|
|
||||||
background-color: rgba(255, 255, 255, 0.2);
|
background-color: rgba(255, 255, 255, 0.2);
|
||||||
`);
|
`,
|
||||||
|
);
|
||||||
|
|
||||||
const animationStyle = cacheNoArgs(() => ({
|
const animationStyle = cacheNoArgs(() => ({
|
||||||
enter: css`
|
enter: css`
|
||||||
@ -64,14 +80,18 @@ const animationStyle = cacheNoArgs(() => ({
|
|||||||
transform: translateX(-50%) scale(0.8);
|
transform: translateX(-50%) scale(0.8);
|
||||||
`,
|
`,
|
||||||
enterActive: css`
|
enterActive: css`
|
||||||
transition: opacity 0.1s cubic-bezier(0.33, 1, 0.68, 1), transform 0.1s cubic-bezier(0.33, 1, 0.68, 1);
|
transition:
|
||||||
|
opacity 0.1s cubic-bezier(0.33, 1, 0.68, 1),
|
||||||
|
transform 0.1s cubic-bezier(0.33, 1, 0.68, 1);
|
||||||
`,
|
`,
|
||||||
exitTo: css`
|
exitTo: css`
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: translateX(-50%) scale(0.8);
|
transform: translateX(-50%) scale(0.8);
|
||||||
`,
|
`,
|
||||||
exitActive: css`
|
exitActive: css`
|
||||||
transition: opacity 0.1s cubic-bezier(0.32, 0, 0.67, 0), transform 0.1s cubic-bezier(0.32, 0, 0.67, 0);
|
transition:
|
||||||
|
opacity 0.1s cubic-bezier(0.32, 0, 0.67, 0),
|
||||||
|
transform 0.1s cubic-bezier(0.32, 0, 0.67, 0);
|
||||||
`,
|
`,
|
||||||
move: css`
|
move: css`
|
||||||
transition: all 0.1s cubic-bezier(0.65, 0, 0.35, 1);
|
transition: all 0.1s cubic-bezier(0.65, 0, 0.35, 1);
|
||||||
@ -89,7 +109,7 @@ export type PanelRendererProps = {
|
|||||||
items: Electron.Menu['items'];
|
items: Electron.Menu['items'];
|
||||||
level?: number[];
|
level?: number[];
|
||||||
onClick?: (commandId: number, radioGroup?: MenuItem[]) => void;
|
onClick?: (commandId: number, radioGroup?: MenuItem[]) => void;
|
||||||
}
|
};
|
||||||
const PanelRenderer = (props: PanelRendererProps) => {
|
const PanelRenderer = (props: PanelRendererProps) => {
|
||||||
const radioGroup = () => props.items.filter((it) => it.type === 'radio');
|
const radioGroup = () => props.items.filter((it) => it.type === 'radio');
|
||||||
|
|
||||||
@ -114,12 +134,12 @@ const PanelRenderer = (props: PanelRendererProps) => {
|
|||||||
name={subItem().label}
|
name={subItem().label}
|
||||||
chip={subItem().sublabel}
|
chip={subItem().sublabel}
|
||||||
toolTip={subItem().toolTip}
|
toolTip={subItem().toolTip}
|
||||||
level={[...props.level ?? [], subItem().commandId]}
|
level={[...(props.level ?? []), subItem().commandId]}
|
||||||
commandId={subItem().commandId}
|
commandId={subItem().commandId}
|
||||||
>
|
>
|
||||||
<PanelRenderer
|
<PanelRenderer
|
||||||
items={subItem().submenu?.items ?? []}
|
items={subItem().submenu?.items ?? []}
|
||||||
level={[...props.level ?? [], subItem().commandId]}
|
level={[...(props.level ?? []), subItem().commandId]}
|
||||||
onClick={props.onClick}
|
onClick={props.onClick}
|
||||||
/>
|
/>
|
||||||
</PanelItem>
|
</PanelItem>
|
||||||
@ -143,11 +163,13 @@ const PanelRenderer = (props: PanelRendererProps) => {
|
|||||||
chip={subItem().sublabel}
|
chip={subItem().sublabel}
|
||||||
toolTip={subItem().toolTip}
|
toolTip={subItem().toolTip}
|
||||||
commandId={subItem().commandId}
|
commandId={subItem().commandId}
|
||||||
onChange={() => props.onClick?.(subItem().commandId, radioGroup())}
|
onChange={() =>
|
||||||
|
props.onClick?.(subItem().commandId, radioGroup())
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={subItem().type === 'separator'}>
|
<Match when={subItem().type === 'separator'}>
|
||||||
<hr class={separatorStyle()}/>
|
<hr class={separatorStyle()} />
|
||||||
</Match>
|
</Match>
|
||||||
</Switch>
|
</Switch>
|
||||||
</Show>
|
</Show>
|
||||||
@ -169,8 +191,13 @@ export const TitleBar = (props: TitleBarProps) => {
|
|||||||
const [menu, setMenu] = createSignal<Menu | null>(null);
|
const [menu, setMenu] = createSignal<Menu | null>(null);
|
||||||
const [mouseY, setMouseY] = createSignal(0);
|
const [mouseY, setMouseY] = createSignal(0);
|
||||||
|
|
||||||
const [data, { refetch }] = createResource(async () => await props.ipc.invoke('get-menu') as Promise<Menu | null>);
|
const [data, { refetch }] = createResource(
|
||||||
const [isMaximized, { refetch: refetchMaximize }] = createResource(async () => await props.ipc.invoke('window-is-maximized') as Promise<boolean>);
|
async () => (await props.ipc.invoke('get-menu')) as Promise<Menu | null>,
|
||||||
|
);
|
||||||
|
const [isMaximized, { refetch: refetchMaximize }] = createResource(
|
||||||
|
async () =>
|
||||||
|
(await props.ipc.invoke('window-is-maximized')) as Promise<boolean>,
|
||||||
|
);
|
||||||
|
|
||||||
const handleToggleMaximize = async () => {
|
const handleToggleMaximize = async () => {
|
||||||
if (isMaximized()) {
|
if (isMaximized()) {
|
||||||
@ -194,10 +221,12 @@ export const TitleBar = (props: TitleBarProps) => {
|
|||||||
)) as MenuItem | null;
|
)) as MenuItem | null;
|
||||||
|
|
||||||
const newMenu = structuredClone(originalMenu);
|
const newMenu = structuredClone(originalMenu);
|
||||||
const stack = [...newMenu?.items ?? []];
|
const stack = [...(newMenu?.items ?? [])];
|
||||||
let now: MenuItem | undefined = stack.pop();
|
let now: MenuItem | undefined = stack.pop();
|
||||||
while (now) {
|
while (now) {
|
||||||
const index = now?.submenu?.items?.findIndex((it) => it.commandId === commandId) ?? -1;
|
const index =
|
||||||
|
now?.submenu?.items?.findIndex((it) => it.commandId === commandId) ??
|
||||||
|
-1;
|
||||||
|
|
||||||
if (index >= 0) {
|
if (index >= 0) {
|
||||||
if (menuItem) now?.submenu?.items?.splice(index, 1, menuItem);
|
if (menuItem) now?.submenu?.items?.splice(index, 1, menuItem);
|
||||||
@ -213,13 +242,16 @@ export const TitleBar = (props: TitleBarProps) => {
|
|||||||
return newMenu;
|
return newMenu;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleItemClick = async (commandId: number, radioGroup?: MenuItem[]) => {
|
const handleItemClick = async (
|
||||||
|
commandId: number,
|
||||||
|
radioGroup?: MenuItem[],
|
||||||
|
) => {
|
||||||
const menuData = menu();
|
const menuData = menu();
|
||||||
if (!menuData) return;
|
if (!menuData) return;
|
||||||
|
|
||||||
if (Array.isArray(radioGroup)) {
|
if (Array.isArray(radioGroup)) {
|
||||||
let newMenu = menuData;
|
let newMenu = menuData;
|
||||||
for await (const item of radioGroup) {
|
for (const item of radioGroup) {
|
||||||
newMenu = await refreshMenuItem(newMenu, item.commandId);
|
newMenu = await refreshMenuItem(newMenu, item.commandId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -272,18 +304,15 @@ export const TitleBar = (props: TitleBarProps) => {
|
|||||||
window.addEventListener('mousemove', listener);
|
window.addEventListener('mousemove', listener);
|
||||||
const ytmusicAppLayout = document.querySelector<HTMLElement>('#layout');
|
const ytmusicAppLayout = document.querySelector<HTMLElement>('#layout');
|
||||||
ytmusicAppLayout?.addEventListener('scroll', () => {
|
ytmusicAppLayout?.addEventListener('scroll', () => {
|
||||||
const scrollValue = ytmusicAppLayout.scrollTop;
|
const scrollValue = ytmusicAppLayout.scrollTop;
|
||||||
if (scrollValue > 20){
|
if (scrollValue > 20) {
|
||||||
ytmusicAppLayout.classList.add('content-scrolled');
|
ytmusicAppLayout.classList.add('content-scrolled');
|
||||||
}
|
} else {
|
||||||
else{
|
ytmusicAppLayout.classList.remove('content-scrolled');
|
||||||
ytmusicAppLayout.classList.remove('content-scrolled');
|
}
|
||||||
}
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
if (!menu() && data()) {
|
if (!menu() && data()) {
|
||||||
setMenu(data() ?? null);
|
setMenu(data() ?? null);
|
||||||
@ -295,7 +324,12 @@ export const TitleBar = (props: TitleBarProps) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<nav data-ytmd-main-panel={true} class={titleStyle()} data-macos={props.isMacOS} data-show={mouseY() < 32}>
|
<nav
|
||||||
|
data-ytmd-main-panel={true}
|
||||||
|
class={titleStyle()}
|
||||||
|
data-macos={props.isMacOS}
|
||||||
|
data-show={mouseY() < 32}
|
||||||
|
>
|
||||||
<IconButton
|
<IconButton
|
||||||
onClick={() => setCollapsed(!collapsed())}
|
onClick={() => setCollapsed(!collapsed())}
|
||||||
style={{
|
style={{
|
||||||
@ -310,15 +344,34 @@ export const TitleBar = (props: TitleBarProps) => {
|
|||||||
</svg>
|
</svg>
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<TransitionGroup
|
<TransitionGroup
|
||||||
enterClass={ignoreTransition() ? animationStyle().fakeTarget : animationStyle().enter}
|
enterClass={
|
||||||
enterActiveClass={ignoreTransition() ? animationStyle().fake : animationStyle().enterActive}
|
ignoreTransition()
|
||||||
exitToClass={ignoreTransition() ? animationStyle().fakeTarget : animationStyle().exitTo}
|
? animationStyle().fakeTarget
|
||||||
exitActiveClass={ignoreTransition() ? animationStyle().fake : animationStyle().exitActive}
|
: animationStyle().enter
|
||||||
|
}
|
||||||
|
enterActiveClass={
|
||||||
|
ignoreTransition()
|
||||||
|
? animationStyle().fake
|
||||||
|
: animationStyle().enterActive
|
||||||
|
}
|
||||||
|
exitToClass={
|
||||||
|
ignoreTransition()
|
||||||
|
? animationStyle().fakeTarget
|
||||||
|
: animationStyle().exitTo
|
||||||
|
}
|
||||||
|
exitActiveClass={
|
||||||
|
ignoreTransition()
|
||||||
|
? animationStyle().fake
|
||||||
|
: animationStyle().exitActive
|
||||||
|
}
|
||||||
onBeforeEnter={(element) => {
|
onBeforeEnter={(element) => {
|
||||||
if (ignoreTransition()) return;
|
if (ignoreTransition()) return;
|
||||||
const index = Number(element.getAttribute('data-index') ?? 0);
|
const index = Number(element.getAttribute('data-index') ?? 0);
|
||||||
|
|
||||||
(element as HTMLElement).style.setProperty('transition-delay', `${(index * 0.025)}s`);
|
(element as HTMLElement).style.setProperty(
|
||||||
|
'transition-delay',
|
||||||
|
`${index * 0.025}s`,
|
||||||
|
);
|
||||||
}}
|
}}
|
||||||
onAfterEnter={(element) => {
|
onAfterEnter={(element) => {
|
||||||
(element as HTMLElement).style.removeProperty('transition-delay');
|
(element as HTMLElement).style.removeProperty('transition-delay');
|
||||||
@ -328,13 +381,18 @@ export const TitleBar = (props: TitleBarProps) => {
|
|||||||
const index = Number(element.getAttribute('data-index') ?? 0);
|
const index = Number(element.getAttribute('data-index') ?? 0);
|
||||||
const length = Number(element.getAttribute('data-length') ?? 1);
|
const length = Number(element.getAttribute('data-length') ?? 1);
|
||||||
|
|
||||||
(element as HTMLElement).style.setProperty('transition-delay', `${(length * 0.025) - (index * 0.025)}s`);
|
(element as HTMLElement).style.setProperty(
|
||||||
|
'transition-delay',
|
||||||
|
`${length * 0.025 - index * 0.025}s`,
|
||||||
|
);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Show when={!collapsed()}>
|
<Show when={!collapsed()}>
|
||||||
<Index each={menu()?.items}>
|
<Index each={menu()?.items}>
|
||||||
{(item, index) => {
|
{(item, index) => {
|
||||||
const [anchor, setAnchor] = createSignal<HTMLElement | null>(null);
|
const [anchor, setAnchor] = createSignal<HTMLElement | null>(
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
|
||||||
const handleClick = () => {
|
const handleClick = () => {
|
||||||
if (openTarget() === anchor()) {
|
if (openTarget() === anchor()) {
|
||||||
@ -372,7 +430,7 @@ export const TitleBar = (props: TitleBarProps) => {
|
|||||||
</Show>
|
</Show>
|
||||||
</TransitionGroup>
|
</TransitionGroup>
|
||||||
<Show when={props.enableController}>
|
<Show when={props.enableController}>
|
||||||
<div style={{ flex: 1 }}/>
|
<div style={{ flex: 1 }} />
|
||||||
<WindowController
|
<WindowController
|
||||||
isMaximize={isMaximized()}
|
isMaximize={isMaximized()}
|
||||||
onToggleMaximize={handleToggleMaximize}
|
onToggleMaximize={handleToggleMaximize}
|
||||||
@ -383,4 +441,3 @@ export const TitleBar = (props: TitleBarProps) => {
|
|||||||
</nav>
|
</nav>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -4,19 +4,21 @@ import { Show } from 'solid-js';
|
|||||||
import { IconButton } from './IconButton';
|
import { IconButton } from './IconButton';
|
||||||
import { cacheNoArgs } from '@/providers/decorators';
|
import { cacheNoArgs } from '@/providers/decorators';
|
||||||
|
|
||||||
const containerStyle = cacheNoArgs(() => css`
|
const containerStyle = cacheNoArgs(
|
||||||
display: flex;
|
() => css`
|
||||||
justify-content: flex-end;
|
display: flex;
|
||||||
align-items: center;
|
justify-content: flex-end;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
& > *:last-of-type {
|
& > *:last-of-type {
|
||||||
border-top-right-radius: 4px;
|
border-top-right-radius: 4px;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background: rgba(255, 0, 0, 0.5);
|
background: rgba(255, 0, 0, 0.5);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
`,
|
||||||
`);
|
);
|
||||||
|
|
||||||
export type WindowControllerProps = {
|
export type WindowControllerProps = {
|
||||||
isMaximize?: boolean;
|
isMaximize?: boolean;
|
||||||
@ -24,20 +26,35 @@ export type WindowControllerProps = {
|
|||||||
onToggleMaximize?: () => void;
|
onToggleMaximize?: () => void;
|
||||||
onMinimize?: () => void;
|
onMinimize?: () => void;
|
||||||
onClose?: () => void;
|
onClose?: () => void;
|
||||||
}
|
};
|
||||||
export const WindowController = (props: WindowControllerProps) => {
|
export const WindowController = (props: WindowControllerProps) => {
|
||||||
return (
|
return (
|
||||||
<div class={containerStyle()}>
|
<div class={containerStyle()}>
|
||||||
<IconButton onClick={props.onMinimize}>
|
<IconButton onClick={props.onMinimize}>
|
||||||
<svg width={16} height={16} fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
<svg
|
||||||
<path fill="currentColor" d="M3.755 12.5h16.492a.75.75 0 0 0 0-1.5H3.755a.75.75 0 0 0 0 1.5Z"/>
|
width={16}
|
||||||
|
height={16}
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill="currentColor"
|
||||||
|
d="M3.755 12.5h16.492a.75.75 0 0 0 0-1.5H3.755a.75.75 0 0 0 0 1.5Z"
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<IconButton onClick={props.onToggleMaximize}>
|
<IconButton onClick={props.onToggleMaximize}>
|
||||||
<Show
|
<Show
|
||||||
when={props.isMaximize}
|
when={props.isMaximize}
|
||||||
fallback={
|
fallback={
|
||||||
<svg width={16} height={16} fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
<svg
|
||||||
|
width={16}
|
||||||
|
height={16}
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
<path
|
<path
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
d="M6 3h12a3 3 0 0 1 3 3v12a3 3 0 0 1-3 3H6a3 3 0 0 1-3-3V6a3 3 0 0 1 3-3Zm0 2a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V6a1 1 0 0 0-1-1H6Z"
|
d="M6 3h12a3 3 0 0 1 3 3v12a3 3 0 0 1-3 3H6a3 3 0 0 1-3-3V6a3 3 0 0 1 3-3Zm0 2a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V6a1 1 0 0 0-1-1H6Z"
|
||||||
@ -45,7 +62,13 @@ export const WindowController = (props: WindowControllerProps) => {
|
|||||||
</svg>
|
</svg>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<svg width={16} height={16} fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
<svg
|
||||||
|
width={16}
|
||||||
|
height={16}
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
<path
|
<path
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
d="M7.518 5H6.009a3.25 3.25 0 0 1 3.24-3h8.001A4.75 4.75 0 0 1 22 6.75v8a3.25 3.25 0 0 1-3 3.24v-1.508a1.75 1.75 0 0 0 1.5-1.732v-8a3.25 3.25 0 0 0-3.25-3.25h-8A1.75 1.75 0 0 0 7.518 5ZM5.25 6A3.25 3.25 0 0 0 2 9.25v9.5A3.25 3.25 0 0 0 5.25 22h9.5A3.25 3.25 0 0 0 18 18.75v-9.5A3.25 3.25 0 0 0 14.75 6h-9.5ZM3.5 9.25c0-.966.784-1.75 1.75-1.75h9.5c.967 0 1.75.784 1.75 1.75v9.5a1.75 1.75 0 0 1-1.75 1.75h-9.5a1.75 1.75 0 0 1-1.75-1.75v-9.5Z"
|
d="M7.518 5H6.009a3.25 3.25 0 0 1 3.24-3h8.001A4.75 4.75 0 0 1 22 6.75v8a3.25 3.25 0 0 1-3 3.24v-1.508a1.75 1.75 0 0 0 1.5-1.732v-8a3.25 3.25 0 0 0-3.25-3.25h-8A1.75 1.75 0 0 0 7.518 5ZM5.25 6A3.25 3.25 0 0 0 2 9.25v9.5A3.25 3.25 0 0 0 5.25 22h9.5A3.25 3.25 0 0 0 18 18.75v-9.5A3.25 3.25 0 0 0 14.75 6h-9.5ZM3.5 9.25c0-.966.784-1.75 1.75-1.75h9.5c.967 0 1.75.784 1.75 1.75v9.5a1.75 1.75 0 0 1-1.75 1.75h-9.5a1.75 1.75 0 0 1-1.75-1.75v-9.5Z"
|
||||||
@ -54,7 +77,13 @@ export const WindowController = (props: WindowControllerProps) => {
|
|||||||
</Show>
|
</Show>
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<IconButton onClick={props.onClose}>
|
<IconButton onClick={props.onClose}>
|
||||||
<svg width={16} height={16} fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
<svg
|
||||||
|
width={16}
|
||||||
|
height={16}
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
<path
|
<path
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
d="m4.21 4.387.083-.094a1 1 0 0 1 1.32-.083l.094.083L12 10.585l6.293-6.292a1 1 0 1 1 1.414 1.414L13.415 12l6.292 6.293a1 1 0 0 1 .083 1.32l-.083.094a1 1 0 0 1-1.32.083l-.094-.083L12 13.415l-6.293 6.292a1 1 0 0 1-1.414-1.414L10.585 12 4.293 5.707a1 1 0 0 1-.083-1.32l.083-.094-.083.094Z"
|
d="m4.21 4.387.083-.094a1 1 0 0 1 1.32-.083l.094.083L12 10.585l6.293-6.292a1 1 0 1 1 1.414 1.414L13.415 12l6.292 6.293a1 1 0 0 1 .083 1.32l-.083.094a1 1 0 0 1-1.32.083l-.094-.083L12 13.415l-6.293 6.292a1 1 0 0 1-1.414-1.414L10.585 12 4.293 5.707a1 1 0 0 1-.083-1.32l.083-.094-.083.094Z"
|
||||||
|
|||||||
@ -77,7 +77,7 @@ export const onRendererLoad = ({
|
|||||||
applyLyricsTabState();
|
applyLyricsTabState();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const applyLyricsTabState = () => {
|
const applyLyricsTabState = () => {
|
||||||
if (lyrics) {
|
if (lyrics) {
|
||||||
tabs.lyrics.removeAttribute('disabled');
|
tabs.lyrics.removeAttribute('disabled');
|
||||||
|
|||||||
@ -3,13 +3,15 @@ import { DataConnection, Peer } from 'peerjs';
|
|||||||
import type { Permission, Profile, VideoData } from './types';
|
import type { Permission, Profile, VideoData } from './types';
|
||||||
|
|
||||||
export type ConnectionEventMap = {
|
export type ConnectionEventMap = {
|
||||||
ADD_SONGS: { videoList: VideoData[], index?: number };
|
ADD_SONGS: { videoList: VideoData[]; index?: number };
|
||||||
REMOVE_SONG: { index: number };
|
REMOVE_SONG: { index: number };
|
||||||
MOVE_SONG: { fromIndex: number; toIndex: number };
|
MOVE_SONG: { fromIndex: number; toIndex: number };
|
||||||
IDENTIFY: { profile: Profile } | undefined;
|
IDENTIFY: { profile: Profile } | undefined;
|
||||||
SYNC_PROFILE: { profiles: Record<string, Profile> } | undefined;
|
SYNC_PROFILE: { profiles: Record<string, Profile> } | undefined;
|
||||||
SYNC_QUEUE: { videoList: VideoData[] } | undefined;
|
SYNC_QUEUE: { videoList: VideoData[] } | undefined;
|
||||||
SYNC_PROGRESS: { progress?: number; state?: number; index?: number; } | undefined;
|
SYNC_PROGRESS:
|
||||||
|
| { progress?: number; state?: number; index?: number }
|
||||||
|
| undefined;
|
||||||
PERMISSION: Permission | undefined;
|
PERMISSION: Permission | undefined;
|
||||||
};
|
};
|
||||||
export type ConnectionEventUnion = {
|
export type ConnectionEventUnion = {
|
||||||
@ -24,9 +26,12 @@ type PromiseUtil<T> = {
|
|||||||
promise: Promise<T>;
|
promise: Promise<T>;
|
||||||
resolve: (id: T) => void;
|
resolve: (id: T) => void;
|
||||||
reject: (err: unknown) => void;
|
reject: (err: unknown) => void;
|
||||||
}
|
};
|
||||||
|
|
||||||
export type ConnectionListener = (event: ConnectionEventUnion, conn: DataConnection) => void;
|
export type ConnectionListener = (
|
||||||
|
event: ConnectionEventUnion,
|
||||||
|
conn: DataConnection,
|
||||||
|
) => void;
|
||||||
export type ConnectionMode = 'host' | 'guest' | 'disconnected';
|
export type ConnectionMode = 'host' | 'guest' | 'disconnected';
|
||||||
export class Connection {
|
export class Connection {
|
||||||
private peer: Peer;
|
private peer: Peer;
|
||||||
@ -95,9 +100,12 @@ export class Connection {
|
|||||||
return Object.values(this.connections);
|
return Object.values(this.connections);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async broadcast<Event extends keyof ConnectionEventMap>(type: Event, payload: ConnectionEventMap[Event]) {
|
public async broadcast<Event extends keyof ConnectionEventMap>(
|
||||||
|
type: Event,
|
||||||
|
payload: ConnectionEventMap[Event],
|
||||||
|
) {
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
this.getConnections().map((conn) => conn.send({ type, payload }))
|
this.getConnections().map((conn) => conn.send({ type, payload })),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -125,7 +133,13 @@ export class Connection {
|
|||||||
this.connectionListeners.forEach((listener) => listener(conn));
|
this.connectionListeners.forEach((listener) => listener(conn));
|
||||||
|
|
||||||
conn.on('data', (data) => {
|
conn.on('data', (data) => {
|
||||||
if (!data || typeof data !== 'object' || !('type' in data) || !('payload' in data) || !data.type) {
|
if (
|
||||||
|
!data ||
|
||||||
|
typeof data !== 'object' ||
|
||||||
|
!('type' in data) ||
|
||||||
|
!('payload' in data) ||
|
||||||
|
!data.type
|
||||||
|
) {
|
||||||
console.warn('Music Together: Invalid data', data);
|
console.warn('Music Together: Invalid data', data);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import itemHTML from './templates/item.html?raw';
|
|||||||
import popupHTML from './templates/popup.html?raw';
|
import popupHTML from './templates/popup.html?raw';
|
||||||
|
|
||||||
type Placement =
|
type Placement =
|
||||||
'top'
|
| 'top'
|
||||||
| 'bottom'
|
| 'bottom'
|
||||||
| 'right'
|
| 'right'
|
||||||
| 'left'
|
| 'left'
|
||||||
@ -15,32 +15,40 @@ type Placement =
|
|||||||
| 'top-right'
|
| 'top-right'
|
||||||
| 'bottom-left'
|
| 'bottom-left'
|
||||||
| 'bottom-right';
|
| 'bottom-right';
|
||||||
type PopupItem = (ItemRendererProps & { type: 'item'; })
|
type PopupItem =
|
||||||
| { type: 'divider'; }
|
| (ItemRendererProps & { type: 'item' })
|
||||||
| { type: 'custom'; element: HTMLElement; };
|
| { type: 'divider' }
|
||||||
|
| { type: 'custom'; element: HTMLElement };
|
||||||
|
|
||||||
type PopupProps = {
|
type PopupProps = {
|
||||||
data: PopupItem[];
|
data: PopupItem[];
|
||||||
anchorAt?: Placement;
|
anchorAt?: Placement;
|
||||||
popupAt?: Placement;
|
popupAt?: Placement;
|
||||||
}
|
};
|
||||||
export const Popup = (props: PopupProps) => {
|
export const Popup = (props: PopupProps) => {
|
||||||
const popup = ElementFromHtml(popupHTML);
|
const popup = ElementFromHtml(popupHTML);
|
||||||
const container = popup.querySelector<HTMLElement>('.music-together-popup-container')!;
|
const container = popup.querySelector<HTMLElement>(
|
||||||
|
'.music-together-popup-container',
|
||||||
|
)!;
|
||||||
const items = props.data
|
const items = props.data
|
||||||
.map((props) => {
|
.map((props) => {
|
||||||
if (props.type === 'item') return {
|
if (props.type === 'item')
|
||||||
type: 'item' as const,
|
return {
|
||||||
...ItemRenderer(props),
|
type: 'item' as const,
|
||||||
};
|
...ItemRenderer(props),
|
||||||
if (props.type === 'divider') return {
|
};
|
||||||
type: 'divider' as const,
|
if (props.type === 'divider')
|
||||||
element: ElementFromHtml('<div class="music-together-divider horizontal"></div>'),
|
return {
|
||||||
};
|
type: 'divider' as const,
|
||||||
if (props.type === 'custom') return {
|
element: ElementFromHtml(
|
||||||
type: 'custom' as const,
|
'<div class="music-together-divider horizontal"></div>',
|
||||||
element: props.element,
|
),
|
||||||
};
|
};
|
||||||
|
if (props.type === 'custom')
|
||||||
|
return {
|
||||||
|
type: 'custom' as const,
|
||||||
|
element: props.element,
|
||||||
|
};
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
})
|
})
|
||||||
@ -80,7 +88,9 @@ export const Popup = (props: PopupProps) => {
|
|||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const onClose = (event: MouseEvent) => {
|
const onClose = (event: MouseEvent) => {
|
||||||
const isPopupClick = event.composedPath().some((element) => element === popup);
|
const isPopupClick = event
|
||||||
|
.composedPath()
|
||||||
|
.some((element) => element === popup);
|
||||||
if (!isPopupClick) {
|
if (!isPopupClick) {
|
||||||
this.dismiss();
|
this.dismiss();
|
||||||
document.removeEventListener('click', onClose);
|
document.removeEventListener('click', onClose);
|
||||||
@ -101,7 +111,7 @@ export const Popup = (props: PopupProps) => {
|
|||||||
dismiss() {
|
dismiss() {
|
||||||
popup.style.setProperty('opacity', '0');
|
popup.style.setProperty('opacity', '0');
|
||||||
popup.style.setProperty('pointer-events', 'none');
|
popup.style.setProperty('pointer-events', 'none');
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -133,6 +143,6 @@ export const ItemRenderer = (props: ItemRendererProps) => {
|
|||||||
setText(text: string) {
|
setText(text: string) {
|
||||||
textContainer.replaceChildren(text);
|
textContainer.replaceChildren(text);
|
||||||
},
|
},
|
||||||
id: props.id
|
id: props.id,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@ -6,7 +6,12 @@ import { t } from '@/i18n';
|
|||||||
import { createPlugin } from '@/utils';
|
import { createPlugin } from '@/utils';
|
||||||
import promptOptions from '@/providers/prompt-options';
|
import promptOptions from '@/providers/prompt-options';
|
||||||
|
|
||||||
import { getDefaultProfile, type Permission, type Profile, type VideoData } from './types';
|
import {
|
||||||
|
getDefaultProfile,
|
||||||
|
type Permission,
|
||||||
|
type Profile,
|
||||||
|
type VideoData,
|
||||||
|
} from './types';
|
||||||
import { Queue } from './queue';
|
import { Queue } from './queue';
|
||||||
import { Connection, type ConnectionEventUnion } from './connection';
|
import { Connection, type ConnectionEventUnion } from './connection';
|
||||||
import { createHostPopup } from './ui/host';
|
import { createHostPopup } from './ui/host';
|
||||||
@ -26,7 +31,7 @@ type RawAccountData = {
|
|||||||
runs: { text: string }[];
|
runs: { text: string }[];
|
||||||
};
|
};
|
||||||
accountPhoto: {
|
accountPhoto: {
|
||||||
thumbnails: { url: string; width: number; height: number; }[];
|
thumbnails: { url: string; width: number; height: number }[];
|
||||||
};
|
};
|
||||||
settingsEndpoint: unknown;
|
settingsEndpoint: unknown;
|
||||||
manageAccountTitle: unknown;
|
manageAccountTitle: unknown;
|
||||||
@ -59,7 +64,7 @@ export default createPlugin<
|
|||||||
stateInterval?: number;
|
stateInterval?: number;
|
||||||
updateNext: boolean;
|
updateNext: boolean;
|
||||||
ignoreChange: boolean;
|
ignoreChange: boolean;
|
||||||
rollbackInjector?: (() => void);
|
rollbackInjector?: () => void;
|
||||||
me?: Omit<Profile, 'id'>;
|
me?: Omit<Profile, 'id'>;
|
||||||
profiles: Record<string, Profile>;
|
profiles: Record<string, Profile>;
|
||||||
permission: Permission;
|
permission: Permission;
|
||||||
@ -79,16 +84,18 @@ export default createPlugin<
|
|||||||
restartNeeded: false,
|
restartNeeded: false,
|
||||||
addedVersion: '3.2.X',
|
addedVersion: '3.2.X',
|
||||||
config: {
|
config: {
|
||||||
enabled: false
|
enabled: false,
|
||||||
},
|
},
|
||||||
stylesheets: [style],
|
stylesheets: [style],
|
||||||
backend({ ipc }) {
|
backend({ ipc }) {
|
||||||
ipc.handle('music-together:prompt', async (title: string, label: string) => prompt({
|
ipc.handle('music-together:prompt', async (title: string, label: string) =>
|
||||||
title,
|
prompt({
|
||||||
label,
|
title,
|
||||||
type: 'input',
|
label,
|
||||||
...promptOptions()
|
type: 'input',
|
||||||
}));
|
...promptOptions(),
|
||||||
|
}),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
renderer: {
|
renderer: {
|
||||||
updateNext: false,
|
updateNext: false,
|
||||||
@ -112,15 +119,19 @@ export default createPlugin<
|
|||||||
videoChangeListener(event: CustomEvent<VideoDataChanged>) {
|
videoChangeListener(event: CustomEvent<VideoDataChanged>) {
|
||||||
if (event.detail.name === 'dataloaded' || this.updateNext) {
|
if (event.detail.name === 'dataloaded' || this.updateNext) {
|
||||||
if (this.connection?.mode === 'host') {
|
if (this.connection?.mode === 'host') {
|
||||||
const videoList: VideoData[] = this.queue?.flatItems.map((it) => ({
|
const videoList: VideoData[] =
|
||||||
videoId: it!.videoId,
|
this.queue?.flatItems.map(
|
||||||
ownerId: this.connection!.id
|
(it) =>
|
||||||
} satisfies VideoData)) ?? [];
|
({
|
||||||
|
videoId: it!.videoId,
|
||||||
|
ownerId: this.connection!.id,
|
||||||
|
}) satisfies VideoData,
|
||||||
|
) ?? [];
|
||||||
|
|
||||||
this.queue?.setVideoList(videoList, false);
|
this.queue?.setVideoList(videoList, false);
|
||||||
this.queue?.syncQueueOwner();
|
this.queue?.syncQueueOwner();
|
||||||
this.connection.broadcast('SYNC_QUEUE', {
|
this.connection.broadcast('SYNC_QUEUE', {
|
||||||
videoList
|
videoList,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.updateNext = event.detail.name === 'dataloaded';
|
this.updateNext = event.detail.name === 'dataloaded';
|
||||||
@ -138,7 +149,7 @@ export default createPlugin<
|
|||||||
|
|
||||||
this.connection.broadcast('SYNC_PROGRESS', {
|
this.connection.broadcast('SYNC_PROGRESS', {
|
||||||
// progress: this.playerApi?.getCurrentTime(),
|
// progress: this.playerApi?.getCurrentTime(),
|
||||||
state: this.playerApi?.getPlayerState()
|
state: this.playerApi?.getPlayerState(),
|
||||||
// index: this.queue?.selectedIndex ?? 0,
|
// index: this.queue?.selectedIndex ?? 0,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@ -150,13 +161,17 @@ export default createPlugin<
|
|||||||
if (!wait) return false;
|
if (!wait) return false;
|
||||||
|
|
||||||
if (!this.me) this.me = getDefaultProfile(this.connection.id);
|
if (!this.me) this.me = getDefaultProfile(this.connection.id);
|
||||||
const rawItems = this.queue?.flatItems?.map((it) => ({
|
const rawItems =
|
||||||
videoId: it!.videoId,
|
this.queue?.flatItems?.map(
|
||||||
ownerId: this.connection!.id
|
(it) =>
|
||||||
} satisfies VideoData)) ?? [];
|
({
|
||||||
|
videoId: it!.videoId,
|
||||||
|
ownerId: this.connection!.id,
|
||||||
|
}) satisfies VideoData,
|
||||||
|
) ?? [];
|
||||||
this.queue?.setOwner({
|
this.queue?.setOwner({
|
||||||
id: this.connection.id,
|
id: this.connection.id,
|
||||||
...this.me
|
...this.me,
|
||||||
});
|
});
|
||||||
this.queue?.setVideoList(rawItems, false);
|
this.queue?.setVideoList(rawItems, false);
|
||||||
this.queue?.syncQueueOwner();
|
this.queue?.syncQueueOwner();
|
||||||
@ -166,31 +181,41 @@ export default createPlugin<
|
|||||||
this.profiles = {};
|
this.profiles = {};
|
||||||
this.connection.onConnections((connection) => {
|
this.connection.onConnections((connection) => {
|
||||||
if (!connection) {
|
if (!connection) {
|
||||||
this.api?.openToast(t('plugins.music-together.toast.disconnected'));
|
this.api?.toastService?.show(
|
||||||
|
t('plugins.music-together.toast.disconnected'),
|
||||||
|
);
|
||||||
this.onStop();
|
this.onStop();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!connection.open) {
|
if (!connection.open) {
|
||||||
this.api?.openToast(t('plugins.music-together.toast.user-disconnected', {
|
this.api?.toastService?.show(
|
||||||
name: this.profiles[connection.peer]?.name
|
t('plugins.music-together.toast.user-disconnected', {
|
||||||
}));
|
name: this.profiles[connection.peer]?.name,
|
||||||
|
}),
|
||||||
|
);
|
||||||
this.putProfile(connection.peer, undefined);
|
this.putProfile(connection.peer, undefined);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
this.putProfile(this.connection.id, {
|
this.putProfile(this.connection.id, {
|
||||||
id: this.connection.id,
|
id: this.connection.id,
|
||||||
...this.me
|
...this.me,
|
||||||
});
|
});
|
||||||
|
|
||||||
const listener = async (event: ConnectionEventUnion, conn?: DataConnection) => {
|
const listener = async (
|
||||||
|
event: ConnectionEventUnion,
|
||||||
|
conn?: DataConnection,
|
||||||
|
) => {
|
||||||
this.ignoreChange = true;
|
this.ignoreChange = true;
|
||||||
|
|
||||||
switch (event.type) {
|
switch (event.type) {
|
||||||
case 'ADD_SONGS': {
|
case 'ADD_SONGS': {
|
||||||
if (conn && this.permission === 'host-only') return;
|
if (conn && this.permission === 'host-only') return;
|
||||||
|
|
||||||
await this.queue?.addVideos(event.payload.videoList, event.payload.index);
|
await this.queue?.addVideos(
|
||||||
|
event.payload.videoList,
|
||||||
|
event.payload.index,
|
||||||
|
);
|
||||||
await this.connection?.broadcast('ADD_SONGS', event.payload);
|
await this.connection?.broadcast('ADD_SONGS', event.payload);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -204,27 +229,38 @@ export default createPlugin<
|
|||||||
case 'MOVE_SONG': {
|
case 'MOVE_SONG': {
|
||||||
if (conn && this.permission === 'host-only') {
|
if (conn && this.permission === 'host-only') {
|
||||||
await this.connection?.broadcast('SYNC_QUEUE', {
|
await this.connection?.broadcast('SYNC_QUEUE', {
|
||||||
videoList: this.queue?.videoList ?? []
|
videoList: this.queue?.videoList ?? [],
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.queue?.moveItem(event.payload.fromIndex, event.payload.toIndex);
|
this.queue?.moveItem(
|
||||||
|
event.payload.fromIndex,
|
||||||
|
event.payload.toIndex,
|
||||||
|
);
|
||||||
await this.connection?.broadcast('MOVE_SONG', event.payload);
|
await this.connection?.broadcast('MOVE_SONG', event.payload);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'IDENTIFY': {
|
case 'IDENTIFY': {
|
||||||
if (!event.payload || !conn) {
|
if (!event.payload || !conn) {
|
||||||
console.warn('Music Together [Host]: Received "IDENTIFY" event without payload or connection');
|
console.warn(
|
||||||
|
'Music Together [Host]: Received "IDENTIFY" event without payload or connection',
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.api?.openToast(t('plugins.music-together.toast.user-connected', { name: event.payload.profile.name }));
|
this.api?.toastService?.show(
|
||||||
|
t('plugins.music-together.toast.user-connected', {
|
||||||
|
name: event.payload.profile.name,
|
||||||
|
}),
|
||||||
|
);
|
||||||
this.putProfile(conn.peer, event.payload.profile);
|
this.putProfile(conn.peer, event.payload.profile);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'SYNC_PROFILE': {
|
case 'SYNC_PROFILE': {
|
||||||
await this.connection?.broadcast('SYNC_PROFILE', { profiles: this.profiles });
|
await this.connection?.broadcast('SYNC_PROFILE', {
|
||||||
|
profiles: this.profiles,
|
||||||
|
});
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -237,7 +273,7 @@ export default createPlugin<
|
|||||||
}
|
}
|
||||||
case 'SYNC_QUEUE': {
|
case 'SYNC_QUEUE': {
|
||||||
await this.connection?.broadcast('SYNC_QUEUE', {
|
await this.connection?.broadcast('SYNC_QUEUE', {
|
||||||
videoList: this.queue?.videoList ?? []
|
videoList: this.queue?.videoList ?? [],
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -251,7 +287,8 @@ export default createPlugin<
|
|||||||
if (permissionLevel >= 2) {
|
if (permissionLevel >= 2) {
|
||||||
if (typeof event.payload?.progress === 'number') {
|
if (typeof event.payload?.progress === 'number') {
|
||||||
const currentTime = this.playerApi?.getCurrentTime() ?? 0;
|
const currentTime = this.playerApi?.getCurrentTime() ?? 0;
|
||||||
if (Math.abs(event.payload.progress - currentTime) > 3) this.playerApi?.seekTo(event.payload.progress);
|
if (Math.abs(event.payload.progress - currentTime) > 3)
|
||||||
|
this.playerApi?.seekTo(event.payload.progress);
|
||||||
}
|
}
|
||||||
if (this.playerApi?.getPlayerState() !== event.payload?.state) {
|
if (this.playerApi?.getPlayerState() !== event.payload?.state) {
|
||||||
if (event.payload?.state === 2) this.playerApi?.pauseVideo();
|
if (event.payload?.state === 2) this.playerApi?.pauseVideo();
|
||||||
@ -300,25 +337,32 @@ export default createPlugin<
|
|||||||
|
|
||||||
this.profiles = {};
|
this.profiles = {};
|
||||||
|
|
||||||
const id = await this.showPrompt(t('plugins.music-together.name'), t('plugins.music-together.dialog.enter-host'));
|
const id = await this.showPrompt(
|
||||||
|
t('plugins.music-together.name'),
|
||||||
|
t('plugins.music-together.dialog.enter-host'),
|
||||||
|
);
|
||||||
if (typeof id !== 'string') return false;
|
if (typeof id !== 'string') return false;
|
||||||
|
|
||||||
const connection = await this.connection.connect(id).catch(() => false);
|
const connection = await this.connection.connect(id).catch(() => false);
|
||||||
if (!connection) return false;
|
if (!connection) return false;
|
||||||
this.connection.onConnections((connection) => {
|
this.connection.onConnections((connection) => {
|
||||||
if (!connection?.open) {
|
if (!connection?.open) {
|
||||||
this.api?.openToast(t('plugins.music-together.toast.disconnected'));
|
this.api?.toastService?.show(
|
||||||
|
t('plugins.music-together.toast.disconnected'),
|
||||||
|
);
|
||||||
this.onStop();
|
this.onStop();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
let resolveIgnore: number | null = null;
|
let resolveIgnore: number | null = null;
|
||||||
const listener = async (event: ConnectionEventUnion) => {
|
const listener = async (event: ConnectionEventUnion) => {
|
||||||
this.ignoreChange = true;
|
this.ignoreChange = true;
|
||||||
switch (event.type) {
|
switch (event.type) {
|
||||||
case 'ADD_SONGS': {
|
case 'ADD_SONGS': {
|
||||||
await this.queue?.addVideos(event.payload.videoList, event.payload.index);
|
await this.queue?.addVideos(
|
||||||
|
event.payload.videoList,
|
||||||
|
event.payload.index,
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'REMOVE_SONG': {
|
case 'REMOVE_SONG': {
|
||||||
@ -326,11 +370,16 @@ export default createPlugin<
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'MOVE_SONG': {
|
case 'MOVE_SONG': {
|
||||||
this.queue?.moveItem(event.payload.fromIndex, event.payload.toIndex);
|
this.queue?.moveItem(
|
||||||
|
event.payload.fromIndex,
|
||||||
|
event.payload.toIndex,
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'IDENTIFY': {
|
case 'IDENTIFY': {
|
||||||
console.warn('Music Together [Guest]: Received "IDENTIFY" event from guest');
|
console.warn(
|
||||||
|
'Music Together [Guest]: Received "IDENTIFY" event from guest',
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'SYNC_QUEUE': {
|
case 'SYNC_QUEUE': {
|
||||||
@ -341,7 +390,9 @@ export default createPlugin<
|
|||||||
}
|
}
|
||||||
case 'SYNC_PROFILE': {
|
case 'SYNC_PROFILE': {
|
||||||
if (!event.payload) {
|
if (!event.payload) {
|
||||||
console.warn('Music Together [Guest]: Received "SYNC_PROFILE" event without payload');
|
console.warn(
|
||||||
|
'Music Together [Guest]: Received "SYNC_PROFILE" event without payload',
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -353,7 +404,8 @@ export default createPlugin<
|
|||||||
case 'SYNC_PROGRESS': {
|
case 'SYNC_PROGRESS': {
|
||||||
if (typeof event.payload?.progress === 'number') {
|
if (typeof event.payload?.progress === 'number') {
|
||||||
const currentTime = this.playerApi?.getCurrentTime() ?? 0;
|
const currentTime = this.playerApi?.getCurrentTime() ?? 0;
|
||||||
if (Math.abs(event.payload.progress - currentTime) > 3) this.playerApi?.seekTo(event.payload.progress);
|
if (Math.abs(event.payload.progress - currentTime) > 3)
|
||||||
|
this.playerApi?.seekTo(event.payload.progress);
|
||||||
}
|
}
|
||||||
if (this.playerApi?.getPlayerState() !== event.payload?.state) {
|
if (this.playerApi?.getPlayerState() !== event.payload?.state) {
|
||||||
if (event.payload?.state === 2) this.playerApi?.pauseVideo();
|
if (event.payload?.state === 2) this.playerApi?.pauseVideo();
|
||||||
@ -370,7 +422,9 @@ export default createPlugin<
|
|||||||
}
|
}
|
||||||
case 'PERMISSION': {
|
case 'PERMISSION': {
|
||||||
if (!event.payload) {
|
if (!event.payload) {
|
||||||
console.warn('Music Together [Guest]: Received "PERMISSION" event without payload');
|
console.warn(
|
||||||
|
'Music Together [Guest]: Received "PERMISSION" event without payload',
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -379,9 +433,15 @@ export default createPlugin<
|
|||||||
this.popups.host.setPermission(this.permission);
|
this.popups.host.setPermission(this.permission);
|
||||||
this.popups.setting.setPermission(this.permission);
|
this.popups.setting.setPermission(this.permission);
|
||||||
|
|
||||||
const permissionLabel = t(`plugins.music-together.menu.permission.${this.permission}`);
|
const permissionLabel = t(
|
||||||
|
`plugins.music-together.menu.permission.${this.permission}`,
|
||||||
|
);
|
||||||
|
|
||||||
this.api?.openToast(t('plugins.music-together.toast.permission-changed', { permission: permissionLabel }));
|
this.api?.toastService?.show(
|
||||||
|
t('plugins.music-together.toast.permission-changed', {
|
||||||
|
permission: permissionLabel,
|
||||||
|
}),
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
default: {
|
default: {
|
||||||
@ -415,8 +475,10 @@ export default createPlugin<
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'SYNC_PROGRESS': {
|
case 'SYNC_PROGRESS': {
|
||||||
if (this.permission === 'host-only') await this.connection?.broadcast('SYNC_QUEUE', undefined);
|
if (this.permission === 'host-only')
|
||||||
else await this.connection?.broadcast('SYNC_PROGRESS', event.payload);
|
await this.connection?.broadcast('SYNC_QUEUE', undefined);
|
||||||
|
else
|
||||||
|
await this.connection?.broadcast('SYNC_PROGRESS', event.payload);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -431,12 +493,16 @@ export default createPlugin<
|
|||||||
this.queue?.injection();
|
this.queue?.injection();
|
||||||
this.queue?.setOwner({
|
this.queue?.setOwner({
|
||||||
id: this.connection.id,
|
id: this.connection.id,
|
||||||
...this.me
|
...this.me,
|
||||||
});
|
});
|
||||||
|
|
||||||
const progress = Array.from(document.querySelectorAll<HTMLElement & {
|
const progress = Array.from(
|
||||||
_update: (...args: unknown[]) => void
|
document.querySelectorAll<
|
||||||
}>('tp-yt-paper-progress'));
|
HTMLElement & {
|
||||||
|
_update: (...args: unknown[]) => void;
|
||||||
|
}
|
||||||
|
>('tp-yt-paper-progress'),
|
||||||
|
);
|
||||||
const rollbackList = progress.map((progress) => {
|
const rollbackList = progress.map((progress) => {
|
||||||
const original = progress._update;
|
const original = progress._update;
|
||||||
progress._update = (...args) => {
|
progress._update = (...args) => {
|
||||||
@ -444,10 +510,11 @@ export default createPlugin<
|
|||||||
|
|
||||||
if (this.permission === 'all' && typeof now === 'number') {
|
if (this.permission === 'all' && typeof now === 'number') {
|
||||||
const currentTime = this.playerApi?.getCurrentTime() ?? 0;
|
const currentTime = this.playerApi?.getCurrentTime() ?? 0;
|
||||||
if (Math.abs(now - currentTime) > 3) this.connection?.broadcast('SYNC_PROGRESS', {
|
if (Math.abs(now - currentTime) > 3)
|
||||||
progress: now,
|
this.connection?.broadcast('SYNC_PROGRESS', {
|
||||||
state: this.playerApi?.getPlayerState()
|
progress: now,
|
||||||
});
|
state: this.playerApi?.getPlayerState(),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
original.call(progress, ...args);
|
original.call(progress, ...args);
|
||||||
@ -466,8 +533,8 @@ export default createPlugin<
|
|||||||
id: this.connection.id,
|
id: this.connection.id,
|
||||||
handleId: this.me.handleId,
|
handleId: this.me.handleId,
|
||||||
name: this.me.name,
|
name: this.me.name,
|
||||||
thumbnail: this.me.thumbnail
|
thumbnail: this.me.thumbnail,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
this.connection.broadcast('SYNC_PROFILE', undefined);
|
this.connection.broadcast('SYNC_PROFILE', undefined);
|
||||||
@ -525,14 +592,18 @@ export default createPlugin<
|
|||||||
},
|
},
|
||||||
|
|
||||||
initMyProfile() {
|
initMyProfile() {
|
||||||
const accountButton = document.querySelector<HTMLElement & {
|
const accountButton = document.querySelector<
|
||||||
onButtonTap: () => void
|
HTMLElement & {
|
||||||
}>('ytmusic-settings-button');
|
onButtonTap: () => void;
|
||||||
|
}
|
||||||
|
>('ytmusic-settings-button');
|
||||||
|
|
||||||
accountButton?.onButtonTap();
|
accountButton?.onButtonTap();
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
accountButton?.onButtonTap();
|
accountButton?.onButtonTap();
|
||||||
const renderer = document.querySelector<HTMLElement & { data: unknown }>('ytd-active-account-header-renderer');
|
const renderer = document.querySelector<
|
||||||
|
HTMLElement & { data: unknown }
|
||||||
|
>('ytd-active-account-header-renderer');
|
||||||
if (!accountButton || !renderer) {
|
if (!accountButton || !renderer) {
|
||||||
console.warn('Music Together: Cannot find account');
|
console.warn('Music Together: Cannot find account');
|
||||||
this.me = getDefaultProfile(this.connection?.id ?? '');
|
this.me = getDefaultProfile(this.connection?.id ?? '');
|
||||||
@ -543,7 +614,7 @@ export default createPlugin<
|
|||||||
this.me = {
|
this.me = {
|
||||||
handleId: accountData.channelHandle.runs[0].text,
|
handleId: accountData.channelHandle.runs[0].text,
|
||||||
name: accountData.accountName.runs[0].text,
|
name: accountData.accountName.runs[0].text,
|
||||||
thumbnail: accountData.accountPhoto.thumbnails[0].url
|
thumbnail: accountData.accountPhoto.thumbnails[0].url,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (this.me.thumbnail) {
|
if (this.me.thumbnail) {
|
||||||
@ -557,14 +628,23 @@ export default createPlugin<
|
|||||||
|
|
||||||
start({ ipc }) {
|
start({ ipc }) {
|
||||||
this.ipc = ipc;
|
this.ipc = ipc;
|
||||||
this.showPrompt = async (title: string, label: string) => ipc.invoke('music-together:prompt', title, label) as Promise<string>;
|
this.showPrompt = async (title: string, label: string) =>
|
||||||
|
ipc.invoke('music-together:prompt', title, label) as Promise<string>;
|
||||||
this.api = document.querySelector<AppElement>('ytmusic-app');
|
this.api = document.querySelector<AppElement>('ytmusic-app');
|
||||||
|
|
||||||
/* setup */
|
/* setup */
|
||||||
document.querySelector('#right-content > ytmusic-settings-button')?.insertAdjacentHTML('beforebegin', settingHTML);
|
document
|
||||||
const setting = document.querySelector<HTMLElement>('#music-together-setting-button');
|
.querySelector('#right-content > ytmusic-settings-button')
|
||||||
const icon = document.querySelector<SVGElement>('#music-together-setting-button > svg');
|
?.insertAdjacentHTML('beforebegin', settingHTML);
|
||||||
const spinner = document.querySelector<HTMLElement>('#music-together-setting-button > tp-yt-paper-spinner-lite');
|
const setting = document.querySelector<HTMLElement>(
|
||||||
|
'#music-together-setting-button',
|
||||||
|
);
|
||||||
|
const icon = document.querySelector<SVGElement>(
|
||||||
|
'#music-together-setting-button > svg',
|
||||||
|
);
|
||||||
|
const spinner = document.querySelector<HTMLElement>(
|
||||||
|
'#music-together-setting-button > tp-yt-paper-spinner-lite',
|
||||||
|
);
|
||||||
if (!setting || !icon || !spinner) {
|
if (!setting || !icon || !spinner) {
|
||||||
console.warn('Music Together: Cannot inject html');
|
console.warn('Music Together: Cannot inject html');
|
||||||
console.log(setting, icon, spinner);
|
console.log(setting, icon, spinner);
|
||||||
@ -574,7 +654,7 @@ export default createPlugin<
|
|||||||
this.elements = {
|
this.elements = {
|
||||||
setting,
|
setting,
|
||||||
icon,
|
icon,
|
||||||
spinner
|
spinner,
|
||||||
};
|
};
|
||||||
|
|
||||||
this.stateInterval = window.setInterval(() => {
|
this.stateInterval = window.setInterval(() => {
|
||||||
@ -584,7 +664,7 @@ export default createPlugin<
|
|||||||
this.connection.broadcast('SYNC_PROGRESS', {
|
this.connection.broadcast('SYNC_PROGRESS', {
|
||||||
progress: this.playerApi?.getCurrentTime(),
|
progress: this.playerApi?.getCurrentTime(),
|
||||||
state: this.playerApi?.getPlayerState(),
|
state: this.playerApi?.getPlayerState(),
|
||||||
index
|
index,
|
||||||
});
|
});
|
||||||
}, 1000);
|
}, 1000);
|
||||||
|
|
||||||
@ -593,18 +673,25 @@ export default createPlugin<
|
|||||||
onItemClick: (id) => {
|
onItemClick: (id) => {
|
||||||
if (id === 'music-together-close') {
|
if (id === 'music-together-close') {
|
||||||
this.onStop();
|
this.onStop();
|
||||||
this.api?.openToast(t('plugins.music-together.toast.closed'));
|
this.api?.toastService?.show(
|
||||||
|
t('plugins.music-together.toast.closed'),
|
||||||
|
);
|
||||||
hostPopup.dismiss();
|
hostPopup.dismiss();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (id === 'music-together-copy-id') {
|
if (id === 'music-together-copy-id') {
|
||||||
navigator.clipboard.writeText(this.connection?.id ?? '')
|
navigator.clipboard
|
||||||
|
.writeText(this.connection?.id ?? '')
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.api?.openToast(t('plugins.music-together.toast.id-copied'));
|
this.api?.toastService?.show(
|
||||||
|
t('plugins.music-together.toast.id-copied'),
|
||||||
|
);
|
||||||
hostPopup.dismiss();
|
hostPopup.dismiss();
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
this.api?.openToast(t('plugins.music-together.toast.id-copy-failed'));
|
this.api?.toastService?.show(
|
||||||
|
t('plugins.music-together.toast.id-copy-failed'),
|
||||||
|
);
|
||||||
hostPopup.dismiss();
|
hostPopup.dismiss();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -612,30 +699,39 @@ export default createPlugin<
|
|||||||
if (id === 'music-together-permission') {
|
if (id === 'music-together-permission') {
|
||||||
if (this.permission === 'all') this.permission = 'host-only';
|
if (this.permission === 'all') this.permission = 'host-only';
|
||||||
else if (this.permission === 'playlist') this.permission = 'all';
|
else if (this.permission === 'playlist') this.permission = 'all';
|
||||||
else if (this.permission === 'host-only') this.permission = 'playlist';
|
else if (this.permission === 'host-only')
|
||||||
|
this.permission = 'playlist';
|
||||||
this.connection?.broadcast('PERMISSION', this.permission);
|
this.connection?.broadcast('PERMISSION', this.permission);
|
||||||
|
|
||||||
hostPopup.setPermission(this.permission);
|
hostPopup.setPermission(this.permission);
|
||||||
guestPopup.setPermission(this.permission);
|
guestPopup.setPermission(this.permission);
|
||||||
settingPopup.setPermission(this.permission);
|
settingPopup.setPermission(this.permission);
|
||||||
|
|
||||||
const permissionLabel = t(`plugins.music-together.menu.permission.${this.permission}`);
|
const permissionLabel = t(
|
||||||
this.api?.openToast(t('plugins.music-together.toast.permission-changed', { permission: permissionLabel }));
|
`plugins.music-together.menu.permission.${this.permission}`,
|
||||||
|
);
|
||||||
|
this.api?.toastService?.show(
|
||||||
|
t('plugins.music-together.toast.permission-changed', {
|
||||||
|
permission: permissionLabel,
|
||||||
|
}),
|
||||||
|
);
|
||||||
const item = hostPopup.items.find((it) => it?.element.id === id);
|
const item = hostPopup.items.find((it) => it?.element.id === id);
|
||||||
if (item?.type === 'item') {
|
if (item?.type === 'item') {
|
||||||
item.setText(t('plugins.music-together.menu.set-permission'));
|
item.setText(t('plugins.music-together.menu.set-permission'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
const guestPopup = createGuestPopup({
|
const guestPopup = createGuestPopup({
|
||||||
onItemClick: (id) => {
|
onItemClick: (id) => {
|
||||||
if (id === 'music-together-disconnect') {
|
if (id === 'music-together-disconnect') {
|
||||||
this.onStop();
|
this.onStop();
|
||||||
this.api?.openToast(t('plugins.music-together.toast.disconnected'));
|
this.api?.toastService?.show(
|
||||||
|
t('plugins.music-together.toast.disconnected'),
|
||||||
|
);
|
||||||
guestPopup.dismiss();
|
guestPopup.dismiss();
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
const settingPopup = createSettingPopup({
|
const settingPopup = createSettingPopup({
|
||||||
onItemClick: async (id) => {
|
onItemClick: async (id) => {
|
||||||
@ -646,16 +742,24 @@ export default createPlugin<
|
|||||||
this.hideSpinner();
|
this.hideSpinner();
|
||||||
|
|
||||||
if (result) {
|
if (result) {
|
||||||
navigator.clipboard.writeText(this.connection?.id ?? '')
|
navigator.clipboard
|
||||||
|
.writeText(this.connection?.id ?? '')
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.api?.openToast(t('plugins.music-together.toast.id-copied'));
|
this.api?.toastService?.show(
|
||||||
|
t('plugins.music-together.toast.id-copied'),
|
||||||
|
);
|
||||||
hostPopup.showAtAnchor(setting);
|
hostPopup.showAtAnchor(setting);
|
||||||
}).catch(() => {
|
})
|
||||||
this.api?.openToast(t('plugins.music-together.toast.id-copy-failed'));
|
.catch(() => {
|
||||||
|
this.api?.toastService?.show(
|
||||||
|
t('plugins.music-together.toast.id-copy-failed'),
|
||||||
|
);
|
||||||
hostPopup.showAtAnchor(setting);
|
hostPopup.showAtAnchor(setting);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
this.api?.openToast(t('plugins.music-together.toast.host-failed'));
|
this.api?.toastService?.show(
|
||||||
|
t('plugins.music-together.toast.host-failed'),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -666,18 +770,22 @@ export default createPlugin<
|
|||||||
this.hideSpinner();
|
this.hideSpinner();
|
||||||
|
|
||||||
if (result) {
|
if (result) {
|
||||||
this.api?.openToast(t('plugins.music-together.toast.joined'));
|
this.api?.toastService?.show(
|
||||||
|
t('plugins.music-together.toast.joined'),
|
||||||
|
);
|
||||||
guestPopup.showAtAnchor(setting);
|
guestPopup.showAtAnchor(setting);
|
||||||
} else {
|
} else {
|
||||||
this.api?.openToast(t('plugins.music-together.toast.join-failed'));
|
this.api?.toastService?.show(
|
||||||
|
t('plugins.music-together.toast.join-failed'),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
this.popups = {
|
this.popups = {
|
||||||
host: hostPopup,
|
host: hostPopup,
|
||||||
guest: guestPopup,
|
guest: guestPopup,
|
||||||
setting: settingPopup
|
setting: settingPopup,
|
||||||
};
|
};
|
||||||
setting.addEventListener('click', () => {
|
setting.addEventListener('click', () => {
|
||||||
let popup = settingPopup;
|
let popup = settingPopup;
|
||||||
@ -695,24 +803,38 @@ export default createPlugin<
|
|||||||
this.queue = new Queue({
|
this.queue = new Queue({
|
||||||
owner: {
|
owner: {
|
||||||
id: this.connection?.id ?? '',
|
id: this.connection?.id ?? '',
|
||||||
...this.me!
|
...this.me!,
|
||||||
},
|
},
|
||||||
getProfile: (id) => this.profiles[id]
|
getProfile: (id) => this.profiles[id],
|
||||||
});
|
});
|
||||||
this.playerApi = playerApi;
|
this.playerApi = playerApi;
|
||||||
|
|
||||||
this.playerApi.addEventListener('onStateChange', this.videoStateChangeListener);
|
this.playerApi.addEventListener(
|
||||||
|
'onStateChange',
|
||||||
|
this.videoStateChangeListener,
|
||||||
|
);
|
||||||
document.addEventListener('videodatachange', this.videoChangeListener);
|
document.addEventListener('videodatachange', this.videoChangeListener);
|
||||||
},
|
},
|
||||||
stop() {
|
stop() {
|
||||||
const dividers = Array.from(document.querySelectorAll('.music-together-divider'));
|
const dividers = Array.from(
|
||||||
|
document.querySelectorAll('.music-together-divider'),
|
||||||
|
);
|
||||||
dividers.forEach((divider) => divider.remove());
|
dividers.forEach((divider) => divider.remove());
|
||||||
|
|
||||||
this.elements.setting?.remove();
|
this.elements.setting?.remove();
|
||||||
this.onStop();
|
this.onStop();
|
||||||
if (typeof this.stateInterval === 'number') clearInterval(this.stateInterval);
|
if (typeof this.stateInterval === 'number')
|
||||||
if (this.playerApi) this.playerApi.removeEventListener('onStateChange', this.videoStateChangeListener);
|
clearInterval(this.stateInterval);
|
||||||
if (this.videoChangeListener) document.removeEventListener('videodatachange', this.videoChangeListener);
|
if (this.playerApi)
|
||||||
}
|
this.playerApi.removeEventListener(
|
||||||
}
|
'onStateChange',
|
||||||
|
this.videoStateChangeListener,
|
||||||
|
);
|
||||||
|
if (this.videoChangeListener)
|
||||||
|
document.removeEventListener(
|
||||||
|
'videodatachange',
|
||||||
|
this.videoChangeListener,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,11 +1,20 @@
|
|||||||
import { SHA1Hash } from './sha1hash';
|
import { SHA1Hash } from './sha1hash';
|
||||||
|
|
||||||
export const extractToken = (cookie = document.cookie) => cookie.match(/SAPISID=([^;]+);/)?.[1] ?? cookie.match(/__Secure-3PAPISID=([^;]+);/)?.[1];
|
export const extractToken = (cookie = document.cookie) =>
|
||||||
|
cookie.match(/SAPISID=([^;]+);/)?.[1] ??
|
||||||
|
cookie.match(/__Secure-3PAPISID=([^;]+);/)?.[1];
|
||||||
|
|
||||||
export const getHash = async (papisid: string, millis = Date.now(), origin: string = 'https://music.youtube.com') =>
|
export const getHash = async (
|
||||||
(await SHA1Hash(`${millis} ${papisid} ${origin}`)).toLowerCase();
|
papisid: string,
|
||||||
|
millis = Date.now(),
|
||||||
|
origin: string = 'https://music.youtube.com',
|
||||||
|
) => (await SHA1Hash(`${millis} ${papisid} ${origin}`)).toLowerCase();
|
||||||
|
|
||||||
export const getAuthorizationHeader = async (papisid: string, millis = Date.now(), origin: string = 'https://music.youtube.com') => {
|
export const getAuthorizationHeader = async (
|
||||||
|
papisid: string,
|
||||||
|
millis = Date.now(),
|
||||||
|
origin: string = 'https://music.youtube.com',
|
||||||
|
) => {
|
||||||
return `SAPISIDHASH ${millis}_${await getHash(papisid, millis, origin)}`;
|
return `SAPISIDHASH ${millis}_${await getHash(papisid, millis, origin)}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -23,15 +32,17 @@ export const getClient = () => {
|
|||||||
platform: 'DESKTOP',
|
platform: 'DESKTOP',
|
||||||
timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||||
locationInfo: {
|
locationInfo: {
|
||||||
locationPermissionAuthorizationStatus: 'LOCATION_PERMISSION_AUTHORIZATION_STATUS_UNSUPPORTED',
|
locationPermissionAuthorizationStatus:
|
||||||
|
'LOCATION_PERMISSION_AUTHORIZATION_STATUS_UNSUPPORTED',
|
||||||
},
|
},
|
||||||
musicAppInfo: {
|
musicAppInfo: {
|
||||||
pwaInstallabilityStatus: 'PWA_INSTALLABILITY_STATUS_UNKNOWN',
|
pwaInstallabilityStatus: 'PWA_INSTALLABILITY_STATUS_UNKNOWN',
|
||||||
webDisplayMode: 'WEB_DISPLAY_MODE_BROWSER',
|
webDisplayMode: 'WEB_DISPLAY_MODE_BROWSER',
|
||||||
storeDigitalGoodsApiSupportStatus: {
|
storeDigitalGoodsApiSupportStatus: {
|
||||||
playStoreDigitalGoodsApiSupportStatus: 'DIGITAL_GOODS_API_SUPPORT_STATUS_UNSUPPORTED',
|
playStoreDigitalGoodsApiSupportStatus:
|
||||||
|
'DIGITAL_GOODS_API_SUPPORT_STATUS_UNSUPPORTED',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
utcOffsetMinutes: -1 * (new Date()).getTimezoneOffset(),
|
utcOffsetMinutes: -1 * new Date().getTimezoneOffset(),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@ -54,46 +54,46 @@ const getHeaderPayload = (() => {
|
|||||||
title: {
|
title: {
|
||||||
runs: [
|
runs: [
|
||||||
{
|
{
|
||||||
text: t('plugins.music-together.internal.track-source')
|
text: t('plugins.music-together.internal.track-source'),
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
},
|
},
|
||||||
subtitle: {
|
subtitle: {
|
||||||
runs: [
|
runs: [
|
||||||
{
|
{
|
||||||
text: t('plugins.music-together.name')
|
text: t('plugins.music-together.name'),
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
},
|
},
|
||||||
buttons: [
|
buttons: [
|
||||||
{
|
{
|
||||||
chipCloudChipRenderer: {
|
chipCloudChipRenderer: {
|
||||||
style: {
|
style: {
|
||||||
styleType: 'STYLE_TRANSPARENT'
|
styleType: 'STYLE_TRANSPARENT',
|
||||||
},
|
},
|
||||||
text: {
|
text: {
|
||||||
runs: [
|
runs: [
|
||||||
{
|
{
|
||||||
text: t('plugins.music-together.internal.save')
|
text: t('plugins.music-together.internal.save'),
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
},
|
},
|
||||||
navigationEndpoint: {
|
navigationEndpoint: {
|
||||||
saveQueueToPlaylistCommand: {}
|
saveQueueToPlaylistCommand: {},
|
||||||
},
|
},
|
||||||
icon: {
|
icon: {
|
||||||
iconType: 'ADD_TO_PLAYLIST'
|
iconType: 'ADD_TO_PLAYLIST',
|
||||||
},
|
},
|
||||||
accessibilityData: {
|
accessibilityData: {
|
||||||
accessibilityData: {
|
accessibilityData: {
|
||||||
label: t('plugins.music-together.internal.save')
|
label: t('plugins.music-together.internal.save'),
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
isSelected: false,
|
isSelected: false,
|
||||||
uniqueId: t('plugins.music-together.internal.save')
|
uniqueId: t('plugins.music-together.internal.save'),
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -106,7 +106,7 @@ export type QueueOptions = {
|
|||||||
owner?: Profile;
|
owner?: Profile;
|
||||||
queue?: QueueElement;
|
queue?: QueueElement;
|
||||||
getProfile: (id: string) => Profile | undefined;
|
getProfile: (id: string) => Profile | undefined;
|
||||||
}
|
};
|
||||||
export type QueueEventListener = (event: ConnectionEventUnion) => void;
|
export type QueueEventListener = (event: ConnectionEventUnion) => void;
|
||||||
|
|
||||||
export class Queue {
|
export class Queue {
|
||||||
@ -114,7 +114,7 @@ export class Queue {
|
|||||||
|
|
||||||
private originalDispatch?: (obj: {
|
private originalDispatch?: (obj: {
|
||||||
type: string;
|
type: string;
|
||||||
payload?: { items?: QueueItem[] | undefined; };
|
payload?: { items?: QueueItem[] | undefined };
|
||||||
}) => void;
|
}) => void;
|
||||||
|
|
||||||
private internalDispatch = false;
|
private internalDispatch = false;
|
||||||
@ -126,7 +126,8 @@ export class Queue {
|
|||||||
|
|
||||||
constructor(options: QueueOptions) {
|
constructor(options: QueueOptions) {
|
||||||
this.getProfile = options.getProfile;
|
this.getProfile = options.getProfile;
|
||||||
this.queue = options.queue ?? (document.querySelector<QueueElement>('#queue')!);
|
this.queue =
|
||||||
|
options.queue ?? document.querySelector<QueueElement>('#queue')!;
|
||||||
this.owner = options.owner ?? null;
|
this.owner = options.owner ?? null;
|
||||||
this._videoList = options.videoList ?? [];
|
this._videoList = options.videoList ?? [];
|
||||||
}
|
}
|
||||||
@ -139,7 +140,12 @@ export class Queue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get selectedIndex() {
|
get selectedIndex() {
|
||||||
return mapQueueItem((it) => it?.selected, this.queue.queue.store.store.getState().queue.items).findIndex(Boolean) ?? 0;
|
return (
|
||||||
|
mapQueueItem(
|
||||||
|
(it) => it?.selected,
|
||||||
|
this.queue.queue.store.store.getState().queue.items,
|
||||||
|
).findIndex(Boolean) ?? 0
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
get rawItems() {
|
get rawItems() {
|
||||||
@ -162,7 +168,9 @@ export class Queue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async addVideos(videos: VideoData[], index?: number) {
|
async addVideos(videos: VideoData[], index?: number) {
|
||||||
const response = await getMusicQueueRenderer(videos.map((it) => it.videoId));
|
const response = await getMusicQueueRenderer(
|
||||||
|
videos.map((it) => it.videoId),
|
||||||
|
);
|
||||||
if (!response) return false;
|
if (!response) return false;
|
||||||
|
|
||||||
const items = response.queueDatas.map((it) => it?.content).filter(Boolean);
|
const items = response.queueDatas.map((it) => it?.content).filter(Boolean);
|
||||||
@ -173,12 +181,16 @@ export class Queue {
|
|||||||
this.queue?.dispatch({
|
this.queue?.dispatch({
|
||||||
type: 'ADD_ITEMS',
|
type: 'ADD_ITEMS',
|
||||||
payload: {
|
payload: {
|
||||||
nextQueueItemId: this.queue.queue.store.store.getState().queue.nextQueueItemId,
|
nextQueueItemId:
|
||||||
index: index ?? this.queue.queue.store.store.getState().queue.items.length ?? 0,
|
this.queue.queue.store.store.getState().queue.nextQueueItemId,
|
||||||
|
index:
|
||||||
|
index ??
|
||||||
|
this.queue.queue.store.store.getState().queue.items.length ??
|
||||||
|
0,
|
||||||
items,
|
items,
|
||||||
shuffleEnabled: false,
|
shuffleEnabled: false,
|
||||||
shouldAssignIds: true
|
shouldAssignIds: true,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
this.internalDispatch = false;
|
this.internalDispatch = false;
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@ -194,7 +206,7 @@ export class Queue {
|
|||||||
this._videoList.splice(index, 1);
|
this._videoList.splice(index, 1);
|
||||||
this.queue?.dispatch({
|
this.queue?.dispatch({
|
||||||
type: 'REMOVE_ITEM',
|
type: 'REMOVE_ITEM',
|
||||||
payload: index
|
payload: index,
|
||||||
});
|
});
|
||||||
this.internalDispatch = false;
|
this.internalDispatch = false;
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@ -207,7 +219,7 @@ export class Queue {
|
|||||||
this.internalDispatch = true;
|
this.internalDispatch = true;
|
||||||
this.queue?.dispatch({
|
this.queue?.dispatch({
|
||||||
type: 'SET_INDEX',
|
type: 'SET_INDEX',
|
||||||
payload: index
|
payload: index,
|
||||||
});
|
});
|
||||||
this.internalDispatch = false;
|
this.internalDispatch = false;
|
||||||
}
|
}
|
||||||
@ -220,8 +232,8 @@ export class Queue {
|
|||||||
type: 'MOVE_ITEM',
|
type: 'MOVE_ITEM',
|
||||||
payload: {
|
payload: {
|
||||||
fromIndex,
|
fromIndex,
|
||||||
toIndex
|
toIndex,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
this.internalDispatch = false;
|
this.internalDispatch = false;
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@ -234,7 +246,7 @@ export class Queue {
|
|||||||
this.internalDispatch = true;
|
this.internalDispatch = true;
|
||||||
this._videoList = [];
|
this._videoList = [];
|
||||||
this.queue?.dispatch({
|
this.queue?.dispatch({
|
||||||
type: 'CLEAR'
|
type: 'CLEAR',
|
||||||
});
|
});
|
||||||
this.internalDispatch = false;
|
this.internalDispatch = false;
|
||||||
}
|
}
|
||||||
@ -253,7 +265,8 @@ export class Queue {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.originalDispatch) this.queue.queue.store.store.dispatch = this.originalDispatch;
|
if (this.originalDispatch)
|
||||||
|
this.queue.queue.store.store.dispatch = this.originalDispatch;
|
||||||
}
|
}
|
||||||
|
|
||||||
injection() {
|
injection() {
|
||||||
@ -276,40 +289,54 @@ export class Queue {
|
|||||||
if (event.type === 'ADD_ITEMS') {
|
if (event.type === 'ADD_ITEMS') {
|
||||||
if (this.ignoreFlag) {
|
if (this.ignoreFlag) {
|
||||||
this.ignoreFlag = false;
|
this.ignoreFlag = false;
|
||||||
const videoList = mapQueueItem((it) => ({
|
const videoList = mapQueueItem(
|
||||||
videoId: it!.videoId,
|
(it) =>
|
||||||
ownerId: this.owner!.id
|
({
|
||||||
} satisfies VideoData), event.payload!.items!);
|
videoId: it!.videoId,
|
||||||
|
ownerId: this.owner!.id,
|
||||||
|
}) satisfies VideoData,
|
||||||
|
event.payload!.items!,
|
||||||
|
);
|
||||||
const index = this._videoList.length + videoList.length - 1;
|
const index = this._videoList.length + videoList.length - 1;
|
||||||
|
|
||||||
if (videoList.length > 0) {
|
if (videoList.length > 0) {
|
||||||
this.broadcast({ // play
|
this.broadcast({
|
||||||
|
// play
|
||||||
type: 'ADD_SONGS',
|
type: 'ADD_SONGS',
|
||||||
payload: {
|
payload: {
|
||||||
videoList
|
videoList,
|
||||||
},
|
},
|
||||||
after: [
|
after: [
|
||||||
{
|
{
|
||||||
type: 'SYNC_PROGRESS',
|
type: 'SYNC_PROGRESS',
|
||||||
payload: {
|
payload: {
|
||||||
index
|
index,
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else if ((event.payload as {
|
} else if (
|
||||||
items: unknown[];
|
(
|
||||||
}).items.length === 1) {
|
event.payload as {
|
||||||
this.broadcast({ // add playlist
|
items: unknown[];
|
||||||
|
}
|
||||||
|
).items.length === 1
|
||||||
|
) {
|
||||||
|
this.broadcast({
|
||||||
|
// add playlist
|
||||||
type: 'ADD_SONGS',
|
type: 'ADD_SONGS',
|
||||||
payload: {
|
payload: {
|
||||||
// index: (event.payload as any).index,
|
// index: (event.payload as any).index,
|
||||||
videoList: mapQueueItem((it) => ({
|
videoList: mapQueueItem(
|
||||||
videoId: it!.videoId,
|
(it) =>
|
||||||
ownerId: this.owner!.id
|
({
|
||||||
} satisfies VideoData), event.payload!.items!)
|
videoId: it!.videoId,
|
||||||
}
|
ownerId: this.owner!.id,
|
||||||
|
}) satisfies VideoData,
|
||||||
|
event.payload!.items!,
|
||||||
|
),
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -320,13 +347,17 @@ export class Queue {
|
|||||||
this.broadcast({
|
this.broadcast({
|
||||||
type: 'MOVE_SONG',
|
type: 'MOVE_SONG',
|
||||||
payload: {
|
payload: {
|
||||||
fromIndex: (event.payload as {
|
fromIndex: (
|
||||||
fromIndex: number;
|
event.payload as {
|
||||||
}).fromIndex,
|
fromIndex: number;
|
||||||
toIndex: (event.payload as {
|
}
|
||||||
toIndex: number;
|
).fromIndex,
|
||||||
}).toIndex
|
toIndex: (
|
||||||
}
|
event.payload as {
|
||||||
|
toIndex: number;
|
||||||
|
}
|
||||||
|
).toIndex,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -334,8 +365,8 @@ export class Queue {
|
|||||||
this.broadcast({
|
this.broadcast({
|
||||||
type: 'REMOVE_SONG',
|
type: 'REMOVE_SONG',
|
||||||
payload: {
|
payload: {
|
||||||
index: event.payload as number
|
index: event.payload as number,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -343,8 +374,8 @@ export class Queue {
|
|||||||
this.broadcast({
|
this.broadcast({
|
||||||
type: 'SYNC_PROGRESS',
|
type: 'SYNC_PROGRESS',
|
||||||
payload: {
|
payload: {
|
||||||
index: event.payload as number
|
index: event.payload as number,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -355,7 +386,10 @@ export class Queue {
|
|||||||
event.payload = undefined;
|
event.payload = undefined;
|
||||||
}
|
}
|
||||||
if (event.type === 'SET_PLAYER_UI_STATE') {
|
if (event.type === 'SET_PLAYER_UI_STATE') {
|
||||||
if (event.payload as string === 'INACTIVE' && this.videoList.length > 0) {
|
if (
|
||||||
|
(event.payload as string) === 'INACTIVE' &&
|
||||||
|
this.videoList.length > 0
|
||||||
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -370,7 +404,7 @@ export class Queue {
|
|||||||
store: {
|
store: {
|
||||||
...this.queue.queue.store,
|
...this.queue.queue.store,
|
||||||
dispatch: this.originalDispatch,
|
dispatch: this.originalDispatch,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
this.originalDispatch?.call(fakeContext, event);
|
this.originalDispatch?.call(fakeContext, event);
|
||||||
@ -384,20 +418,22 @@ export class Queue {
|
|||||||
this.internalDispatch = true;
|
this.internalDispatch = true;
|
||||||
this.queue.dispatch({
|
this.queue.dispatch({
|
||||||
type: 'HAS_SHOWN_AUTOPLAY',
|
type: 'HAS_SHOWN_AUTOPLAY',
|
||||||
payload: false
|
payload: false,
|
||||||
});
|
});
|
||||||
this.queue.dispatch({
|
this.queue.dispatch({
|
||||||
type: 'SET_HEADER',
|
type: 'SET_HEADER',
|
||||||
payload: getHeaderPayload(),
|
payload: getHeaderPayload(),
|
||||||
});
|
});
|
||||||
this.queue.dispatch({
|
this.queue.dispatch({
|
||||||
type: 'CLEAR_STEERING_CHIPS'
|
type: 'CLEAR_STEERING_CHIPS',
|
||||||
});
|
});
|
||||||
this.internalDispatch = false;
|
this.internalDispatch = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
async syncVideo() {
|
async syncVideo() {
|
||||||
const response = await getMusicQueueRenderer(this._videoList.map((it) => it.videoId));
|
const response = await getMusicQueueRenderer(
|
||||||
|
this._videoList.map((it) => it.videoId),
|
||||||
|
);
|
||||||
if (!response) return false;
|
if (!response) return false;
|
||||||
|
|
||||||
const items = response.queueDatas.map((it) => it.content);
|
const items = response.queueDatas.map((it) => it.content);
|
||||||
@ -407,10 +443,11 @@ export class Queue {
|
|||||||
type: 'UPDATE_ITEMS',
|
type: 'UPDATE_ITEMS',
|
||||||
payload: {
|
payload: {
|
||||||
items: items,
|
items: items,
|
||||||
nextQueueItemId: this.queue.queue.store.store.getState().queue.nextQueueItemId,
|
nextQueueItemId:
|
||||||
|
this.queue.queue.store.store.getState().queue.nextQueueItemId,
|
||||||
shouldAssignIds: true,
|
shouldAssignIds: true,
|
||||||
currentIndex: -1
|
currentIndex: -1,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
this.internalDispatch = false;
|
this.internalDispatch = false;
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@ -425,7 +462,9 @@ export class Queue {
|
|||||||
const allQueue = document.querySelectorAll('#queue');
|
const allQueue = document.querySelectorAll('#queue');
|
||||||
|
|
||||||
allQueue.forEach((queue) => {
|
allQueue.forEach((queue) => {
|
||||||
const list = Array.from(queue?.querySelectorAll<HTMLElement>('ytmusic-player-queue-item') ?? []);
|
const list = Array.from(
|
||||||
|
queue?.querySelectorAll<HTMLElement>('ytmusic-player-queue-item') ?? [],
|
||||||
|
);
|
||||||
|
|
||||||
list.forEach((item, index: number | undefined) => {
|
list.forEach((item, index: number | undefined) => {
|
||||||
if (typeof index !== 'number') return;
|
if (typeof index !== 'number') return;
|
||||||
@ -433,14 +472,19 @@ export class Queue {
|
|||||||
const id = this._videoList[index]?.ownerId;
|
const id = this._videoList[index]?.ownerId;
|
||||||
const data = this.getProfile(id);
|
const data = this.getProfile(id);
|
||||||
|
|
||||||
const profile = item.querySelector<HTMLImageElement>('.music-together-owner') ?? document.createElement('img');
|
const profile =
|
||||||
|
item.querySelector<HTMLImageElement>('.music-together-owner') ??
|
||||||
|
document.createElement('img');
|
||||||
profile.classList.add('music-together-owner');
|
profile.classList.add('music-together-owner');
|
||||||
profile.dataset.id = id;
|
profile.dataset.id = id;
|
||||||
profile.dataset.index = index.toString();
|
profile.dataset.index = index.toString();
|
||||||
|
|
||||||
const name = item.querySelector<HTMLElement>('.music-together-name') ?? document.createElement('div');
|
const name =
|
||||||
|
item.querySelector<HTMLElement>('.music-together-name') ??
|
||||||
|
document.createElement('div');
|
||||||
name.classList.add('music-together-name');
|
name.classList.add('music-together-name');
|
||||||
name.textContent = data?.name ?? t('plugins.music-together.internal.unknown-user');
|
name.textContent =
|
||||||
|
data?.name ?? t('plugins.music-together.internal.unknown-user');
|
||||||
|
|
||||||
if (data) {
|
if (data) {
|
||||||
profile.dataset.thumbnail = data.thumbnail ?? '';
|
profile.dataset.thumbnail = data.thumbnail ?? '';
|
||||||
@ -463,10 +507,14 @@ export class Queue {
|
|||||||
const allQueue = document.querySelectorAll('#queue');
|
const allQueue = document.querySelectorAll('#queue');
|
||||||
|
|
||||||
allQueue.forEach((queue) => {
|
allQueue.forEach((queue) => {
|
||||||
const list = Array.from(queue?.querySelectorAll<HTMLElement>('ytmusic-player-queue-item') ?? []);
|
const list = Array.from(
|
||||||
|
queue?.querySelectorAll<HTMLElement>('ytmusic-player-queue-item') ?? [],
|
||||||
|
);
|
||||||
|
|
||||||
list.forEach((item) => {
|
list.forEach((item) => {
|
||||||
const profile = item.querySelector<HTMLImageElement>('.music-together-owner');
|
const profile = item.querySelector<HTMLImageElement>(
|
||||||
|
'.music-together-owner',
|
||||||
|
);
|
||||||
const name = item.querySelector<HTMLElement>('.music-together-name');
|
const name = item.querySelector<HTMLElement>('.music-together-name');
|
||||||
profile?.remove();
|
profile?.remove();
|
||||||
name?.remove();
|
name?.remove();
|
||||||
|
|||||||
@ -8,7 +8,9 @@ type QueueRendererResponse = {
|
|||||||
trackingParams: string;
|
trackingParams: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getMusicQueueRenderer = async (videoIds: string[]): Promise<QueueRendererResponse | null> => {
|
export const getMusicQueueRenderer = async (
|
||||||
|
videoIds: string[],
|
||||||
|
): Promise<QueueRendererResponse | null> => {
|
||||||
const token = extractToken();
|
const token = extractToken();
|
||||||
if (!token) return null;
|
if (!token) return null;
|
||||||
|
|
||||||
@ -35,8 +37,8 @@ export const getMusicQueueRenderer = async (videoIds: string[]): Promise<QueueRe
|
|||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'Origin': 'https://music.youtube.com',
|
'Origin': 'https://music.youtube.com',
|
||||||
'Authorization': await getAuthorizationHeader(token),
|
'Authorization': await getAuthorizationHeader(token),
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const text = await response.text();
|
const text = await response.text();
|
||||||
|
|||||||
@ -1,21 +1,26 @@
|
|||||||
import {
|
import {
|
||||||
ItemPlaylistPanelVideoRenderer,
|
ItemPlaylistPanelVideoRenderer,
|
||||||
PlaylistPanelVideoWrapperRenderer,
|
PlaylistPanelVideoWrapperRenderer,
|
||||||
QueueItem
|
QueueItem,
|
||||||
} from '@/types/datahost-get-state';
|
} from '@/types/datahost-get-state';
|
||||||
|
|
||||||
export const mapQueueItem = <T>(map: (item?: ItemPlaylistPanelVideoRenderer) => T, array: QueueItem[]): T[] => array
|
export const mapQueueItem = <T>(
|
||||||
.map((item) => {
|
map: (item?: ItemPlaylistPanelVideoRenderer) => T,
|
||||||
if ('playlistPanelVideoWrapperRenderer' in item) {
|
array: QueueItem[],
|
||||||
const keys = Object.keys(item.playlistPanelVideoWrapperRenderer!.primaryRenderer) as (keyof PlaylistPanelVideoWrapperRenderer['primaryRenderer'])[];
|
): T[] =>
|
||||||
return item.playlistPanelVideoWrapperRenderer!.primaryRenderer[keys[0]];
|
array
|
||||||
}
|
.map((item) => {
|
||||||
if ('playlistPanelVideoRenderer' in item) {
|
if ('playlistPanelVideoWrapperRenderer' in item) {
|
||||||
return item.playlistPanelVideoRenderer;
|
const keys = Object.keys(
|
||||||
}
|
item.playlistPanelVideoWrapperRenderer!.primaryRenderer,
|
||||||
|
) as (keyof PlaylistPanelVideoWrapperRenderer['primaryRenderer'])[];
|
||||||
console.error('Music Together: Unknown item', item);
|
return item.playlistPanelVideoWrapperRenderer!.primaryRenderer[keys[0]];
|
||||||
return undefined;
|
}
|
||||||
})
|
if ('playlistPanelVideoRenderer' in item) {
|
||||||
.map(map);
|
return item.playlistPanelVideoRenderer;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error('Music Together: Unknown item', item);
|
||||||
|
return undefined;
|
||||||
|
})
|
||||||
|
.map(map);
|
||||||
|
|||||||
@ -10,13 +10,16 @@ export type VideoData = {
|
|||||||
};
|
};
|
||||||
export type Permission = 'host-only' | 'playlist' | 'all';
|
export type Permission = 'host-only' | 'playlist' | 'all';
|
||||||
|
|
||||||
export const getDefaultProfile = (connectionID: string, id: string = Date.now().toString()): Profile => {
|
export const getDefaultProfile = (
|
||||||
|
connectionID: string,
|
||||||
|
id: string = Date.now().toString(),
|
||||||
|
): Profile => {
|
||||||
const name = `Guest ${id.slice(0, 4)}`;
|
const name = `Guest ${id.slice(0, 4)}`;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: connectionID,
|
id: connectionID,
|
||||||
handleId: `#music-together:${id}`,
|
handleId: `#music-together:${id}`,
|
||||||
name,
|
name,
|
||||||
thumbnail: `https://ui-avatars.com/api/?name=${name}&background=random`
|
thumbnail: `https://ui-avatars.com/api/?name=${name}&background=random`,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@ -7,7 +7,6 @@ import { createStatus } from '../ui/status';
|
|||||||
|
|
||||||
import IconOff from '../icons/off.svg?raw';
|
import IconOff from '../icons/off.svg?raw';
|
||||||
|
|
||||||
|
|
||||||
export type GuestPopupProps = {
|
export type GuestPopupProps = {
|
||||||
onItemClick: (id: string) => void;
|
onItemClick: (id: string) => void;
|
||||||
};
|
};
|
||||||
@ -33,7 +32,7 @@ export const createGuestPopup = (props: GuestPopupProps) => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
anchorAt: 'bottom-right',
|
anchorAt: 'bottom-right',
|
||||||
popupAt: 'top-right'
|
popupAt: 'top-right',
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@ -22,7 +22,7 @@ export const createHostPopup = (props: HostPopupProps) => {
|
|||||||
element: status.element,
|
element: status.element,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'divider'
|
type: 'divider',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'music-together-copy-id',
|
id: 'music-together-copy-id',
|
||||||
@ -35,7 +35,9 @@ export const createHostPopup = (props: HostPopupProps) => {
|
|||||||
id: 'music-together-permission',
|
id: 'music-together-permission',
|
||||||
type: 'item',
|
type: 'item',
|
||||||
icon: ElementFromHtml(IconTune),
|
icon: ElementFromHtml(IconTune),
|
||||||
text: t('plugins.music-together.menu.set-permission', { permission: t('plugins.music-together.menu.permission.host-only') }),
|
text: t('plugins.music-together.menu.set-permission', {
|
||||||
|
permission: t('plugins.music-together.menu.permission.host-only'),
|
||||||
|
}),
|
||||||
onClick: () => props.onItemClick('music-together-permission'),
|
onClick: () => props.onItemClick('music-together-permission'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@ -39,7 +39,7 @@ export const createSettingPopup = (props: SettingPopupProps) => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
anchorAt: 'bottom-right',
|
anchorAt: 'bottom-right',
|
||||||
popupAt: 'top-right'
|
popupAt: 'top-right',
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@ -7,17 +7,27 @@ import type { Permission, Profile } from '../types';
|
|||||||
|
|
||||||
export const createStatus = () => {
|
export const createStatus = () => {
|
||||||
const element = ElementFromHtml(statusHTML);
|
const element = ElementFromHtml(statusHTML);
|
||||||
const icon = document.querySelector<HTMLImageElement>('ytmusic-settings-button > tp-yt-paper-icon-button > tp-yt-iron-icon#icon img');
|
const icon = document.querySelector<HTMLImageElement>(
|
||||||
|
'ytmusic-settings-button > tp-yt-paper-icon-button > tp-yt-iron-icon#icon img',
|
||||||
|
);
|
||||||
|
|
||||||
const profile = element.querySelector<HTMLImageElement>('.music-together-profile')!;
|
const profile = element.querySelector<HTMLImageElement>(
|
||||||
const statusLabel = element.querySelector<HTMLSpanElement>('#music-together-status-label')!;
|
'.music-together-profile',
|
||||||
const permissionLabel = element.querySelector<HTMLMarqueeElement>('#music-together-permission-label')!;
|
)!;
|
||||||
|
const statusLabel = element.querySelector<HTMLSpanElement>(
|
||||||
|
'#music-together-status-label',
|
||||||
|
)!;
|
||||||
|
const permissionLabel = element.querySelector<HTMLMarqueeElement>(
|
||||||
|
'#music-together-permission-label',
|
||||||
|
)!;
|
||||||
|
|
||||||
profile.src = icon?.src ?? '';
|
profile.src = icon?.src ?? '';
|
||||||
|
|
||||||
const setStatus = (status: 'disconnected' | 'host' | 'guest') => {
|
const setStatus = (status: 'disconnected' | 'host' | 'guest') => {
|
||||||
if (status === 'disconnected') {
|
if (status === 'disconnected') {
|
||||||
statusLabel.textContent = t('plugins.music-together.menu.status.disconnected');
|
statusLabel.textContent = t(
|
||||||
|
'plugins.music-together.menu.status.disconnected',
|
||||||
|
);
|
||||||
statusLabel.style.color = 'rgba(255, 255, 255, 0.5)';
|
statusLabel.style.color = 'rgba(255, 255, 255, 0.5)';
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -34,17 +44,23 @@ export const createStatus = () => {
|
|||||||
|
|
||||||
const setPermission = (permission: Permission) => {
|
const setPermission = (permission: Permission) => {
|
||||||
if (permission === 'host-only') {
|
if (permission === 'host-only') {
|
||||||
permissionLabel.textContent = t('plugins.music-together.menu.permission.host-only');
|
permissionLabel.textContent = t(
|
||||||
|
'plugins.music-together.menu.permission.host-only',
|
||||||
|
);
|
||||||
permissionLabel.style.color = 'rgba(255, 255, 255, 0.5)';
|
permissionLabel.style.color = 'rgba(255, 255, 255, 0.5)';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (permission === 'playlist') {
|
if (permission === 'playlist') {
|
||||||
permissionLabel.textContent = t('plugins.music-together.menu.permission.playlist');
|
permissionLabel.textContent = t(
|
||||||
|
'plugins.music-together.menu.permission.playlist',
|
||||||
|
);
|
||||||
permissionLabel.style.color = 'rgba(255, 255, 255, 0.75)';
|
permissionLabel.style.color = 'rgba(255, 255, 255, 0.75)';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (permission === 'all') {
|
if (permission === 'all') {
|
||||||
permissionLabel.textContent = t('plugins.music-together.menu.permission.all');
|
permissionLabel.textContent = t(
|
||||||
|
'plugins.music-together.menu.permission.all',
|
||||||
|
);
|
||||||
permissionLabel.style.color = 'rgba(255, 255, 255, 1)';
|
permissionLabel.style.color = 'rgba(255, 255, 255, 1)';
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -54,7 +70,9 @@ export const createStatus = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const setUsers = (users: Profile[]) => {
|
const setUsers = (users: Profile[]) => {
|
||||||
const container = element.querySelector<HTMLDivElement>('.music-together-user-container')!;
|
const container = element.querySelector<HTMLDivElement>(
|
||||||
|
'.music-together-user-container',
|
||||||
|
)!;
|
||||||
const empty = element.querySelector<HTMLElement>('.music-together-empty')!;
|
const empty = element.querySelector<HTMLElement>('.music-together-empty')!;
|
||||||
for (const child of Array.from(container.children)) {
|
for (const child of Array.from(container.children)) {
|
||||||
if (child !== empty) child.remove();
|
if (child !== empty) child.remove();
|
||||||
|
|||||||
@ -68,7 +68,9 @@ const observer = new MutationObserver(() => {
|
|||||||
if (!menuUrl?.includes('watch?')) {
|
if (!menuUrl?.includes('watch?')) {
|
||||||
menuUrl = undefined;
|
menuUrl = undefined;
|
||||||
// check for podcast
|
// check for podcast
|
||||||
for (const it of document.querySelectorAll('tp-yt-paper-listbox [tabindex="-1"] #navigation-endpoint')) {
|
for (const it of document.querySelectorAll(
|
||||||
|
'tp-yt-paper-listbox [tabindex="-1"] #navigation-endpoint',
|
||||||
|
)) {
|
||||||
if (it.getAttribute('href')?.includes('podcast/')) {
|
if (it.getAttribute('href')?.includes('podcast/')) {
|
||||||
menuUrl = it.getAttribute('href')!;
|
menuUrl = it.getAttribute('href')!;
|
||||||
break;
|
break;
|
||||||
|
|||||||
@ -53,10 +53,7 @@ const observePopupContainer = () => {
|
|||||||
menu = getSongMenu();
|
menu = getSongMenu();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (menu && !menu.contains(slider)) {
|
||||||
menu &&
|
|
||||||
!menu.contains(slider)
|
|
||||||
) {
|
|
||||||
menu.prepend(slider);
|
menu.prepend(slider);
|
||||||
setupSliderListener();
|
setupSliderListener();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,7 +8,6 @@ const ignored = {
|
|||||||
function overrideAddEventListener() {
|
function overrideAddEventListener() {
|
||||||
// YO WHAT ARE YOU DOING NOW?!?!
|
// YO WHAT ARE YOU DOING NOW?!?!
|
||||||
// Save native addEventListener
|
// Save native addEventListener
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-expect-error - We know what we're doing
|
// @ts-expect-error - We know what we're doing
|
||||||
// eslint-disable-next-line @typescript-eslint/unbound-method
|
// eslint-disable-next-line @typescript-eslint/unbound-method
|
||||||
Element.prototype._addEventListener = Element.prototype.addEventListener;
|
Element.prototype._addEventListener = Element.prototype.addEventListener;
|
||||||
|
|||||||
@ -5,13 +5,13 @@ import { onMenu } from './menu';
|
|||||||
import { backend } from './main';
|
import { backend } from './main';
|
||||||
|
|
||||||
export interface ScrobblerPluginConfig {
|
export interface ScrobblerPluginConfig {
|
||||||
enabled: boolean,
|
enabled: boolean;
|
||||||
/**
|
/**
|
||||||
* Attempt to scrobble other video types (e.g. Podcasts, normal YouTube videos)
|
* Attempt to scrobble other video types (e.g. Podcasts, normal YouTube videos)
|
||||||
*
|
*
|
||||||
* @default true
|
* @default true
|
||||||
*/
|
*/
|
||||||
scrobbleOtherMedia: boolean,
|
scrobbleOtherMedia: boolean;
|
||||||
scrobblers: {
|
scrobblers: {
|
||||||
lastfm: {
|
lastfm: {
|
||||||
/**
|
/**
|
||||||
@ -19,53 +19,53 @@ export interface ScrobblerPluginConfig {
|
|||||||
*
|
*
|
||||||
* @default false
|
* @default false
|
||||||
*/
|
*/
|
||||||
enabled: boolean,
|
enabled: boolean;
|
||||||
/**
|
/**
|
||||||
* Token used for authentication
|
* Token used for authentication
|
||||||
*/
|
*/
|
||||||
token: string | undefined,
|
token: string | undefined;
|
||||||
/**
|
/**
|
||||||
* Session key used for scrobbling
|
* Session key used for scrobbling
|
||||||
*/
|
*/
|
||||||
sessionKey: string | undefined,
|
sessionKey: string | undefined;
|
||||||
/**
|
/**
|
||||||
* Root of the Last.fm API
|
* Root of the Last.fm API
|
||||||
*
|
*
|
||||||
* @default 'http://ws.audioscrobbler.com/2.0/'
|
* @default 'http://ws.audioscrobbler.com/2.0/'
|
||||||
*/
|
*/
|
||||||
apiRoot: string,
|
apiRoot: string;
|
||||||
/**
|
/**
|
||||||
* Last.fm api key registered by @semvis123
|
* Last.fm api key registered by @semvis123
|
||||||
*
|
*
|
||||||
* @default '04d76faaac8726e60988e14c105d421a'
|
* @default '04d76faaac8726e60988e14c105d421a'
|
||||||
*/
|
*/
|
||||||
apiKey: string,
|
apiKey: string;
|
||||||
/**
|
/**
|
||||||
* Last.fm api secret registered by @semvis123
|
* Last.fm api secret registered by @semvis123
|
||||||
*
|
*
|
||||||
* @default 'a5d2a36fdf64819290f6982481eaffa2'
|
* @default 'a5d2a36fdf64819290f6982481eaffa2'
|
||||||
*/
|
*/
|
||||||
secret: string,
|
secret: string;
|
||||||
},
|
};
|
||||||
listenbrainz: {
|
listenbrainz: {
|
||||||
/**
|
/**
|
||||||
* Enable ListenBrainz scrobbling
|
* Enable ListenBrainz scrobbling
|
||||||
*
|
*
|
||||||
* @default false
|
* @default false
|
||||||
*/
|
*/
|
||||||
enabled: boolean,
|
enabled: boolean;
|
||||||
/**
|
/**
|
||||||
* Listenbrainz user token
|
* Listenbrainz user token
|
||||||
*/
|
*/
|
||||||
token: string | undefined,
|
token: string | undefined;
|
||||||
/**
|
/**
|
||||||
* Root of the ListenBrainz API
|
* Root of the ListenBrainz API
|
||||||
*
|
*
|
||||||
* @default 'https://api.listenbrainz.org/1/'
|
* @default 'https://api.listenbrainz.org/1/'
|
||||||
*/
|
*/
|
||||||
apiRoot: string,
|
apiRoot: string;
|
||||||
},
|
};
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export const defaultConfig: ScrobblerPluginConfig = {
|
export const defaultConfig: ScrobblerPluginConfig = {
|
||||||
|
|||||||
@ -1,6 +1,9 @@
|
|||||||
import { BrowserWindow } from 'electron';
|
import { BrowserWindow } from 'electron';
|
||||||
|
|
||||||
import registerCallback, { MediaType, type SongInfo } from '@/providers/song-info';
|
import registerCallback, {
|
||||||
|
MediaType,
|
||||||
|
type SongInfo,
|
||||||
|
} from '@/providers/song-info';
|
||||||
import { createBackend } from '@/utils';
|
import { createBackend } from '@/utils';
|
||||||
|
|
||||||
import { LastFmScrobbler } from './services/lastfm';
|
import { LastFmScrobbler } from './services/lastfm';
|
||||||
@ -13,14 +16,23 @@ export type SetConfType = (
|
|||||||
conf: Partial<Omit<ScrobblerPluginConfig, 'enabled'>>,
|
conf: Partial<Omit<ScrobblerPluginConfig, 'enabled'>>,
|
||||||
) => void | Promise<void>;
|
) => void | Promise<void>;
|
||||||
|
|
||||||
export const backend = createBackend<{
|
export const backend = createBackend<
|
||||||
config?: ScrobblerPluginConfig;
|
{
|
||||||
window?: BrowserWindow;
|
config?: ScrobblerPluginConfig;
|
||||||
enabledScrobblers: Map<string, ScrobblerBase>;
|
window?: BrowserWindow;
|
||||||
toggleScrobblers(config: ScrobblerPluginConfig, window: BrowserWindow): void;
|
enabledScrobblers: Map<string, ScrobblerBase>;
|
||||||
createSessions(config: ScrobblerPluginConfig, setConfig: SetConfType): Promise<void>;
|
toggleScrobblers(
|
||||||
setConfig?: SetConfType;
|
config: ScrobblerPluginConfig,
|
||||||
}, ScrobblerPluginConfig>({
|
window: BrowserWindow,
|
||||||
|
): void;
|
||||||
|
createSessions(
|
||||||
|
config: ScrobblerPluginConfig,
|
||||||
|
setConfig: SetConfType,
|
||||||
|
): Promise<void>;
|
||||||
|
setConfig?: SetConfType;
|
||||||
|
},
|
||||||
|
ScrobblerPluginConfig
|
||||||
|
>({
|
||||||
enabledScrobblers: new Map(),
|
enabledScrobblers: new Map(),
|
||||||
|
|
||||||
toggleScrobblers(config: ScrobblerPluginConfig, window: BrowserWindow) {
|
toggleScrobblers(config: ScrobblerPluginConfig, window: BrowserWindow) {
|
||||||
@ -30,7 +42,10 @@ export const backend = createBackend<{
|
|||||||
this.enabledScrobblers.delete('lastfm');
|
this.enabledScrobblers.delete('lastfm');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (config.scrobblers.listenbrainz && config.scrobblers.listenbrainz.enabled) {
|
if (
|
||||||
|
config.scrobblers.listenbrainz &&
|
||||||
|
config.scrobblers.listenbrainz.enabled
|
||||||
|
) {
|
||||||
this.enabledScrobblers.set('listenbrainz', new ListenbrainzScrobbler());
|
this.enabledScrobblers.set('listenbrainz', new ListenbrainzScrobbler());
|
||||||
} else {
|
} else {
|
||||||
this.enabledScrobblers.delete('listenbrainz');
|
this.enabledScrobblers.delete('listenbrainz');
|
||||||
@ -45,12 +60,8 @@ export const backend = createBackend<{
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async start({
|
async start({ getConfig, setConfig, window }) {
|
||||||
getConfig,
|
const config = (this.config = await getConfig());
|
||||||
setConfig,
|
|
||||||
window,
|
|
||||||
}) {
|
|
||||||
const config = this.config = await getConfig();
|
|
||||||
// This will store the timeout that will trigger addScrobble
|
// This will store the timeout that will trigger addScrobble
|
||||||
let scrobbleTimer: NodeJS.Timeout | undefined;
|
let scrobbleTimer: NodeJS.Timeout | undefined;
|
||||||
|
|
||||||
@ -65,21 +76,38 @@ export const backend = createBackend<{
|
|||||||
if (!songInfo.isPaused) {
|
if (!songInfo.isPaused) {
|
||||||
const configNonnull = this.config!;
|
const configNonnull = this.config!;
|
||||||
// Scrobblers normally have no trouble working with official music videos
|
// Scrobblers normally have no trouble working with official music videos
|
||||||
if (!configNonnull.scrobbleOtherMedia && (songInfo.mediaType !== MediaType.Audio && songInfo.mediaType !== MediaType.OriginalMusicVideo)) {
|
if (
|
||||||
|
!configNonnull.scrobbleOtherMedia &&
|
||||||
|
songInfo.mediaType !== MediaType.Audio &&
|
||||||
|
songInfo.mediaType !== MediaType.OriginalMusicVideo
|
||||||
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Scrobble when the song is halfway through, or has passed the 4-minute mark
|
// Scrobble when the song is halfway through, or has passed the 4-minute mark
|
||||||
const scrobbleTime = Math.min(Math.ceil(songInfo.songDuration / 2), 4 * 60);
|
const scrobbleTime = Math.min(
|
||||||
|
Math.ceil(songInfo.songDuration / 2),
|
||||||
|
4 * 60,
|
||||||
|
);
|
||||||
if (scrobbleTime > (songInfo.elapsedSeconds ?? 0)) {
|
if (scrobbleTime > (songInfo.elapsedSeconds ?? 0)) {
|
||||||
// Scrobble still needs to happen
|
// Scrobble still needs to happen
|
||||||
const timeToWait = (scrobbleTime - (songInfo.elapsedSeconds ?? 0)) * 1000;
|
const timeToWait =
|
||||||
scrobbleTimer = setTimeout((info, config) => {
|
(scrobbleTime - (songInfo.elapsedSeconds ?? 0)) * 1000;
|
||||||
this.enabledScrobblers.forEach((scrobbler) => scrobbler.addScrobble(info, config, setConfig));
|
scrobbleTimer = setTimeout(
|
||||||
}, timeToWait, songInfo, configNonnull);
|
(info, config) => {
|
||||||
|
this.enabledScrobblers.forEach((scrobbler) =>
|
||||||
|
scrobbler.addScrobble(info, config, setConfig),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
timeToWait,
|
||||||
|
songInfo,
|
||||||
|
configNonnull,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.enabledScrobblers.forEach((scrobbler) => scrobbler.setNowPlaying(songInfo, configNonnull, setConfig));
|
this.enabledScrobblers.forEach((scrobbler) =>
|
||||||
|
scrobbler.setNowPlaying(songInfo, configNonnull, setConfig),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@ -88,11 +116,15 @@ export const backend = createBackend<{
|
|||||||
this.enabledScrobblers.clear();
|
this.enabledScrobblers.clear();
|
||||||
|
|
||||||
this.toggleScrobblers(newConfig, this.window!);
|
this.toggleScrobblers(newConfig, this.window!);
|
||||||
for (const [scrobblerName, scrobblerConfig] of Object.entries(newConfig.scrobblers)) {
|
for (const [scrobblerName, scrobblerConfig] of Object.entries(
|
||||||
|
newConfig.scrobblers,
|
||||||
|
)) {
|
||||||
if (scrobblerConfig.enabled) {
|
if (scrobblerConfig.enabled) {
|
||||||
const scrobbler = this.enabledScrobblers.get(scrobblerName);
|
const scrobbler = this.enabledScrobblers.get(scrobblerName);
|
||||||
if (
|
if (
|
||||||
this.config?.scrobblers?.[scrobblerName as keyof typeof newConfig.scrobblers]?.enabled !== scrobblerConfig.enabled &&
|
this.config?.scrobblers?.[
|
||||||
|
scrobblerName as keyof typeof newConfig.scrobblers
|
||||||
|
]?.enabled !== scrobblerConfig.enabled &&
|
||||||
scrobbler &&
|
scrobbler &&
|
||||||
!scrobbler.isSessionCreated(newConfig) &&
|
!scrobbler.isSessionCreated(newConfig) &&
|
||||||
this.setConfig
|
this.setConfig
|
||||||
@ -103,6 +135,5 @@ export const backend = createBackend<{
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.config = newConfig;
|
this.config = newConfig;
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -11,7 +11,11 @@ import { SetConfType, backend } from './main';
|
|||||||
import type { MenuContext } from '@/types/contexts';
|
import type { MenuContext } from '@/types/contexts';
|
||||||
import type { MenuTemplate } from '@/menu';
|
import type { MenuTemplate } from '@/menu';
|
||||||
|
|
||||||
async function promptLastFmOptions(options: ScrobblerPluginConfig, setConfig: SetConfType, window: BrowserWindow) {
|
async function promptLastFmOptions(
|
||||||
|
options: ScrobblerPluginConfig,
|
||||||
|
setConfig: SetConfType,
|
||||||
|
window: BrowserWindow,
|
||||||
|
) {
|
||||||
const output = await prompt(
|
const output = await prompt(
|
||||||
{
|
{
|
||||||
title: t('plugins.scrobbler.menu.lastfm.api-settings'),
|
title: t('plugins.scrobbler.menu.lastfm.api-settings'),
|
||||||
@ -22,16 +26,16 @@ async function promptLastFmOptions(options: ScrobblerPluginConfig, setConfig: Se
|
|||||||
label: t('plugins.scrobbler.prompt.lastfm.api-key'),
|
label: t('plugins.scrobbler.prompt.lastfm.api-key'),
|
||||||
value: options.scrobblers.lastfm?.apiKey,
|
value: options.scrobblers.lastfm?.apiKey,
|
||||||
inputAttrs: {
|
inputAttrs: {
|
||||||
type: 'text'
|
type: 'text',
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: t('plugins.scrobbler.prompt.lastfm.api-secret'),
|
label: t('plugins.scrobbler.prompt.lastfm.api-secret'),
|
||||||
value: options.scrobblers.lastfm?.secret,
|
value: options.scrobblers.lastfm?.secret,
|
||||||
inputAttrs: {
|
inputAttrs: {
|
||||||
type: 'text'
|
type: 'text',
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
resizable: true,
|
resizable: true,
|
||||||
height: 360,
|
height: 360,
|
||||||
@ -53,7 +57,11 @@ async function promptLastFmOptions(options: ScrobblerPluginConfig, setConfig: Se
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function promptListenbrainzOptions(options: ScrobblerPluginConfig, setConfig: SetConfType, window: BrowserWindow) {
|
async function promptListenbrainzOptions(
|
||||||
|
options: ScrobblerPluginConfig,
|
||||||
|
setConfig: SetConfType,
|
||||||
|
window: BrowserWindow,
|
||||||
|
) {
|
||||||
const output = await prompt(
|
const output = await prompt(
|
||||||
{
|
{
|
||||||
title: t('plugins.scrobbler.prompt.listenbrainz.token.title'),
|
title: t('plugins.scrobbler.prompt.listenbrainz.token.title'),
|
||||||
|
|||||||
@ -5,9 +5,20 @@ import type { SongInfo } from '@/providers/song-info';
|
|||||||
export abstract class ScrobblerBase {
|
export abstract class ScrobblerBase {
|
||||||
public abstract isSessionCreated(config: ScrobblerPluginConfig): boolean;
|
public abstract isSessionCreated(config: ScrobblerPluginConfig): boolean;
|
||||||
|
|
||||||
public abstract createSession(config: ScrobblerPluginConfig, setConfig: SetConfType): Promise<ScrobblerPluginConfig>;
|
public abstract createSession(
|
||||||
|
config: ScrobblerPluginConfig,
|
||||||
|
setConfig: SetConfType,
|
||||||
|
): Promise<ScrobblerPluginConfig>;
|
||||||
|
|
||||||
public abstract setNowPlaying(songInfo: SongInfo, config: ScrobblerPluginConfig, setConfig: SetConfType): void;
|
public abstract setNowPlaying(
|
||||||
|
songInfo: SongInfo,
|
||||||
|
config: ScrobblerPluginConfig,
|
||||||
|
setConfig: SetConfType,
|
||||||
|
): void;
|
||||||
|
|
||||||
public abstract addScrobble(songInfo: SongInfo, config: ScrobblerPluginConfig, setConfig: SetConfType): void;
|
public abstract addScrobble(
|
||||||
|
songInfo: SongInfo,
|
||||||
|
config: ScrobblerPluginConfig,
|
||||||
|
setConfig: SetConfType,
|
||||||
|
): void;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -81,7 +81,11 @@ export class LastFmScrobbler extends ScrobblerBase {
|
|||||||
return config;
|
return config;
|
||||||
}
|
}
|
||||||
|
|
||||||
override setNowPlaying(songInfo: SongInfo, config: ScrobblerPluginConfig, setConfig: SetConfType): void {
|
override setNowPlaying(
|
||||||
|
songInfo: SongInfo,
|
||||||
|
config: ScrobblerPluginConfig,
|
||||||
|
setConfig: SetConfType,
|
||||||
|
): void {
|
||||||
if (!config.scrobblers.lastfm.sessionKey) {
|
if (!config.scrobblers.lastfm.sessionKey) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -93,7 +97,11 @@ export class LastFmScrobbler extends ScrobblerBase {
|
|||||||
this.postSongDataToAPI(songInfo, config, data, setConfig);
|
this.postSongDataToAPI(songInfo, config, data, setConfig);
|
||||||
}
|
}
|
||||||
|
|
||||||
override addScrobble(songInfo: SongInfo, config: ScrobblerPluginConfig, setConfig: SetConfType): void {
|
override addScrobble(
|
||||||
|
songInfo: SongInfo,
|
||||||
|
config: ScrobblerPluginConfig,
|
||||||
|
setConfig: SetConfType,
|
||||||
|
): void {
|
||||||
if (!config.scrobblers.lastfm.sessionKey) {
|
if (!config.scrobblers.lastfm.sessionKey) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -101,7 +109,9 @@ export class LastFmScrobbler extends ScrobblerBase {
|
|||||||
// This adds one scrobbled song to last.fm
|
// This adds one scrobbled song to last.fm
|
||||||
const data = {
|
const data = {
|
||||||
method: 'track.scrobble',
|
method: 'track.scrobble',
|
||||||
timestamp: Math.trunc((Date.now() - (songInfo.elapsedSeconds ?? 0)) / 1000),
|
timestamp: Math.trunc(
|
||||||
|
(Date.now() - (songInfo.elapsedSeconds ?? 0)) / 1000,
|
||||||
|
),
|
||||||
};
|
};
|
||||||
this.postSongDataToAPI(songInfo, config, data, setConfig);
|
this.postSongDataToAPI(songInfo, config, data, setConfig);
|
||||||
}
|
}
|
||||||
@ -195,8 +205,7 @@ const createApiSig = (parameters: LastFmSongData, secret: string) => {
|
|||||||
// This function creates the api signature, see: https://www.last.fm/api/authspec
|
// This function creates the api signature, see: https://www.last.fm/api/authspec
|
||||||
let sig = '';
|
let sig = '';
|
||||||
|
|
||||||
Object
|
Object.entries(parameters)
|
||||||
.entries(parameters)
|
|
||||||
.sort(([a], [b]) => a.localeCompare(b))
|
.sort(([a], [b]) => a.localeCompare(b))
|
||||||
.forEach(([key, value]) => {
|
.forEach(([key, value]) => {
|
||||||
if (key === 'format') {
|
if (key === 'format') {
|
||||||
@ -212,12 +221,8 @@ const createApiSig = (parameters: LastFmSongData, secret: string) => {
|
|||||||
|
|
||||||
const createToken = async ({
|
const createToken = async ({
|
||||||
scrobblers: {
|
scrobblers: {
|
||||||
lastfm: {
|
lastfm: { apiKey, apiRoot, secret },
|
||||||
apiKey,
|
},
|
||||||
apiRoot,
|
|
||||||
secret,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}: ScrobblerPluginConfig) => {
|
}: ScrobblerPluginConfig) => {
|
||||||
// Creates and stores the auth token
|
// Creates and stores the auth token
|
||||||
const data: {
|
const data: {
|
||||||
@ -240,7 +245,10 @@ const createToken = async ({
|
|||||||
let authWindowOpened = false;
|
let authWindowOpened = false;
|
||||||
let latestAuthResult = false;
|
let latestAuthResult = false;
|
||||||
|
|
||||||
const authenticate = async (config: ScrobblerPluginConfig, mainWindow: BrowserWindow) => {
|
const authenticate = async (
|
||||||
|
config: ScrobblerPluginConfig,
|
||||||
|
mainWindow: BrowserWindow,
|
||||||
|
) => {
|
||||||
return new Promise<boolean>((resolve) => {
|
return new Promise<boolean>((resolve) => {
|
||||||
if (!authWindowOpened) {
|
if (!authWindowOpened) {
|
||||||
authWindowOpened = true;
|
authWindowOpened = true;
|
||||||
@ -266,9 +274,10 @@ const authenticate = async (config: ScrobblerPluginConfig, mainWindow: BrowserWi
|
|||||||
const url = new URL(newUrl);
|
const url = new URL(newUrl);
|
||||||
if (url.hostname.endsWith('last.fm')) {
|
if (url.hostname.endsWith('last.fm')) {
|
||||||
if (url.pathname === '/api/auth') {
|
if (url.pathname === '/api/auth') {
|
||||||
const isApproveScreen = await browserWindow.webContents.executeJavaScript(
|
const isApproveScreen =
|
||||||
'!!document.getElementsByName(\'confirm\').length'
|
(await browserWindow.webContents.executeJavaScript(
|
||||||
) as boolean;
|
"!!document.getElementsByName('confirm').length",
|
||||||
|
)) as boolean;
|
||||||
// successful authentication
|
// successful authentication
|
||||||
if (!isApproveScreen) {
|
if (!isApproveScreen) {
|
||||||
resolve(true);
|
resolve(true);
|
||||||
@ -287,7 +296,7 @@ const authenticate = async (config: ScrobblerPluginConfig, mainWindow: BrowserWi
|
|||||||
dialog.showMessageBox({
|
dialog.showMessageBox({
|
||||||
title: t('plugins.scrobbler.dialog.lastfm.auth-failed.title'),
|
title: t('plugins.scrobbler.dialog.lastfm.auth-failed.title'),
|
||||||
message: t('plugins.scrobbler.dialog.lastfm.auth-failed.message'),
|
message: t('plugins.scrobbler.dialog.lastfm.auth-failed.message'),
|
||||||
type: 'error'
|
type: 'error',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
authWindowOpened = false;
|
authWindowOpened = false;
|
||||||
|
|||||||
@ -29,12 +29,22 @@ export class ListenbrainzScrobbler extends ScrobblerBase {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
override createSession(config: ScrobblerPluginConfig, _setConfig: SetConfType): Promise<ScrobblerPluginConfig> {
|
override createSession(
|
||||||
|
config: ScrobblerPluginConfig,
|
||||||
|
_setConfig: SetConfType,
|
||||||
|
): Promise<ScrobblerPluginConfig> {
|
||||||
return Promise.resolve(config);
|
return Promise.resolve(config);
|
||||||
}
|
}
|
||||||
|
|
||||||
override setNowPlaying(songInfo: SongInfo, config: ScrobblerPluginConfig, _setConfig: SetConfType): void {
|
override setNowPlaying(
|
||||||
if (!config.scrobblers.listenbrainz.apiRoot || !config.scrobblers.listenbrainz.token) {
|
songInfo: SongInfo,
|
||||||
|
config: ScrobblerPluginConfig,
|
||||||
|
_setConfig: SetConfType,
|
||||||
|
): void {
|
||||||
|
if (
|
||||||
|
!config.scrobblers.listenbrainz.apiRoot ||
|
||||||
|
!config.scrobblers.listenbrainz.token
|
||||||
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -42,8 +52,15 @@ export class ListenbrainzScrobbler extends ScrobblerBase {
|
|||||||
submitListen(body, config);
|
submitListen(body, config);
|
||||||
}
|
}
|
||||||
|
|
||||||
override addScrobble(songInfo: SongInfo, config: ScrobblerPluginConfig, _setConfig: SetConfType): void {
|
override addScrobble(
|
||||||
if (!config.scrobblers.listenbrainz.apiRoot || !config.scrobblers.listenbrainz.token) {
|
songInfo: SongInfo,
|
||||||
|
config: ScrobblerPluginConfig,
|
||||||
|
_setConfig: SetConfType,
|
||||||
|
): void {
|
||||||
|
if (
|
||||||
|
!config.scrobblers.listenbrainz.apiRoot ||
|
||||||
|
!config.scrobblers.listenbrainz.token
|
||||||
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -54,7 +71,10 @@ export class ListenbrainzScrobbler extends ScrobblerBase {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function createRequestBody(listenType: string, songInfo: SongInfo): ListenbrainzRequestBody {
|
function createRequestBody(
|
||||||
|
listenType: string,
|
||||||
|
songInfo: SongInfo,
|
||||||
|
): ListenbrainzRequestBody {
|
||||||
const trackMetadata = {
|
const trackMetadata = {
|
||||||
artist_name: songInfo.artist,
|
artist_name: songInfo.artist,
|
||||||
track_name: songInfo.title,
|
track_name: songInfo.title,
|
||||||
@ -64,7 +84,7 @@ function createRequestBody(listenType: string, songInfo: SongInfo): Listenbrainz
|
|||||||
submission_client: 'YouTube Music Desktop App - Scrobbler Plugin',
|
submission_client: 'YouTube Music Desktop App - Scrobbler Plugin',
|
||||||
origin_url: songInfo.url,
|
origin_url: songInfo.url,
|
||||||
duration: songInfo.songDuration,
|
duration: songInfo.songDuration,
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -72,19 +92,23 @@ function createRequestBody(listenType: string, songInfo: SongInfo): Listenbrainz
|
|||||||
payload: [
|
payload: [
|
||||||
{
|
{
|
||||||
track_metadata: trackMetadata,
|
track_metadata: trackMetadata,
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function submitListen(body: ListenbrainzRequestBody, config: ScrobblerPluginConfig) {
|
function submitListen(
|
||||||
net.fetch(config.scrobblers.listenbrainz.apiRoot + 'submit-listens',
|
body: ListenbrainzRequestBody,
|
||||||
{
|
config: ScrobblerPluginConfig,
|
||||||
|
) {
|
||||||
|
net
|
||||||
|
.fetch(config.scrobblers.listenbrainz.apiRoot + 'submit-listens', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify(body),
|
body: JSON.stringify(body),
|
||||||
headers: {
|
headers: {
|
||||||
'Authorization': 'Token ' + config.scrobblers.listenbrainz.token,
|
'Authorization': 'Token ' + config.scrobblers.listenbrainz.token,
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
}
|
},
|
||||||
}).catch(console.error);
|
})
|
||||||
|
.catch(console.error);
|
||||||
}
|
}
|
||||||
|
|||||||
12
src/plugins/shortcuts/mpris-service.d.ts
vendored
12
src/plugins/shortcuts/mpris-service.d.ts
vendored
@ -176,15 +176,17 @@ declare module '@jellybrick/mpris-service' {
|
|||||||
setActivePlaylist(playlistId: string): void;
|
setActivePlaylist(playlistId: string): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface MprisInterface extends dbusInterface.Interface {
|
export interface MprisInterface extends dbusInterface.Interface {
|
||||||
setProperty(property: string, valuePlain: unknown): void;
|
setProperty(property: string, valuePlain: unknown): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface RootInterface {}
|
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
||||||
|
export interface RootInterface {}
|
||||||
|
|
||||||
interface PlayerInterface {}
|
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
||||||
|
export interface PlayerInterface {}
|
||||||
|
|
||||||
interface TracklistInterface {
|
export interface TracklistInterface {
|
||||||
TrackListReplaced(tracks: Track[]): void;
|
TrackListReplaced(tracks: Track[]): void;
|
||||||
|
|
||||||
TrackAdded(afterTrack: string): void;
|
TrackAdded(afterTrack: string): void;
|
||||||
@ -192,7 +194,7 @@ declare module '@jellybrick/mpris-service' {
|
|||||||
TrackRemoved(trackId: string): void;
|
TrackRemoved(trackId: string): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PlaylistsInterface {
|
export interface PlaylistsInterface {
|
||||||
PlaylistChanged(playlist: unknown[]): void;
|
PlaylistChanged(playlist: unknown[]): void;
|
||||||
|
|
||||||
setActivePlaylistId(playlistId: string): void;
|
setActivePlaylistId(playlistId: string): void;
|
||||||
|
|||||||
@ -192,10 +192,13 @@ function registerMPRIS(win: BrowserWindow) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentPosition = queue.items?.findIndex((it) =>
|
const currentPosition =
|
||||||
it?.playlistPanelVideoRenderer?.selected ||
|
queue.items?.findIndex(
|
||||||
it?.playlistPanelVideoWrapperRenderer?.primaryRenderer?.playlistPanelVideoRenderer?.selected
|
(it) =>
|
||||||
) ?? 0;
|
it?.playlistPanelVideoRenderer?.selected ||
|
||||||
|
it?.playlistPanelVideoWrapperRenderer?.primaryRenderer
|
||||||
|
?.playlistPanelVideoRenderer?.selected,
|
||||||
|
) ?? 0;
|
||||||
player.canGoPrevious = currentPosition !== 0;
|
player.canGoPrevious = currentPosition !== 0;
|
||||||
|
|
||||||
let hasNext: boolean;
|
let hasNext: boolean;
|
||||||
|
|||||||
@ -16,20 +16,24 @@ export default createPlugin<
|
|||||||
restartNeeded: false,
|
restartNeeded: false,
|
||||||
renderer: {
|
renderer: {
|
||||||
start() {
|
start() {
|
||||||
waitForElement<HTMLElement>('#dislike-button-renderer').then((dislikeBtn) => {
|
waitForElement<HTMLElement>('#dislike-button-renderer').then(
|
||||||
this.observer = new MutationObserver(() => {
|
(dislikeBtn) => {
|
||||||
if (dislikeBtn?.getAttribute('like-status') == 'DISLIKE') {
|
this.observer = new MutationObserver(() => {
|
||||||
document
|
if (dislikeBtn?.getAttribute('like-status') == 'DISLIKE') {
|
||||||
.querySelector<HTMLButtonElement>('tp-yt-paper-icon-button.next-button')
|
document
|
||||||
?.click();
|
.querySelector<HTMLButtonElement>(
|
||||||
}
|
'tp-yt-paper-icon-button.next-button',
|
||||||
});
|
)
|
||||||
this.observer.observe(dislikeBtn, {
|
?.click();
|
||||||
attributes: true,
|
}
|
||||||
childList: false,
|
});
|
||||||
subtree: false,
|
this.observer.observe(dislikeBtn, {
|
||||||
});
|
attributes: true,
|
||||||
});
|
childList: false,
|
||||||
|
subtree: false,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
},
|
},
|
||||||
stop() {
|
stop() {
|
||||||
this.observer?.disconnect();
|
this.observer?.disconnect();
|
||||||
|
|||||||
@ -1,7 +1,9 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
/* eslint-disable @typescript-eslint/no-require-imports */
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-undef
|
||||||
const { test, expect } = require('@playwright/test');
|
const { test, expect } = require('@playwright/test');
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-undef
|
||||||
const { sortSegments } = require('../segments');
|
const { sortSegments } = require('../segments');
|
||||||
|
|
||||||
test('Segment sorting', () => {
|
test('Segment sorting', () => {
|
||||||
|
|||||||
@ -31,8 +31,12 @@ export const menu = async ({
|
|||||||
type: 'submenu',
|
type: 'submenu',
|
||||||
submenu: [
|
submenu: [
|
||||||
{
|
{
|
||||||
label: t('plugins.synced-lyrics.menu.line-effect.submenu.scale.label'),
|
label: t(
|
||||||
toolTip: t('plugins.synced-lyrics.menu.line-effect.submenu.scale.tooltip'),
|
'plugins.synced-lyrics.menu.line-effect.submenu.scale.label',
|
||||||
|
),
|
||||||
|
toolTip: t(
|
||||||
|
'plugins.synced-lyrics.menu.line-effect.submenu.scale.tooltip',
|
||||||
|
),
|
||||||
type: 'radio',
|
type: 'radio',
|
||||||
checked: config.lineEffect === 'scale',
|
checked: config.lineEffect === 'scale',
|
||||||
click() {
|
click() {
|
||||||
@ -42,8 +46,12 @@ export const menu = async ({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: t('plugins.synced-lyrics.menu.line-effect.submenu.offset.label'),
|
label: t(
|
||||||
toolTip: t('plugins.synced-lyrics.menu.line-effect.submenu.offset.tooltip'),
|
'plugins.synced-lyrics.menu.line-effect.submenu.offset.label',
|
||||||
|
),
|
||||||
|
toolTip: t(
|
||||||
|
'plugins.synced-lyrics.menu.line-effect.submenu.offset.tooltip',
|
||||||
|
),
|
||||||
type: 'radio',
|
type: 'radio',
|
||||||
checked: config.lineEffect === 'offset',
|
checked: config.lineEffect === 'offset',
|
||||||
click() {
|
click() {
|
||||||
@ -53,8 +61,12 @@ export const menu = async ({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: t('plugins.synced-lyrics.menu.line-effect.submenu.focus.label'),
|
label: t(
|
||||||
toolTip: t('plugins.synced-lyrics.menu.line-effect.submenu.focus.tooltip'),
|
'plugins.synced-lyrics.menu.line-effect.submenu.focus.label',
|
||||||
|
),
|
||||||
|
toolTip: t(
|
||||||
|
'plugins.synced-lyrics.menu.line-effect.submenu.focus.tooltip',
|
||||||
|
),
|
||||||
type: 'radio',
|
type: 'radio',
|
||||||
checked: config.lineEffect === 'focus',
|
checked: config.lineEffect === 'focus',
|
||||||
click() {
|
click() {
|
||||||
@ -125,7 +137,9 @@ export const menu = async ({
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: t('plugins.synced-lyrics.menu.show-lyrics-even-if-inexact.label'),
|
label: t('plugins.synced-lyrics.menu.show-lyrics-even-if-inexact.label'),
|
||||||
toolTip: t('plugins.synced-lyrics.menu.show-lyrics-even-if-inexact.tooltip'),
|
toolTip: t(
|
||||||
|
'plugins.synced-lyrics.menu.show-lyrics-even-if-inexact.tooltip',
|
||||||
|
),
|
||||||
type: 'checkbox',
|
type: 'checkbox',
|
||||||
checked: config.showLyricsEvenIfInexact,
|
checked: config.showLyricsEvenIfInexact,
|
||||||
click(item) {
|
click(item) {
|
||||||
|
|||||||
@ -28,7 +28,7 @@ export const LyricsContainer = () => {
|
|||||||
|
|
||||||
const info = getSongInfo();
|
const info = getSongInfo();
|
||||||
await makeLyricsRequest(info).catch((err) => {
|
await makeLyricsRequest(info).catch((err) => {
|
||||||
setError(`${err}`);
|
setError(String(err));
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -14,12 +14,15 @@ import type { SyncedLyricsPluginConfig } from '../types';
|
|||||||
|
|
||||||
export let _ytAPI: YoutubePlayer | null = null;
|
export let _ytAPI: YoutubePlayer | null = null;
|
||||||
|
|
||||||
export const renderer = createRenderer<{
|
export const renderer = createRenderer<
|
||||||
observerCallback: MutationCallback;
|
{
|
||||||
observer?: MutationObserver;
|
observerCallback: MutationCallback;
|
||||||
videoDataChange: () => Promise<void>;
|
observer?: MutationObserver;
|
||||||
updateTimestampInterval?: NodeJS.Timeout | string | number;
|
videoDataChange: () => Promise<void>;
|
||||||
}, SyncedLyricsPluginConfig>({
|
updateTimestampInterval?: NodeJS.Timeout | string | number;
|
||||||
|
},
|
||||||
|
SyncedLyricsPluginConfig
|
||||||
|
>({
|
||||||
onConfigChange(newConfig) {
|
onConfigChange(newConfig) {
|
||||||
setConfig(newConfig);
|
setConfig(newConfig);
|
||||||
},
|
},
|
||||||
@ -57,9 +60,7 @@ export const renderer = createRenderer<{
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.observer ??= new MutationObserver(
|
this.observer ??= new MutationObserver(this.observerCallback);
|
||||||
this.observerCallback,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Force the lyrics tab to be enabled at all times.
|
// Force the lyrics tab to be enabled at all times.
|
||||||
this.observer.disconnect();
|
this.observer.disconnect();
|
||||||
|
|||||||
@ -8,21 +8,16 @@ import { setDebugInfo, setLineLyrics } from '../components/LyricsContainer';
|
|||||||
import type { SongInfo } from '@/providers/song-info';
|
import type { SongInfo } from '@/providers/song-info';
|
||||||
import type { LineLyrics, LRCLIBSearchResponse } from '../../types';
|
import type { LineLyrics, LRCLIBSearchResponse } from '../../types';
|
||||||
|
|
||||||
// prettier-ignore
|
|
||||||
export const [isInstrumental, setIsInstrumental] = createSignal(false);
|
export const [isInstrumental, setIsInstrumental] = createSignal(false);
|
||||||
// prettier-ignore
|
|
||||||
export const [isFetching, setIsFetching] = createSignal(false);
|
export const [isFetching, setIsFetching] = createSignal(false);
|
||||||
// prettier-ignore
|
|
||||||
export const [hadSecondAttempt, setHadSecondAttempt] = createSignal(false);
|
export const [hadSecondAttempt, setHadSecondAttempt] = createSignal(false);
|
||||||
// prettier-ignore
|
|
||||||
export const [differentDuration, setDifferentDuration] = createSignal(false);
|
export const [differentDuration, setDifferentDuration] = createSignal(false);
|
||||||
// eslint-disable-next-line prefer-const
|
|
||||||
|
|
||||||
export const extractTimeAndText = (
|
export const extractTimeAndText = (
|
||||||
line: string,
|
line: string,
|
||||||
index: number,
|
index: number,
|
||||||
): LineLyrics | null => {
|
): LineLyrics | null => {
|
||||||
const groups = /\[(\d+):(\d+)\.(\d+)\](.+)/.exec(line);
|
const groups = /\[(\d+):(\d+)\.(\d+)](.+)/.exec(line);
|
||||||
if (!groups) return null;
|
if (!groups) return null;
|
||||||
|
|
||||||
const [, rMinutes, rSeconds, rMillis, text] = groups;
|
const [, rMinutes, rSeconds, rMillis, text] = groups;
|
||||||
@ -32,14 +27,13 @@ export const extractTimeAndText = (
|
|||||||
parseInt(rMillis),
|
parseInt(rMillis),
|
||||||
];
|
];
|
||||||
|
|
||||||
// prettier-ignore
|
const timeInMs = minutes * 60 * 1000 + seconds * 1000 + millis;
|
||||||
const timeInMs = (minutes * 60 * 1000) + (seconds * 1000) + millis;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
index,
|
index,
|
||||||
timeInMs,
|
timeInMs,
|
||||||
time: `${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}:${millis}`,
|
time: `${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}:${millis}`,
|
||||||
text: text?.trim() ?? config()!.defaultTextString,
|
text: text?.trim() || config()!.defaultTextString,
|
||||||
status: 'upcoming',
|
status: 'upcoming',
|
||||||
duration: 0,
|
duration: 0,
|
||||||
};
|
};
|
||||||
@ -81,7 +75,6 @@ export const getLyricsList = async (
|
|||||||
track_name: songData.title,
|
track_name: songData.title,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
if (songData.album) {
|
if (songData.album) {
|
||||||
query.set('album_name', songData.album);
|
query.set('album_name', songData.album);
|
||||||
}
|
}
|
||||||
@ -94,7 +87,7 @@ export const getLyricsList = async (
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
let data = await response.json() as LRCLIBSearchResponse;
|
let data = (await response.json()) as LRCLIBSearchResponse;
|
||||||
if (!data || !Array.isArray(data)) {
|
if (!data || !Array.isArray(data)) {
|
||||||
setDebugInfo('Unexpected server response.');
|
setDebugInfo('Unexpected server response.');
|
||||||
return null;
|
return null;
|
||||||
@ -124,7 +117,7 @@ export const getLyricsList = async (
|
|||||||
setHadSecondAttempt(true);
|
setHadSecondAttempt(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
const filteredResults = [];
|
const filteredResults: LRCLIBSearchResponse = [];
|
||||||
for (const item of data) {
|
for (const item of data) {
|
||||||
const { artist } = songData;
|
const { artist } = songData;
|
||||||
const { artistName } = item;
|
const { artistName } = item;
|
||||||
@ -133,7 +126,10 @@ export const getLyricsList = async (
|
|||||||
const itemArtists = artistName.split(/[&,]/g).map((i) => i.trim());
|
const itemArtists = artistName.split(/[&,]/g).map((i) => i.trim());
|
||||||
|
|
||||||
const permutations = artists.flatMap((artistA) =>
|
const permutations = artists.flatMap((artistA) =>
|
||||||
itemArtists.map((artistB) => [artistA.toLowerCase(), artistB.toLowerCase()])
|
itemArtists.map((artistB) => [
|
||||||
|
artistA.toLowerCase(),
|
||||||
|
artistB.toLowerCase(),
|
||||||
|
]),
|
||||||
);
|
);
|
||||||
|
|
||||||
const ratio = Math.max(...permutations.map(([x, y]) => jaroWinkler(x, y)));
|
const ratio = Math.max(...permutations.map(([x, y]) => jaroWinkler(x, y)));
|
||||||
@ -154,7 +150,7 @@ export const getLyricsList = async (
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
setDebugInfo(JSON.stringify(closestResult, null, 4));
|
setDebugInfo(JSON.stringify(closestResult, null, 4));
|
||||||
|
|
||||||
if (Math.abs(closestResult.duration - duration) > 15) {
|
if (Math.abs(closestResult.duration - duration) > 15) {
|
||||||
return null;
|
return null;
|
||||||
@ -179,8 +175,8 @@ export const getLyricsList = async (
|
|||||||
// Add a blank line at the beginning
|
// Add a blank line at the beginning
|
||||||
raw.unshift('[0:0.0] ');
|
raw.unshift('[0:0.0] ');
|
||||||
|
|
||||||
const syncedLyricList = raw.reduce<LineLyrics[]>((acc, line, index) => {
|
const syncedLyricList = raw.reduce<LineLyrics[]>((acc, line) => {
|
||||||
const syncedLine = extractTimeAndText(line, index);
|
const syncedLine = extractTimeAndText(line, acc.length);
|
||||||
if (syncedLine) {
|
if (syncedLine) {
|
||||||
acc.push(syncedLine);
|
acc.push(syncedLine);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,8 +7,11 @@ import type { SyncedLyricsPluginConfig } from '../types';
|
|||||||
|
|
||||||
export const [isVisible, setIsVisible] = createSignal<boolean>(false);
|
export const [isVisible, setIsVisible] = createSignal<boolean>(false);
|
||||||
|
|
||||||
export const [config, setConfig] = createSignal<SyncedLyricsPluginConfig | null>(null);
|
export const [config, setConfig] =
|
||||||
export const [playerState, setPlayerState] = createSignal<VideoDetails | null>(null);
|
createSignal<SyncedLyricsPluginConfig | null>(null);
|
||||||
|
export const [playerState, setPlayerState] = createSignal<VideoDetails | null>(
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
|
||||||
export const LyricsRenderer = () => {
|
export const LyricsRenderer = () => {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { nativeImage } from 'electron';
|
import { type NativeImage, nativeImage, nativeTheme } from 'electron';
|
||||||
|
import { Jimp, JimpMime } from 'jimp';
|
||||||
|
|
||||||
import playIcon from '@assets/media-icons-black/play.png?asset&asarUnpack';
|
import playIcon from '@assets/media-icons-black/play.png?asset&asarUnpack';
|
||||||
import pauseIcon from '@assets/media-icons-black/pause.png?asset&asarUnpack';
|
import pauseIcon from '@assets/media-icons-black/pause.png?asset&asarUnpack';
|
||||||
@ -19,48 +20,13 @@ export default createPlugin({
|
|||||||
enabled: false,
|
enabled: false,
|
||||||
},
|
},
|
||||||
|
|
||||||
backend({ window }) {
|
async backend({ window }) {
|
||||||
let currentSongInfo: SongInfo;
|
let currentSongInfo: SongInfo;
|
||||||
|
|
||||||
const { playPause, next, previous } = getSongControls(window);
|
const { playPause, next, previous } = getSongControls(window);
|
||||||
|
|
||||||
const setThumbar = (songInfo: SongInfo) => {
|
|
||||||
// Wait for song to start before setting thumbar
|
|
||||||
if (!songInfo?.title) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Win32 require full rewrite of components
|
|
||||||
window.setThumbarButtons([
|
|
||||||
{
|
|
||||||
tooltip: 'Previous',
|
|
||||||
icon: nativeImage.createFromPath(get('previous')),
|
|
||||||
click() {
|
|
||||||
previous();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
tooltip: 'Play/Pause',
|
|
||||||
// Update icon based on play state
|
|
||||||
icon: nativeImage.createFromPath(
|
|
||||||
songInfo.isPaused ? get('play') : get('pause'),
|
|
||||||
),
|
|
||||||
click() {
|
|
||||||
playPause();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
tooltip: 'Next',
|
|
||||||
icon: nativeImage.createFromPath(get('next')),
|
|
||||||
click() {
|
|
||||||
next();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Util
|
// Util
|
||||||
const get = (kind: keyof typeof mediaIcons): string => {
|
const getImagePath = (kind: keyof typeof mediaIcons): string => {
|
||||||
switch (kind) {
|
switch (kind) {
|
||||||
case 'play':
|
case 'play':
|
||||||
return playIcon;
|
return playIcon;
|
||||||
@ -75,6 +41,67 @@ export default createPlugin({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getNativeImage = async (
|
||||||
|
kind: keyof typeof mediaIcons,
|
||||||
|
): Promise<NativeImage> => {
|
||||||
|
const imagePath = getImagePath(kind);
|
||||||
|
|
||||||
|
if (imagePath) {
|
||||||
|
console.log('imagePath', imagePath);
|
||||||
|
const jimpImageBuffer = await Jimp.read(imagePath).then((img) => {
|
||||||
|
if (imagePath && nativeTheme.shouldUseDarkColors) {
|
||||||
|
return img.invert().getBuffer(JimpMime.png);
|
||||||
|
}
|
||||||
|
return img.getBuffer(JimpMime.png);
|
||||||
|
});
|
||||||
|
|
||||||
|
return nativeImage.createFromBuffer(jimpImageBuffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
// return empty image
|
||||||
|
return nativeImage.createEmpty();
|
||||||
|
};
|
||||||
|
|
||||||
|
const images = {
|
||||||
|
play: await getNativeImage('play'),
|
||||||
|
pause: await getNativeImage('pause'),
|
||||||
|
next: await getNativeImage('next'),
|
||||||
|
previous: await getNativeImage('previous'),
|
||||||
|
};
|
||||||
|
|
||||||
|
const setThumbar = (songInfo: SongInfo) => {
|
||||||
|
// Wait for song to start before setting thumbar
|
||||||
|
if (!songInfo?.title) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Win32 require full rewrite of components
|
||||||
|
window.setThumbarButtons([
|
||||||
|
{
|
||||||
|
tooltip: 'Previous',
|
||||||
|
icon: images.previous,
|
||||||
|
click() {
|
||||||
|
previous();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tooltip: 'Play/Pause',
|
||||||
|
// Update icon based on play state
|
||||||
|
icon: songInfo.isPaused ? images.play : images.pause,
|
||||||
|
click() {
|
||||||
|
playPause();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tooltip: 'Next',
|
||||||
|
icon: images.next,
|
||||||
|
click() {
|
||||||
|
next();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
|
||||||
registerCallback((songInfo) => {
|
registerCallback((songInfo) => {
|
||||||
// Update currentsonginfo for win.on('show')
|
// Update currentsonginfo for win.on('show')
|
||||||
currentSongInfo = songInfo;
|
currentSongInfo = songInfo;
|
||||||
@ -83,8 +110,6 @@ export default createPlugin({
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Need to set thumbar again after win.show
|
// Need to set thumbar again after win.show
|
||||||
window.on('show', () => {
|
window.on('show', () => setThumbar(currentSongInfo));
|
||||||
setThumbar(currentSongInfo);
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user