mirror of
https://github.com/th-ch/youtube-music.git
synced 2026-01-19 14:12:07 +00:00
Compare commits
60 Commits
f1d264a6c7
...
789a30312b
| Author | SHA1 | Date | |
|---|---|---|---|
| 789a30312b | |||
| 623d97b1e2 | |||
| 4ed97f0145 | |||
| 77a2bbf02a | |||
| 7a9a1531d4 | |||
| 16b59698d6 | |||
| a85a2e0c58 | |||
| 97f1a20a4f | |||
| 8dbe151ddd | |||
| 87144e03c2 | |||
| 2046b253e3 | |||
| c068e11fc5 | |||
| 10384b6c4c | |||
| 4d83bd587d | |||
| aae523989b | |||
| afacec973b | |||
| 75fb51e290 | |||
| cb85048af4 | |||
| 8b10872e83 | |||
| 96ea114335 | |||
| 7c1c3ef28d | |||
| 7a7ad4261c | |||
| c0dbc204a0 | |||
| 68e63f809c | |||
| 4b188ec205 | |||
| 23013cddb9 | |||
| 588b84ecd0 | |||
| fd68c204f6 | |||
| 313bb6e43f | |||
| 0a6f244035 | |||
| 8e4e2c42f6 | |||
| f31053cf3c | |||
| d5758790c0 | |||
| bbd243a534 | |||
| 2fc0d6f3b0 | |||
| 6b15018a9b | |||
| acc977db7c | |||
| 270100a14c | |||
| 094e6fa2d6 | |||
| 9f81f7001c | |||
| e6c78dd5e0 | |||
| b64e1394ae | |||
| dcc611c7d0 | |||
| d329076b52 | |||
| 1435559a56 | |||
| 1feeeedf10 | |||
| 443d716e45 | |||
| 3d65a96e38 | |||
| e20f3fe24c | |||
| 5d3afb52d8 | |||
| 5e0341c8d5 | |||
| b84a8c512a | |||
| cd0f4bbc1d | |||
| e37367c5e5 | |||
| 8a765be912 | |||
| a213dae14d | |||
| 40c429f3c1 | |||
| 3125520e68 | |||
| 1c57fec016 | |||
| 2708b4fffc |
12
.github/workflows/build.yml
vendored
12
.github/workflows/build.yml
vendored
@ -18,7 +18,7 @@ jobs:
|
|||||||
os: [ macos-latest, ubuntu-latest, windows-latest ]
|
os: [ macos-latest, ubuntu-latest, windows-latest ]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
|
|
||||||
- name: Install pnpm
|
- name: Install pnpm
|
||||||
uses: pnpm/action-setup@v4
|
uses: pnpm/action-setup@v4
|
||||||
@ -28,14 +28,14 @@ jobs:
|
|||||||
|
|
||||||
- name: Setup NodeJS
|
- name: Setup NodeJS
|
||||||
if: startsWith(matrix.os, 'macOS') != true
|
if: startsWith(matrix.os, 'macOS') != true
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v5
|
||||||
with:
|
with:
|
||||||
node-version: ${{ env.NODE_VERSION }}
|
node-version: ${{ env.NODE_VERSION }}
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
|
|
||||||
- name: Setup NodeJS for macOS
|
- name: Setup NodeJS for macOS
|
||||||
if: startsWith(matrix.os, 'macOS')
|
if: startsWith(matrix.os, 'macOS')
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v5
|
||||||
with:
|
with:
|
||||||
node-version: ${{ env.NODE_VERSION }}
|
node-version: ${{ env.NODE_VERSION }}
|
||||||
|
|
||||||
@ -91,7 +91,7 @@ jobs:
|
|||||||
if: github.repository == 'th-ch/youtube-music' && github.ref == 'refs/heads/master'
|
if: github.repository == 'th-ch/youtube-music' && github.ref == 'refs/heads/master'
|
||||||
needs: build
|
needs: build
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
@ -103,14 +103,14 @@ jobs:
|
|||||||
|
|
||||||
- name: Setup NodeJS
|
- name: Setup NodeJS
|
||||||
if: startsWith(matrix.os, 'macOS') != true
|
if: startsWith(matrix.os, 'macOS') != true
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v5
|
||||||
with:
|
with:
|
||||||
node-version: ${{ env.NODE_VERSION }}
|
node-version: ${{ env.NODE_VERSION }}
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
|
|
||||||
- name: Setup NodeJS for macOS
|
- name: Setup NodeJS for macOS
|
||||||
if: startsWith(matrix.os, 'macOS')
|
if: startsWith(matrix.os, 'macOS')
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v5
|
||||||
with:
|
with:
|
||||||
node-version: ${{ env.NODE_VERSION }}
|
node-version: ${{ env.NODE_VERSION }}
|
||||||
|
|
||||||
|
|||||||
3
.github/workflows/dependency-review.yml
vendored
3
.github/workflows/dependency-review.yml
vendored
@ -15,6 +15,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: "Checkout Repository"
|
- name: "Checkout Repository"
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
- name: "Dependency Review"
|
- name: "Dependency Review"
|
||||||
uses: actions/dependency-review-action@v4
|
uses: actions/dependency-review-action@v4
|
||||||
|
|||||||
41
.github/workflows/reviewdog.yml
vendored
Normal file
41
.github/workflows/reviewdog.yml
vendored
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
name: reviewdog
|
||||||
|
|
||||||
|
on: [pull_request_target]
|
||||||
|
|
||||||
|
env:
|
||||||
|
NODE_VERSION: "22.x"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
eslint:
|
||||||
|
name: runner / eslint
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
pull-requests: write
|
||||||
|
checks: write
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v5
|
||||||
|
with:
|
||||||
|
ref: ${{ github.event.pull_request.head.sha }}
|
||||||
|
|
||||||
|
- name: Install pnpm
|
||||||
|
uses: pnpm/action-setup@v4
|
||||||
|
with:
|
||||||
|
version: 10
|
||||||
|
run_install: false
|
||||||
|
|
||||||
|
- name: Setup NodeJS
|
||||||
|
uses: actions/setup-node@v5
|
||||||
|
with:
|
||||||
|
node-version: ${{ env.NODE_VERSION }}
|
||||||
|
cache: 'pnpm'
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
- uses: reviewdog/action-eslint@v1.34.0
|
||||||
|
with:
|
||||||
|
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
reporter: github-pr-review # Change reporter.
|
||||||
|
eslint_flags: './src'
|
||||||
|
fail_level: error
|
||||||
@ -478,7 +478,7 @@
|
|||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<div class="footer-copyright">© 2024 th-ch</div>
|
<div class="footer-copyright">© 2025 th-ch</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -31,11 +31,19 @@ export default tsEslint.config(
|
|||||||
rules: {
|
rules: {
|
||||||
'stylistic/arrow-parens': ['error', 'always'],
|
'stylistic/arrow-parens': ['error', 'always'],
|
||||||
'stylistic/object-curly-spacing': ['error', 'always'],
|
'stylistic/object-curly-spacing': ['error', 'always'],
|
||||||
|
'stylistic/jsx-pascal-case': 'error',
|
||||||
|
'stylistic/jsx-curly-spacing': ['error', { when: 'never', children: true }],
|
||||||
|
'stylistic/jsx-sort-props': 'error',
|
||||||
'prettier/prettier': ['error', { singleQuote: true, semi: true, tabWidth: 2, trailingComma: 'all', quoteProps: 'preserve' }],
|
'prettier/prettier': ['error', { singleQuote: true, semi: true, tabWidth: 2, trailingComma: 'all', quoteProps: 'preserve' }],
|
||||||
'@typescript-eslint/no-floating-promises': 'off',
|
'@typescript-eslint/no-floating-promises': 'off',
|
||||||
'@typescript-eslint/no-misused-promises': ['off', { checksVoidReturn: false }],
|
'@typescript-eslint/no-misused-promises': ['off', { checksVoidReturn: false }],
|
||||||
'@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }],
|
'@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }],
|
||||||
'@typescript-eslint/no-non-null-assertion': 'off',
|
'@typescript-eslint/no-non-null-assertion': 'off',
|
||||||
|
'@typescript-eslint/consistent-type-imports': ['error', {
|
||||||
|
fixStyle: 'inline-type-imports',
|
||||||
|
prefer: 'type-imports',
|
||||||
|
disallowTypeAnnotations: false,
|
||||||
|
}],
|
||||||
'importPlugin/first': 'error',
|
'importPlugin/first': 'error',
|
||||||
'importPlugin/newline-after-import': 'off',
|
'importPlugin/newline-after-import': 'off',
|
||||||
'importPlugin/no-default-export': 'off',
|
'importPlugin/no-default-export': 'off',
|
||||||
|
|||||||
78
package.json
78
package.json
@ -14,16 +14,16 @@
|
|||||||
"url": "https://github.com/th-ch/youtube-music"
|
"url": "https://github.com/th-ch/youtube-music"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "playwright test",
|
"test": "pnpm playwright test",
|
||||||
"test:debug": "cross-env DEBUG=pw:*,-pw:test:protocol playwright test",
|
"test:debug": "pnpm cross-env DEBUG=pw:*,-pw:test:protocol playwright test",
|
||||||
"build": "electron-vite build",
|
"build": "pnpm electron-vite build",
|
||||||
"vite:inspect": "pnpm clean && electron-vite build --mode development && pnpm exec serve .vite-inspect",
|
"vite:inspect": "pnpm clean && electron-vite build --mode development && pnpm exec serve .vite-inspect",
|
||||||
"start": "electron-vite preview",
|
"start": "pnpm electron-vite preview",
|
||||||
"start:debug": "cross-env ELECTRON_ENABLE_LOGGING=1 pnpm start",
|
"start:debug": "pnpm cross-env ELECTRON_ENABLE_LOGGING=1 pnpm start",
|
||||||
"dev": "cross-env NODE_OPTIONS=--enable-source-maps electron-vite dev --watch",
|
"dev": "pnpm cross-env NODE_OPTIONS=--enable-source-maps electron-vite dev --watch",
|
||||||
"dev:renderer": "cross-env NODE_OPTIONS=--enable-source-maps electron-vite dev",
|
"dev:renderer": "pnpm cross-env NODE_OPTIONS=--enable-source-maps electron-vite dev",
|
||||||
"dev:debug": "cross-env ELECTRON_ENABLE_LOGGING=1 pnpm dev",
|
"dev:debug": "pnpm cross-env ELECTRON_ENABLE_LOGGING=1 pnpm dev",
|
||||||
"clean": "del-cli dist && del-cli pack && del-cli .vite-inspect",
|
"clean": "pnpm del-cli dist && pnpm del-cli pack && pnpm del-cli .vite-inspect",
|
||||||
"dist": "pnpm clean && pnpm build && pnpm electron-builder --win --mac --linux -p never",
|
"dist": "pnpm clean && pnpm build && pnpm electron-builder --win --mac --linux -p never",
|
||||||
"dist:linux": "pnpm clean && pnpm build && pnpm electron-builder --linux -p never",
|
"dist:linux": "pnpm clean && pnpm build && pnpm electron-builder --linux -p never",
|
||||||
"dist:linux:deb-arm64": "pnpm clean && pnpm build && pnpm electron-builder --linux deb:arm64 -p never",
|
"dist:linux:deb-arm64": "pnpm clean && pnpm build && pnpm electron-builder --linux deb:arm64 -p never",
|
||||||
@ -32,12 +32,12 @@
|
|||||||
"dist:mac:arm64": "pnpm clean && pnpm build && pnpm electron-builder --mac dmg:arm64 -p never",
|
"dist:mac:arm64": "pnpm clean && pnpm build && pnpm electron-builder --mac dmg:arm64 -p never",
|
||||||
"dist:win": "pnpm clean && pnpm build && pnpm electron-builder --win -p never",
|
"dist:win": "pnpm clean && pnpm build && pnpm electron-builder --win -p never",
|
||||||
"dist:win:x64": "pnpm clean && pnpm build && pnpm electron-builder --win nsis-web:x64 -p never",
|
"dist:win:x64": "pnpm clean && pnpm build && pnpm electron-builder --win nsis-web:x64 -p never",
|
||||||
"lint": "eslint .",
|
"lint": "pnpm eslint ./src",
|
||||||
"changelog": "npx --yes auto-changelog",
|
"changelog": "pnpm dlx --yes auto-changelog",
|
||||||
"release:linux": "pnpm clean && pnpm build && pnpm electron-builder --linux -p always -c.snap.publish=github",
|
"release:linux": "pnpm clean && pnpm build && pnpm electron-builder --linux -p always -c.snap.publish=github",
|
||||||
"release:mac": "pnpm clean && pnpm build && pnpm electron-builder --mac -p always",
|
"release:mac": "pnpm clean && pnpm build && pnpm electron-builder --mac -p always",
|
||||||
"release:win": "pnpm clean && pnpm build && pnpm electron-builder --win -p always",
|
"release:win": "pnpm clean && pnpm build && pnpm electron-builder --win -p always",
|
||||||
"typecheck": "tsc -p tsconfig.json --noEmit"
|
"typecheck": "pnpm tsc -p tsconfig.json --noEmit"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=22",
|
"node": ">=22",
|
||||||
@ -45,30 +45,34 @@
|
|||||||
},
|
},
|
||||||
"pnpm": {
|
"pnpm": {
|
||||||
"overrides": {
|
"overrides": {
|
||||||
"vite": "npm:rolldown-vite@7.1.2",
|
"vite": "npm:rolldown-vite@7.1.5",
|
||||||
"node-gyp": "11.3.0",
|
"node-gyp": "11.4.2",
|
||||||
"xml2js": "0.6.2",
|
"xml2js": "0.6.2",
|
||||||
"node-fetch": "3.3.2",
|
"node-fetch": "3.3.2",
|
||||||
"@electron/universal": "3.0.1",
|
"@electron/universal": "3.0.1",
|
||||||
"@babel/runtime": "7.28.3"
|
"@babel/runtime": "7.28.4"
|
||||||
},
|
},
|
||||||
"patchedDependencies": {
|
"patchedDependencies": {
|
||||||
"vudio@2.1.1": "patches/vudio@2.1.1.patch",
|
"vudio@2.1.1": "patches/vudio@2.1.1.patch",
|
||||||
"@malept/flatpak-bundler@0.4.0": "patches/@malept__flatpak-bundler@0.4.0.patch",
|
"@malept/flatpak-bundler@0.4.0": "patches/@malept__flatpak-bundler@0.4.0.patch",
|
||||||
"kuromoji@0.1.2": "patches/kuromoji@0.1.2.patch",
|
"kuromoji@0.1.2": "patches/kuromoji@0.1.2.patch",
|
||||||
"file-type@16.5.4": "patches/file-type@16.5.4.patch"
|
"file-type@16.5.4": "patches/file-type@16.5.4.patch",
|
||||||
|
"electron-is@3.0.0": "patches/electron-is@3.0.0.patch"
|
||||||
},
|
},
|
||||||
"neverBuiltDependencies": []
|
"neverBuiltDependencies": []
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@dehoist/romanize-thai": "1.0.0",
|
||||||
|
"@electron-toolkit/tsconfig": "1.0.1",
|
||||||
"@electron/remote": "2.1.3",
|
"@electron/remote": "2.1.3",
|
||||||
"@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.7.3",
|
"@floating-ui/dom": "1.7.4",
|
||||||
"@foobar404/wave": "2.0.5",
|
"@foobar404/wave": "2.0.5",
|
||||||
"@ghostery/adblocker-electron": "2.11.3",
|
"@ghostery/adblocker-electron": "2.11.6",
|
||||||
"@ghostery/adblocker-electron-preload": "2.11.3",
|
"@ghostery/adblocker-electron-preload": "2.11.6",
|
||||||
"@hono/node-server": "1.18.2",
|
"@hono/node-server": "1.19.1",
|
||||||
|
"@hono/node-ws": "1.2.0",
|
||||||
"@hono/swagger-ui": "0.5.2",
|
"@hono/swagger-ui": "0.5.2",
|
||||||
"@hono/zod-openapi": "1.1.0",
|
"@hono/zod-openapi": "1.1.0",
|
||||||
"@hono/zod-validator": "0.7.2",
|
"@hono/zod-validator": "0.7.2",
|
||||||
@ -100,10 +104,10 @@
|
|||||||
"filenamify": "6.0.0",
|
"filenamify": "6.0.0",
|
||||||
"hanja": "1.1.5",
|
"hanja": "1.1.5",
|
||||||
"happy-dom": "18.0.1",
|
"happy-dom": "18.0.1",
|
||||||
"hono": "4.9.2",
|
"hono": "4.9.6",
|
||||||
"howler": "2.2.4",
|
"howler": "2.2.4",
|
||||||
"html-to-text": "9.0.5",
|
"html-to-text": "9.0.5",
|
||||||
"i18next": "25.3.6",
|
"i18next": "25.5.2",
|
||||||
"jimp": "1.6.0",
|
"jimp": "1.6.0",
|
||||||
"keyboardevent-from-electron-accelerator": "2.0.0",
|
"keyboardevent-from-electron-accelerator": "2.0.0",
|
||||||
"keyboardevents-areequal": "0.2.2",
|
"keyboardevents-areequal": "0.2.2",
|
||||||
@ -115,7 +119,7 @@
|
|||||||
"node-id3": "0.2.9",
|
"node-id3": "0.2.9",
|
||||||
"peerjs": "1.5.5",
|
"peerjs": "1.5.5",
|
||||||
"semver": "7.7.2",
|
"semver": "7.7.2",
|
||||||
"serve": "14.2.4",
|
"serve": "14.2.5",
|
||||||
"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",
|
||||||
"socks": "2.8.7",
|
"socks": "2.8.7",
|
||||||
"solid-element": "1.9.1",
|
"solid-element": "1.9.1",
|
||||||
@ -125,35 +129,35 @@
|
|||||||
"solid-transition-group": "0.3.0",
|
"solid-transition-group": "0.3.0",
|
||||||
"tiny-pinyin": "1.3.2",
|
"tiny-pinyin": "1.3.2",
|
||||||
"tinyld": "1.3.4",
|
"tinyld": "1.3.4",
|
||||||
"virtua": "0.41.5",
|
"virtua": "0.42.2",
|
||||||
"vudio": "2.1.1",
|
"vudio": "2.1.1",
|
||||||
"x11": "2.3.0",
|
"x11": "2.3.0",
|
||||||
"youtubei.js": "15.0.1",
|
"youtubei.js": "15.0.1",
|
||||||
"zod": "4.0.17"
|
"zod": "4.1.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@electron-toolkit/tsconfig": "1.0.1",
|
"@electron-toolkit/tsconfig": "1.0.1",
|
||||||
"@eslint/js": "9.33.0",
|
"@eslint/js": "9.35.0",
|
||||||
"@malept/flatpak-bundler": "0.4.0",
|
"@malept/flatpak-bundler": "0.4.0",
|
||||||
"@playwright/test": "1.54.2",
|
"@playwright/test": "1.55.0",
|
||||||
"@stylistic/eslint-plugin": "5.2.3",
|
"@stylistic/eslint-plugin": "5.3.1",
|
||||||
"@total-typescript/ts-reset": "0.6.1",
|
"@total-typescript/ts-reset": "0.6.1",
|
||||||
"@types/electron-localshortcut": "3.1.3",
|
"@types/electron-localshortcut": "3.1.3",
|
||||||
"@types/howler": "2.2.12",
|
"@types/howler": "2.2.12",
|
||||||
"@types/html-to-text": "9.0.4",
|
"@types/html-to-text": "9.0.4",
|
||||||
"@types/semver": "7.7.0",
|
"@types/semver": "7.7.1",
|
||||||
"@types/trusted-types": "2.0.7",
|
"@types/trusted-types": "2.0.7",
|
||||||
"bufferutil": "4.0.9",
|
"bufferutil": "4.0.9",
|
||||||
"builtin-modules": "5.0.0",
|
"builtin-modules": "5.0.0",
|
||||||
"cross-env": "10.0.0",
|
"cross-env": "10.0.0",
|
||||||
"del-cli": "6.0.0",
|
"del-cli": "6.0.0",
|
||||||
"discord-api-types": "0.38.20",
|
"discord-api-types": "0.38.23",
|
||||||
"electron": "37.3.0",
|
"electron": "38.0.0",
|
||||||
"electron-builder": "26.0.12",
|
"electron-builder": "26.0.12",
|
||||||
"electron-builder-squirrel-windows": "26.0.12",
|
"electron-builder-squirrel-windows": "26.0.12",
|
||||||
"electron-devtools-installer": "4.0.0",
|
"electron-devtools-installer": "4.0.0",
|
||||||
"electron-vite": "4.0.0",
|
"electron-vite": "4.0.0",
|
||||||
"eslint": "9.33.0",
|
"eslint": "9.35.0",
|
||||||
"eslint-config-prettier": "10.1.8",
|
"eslint-config-prettier": "10.1.8",
|
||||||
"eslint-import-resolver-exports": "1.0.0-beta.5",
|
"eslint-import-resolver-exports": "1.0.0-beta.5",
|
||||||
"eslint-import-resolver-typescript": "4.4.4",
|
"eslint-import-resolver-typescript": "4.4.4",
|
||||||
@ -161,14 +165,14 @@
|
|||||||
"eslint-plugin-prettier": "5.5.4",
|
"eslint-plugin-prettier": "5.5.4",
|
||||||
"eslint-plugin-solid": "0.14.5",
|
"eslint-plugin-solid": "0.14.5",
|
||||||
"glob": "11.0.3",
|
"glob": "11.0.3",
|
||||||
"node-gyp": "11.3.0",
|
"node-gyp": "11.4.2",
|
||||||
"playwright": "1.54.2",
|
"playwright": "1.55.0",
|
||||||
"ts-morph": "26.0.0",
|
"ts-morph": "26.0.0",
|
||||||
"typescript": "5.9.2",
|
"typescript": "5.9.2",
|
||||||
"typescript-eslint": "8.39.1",
|
"typescript-eslint": "8.42.0",
|
||||||
"utf-8-validate": "6.0.5",
|
"utf-8-validate": "6.0.5",
|
||||||
"vite": "npm:rolldown-vite@7.1.2",
|
"vite": "npm:rolldown-vite@7.1.5",
|
||||||
"vite-plugin-inspect": "11.3.2",
|
"vite-plugin-inspect": "11.3.3",
|
||||||
"vite-plugin-resolve": "2.5.2",
|
"vite-plugin-resolve": "2.5.2",
|
||||||
"vite-plugin-solid": "2.11.8",
|
"vite-plugin-solid": "2.11.8",
|
||||||
"ws": "8.18.3"
|
"ws": "8.18.3"
|
||||||
|
|||||||
27
patches/electron-is@3.0.0.patch
Normal file
27
patches/electron-is@3.0.0.patch
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
diff --git a/is.d.ts b/is.d.ts
|
||||||
|
index fb861f7b401914f0f89cb4edf25c51df5cb05812..82144733cd34d88e2deb2e4713b104418e673f2e 100644
|
||||||
|
--- a/is.d.ts
|
||||||
|
+++ b/is.d.ts
|
||||||
|
@@ -5,6 +5,7 @@ declare namespace is {
|
||||||
|
export function macOS(): boolean;
|
||||||
|
export function windows(): boolean;
|
||||||
|
export function linux(): boolean;
|
||||||
|
+ export function freebsd(): boolean;
|
||||||
|
export function x86(): boolean;
|
||||||
|
export function x64(): boolean;
|
||||||
|
export function production(): boolean;
|
||||||
|
diff --git a/is.js b/is.js
|
||||||
|
index a76bb1755a2728bde185b35d847031d3b8ea4ab0..f6b03406c17342f5af078de069e5bbbd2246e152 100644
|
||||||
|
--- a/is.js
|
||||||
|
+++ b/is.js
|
||||||
|
@@ -39,6 +39,10 @@ module.exports = {
|
||||||
|
linux: function () {
|
||||||
|
return process.platform === 'linux'
|
||||||
|
},
|
||||||
|
+ // Checks if we are under FreeBSD OS
|
||||||
|
+ freebsd: function () {
|
||||||
|
+ return process.platform === "freebsd"
|
||||||
|
+ },
|
||||||
|
// Checks if we are the processor's arch is x86
|
||||||
|
x86: function () {
|
||||||
|
return process.arch === 'ia32'
|
||||||
837
pnpm-lock.yaml
generated
837
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -2,7 +2,7 @@ import { deepmergeCustom } from 'deepmerge-ts';
|
|||||||
|
|
||||||
import defaultConfig from './defaults';
|
import defaultConfig from './defaults';
|
||||||
|
|
||||||
import store, { IStore } from './store';
|
import store, { type IStore } from './store';
|
||||||
import plugins from './plugins';
|
import plugins from './plugins';
|
||||||
|
|
||||||
import { restart } from '@/providers/app-controls';
|
import { restart } from '@/providers/app-controls';
|
||||||
|
|||||||
2
src/custom-electron-prompt.d.ts
vendored
2
src/custom-electron-prompt.d.ts
vendored
@ -1,5 +1,5 @@
|
|||||||
declare module 'custom-electron-prompt' {
|
declare module 'custom-electron-prompt' {
|
||||||
import { BrowserWindow } from 'electron';
|
import { type BrowserWindow } from 'electron';
|
||||||
|
|
||||||
export type SelectOptions = Record<string, string>;
|
export type SelectOptions = Record<string, string>;
|
||||||
|
|
||||||
|
|||||||
@ -150,6 +150,13 @@
|
|||||||
"visual-tweaks": {
|
"visual-tweaks": {
|
||||||
"label": "Οπτικές προσαρμογές",
|
"label": "Οπτικές προσαρμογές",
|
||||||
"submenu": {
|
"submenu": {
|
||||||
|
"custom-window-title": {
|
||||||
|
"label": "Προσαρμοσμένος τίτλος παραθύρου",
|
||||||
|
"prompt": {
|
||||||
|
"label": "Εισαγωγή προσαρμοσμένου τίτλου παραθύρου: (κενό για απενεργοποίηση)",
|
||||||
|
"placeholder": "Παράδειγμα: YouTube Music"
|
||||||
|
}
|
||||||
|
},
|
||||||
"like-buttons": {
|
"like-buttons": {
|
||||||
"default": "Προεπιλογή",
|
"default": "Προεπιλογή",
|
||||||
"force-show": "Επιβολή εμφάνισης",
|
"force-show": "Επιβολή εμφάνισης",
|
||||||
@ -381,6 +388,11 @@
|
|||||||
},
|
},
|
||||||
"templates": {
|
"templates": {
|
||||||
"title": "Ανοίξτε τον επιλογέα λεζάντας"
|
"title": "Ανοίξτε τον επιλογέα λεζάντας"
|
||||||
|
},
|
||||||
|
"toast": {
|
||||||
|
"caption-changed": "Λεζάντα άλλαξε σε {{language}}",
|
||||||
|
"caption-disabled": "Λεζάντες απενεργοποιήθηκαν",
|
||||||
|
"no-captions": "Λεζάντες μη διαθέσιμες για αυτό το τραγούδι"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"compact-sidebar": {
|
"compact-sidebar": {
|
||||||
@ -600,7 +612,15 @@
|
|||||||
},
|
},
|
||||||
"navigation": {
|
"navigation": {
|
||||||
"description": "Βέλη πλοήγησης Επόμενο/Πίσω ενσωματωμένα απευθείας στο περιβάλλον εργασίας, όπως στο αγαπημένο σας πρόγραμμα περιήγησης",
|
"description": "Βέλη πλοήγησης Επόμενο/Πίσω ενσωματωμένα απευθείας στο περιβάλλον εργασίας, όπως στο αγαπημένο σας πρόγραμμα περιήγησης",
|
||||||
"name": "Πλοήγηση"
|
"name": "Πλοήγηση",
|
||||||
|
"templates": {
|
||||||
|
"back": {
|
||||||
|
"title": "Μετάβαση στην προηγούμενη σελίδα"
|
||||||
|
},
|
||||||
|
"forward": {
|
||||||
|
"title": "Μετάβαση στην επόμενη σελίδα"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"no-google-login": {
|
"no-google-login": {
|
||||||
"description": "Αφαίρεση των κουμπιών και των συνδέσμων σύνδεσης Google από το περιβάλλον εργασίας",
|
"description": "Αφαίρεση των κουμπιών και των συνδέσμων σύνδεσης Google από το περιβάλλον εργασίας",
|
||||||
@ -692,7 +712,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"description": "Επιτρέπει την αλλαγή της ποιότητας βίντεο με ένα κουμπί στην επικάλυψη βίντεο",
|
"description": "Επιτρέπει την αλλαγή της ποιότητας βίντεο με ένα κουμπί στην επικάλυψη βίντεο",
|
||||||
"name": "Αλλαγή ποιότητας βίντεο"
|
"name": "Αλλαγή ποιότητας βίντεο",
|
||||||
|
"renderer": {
|
||||||
|
"quality-settings-button": {
|
||||||
|
"label": "Άνοιγμα ρυθμίσεων ποιότητας αναπαραγωγέα"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"scrobbler": {
|
"scrobbler": {
|
||||||
"description": "Προσθήκη υποστήριξης scrobbling (κ.λπ. last.fm, Listenbrainz)",
|
"description": "Προσθήκη υποστήριξης scrobbling (κ.λπ. last.fm, Listenbrainz)",
|
||||||
@ -859,7 +884,8 @@
|
|||||||
},
|
},
|
||||||
"name": "Εναλλαγή βίντεο",
|
"name": "Εναλλαγή βίντεο",
|
||||||
"templates": {
|
"templates": {
|
||||||
"button-song": "Τραγούδι"
|
"button-song": "Τραγούδι",
|
||||||
|
"button-video": "Βίντεο"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"visualizer": {
|
"visualizer": {
|
||||||
|
|||||||
@ -421,6 +421,19 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"custom-output-device": {
|
||||||
|
"description": "Configure a custom output media device for songs",
|
||||||
|
"menu": {
|
||||||
|
"device-selector": "Select Device"
|
||||||
|
},
|
||||||
|
"name": "Custom Output Device",
|
||||||
|
"prompt": {
|
||||||
|
"device-selector": {
|
||||||
|
"label": "Choose the output media device to be used",
|
||||||
|
"title": "Select Output Device"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"disable-autoplay": {
|
"disable-autoplay": {
|
||||||
"description": "Makes song start in \"paused\" mode",
|
"description": "Makes song start in \"paused\" mode",
|
||||||
"menu": {
|
"menu": {
|
||||||
@ -444,7 +457,15 @@
|
|||||||
"hide-duration-left": "Hide duration left",
|
"hide-duration-left": "Hide duration left",
|
||||||
"hide-github-button": "Hide GitHub link Button",
|
"hide-github-button": "Hide GitHub link Button",
|
||||||
"play-on-youtube-music": "Play on YouTube Music",
|
"play-on-youtube-music": "Play on YouTube Music",
|
||||||
"set-inactivity-timeout": "Set inactivity timeout"
|
"set-inactivity-timeout": "Set inactivity timeout",
|
||||||
|
"set-status-display-type": {
|
||||||
|
"label": "Status text",
|
||||||
|
"submenu": {
|
||||||
|
"youtube-music": "Listening to YouTube Music",
|
||||||
|
"artist": "Listening to {artist}",
|
||||||
|
"title": "Listening to {song title}"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"name": "Discord Rich Presence",
|
"name": "Discord Rich Presence",
|
||||||
"prompt": {
|
"prompt": {
|
||||||
|
|||||||
@ -421,6 +421,19 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"custom-output-device": {
|
||||||
|
"description": "Configura un dispositivo de salida de audio personalizado para las canciones",
|
||||||
|
"menu": {
|
||||||
|
"device-selector": "Seleccionar un dispositivo"
|
||||||
|
},
|
||||||
|
"name": "Dispositivo de audio personalizado",
|
||||||
|
"prompt": {
|
||||||
|
"device-selector": {
|
||||||
|
"label": "Escoge el dispositivo de salida de audio que se va a usar",
|
||||||
|
"title": "Seleccionar un dispositivo de audio"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"disable-autoplay": {
|
"disable-autoplay": {
|
||||||
"description": "Hace que la canción comience en modo \"pausado\"",
|
"description": "Hace que la canción comience en modo \"pausado\"",
|
||||||
"menu": {
|
"menu": {
|
||||||
|
|||||||
@ -2,14 +2,14 @@
|
|||||||
"common": {
|
"common": {
|
||||||
"console": {
|
"console": {
|
||||||
"plugins": {
|
"plugins": {
|
||||||
"execute-failed": "Misslyckades med att köra plugin {{pluginName}}::{{contextName}}",
|
"execute-failed": "Misslyckades med att köra tillägget {{pluginName}}::{{contextName}}",
|
||||||
"executed-at-ms": "Plugin {{pluginName}}::{{contextName}} kördes på {{ms}} ms",
|
"executed-at-ms": "Tillägget {{pluginName}}::{{contextName}} kördes på {{ms}}ms",
|
||||||
"initialize-failed": "Misslyckades med att initialisera pluginen \"{{pluginName}}\"",
|
"initialize-failed": "Misslyckades med att initialisera tillägget \"{{pluginName}}\"",
|
||||||
"load-all": "Laddar alla pluginer",
|
"load-all": "Laddar alla tillägg",
|
||||||
"load-failed": "Misslyckades med att ladda pluginen \"{{pluginName}}\"",
|
"load-failed": "Misslyckades med att ladda tillägget \"{{pluginName}}\"",
|
||||||
"loaded": "Pluginen \"{{pluginName}}\" laddad",
|
"loaded": "Tillägget \"{{pluginName}}\" laddades in",
|
||||||
"unload-failed": "Misslyckades med att avlasta pluginen \"{{pluginName}}\"",
|
"unload-failed": "Kunde inte inaktivera {{pluginName}}-tillägget",
|
||||||
"unloaded": "Pluginen \"{{pluginName}}\" avlastad"
|
"unloaded": "{{pluginName}}-tillägget inaktiverat"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -21,7 +21,7 @@
|
|||||||
"main": {
|
"main": {
|
||||||
"console": {
|
"console": {
|
||||||
"did-finish-load": {
|
"did-finish-load": {
|
||||||
"dev-tools": "Laddning klar. DevTools öppnad"
|
"dev-tools": "Laddning slutförd. Utvecklarverktyg öppnad"
|
||||||
},
|
},
|
||||||
"i18n": {
|
"i18n": {
|
||||||
"loaded": "i18n laddad"
|
"loaded": "i18n laddad"
|
||||||
@ -45,16 +45,16 @@
|
|||||||
"dialog": {
|
"dialog": {
|
||||||
"hide-menu-enabled": {
|
"hide-menu-enabled": {
|
||||||
"detail": "Menyn är dold, använd 'Alt' för att visa den (eller 'Escape' om du använder inbyggd meny)",
|
"detail": "Menyn är dold, använd 'Alt' för att visa den (eller 'Escape' om du använder inbyggd meny)",
|
||||||
"message": "Dölj Meny är aktiverat",
|
"message": "Dölj meny är aktiverad",
|
||||||
"title": "Dölj Meny aktiverad"
|
"title": "Dölj meny aktiverad"
|
||||||
},
|
},
|
||||||
"need-to-restart": {
|
"need-to-restart": {
|
||||||
"buttons": {
|
"buttons": {
|
||||||
"later": "Senare",
|
"later": "Senare",
|
||||||
"restart-now": "Starta om nu"
|
"restart-now": "Starta om nu"
|
||||||
},
|
},
|
||||||
"detail": "\"{{pluginName}}\" pluginen kräver en omstart för att träda i kraft",
|
"detail": "\"{{pluginName}}\"-tillägget kräver en omstart för att träda i kraft",
|
||||||
"message": "\"{{pluginName}}\" behöver startas om",
|
"message": "\"{{pluginName}}\"-tillägget behöver startas om",
|
||||||
"title": "Omstart krävs"
|
"title": "Omstart krävs"
|
||||||
},
|
},
|
||||||
"unresponsive": {
|
"unresponsive": {
|
||||||
@ -84,17 +84,17 @@
|
|||||||
"label": "Navigering",
|
"label": "Navigering",
|
||||||
"submenu": {
|
"submenu": {
|
||||||
"copy-current-url": "Kopiera nuvarande länk",
|
"copy-current-url": "Kopiera nuvarande länk",
|
||||||
"go-back": "Föregående",
|
"go-back": "Gå tillbaka",
|
||||||
"go-forward": "Nästa",
|
"go-forward": "Gå framåt",
|
||||||
"quit": "Lämna",
|
"quit": "Lämna",
|
||||||
"restart": "Starta om appen"
|
"restart": "Starta om appen"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"options": {
|
"options": {
|
||||||
"label": "Valmöjligheter",
|
"label": "Alternativ",
|
||||||
"submenu": {
|
"submenu": {
|
||||||
"advanced-options": {
|
"advanced-options": {
|
||||||
"label": "Avancerade valmöjligheter",
|
"label": "Avancerade alternativ",
|
||||||
"submenu": {
|
"submenu": {
|
||||||
"auto-reset-app-cache": "Nollställ appcache när appen startar",
|
"auto-reset-app-cache": "Nollställ appcache när appen startar",
|
||||||
"disable-hardware-acceleration": "Stäng av hårdvaruacceleration",
|
"disable-hardware-acceleration": "Stäng av hårdvaruacceleration",
|
||||||
@ -114,60 +114,589 @@
|
|||||||
},
|
},
|
||||||
"always-on-top": "Alltid överst",
|
"always-on-top": "Alltid överst",
|
||||||
"auto-update": "Uppdatera automatiskt",
|
"auto-update": "Uppdatera automatiskt",
|
||||||
|
"hide-menu": {
|
||||||
|
"dialog": {
|
||||||
|
"message": "Menyn kommer att döljas efter omstart, använd [Alt] för att visa menyn (eller [`] vid användning av menyn inuti applikationen)",
|
||||||
|
"title": "Dölj meny aktiverad"
|
||||||
|
},
|
||||||
|
"label": "Dölj meny"
|
||||||
|
},
|
||||||
"language": {
|
"language": {
|
||||||
"dialog": {
|
"dialog": {
|
||||||
"message": "Språket ändras efter omstart",
|
"message": "Språket ändras efter omstart",
|
||||||
"title": "Språket har ändrats"
|
"title": "Språket har ändrats"
|
||||||
},
|
},
|
||||||
"label": "Språk"
|
"label": "Språk",
|
||||||
|
"submenu": {
|
||||||
|
"to-help-translate": "Vill du hjälpa till att översätta? Klicka här"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"resume-on-start": "Fortsätt spela när appen öppnas"
|
"resume-on-start": "Fortsätt spela när appen öppnas",
|
||||||
|
"single-instance-lock": "Lås enskild instans",
|
||||||
|
"start-at-login": "Starta vid inloggning",
|
||||||
|
"starting-page": {
|
||||||
|
"label": "Startsidа",
|
||||||
|
"unset": "Ej inställt"
|
||||||
|
},
|
||||||
|
"tray": {
|
||||||
|
"label": "Systemfält",
|
||||||
|
"submenu": {
|
||||||
|
"disabled": "Inaktiverad",
|
||||||
|
"enabled-and-hide-app": "Aktiverad och dölj app",
|
||||||
|
"enabled-and-show-app": "Aktiverad och visa app",
|
||||||
|
"play-pause-on-click": "Spela/Pausa vid klick"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"visual-tweaks": {
|
||||||
|
"label": "Visuella justeringar",
|
||||||
|
"submenu": {
|
||||||
|
"custom-window-title": {
|
||||||
|
"label": "Anpassad titel på fönstret",
|
||||||
|
"prompt": {
|
||||||
|
"label": "Ange anpassad fönstertitel: (lämna tomt för att inaktivera)",
|
||||||
|
"placeholder": "Exempelvis: YouTube Music"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"like-buttons": {
|
||||||
|
"default": "Standard",
|
||||||
|
"force-show": "Tvinga fram visning",
|
||||||
|
"hide": "Dölj",
|
||||||
|
"label": "Gilla-knappar"
|
||||||
|
},
|
||||||
|
"remove-upgrade-button": "Ta bort knappen för uppgradering",
|
||||||
|
"theme": {
|
||||||
|
"dialog": {
|
||||||
|
"button": {
|
||||||
|
"cancel": "Avbryt",
|
||||||
|
"remove": "Ta bort"
|
||||||
|
},
|
||||||
|
"remove-theme": "Vill du verkligen radera det anpassade temat?",
|
||||||
|
"remove-theme-message": "Det här raderar ditt anpassade tema"
|
||||||
|
},
|
||||||
|
"label": "Tema",
|
||||||
|
"submenu": {
|
||||||
|
"import-css-file": "Importera anpassad CSS-fil",
|
||||||
|
"no-theme": "Inget tema"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"plugins": {
|
||||||
|
"enabled": "Aktiverad",
|
||||||
|
"label": "Tillägg",
|
||||||
|
"new": "NY"
|
||||||
|
},
|
||||||
|
"view": {
|
||||||
|
"label": "Visa",
|
||||||
|
"submenu": {
|
||||||
|
"force-reload": "Tvinga omladdning",
|
||||||
|
"reload": "Ladda om",
|
||||||
|
"reset-zoom": "Verklig storlek",
|
||||||
|
"toggle-fullscreen": "Växla helskärm",
|
||||||
|
"zoom-in": "Zooma in",
|
||||||
|
"zoom-out": "Zooma ut"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tray": {
|
||||||
|
"next": "Nästa",
|
||||||
|
"play-pause": "Spela/Pausa",
|
||||||
|
"previous": "Föregående",
|
||||||
|
"quit": "Stäng",
|
||||||
|
"restart": "Starta om appen",
|
||||||
|
"show": "Visa fönster",
|
||||||
|
"tooltip": {
|
||||||
|
"default": "YouTube Music",
|
||||||
|
"with-song-info": "YouTube Music: {{artist}} – {{title}}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"plugins": {
|
"plugins": {
|
||||||
|
"ad-speedup": {
|
||||||
|
"description": "Om en annons spelas, tystas ljudet och uppspelningshastigheten sätts till 16×",
|
||||||
|
"name": "Snabba upp annonser"
|
||||||
|
},
|
||||||
|
"adblocker": {
|
||||||
|
"description": "Blockerar annonser och spårning automatiskt",
|
||||||
|
"menu": {
|
||||||
|
"blocker": "Blockerare"
|
||||||
|
},
|
||||||
|
"name": "Annonsblockerare"
|
||||||
|
},
|
||||||
|
"album-actions": {
|
||||||
|
"description": "Lägger till knappar för Undislike, Dislike, Like och Unlike för att använda detta på alla spår i en spellista eller ett album",
|
||||||
|
"name": "Albumåtgärder"
|
||||||
|
},
|
||||||
|
"album-color-theme": {
|
||||||
|
"description": "Använder ett dynamiskt tema och visuella effekter baserat på albumets färgpalett",
|
||||||
|
"menu": {
|
||||||
|
"color-mix-ratio": {
|
||||||
|
"label": "Färgblandningsförhållande",
|
||||||
|
"submenu": {
|
||||||
|
"percent": "{{ratio}} %"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"name": "Albumfärgtema"
|
||||||
|
},
|
||||||
|
"ambient-mode": {
|
||||||
|
"description": "Ger en ljuseffekt genom att försiktigt kasta färger från videon på skärmens bakgrund",
|
||||||
|
"menu": {
|
||||||
|
"blur-amount": {
|
||||||
|
"label": "Oskärpa",
|
||||||
|
"submenu": {
|
||||||
|
"pixels": "{{blurAmount}} pixlar"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"buffer": {
|
||||||
|
"label": "Buffert",
|
||||||
|
"submenu": {
|
||||||
|
"buffer": "{{buffer}}"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"opacity": {
|
||||||
|
"label": "Opacitet",
|
||||||
|
"submenu": {
|
||||||
|
"percent": "{{opacity}}%"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"quality": {
|
||||||
|
"label": "Kvalitet",
|
||||||
|
"submenu": {
|
||||||
|
"pixels": "{{quality}} pixlar"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"size": {
|
||||||
|
"label": "Storlek",
|
||||||
|
"submenu": {
|
||||||
|
"percent": "{{size}}%"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"smoothness-transition": {
|
||||||
|
"label": "Mjuk övergång",
|
||||||
|
"submenu": {
|
||||||
|
"during": "Under {{interpolationTime}} s"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"use-fullscreen": {
|
||||||
|
"label": "Använder helskärm"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"name": "Ambiensläge"
|
||||||
|
},
|
||||||
|
"amuse": {
|
||||||
|
"description": "Lägger till stöd för YouTube Music i Amuse ‘Now Playing’-widgeten av 6K Labs",
|
||||||
|
"name": "Amuse",
|
||||||
|
"response": {
|
||||||
|
"query": "Amuse API-servern körs. Använd GET /query för att hämta information om låt."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"api-server": {
|
||||||
|
"description": "Lägger till en API-server för att styra spelaren",
|
||||||
|
"dialog": {
|
||||||
|
"request": {
|
||||||
|
"buttons": {
|
||||||
|
"allow": "Tillåt",
|
||||||
|
"deny": "Avvisa"
|
||||||
|
},
|
||||||
|
"message": "Tillåt {{ID}} ({{origin}}) att få åtkomst till API:et?",
|
||||||
|
"title": "Förfrågan om API-åtkomst"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"menu": {
|
||||||
|
"auth-strategy": {
|
||||||
|
"label": "Metod för åtkomstkontroll",
|
||||||
|
"submenu": {
|
||||||
|
"auth-at-first": {
|
||||||
|
"label": "Ge åtkomst vid första begäran"
|
||||||
|
},
|
||||||
|
"none": {
|
||||||
|
"label": "Ingen åtkomstkontroll"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"hostname": {
|
||||||
|
"label": "Värdnamn"
|
||||||
|
},
|
||||||
|
"port": {
|
||||||
|
"label": "Port"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"name": "API-server [Beta]",
|
||||||
|
"prompt": {
|
||||||
|
"hostname": {
|
||||||
|
"label": "Ange värdnamnet (t.ex. 0.0.0.0) för API-servern:",
|
||||||
|
"title": "Värdnamn"
|
||||||
|
},
|
||||||
|
"port": {
|
||||||
|
"label": "Ange porten för API-servern:",
|
||||||
|
"title": "Port"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"audio-compressor": {
|
||||||
|
"description": "Applicera komprimering på ljudet (sänker volymen på de starkaste delarna av signalen och höjer volymen på de svagaste delarna)",
|
||||||
|
"name": "Ljudkompressor"
|
||||||
|
},
|
||||||
|
"auth-proxy-adapter": {
|
||||||
|
"description": "Stöd för användning av autentiseringsproxy-tjänster",
|
||||||
|
"menu": {
|
||||||
|
"disable": "Inaktivera proxy-adapter",
|
||||||
|
"enable": "Aktivera proxy-adapter",
|
||||||
|
"hostname": {
|
||||||
|
"label": "Värdnamn"
|
||||||
|
},
|
||||||
|
"port": {
|
||||||
|
"label": "Port"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"name": "Adapter För Autentiseringsproxy",
|
||||||
|
"prompt": {
|
||||||
|
"hostname": {
|
||||||
|
"label": "Ange värdnamn för lokal proxyserver (kräver omstart):",
|
||||||
|
"title": "Proxy-värdnamn"
|
||||||
|
},
|
||||||
|
"port": {
|
||||||
|
"label": "Ange port för lokal proxyserver (kräver omstart):",
|
||||||
|
"title": "Port för proxy"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"blur-nav-bar": {
|
||||||
|
"description": "Gör navigeringsfältet transparent och suddigt",
|
||||||
|
"name": "Suddigt Navigeringsfält"
|
||||||
|
},
|
||||||
|
"bypass-age-restrictions": {
|
||||||
|
"description": "Hoppa över YouTubes åldersverifiering",
|
||||||
|
"name": "Hoppa Över Åldersbegränsningar"
|
||||||
|
},
|
||||||
|
"captions-selector": {
|
||||||
|
"description": "Välj textning för YouTube Music-ljudspår",
|
||||||
|
"menu": {
|
||||||
|
"autoload": "Välj automatiskt senast använda textning",
|
||||||
|
"disable-captions": "Ingen textning som standard"
|
||||||
|
},
|
||||||
|
"name": "Textväljare",
|
||||||
|
"prompt": {
|
||||||
|
"selector": {
|
||||||
|
"label": "Aktuellt textningsspråk: {{language}}",
|
||||||
|
"none": "Inget",
|
||||||
|
"title": "Välj textspråk"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"templates": {
|
||||||
|
"title": "Öppna textväljaren"
|
||||||
|
},
|
||||||
|
"toast": {
|
||||||
|
"caption-changed": "Textning ändrad till {{language}}",
|
||||||
|
"caption-disabled": "Textning inaktiverad",
|
||||||
|
"no-captions": "Inga undertexter tillgängliga för denna låt"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compact-sidebar": {
|
||||||
|
"description": "Sätt alltid sidomenyn i kompakt läge",
|
||||||
|
"name": "Kompakt Sidomeny"
|
||||||
|
},
|
||||||
|
"crossfade": {
|
||||||
|
"description": "Mjuk övergång mellan låtar",
|
||||||
|
"menu": {
|
||||||
|
"advanced": "Avancerat"
|
||||||
|
},
|
||||||
|
"name": "Mjuk Övergång [Beta]",
|
||||||
|
"prompt": {
|
||||||
|
"options": {
|
||||||
|
"multi-input": {
|
||||||
|
"fade-in-duration": "Fade-in-varaktighet (ms)",
|
||||||
|
"fade-out-duration": "Fade-out-varaktighet (ms)",
|
||||||
|
"fade-scaling": {
|
||||||
|
"label": "Fade-skalning",
|
||||||
|
"linear": "Linjär",
|
||||||
|
"logarithmic": "Logaritmisk"
|
||||||
|
},
|
||||||
|
"seconds-before-end": "Övergång i sekunder före slutet"
|
||||||
|
},
|
||||||
|
"title": "Övergångsinställningar"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"disable-autoplay": {
|
||||||
|
"description": "Starta låt i \"pausat\" läge",
|
||||||
|
"menu": {
|
||||||
|
"apply-once": "Gäller endast vid uppstart"
|
||||||
|
},
|
||||||
|
"name": "Inaktivera Automatisk Uppspelning"
|
||||||
|
},
|
||||||
|
"discord": {
|
||||||
|
"backend": {
|
||||||
|
"already-connected": "Försökte ansluta med aktiv anslutning",
|
||||||
|
"connected": "Ansluten till Discord",
|
||||||
|
"disconnected": "Frånkopplad från Discord"
|
||||||
|
},
|
||||||
|
"description": "Visa dina vänner vad du lyssnar på med Aktivitetsdelning",
|
||||||
|
"menu": {
|
||||||
|
"auto-reconnect": "Automatisk återanslutning",
|
||||||
|
"clear-activity": "Rensa aktivitet",
|
||||||
|
"clear-activity-after-timeout": "Rensa aktivitet efter tidsgräns",
|
||||||
|
"connected": "Ansluten",
|
||||||
|
"disconnected": "Frånkopplad",
|
||||||
|
"hide-duration-left": "Dölj återstående tid",
|
||||||
|
"hide-github-button": "Dölj knapp för GitHub-länk",
|
||||||
|
"play-on-youtube-music": "Spela på YouTube Music",
|
||||||
|
"set-inactivity-timeout": "Ställ in inaktivitetstid"
|
||||||
|
},
|
||||||
|
"name": "Discord Aktivitetsdelning",
|
||||||
|
"prompt": {
|
||||||
|
"set-inactivity-timeout": {
|
||||||
|
"label": "Ange inaktivitetstid i sekunder:",
|
||||||
|
"title": "Ställ in inaktivitetstid"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"downloader": {
|
||||||
|
"backend": {
|
||||||
|
"dialog": {
|
||||||
|
"error": {
|
||||||
|
"buttons": {
|
||||||
|
"ok": "OK"
|
||||||
|
},
|
||||||
|
"message": "Hoppsan! Nedladdningen misslyckades…",
|
||||||
|
"title": "Fel vid nedladdning!"
|
||||||
|
},
|
||||||
|
"start-download-playlist": {
|
||||||
|
"buttons": {
|
||||||
|
"ok": "OK"
|
||||||
|
},
|
||||||
|
"detail": "({{playlistSize}} låtar)",
|
||||||
|
"message": "Laddar ner {{playlistTitle}}-spellistan",
|
||||||
|
"title": "Nedladdning påbörjad"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"feedback": {
|
||||||
|
"conversion-progress": "Konvertering: {{percent}}%",
|
||||||
|
"converting": "Konverterar…",
|
||||||
|
"done": "Klart: {{filePath}}",
|
||||||
|
"download-info": "Laddar ner {{artist}} - {{title}} [{{videoId}}",
|
||||||
|
"download-progress": "Nedladdning: {{percent}}%",
|
||||||
|
"downloading": "Laddar ner…",
|
||||||
|
"downloading-counter": "Laddar ner {{current}}/{{total}}…",
|
||||||
|
"downloading-playlist": "Laddar ner {{playlistTitle}}-spellistan — {{playlistSize}} spår ({{playlistId}})",
|
||||||
|
"error-while-downloading": "Fel vid nedladdning \"{{author}} - {{title}}\": {{error}}",
|
||||||
|
"folder-already-exists": "Mappen {{playlistFolder}} finns redan",
|
||||||
|
"getting-playlist-info": "Hämtar information om spellista…",
|
||||||
|
"loading": "Laddar…",
|
||||||
|
"playlist-has-only-one-song": "Spellistan innehåller bara ett objekt. Laddar ner direkt.",
|
||||||
|
"playlist-id-not-found": "Hittade inget ID för spellista",
|
||||||
|
"playlist-is-empty": "Spellistan är tom",
|
||||||
|
"playlist-is-mix-or-private": "Fel vid hämtning av spellisteinformation. Se till att den inte är privat eller en 'Mixed for you'-spellista\n\n{{error}}",
|
||||||
|
"preparing-file": "Förbereder fil…",
|
||||||
|
"saving": "Sparar…",
|
||||||
|
"trying-to-get-playlist-id": "Försöker hämta spelliste-ID: {{playlistId}}",
|
||||||
|
"video-id-not-found": "Videon hittades inte",
|
||||||
|
"writing-id3": "Skriver ID3-taggar…"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"description": "Laddar ner MP3 / originalljud direkt från gränssnittet",
|
||||||
|
"menu": {
|
||||||
|
"choose-download-folder": "Välj nedladdningsmapp",
|
||||||
|
"download-finish-settings": {
|
||||||
|
"label": "Ladda ner när klart",
|
||||||
|
"prompt": {
|
||||||
|
"last-percent": "Efter x procent",
|
||||||
|
"last-seconds": "Senaste x sekunderna",
|
||||||
|
"title": "Ställ in när nedladdning ska ske"
|
||||||
|
},
|
||||||
|
"submenu": {
|
||||||
|
"advanced": "Avancerat",
|
||||||
|
"enabled": "Aktiverad",
|
||||||
|
"mode": "Tidsläge",
|
||||||
|
"percent": "Procent",
|
||||||
|
"seconds": "Sekunder"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"download-playlist": "Ladda ner spellista",
|
||||||
|
"presets": "Förinställningar",
|
||||||
|
"skip-existing": "Hoppa över befintliga filer"
|
||||||
|
},
|
||||||
|
"name": "Nedladdare",
|
||||||
|
"renderer": {
|
||||||
|
"can-not-update-progress": "Kan inte uppdatera förlopp"
|
||||||
|
},
|
||||||
|
"templates": {
|
||||||
|
"button": "Ladda ner"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"equalizer": {
|
||||||
|
"description": "Lägger till en equalizer i spelaren",
|
||||||
|
"menu": {
|
||||||
|
"presets": {
|
||||||
|
"label": "Förinställningar",
|
||||||
|
"list": {
|
||||||
|
"bass-booster": "Basförstärkning"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"name": "Equalizer"
|
||||||
|
},
|
||||||
|
"exponential-volume": {
|
||||||
|
"description": "Gör volymreglaget exponentiellt så att det blir lättare att välja lägre volymer.",
|
||||||
|
"name": "Exponentiell Volym"
|
||||||
|
},
|
||||||
|
"in-app-menu": {
|
||||||
|
"description": "Ger menyrader ett snyggt, mörkt, eller albumfärgat utseende",
|
||||||
|
"menu": {
|
||||||
|
"hide-dom-window-controls": "Dölj DOM-fönsterkontroller"
|
||||||
|
},
|
||||||
|
"name": "Meny I Appen"
|
||||||
|
},
|
||||||
|
"lumiastream": {
|
||||||
|
"description": "Lägger till stöd för Lumia Stream",
|
||||||
|
"name": "Lumia Stream [Beta]"
|
||||||
|
},
|
||||||
|
"lyrics-genius": {
|
||||||
|
"description": "Lägger till stöd för texter till de flesta låtar",
|
||||||
|
"menu": {
|
||||||
|
"romanized-lyrics": "Romiserade texter"
|
||||||
|
},
|
||||||
|
"name": "Texter från Genius",
|
||||||
|
"renderer": {
|
||||||
|
"fetched-lyrics": "Hämtade texter från Genius"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"music-together": {
|
||||||
|
"description": "Dela en spellista med andra. När värden spelar en låt kommer alla andra höra samma låt",
|
||||||
|
"dialog": {
|
||||||
|
"enter-host": "Ange värd-ID"
|
||||||
|
},
|
||||||
|
"internal": {
|
||||||
|
"save": "Spara",
|
||||||
|
"track-source": "Ljudkälla",
|
||||||
|
"unknown-user": "Okänd användare"
|
||||||
|
},
|
||||||
|
"menu": {
|
||||||
|
"click-to-copy-id": "Kopiera värd-ID",
|
||||||
|
"close": "Stäng \"Music Together\"",
|
||||||
|
"connected-users": "Anslutna användare",
|
||||||
|
"disconnect": "Koppla från \"Music Together\"",
|
||||||
|
"empty-user": "Inga anslutna användare",
|
||||||
|
"host": "Värd för \"Music Together\"",
|
||||||
|
"join": "Gå med i \"Music Together\"",
|
||||||
|
"permission": {
|
||||||
|
"all": "Tillåt gäster att styra spellista och spelare",
|
||||||
|
"host-only": "Endast värden kan styra spellista och spelare",
|
||||||
|
"playlist": "Tillåt gäster att styra spellistan"
|
||||||
|
},
|
||||||
|
"set-permission": "Ändra behörighet för styrning",
|
||||||
|
"status": {
|
||||||
|
"disconnected": "Frånkopplad",
|
||||||
|
"guest": "Ansluten som gäst",
|
||||||
|
"host": "Ansluten som värd"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"name": "Music Together [Beta]",
|
||||||
|
"toast": {
|
||||||
|
"add-song-failed": "Misslyckades med att lägga till låt",
|
||||||
|
"closed": "\"Music Together\" stängdes",
|
||||||
|
"disconnected": "\"Music Together\" frånkopplad",
|
||||||
|
"host-failed": "Misslyckades med att vara värd för \"Music Together\"",
|
||||||
|
"id-copied": "Värd-ID kopierat till urklipp",
|
||||||
|
"id-copy-failed": "Misslyckades med att kopiera värd-ID till urklipp",
|
||||||
|
"join-failed": "Misslyckades med att gå med i \"Music Together\"",
|
||||||
|
"joined": "Gick med i \"Music Together\"",
|
||||||
|
"permission-changed": "Behörighet för \"Music Together\" ändrad till \"{{permission}}\"",
|
||||||
|
"remove-song-failed": "Misslyckades med att radera låt",
|
||||||
|
"user-connected": "{{name}} gick med i \"Music Together\"",
|
||||||
|
"user-disconnected": "{{name}} lämnade \"Music Together\""
|
||||||
|
}
|
||||||
|
},
|
||||||
"navigation": {
|
"navigation": {
|
||||||
"name": "Navigering"
|
"description": "Direkt integrering av Nästa-/Tillbaka-navigeringspilar i gränssnittet, som i din favoritwebbläsare",
|
||||||
|
"name": "Navigering",
|
||||||
|
"templates": {
|
||||||
|
"back": {
|
||||||
|
"title": "Gå till föregående sida"
|
||||||
|
},
|
||||||
|
"forward": {
|
||||||
|
"title": "Gå till nästa sida"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"no-google-login": {
|
"no-google-login": {
|
||||||
"name": "Inget Google Login"
|
"description": "Ta bort Google-inloggningsknappar och länkar från gränssnittet",
|
||||||
|
"name": "Ingen Google-inloggning"
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
|
"description": "Visa en notis när en låt börjar spelas (interaktiva notiser finns på Windows)",
|
||||||
|
"menu": {
|
||||||
|
"interactive": "Interaktiva notiser",
|
||||||
|
"interactive-settings": {
|
||||||
|
"label": "Interaktiva inställningar",
|
||||||
|
"submenu": {
|
||||||
|
"hide-button-text": "Dölj knapptext",
|
||||||
|
"refresh-on-play-pause": "Uppdatera vid Play/Pause",
|
||||||
|
"tray-controls": "Öppna/stäng vid klick i systemfältet"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"priority": "Notisprioritet",
|
||||||
|
"toast-style": "Stil för \"toast\"-notiser",
|
||||||
|
"unpause-notification": "Visa notis när uppspelning återupptas"
|
||||||
|
},
|
||||||
"name": "Notiser"
|
"name": "Notiser"
|
||||||
},
|
},
|
||||||
|
"performance-improvement": {
|
||||||
|
"description": "Förbättra prestanda genom att aktivera experimentella skript",
|
||||||
|
"name": "Prestandaförbättring [Beta]"
|
||||||
|
},
|
||||||
"picture-in-picture": {
|
"picture-in-picture": {
|
||||||
|
"description": "Tillåter appen att växla till bild-i-bild-läge",
|
||||||
"menu": {
|
"menu": {
|
||||||
|
"always-on-top": "Alltid överst",
|
||||||
"hotkey": {
|
"hotkey": {
|
||||||
"label": "Snabbkommando",
|
"label": "Snabbkommando",
|
||||||
"prompt": {
|
"prompt": {
|
||||||
"keybind-options": {
|
"keybind-options": {
|
||||||
"hotkey": "Snabbkommando"
|
"hotkey": "Snabbkommando"
|
||||||
},
|
},
|
||||||
|
"label": "Välj ett snabbkommando för att växla bild-i-bild-läge",
|
||||||
"title": "Bild-I-Bild genväg"
|
"title": "Bild-I-Bild genväg"
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"save-window-position": "Spara fönsterposition",
|
||||||
|
"save-window-size": "Spara fönsterstorlek",
|
||||||
|
"use-native-pip": "Använd webbläsarens inbyggda bild-i-bild"
|
||||||
},
|
},
|
||||||
"name": "Bild-I-Bild",
|
"name": "Bild-i-bild",
|
||||||
"templates": {
|
"templates": {
|
||||||
"button": "Bild-i-bild"
|
"button": "Bild-i-bild"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"playback-speed": {
|
"playback-speed": {
|
||||||
|
"description": "Lägger till ett reglage för att ändra uppspelningshastighet",
|
||||||
"name": "Uppspelningshastighet",
|
"name": "Uppspelningshastighet",
|
||||||
"templates": {
|
"templates": {
|
||||||
"button": "Hasighet"
|
"button": "Hastighet"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"precise-volume": {
|
"precise-volume": {
|
||||||
|
"description": "Styr ljudstyrkan exakt med mushjul/snabbtangenter, med anpassat skärmlager och justerbara volymsteg",
|
||||||
|
"menu": {
|
||||||
|
"arrows-shortcuts": "Kontroller för lokala piltangenter",
|
||||||
|
"custom-volume-steps": "Ställ in egna volymsteg",
|
||||||
|
"global-shortcuts": "Globala snabbkommandon"
|
||||||
|
},
|
||||||
|
"name": "Noggrann Volymkontroll",
|
||||||
"prompt": {
|
"prompt": {
|
||||||
"global-shortcuts": {
|
"global-shortcuts": {
|
||||||
"keybind-options": {
|
"keybind-options": {
|
||||||
"decrease": "Minska Volym",
|
"decrease": "Sänk volymen",
|
||||||
"increase": "Öka Volym"
|
"increase": "Öka volymen"
|
||||||
}
|
},
|
||||||
|
"label": "Välj globala kortkommandon för volym:",
|
||||||
|
"title": "Globala kortkommandon för volym"
|
||||||
},
|
},
|
||||||
"volume-steps": {
|
"volume-steps": {
|
||||||
|
"label": "Välj volymsteg för ökning/minskning",
|
||||||
"title": "Volymsteg"
|
"title": "Volymsteg"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -176,54 +705,195 @@
|
|||||||
"backend": {
|
"backend": {
|
||||||
"dialog": {
|
"dialog": {
|
||||||
"quality-changer": {
|
"quality-changer": {
|
||||||
"detail": "Nuvarande kvalité: {{quality}}",
|
"detail": "Nuvarande kvalitet: {{quality}}",
|
||||||
"message": "Välj Video Kvalité:",
|
"message": "Välj videokvalitet:",
|
||||||
"title": "Välj Video Kvalité"
|
"title": "Välj videokvalitet"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"description": "Tillåter att ändra videokvalitet med en knapp i videons overlay",
|
||||||
|
"name": "Videokvalitetsväxlare",
|
||||||
|
"renderer": {
|
||||||
|
"quality-settings-button": {
|
||||||
|
"label": "Öppna kvalitetsväxlare för spelaren"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"scrobbler": {
|
"scrobbler": {
|
||||||
|
"description": "Lägg till scrobbling-stöd (t.ex. last.fm, Listenbrainz)",
|
||||||
|
"dialog": {
|
||||||
|
"lastfm": {
|
||||||
|
"auth-failed": {
|
||||||
|
"message": "Misslyckades att autentisera med Last.fm\nDölj popup-fönstret till nästa omstart.",
|
||||||
|
"title": "Autentisering misslyckades"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"menu": {
|
||||||
|
"lastfm": {
|
||||||
|
"api-settings": "Last.fm API-inställningar"
|
||||||
|
},
|
||||||
|
"listenbrainz": {
|
||||||
|
"token": "Ange ListenBrainz användartoken"
|
||||||
|
},
|
||||||
|
"scrobble-alternative-title": "Använd alternativa titlar",
|
||||||
|
"scrobble-other-media": "Scrobbla annan media"
|
||||||
|
},
|
||||||
|
"name": "Scrobbler",
|
||||||
"prompt": {
|
"prompt": {
|
||||||
"lastfm": {
|
"lastfm": {
|
||||||
"api-key": "Last.fm API nyckel"
|
"api-key": "Last.fm API nyckel",
|
||||||
|
"api-secret": "Last.fm API-hemlighet"
|
||||||
},
|
},
|
||||||
"listenbrainz": {
|
"listenbrainz": {
|
||||||
"token": {
|
"token": {
|
||||||
|
"label": "Ange din ListenBrainz användartoken:",
|
||||||
"title": "ListenBrainz token"
|
"title": "ListenBrainz token"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"shortcuts": {
|
"shortcuts": {
|
||||||
|
"description": "Tillåter inställning av globala kortkommandon för uppspelning (spela/pausa/nästa/föregående) och inaktiverar medie-OSD genom att åsidosätta medietangenter. Aktiverar Ctrl/CMD + F för sökning. Aktiverar Linux MPRIS-stöd för medietangenter och anpassade kortkommandon för avancerade användare",
|
||||||
|
"menu": {
|
||||||
|
"override-media-keys": "Åsidosätt medietangenter",
|
||||||
|
"set-keybinds": "Ställ in globala kontroller för låtar"
|
||||||
|
},
|
||||||
|
"name": "Genvägar (& MPRIS)",
|
||||||
"prompt": {
|
"prompt": {
|
||||||
"keybind": {
|
"keybind": {
|
||||||
"keybind-options": {
|
"keybind-options": {
|
||||||
"next": "Nästa",
|
"next": "Nästa",
|
||||||
"play-pause": "Spela / Pausa",
|
"play-pause": "Spela / Pausa",
|
||||||
"previous": "Föregående"
|
"previous": "Föregående"
|
||||||
}
|
},
|
||||||
|
"label": "Välj globala kortkommandon för kontroll av låtar:",
|
||||||
|
"title": "Globala kortkommandon"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"skip-disliked-songs": {
|
||||||
|
"description": "Hoppar över låtar du inte gillar",
|
||||||
|
"name": "Hoppa Över Låtar Du Inte Gillar"
|
||||||
|
},
|
||||||
|
"skip-silences": {
|
||||||
|
"description": "Hoppa automatiskt över tysta partier i låtar",
|
||||||
|
"name": "Hoppa Över Tysta Partier"
|
||||||
|
},
|
||||||
|
"sponsorblock": {
|
||||||
|
"description": "Hoppar automatiskt över icke-musikdelar som intro/outro eller delar av musikvideor där ingen musik spelas",
|
||||||
|
"name": "Blockera Sponsorer"
|
||||||
|
},
|
||||||
|
"synced-lyrics": {
|
||||||
|
"description": "Visar synkroniserade låttexter med hjälp av tjänster som LRClib.",
|
||||||
|
"errors": {
|
||||||
|
"fetch": "⚠️ Ett fel uppstod när texterna skulle hämtas.\n\tFörsök igen senare.",
|
||||||
|
"not-found": "⚠️ Inga texter hittades för denna låt."
|
||||||
|
},
|
||||||
|
"menu": {
|
||||||
|
"default-text-string": {
|
||||||
|
"label": "Standardtecken mellan låttexter",
|
||||||
|
"tooltip": "Välj standardtecken att använda för mellanrummet mellan låttexter"
|
||||||
|
},
|
||||||
|
"line-effect": {
|
||||||
|
"label": "Linjeeffekt",
|
||||||
|
"submenu": {
|
||||||
|
"fancy": {
|
||||||
|
"label": "Stiligt",
|
||||||
|
"tooltip": "Använd stora, app-liknande effekter på den aktuella raden"
|
||||||
|
},
|
||||||
|
"focus": {
|
||||||
|
"label": "Fokus",
|
||||||
|
"tooltip": "Gör endast den aktuella raden vit"
|
||||||
|
},
|
||||||
|
"offset": {
|
||||||
|
"label": "Förskjutning",
|
||||||
|
"tooltip": "Förskjut den aktuella raden åt höger"
|
||||||
|
},
|
||||||
|
"scale": {
|
||||||
|
"label": "Skala",
|
||||||
|
"tooltip": "Skala den aktuella raden"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tooltip": "Välj effekt att applicera på den aktuella raden"
|
||||||
|
},
|
||||||
|
"precise-timing": {
|
||||||
|
"label": "Gör låttexterna perfekt synkroniserade",
|
||||||
|
"tooltip": "Beräkna till millisekunden när nästa rad ska visas (kan ha en liten inverkan på prestanda)"
|
||||||
|
},
|
||||||
|
"romanization": {
|
||||||
|
"label": "Romanisera låttexter",
|
||||||
|
"tooltip": "Om låttexterna är på ett annat språk, försök visa en latinsk version."
|
||||||
|
},
|
||||||
|
"show-lyrics-even-if-inexact": {
|
||||||
|
"label": "Visa låttexter även om de inte är exakta",
|
||||||
|
"tooltip": "Om låten inte hittas försöker tillägget igen med en annan sökförfrågan.\nResultatet från det andra försöket kanske inte är exakt."
|
||||||
|
},
|
||||||
|
"show-time-codes": {
|
||||||
|
"label": "Visa tidskoder",
|
||||||
|
"tooltip": "Visa tidskoderna bredvid låttexterna"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"name": "Synkroniserade Låttexter",
|
||||||
|
"refetch-btn": {
|
||||||
|
"fetching": "Hämtar...",
|
||||||
|
"normal": "Hämta låttexter igen"
|
||||||
|
},
|
||||||
|
"warnings": {
|
||||||
|
"duration-mismatch": "⚠️ - Texterna kan vara osynkroniserade på grund av en skillnad i spårlängd.",
|
||||||
|
"inexact": "⚠️ - Låttexterna för den här låten kanske inte är exakta",
|
||||||
|
"instrumental": "⚠️ - Det här är en instrumentallåt"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"taskbar-mediacontrol": {
|
||||||
|
"description": "Kontrollera uppspelning från aktivitetsfältet i Windows",
|
||||||
|
"name": "Mediakontroll i aktivitetsfältet"
|
||||||
|
},
|
||||||
|
"touchbar": {
|
||||||
|
"description": "Lägger till en TouchBar-widget för macOS-användare",
|
||||||
|
"name": "TouchBar"
|
||||||
|
},
|
||||||
|
"tuna-obs": {
|
||||||
|
"description": "Integration med OBS-pluginprogrammet Tuna",
|
||||||
|
"name": "Tuna OBS"
|
||||||
|
},
|
||||||
|
"unobtrusive-player": {
|
||||||
|
"description": "Undviker att spelaren visas när musik spelas",
|
||||||
|
"name": "Diskret Spelare"
|
||||||
|
},
|
||||||
"video-toggle": {
|
"video-toggle": {
|
||||||
|
"description": "Lägger till en knapp för att växla mellan video/musik-läge. Kan också valfritt ta bort hela videofliken",
|
||||||
"menu": {
|
"menu": {
|
||||||
"align": {
|
"align": {
|
||||||
|
"label": "Justering",
|
||||||
"submenu": {
|
"submenu": {
|
||||||
"left": "Vänster",
|
"left": "Vänster",
|
||||||
"middle": "Mitten",
|
"middle": "Mitten",
|
||||||
"right": "Höger"
|
"right": "Höger"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"force-hide": "Tvinga borttagning av videoflik",
|
||||||
"mode": {
|
"mode": {
|
||||||
|
"label": "Läge",
|
||||||
"submenu": {
|
"submenu": {
|
||||||
"disabled": "Inaktiverad"
|
"custom": "Anpassad växling",
|
||||||
|
"disabled": "Inaktiverad",
|
||||||
|
"native": "Inbyggd växling"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"name": "Video PÅ/AV",
|
||||||
"templates": {
|
"templates": {
|
||||||
"button-song": "Låt"
|
"button-song": "Låt",
|
||||||
|
"button-video": "Video"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"visualizer": {
|
||||||
|
"description": "Lägger till en visualisering i spelaren",
|
||||||
|
"menu": {
|
||||||
|
"visualizer-type": "Visualiseringstyp"
|
||||||
|
},
|
||||||
|
"name": "Visualiserare"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -444,7 +444,15 @@
|
|||||||
"hide-duration-left": "ซ่อนระยะเวลาที่เหลือ",
|
"hide-duration-left": "ซ่อนระยะเวลาที่เหลือ",
|
||||||
"hide-github-button": "ซ่อนปุ่มลิงก์ GitHub",
|
"hide-github-button": "ซ่อนปุ่มลิงก์ GitHub",
|
||||||
"play-on-youtube-music": "เล่นบน YouTube Music",
|
"play-on-youtube-music": "เล่นบน YouTube Music",
|
||||||
"set-inactivity-timeout": "ตั้งระยะเวลาไม่มีกิจกรรม"
|
"set-inactivity-timeout": "ตั้งระยะเวลาไม่มีกิจกรรม",
|
||||||
|
"set-status-display-type": {
|
||||||
|
"label": "ข้อความสถานะ",
|
||||||
|
"submenu": {
|
||||||
|
"artist": "กำลังฟัง {ชื่อนักร้อง}",
|
||||||
|
"title": "กำลังฟัง {ชื่อเพลง}",
|
||||||
|
"youtube-music": "กำลังฟัง YouTube Music"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"name": "แสดงกิจกรรมบนดิสคอร์ด",
|
"name": "แสดงกิจกรรมบนดิสคอร์ด",
|
||||||
"prompt": {
|
"prompt": {
|
||||||
|
|||||||
40
src/index.ts
40
src/index.ts
@ -15,7 +15,7 @@ import {
|
|||||||
type BrowserWindowConstructorOptions,
|
type BrowserWindowConstructorOptions,
|
||||||
} from 'electron';
|
} from 'electron';
|
||||||
import enhanceWebRequest, {
|
import enhanceWebRequest, {
|
||||||
BetterSession,
|
type BetterSession,
|
||||||
} from '@jellybrick/electron-better-web-request';
|
} from '@jellybrick/electron-better-web-request';
|
||||||
import is from 'electron-is';
|
import is from 'electron-is';
|
||||||
import unhandled from 'electron-unhandled';
|
import unhandled from 'electron-unhandled';
|
||||||
@ -59,14 +59,7 @@ import ErrorHtmlAsset from '@assets/error.html?asset';
|
|||||||
|
|
||||||
import { defaultAuthProxyConfig } from '@/plugins/auth-proxy-adapter/config';
|
import { defaultAuthProxyConfig } from '@/plugins/auth-proxy-adapter/config';
|
||||||
|
|
||||||
import type { PluginConfig } from '@/types/plugins';
|
import { type PluginConfig } from '@/types/plugins';
|
||||||
|
|
||||||
if (!is.macOS()) {
|
|
||||||
delete (await allPlugins())['touchbar'];
|
|
||||||
}
|
|
||||||
if (!is.windows()) {
|
|
||||||
delete (await allPlugins())['taskbar-mediacontrol'];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Catch errors and log them
|
// Catch errors and log them
|
||||||
unhandled({
|
unhandled({
|
||||||
@ -345,8 +338,8 @@ async function createMainWindow() {
|
|||||||
titleBarStyle: useInlineMenu
|
titleBarStyle: useInlineMenu
|
||||||
? 'hidden'
|
? 'hidden'
|
||||||
: is.macOS()
|
: is.macOS()
|
||||||
? 'hiddenInset'
|
? 'hiddenInset'
|
||||||
: 'default',
|
: 'default',
|
||||||
autoHideMenuBar: config.get('options.hideMenu'),
|
autoHideMenuBar: config.get('options.hideMenu'),
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -360,6 +353,8 @@ async function createMainWindow() {
|
|||||||
icon,
|
icon,
|
||||||
width: windowSize.width,
|
width: windowSize.width,
|
||||||
height: windowSize.height,
|
height: windowSize.height,
|
||||||
|
minWidth: 325,
|
||||||
|
minHeight: 425,
|
||||||
backgroundColor: '#000',
|
backgroundColor: '#000',
|
||||||
show: false,
|
show: false,
|
||||||
webPreferences: {
|
webPreferences: {
|
||||||
@ -534,8 +529,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;
|
||||||
@ -956,18 +951,15 @@ function removeContentSecurityPolicy(
|
|||||||
betterSession.webRequest.setResolver(
|
betterSession.webRequest.setResolver(
|
||||||
'onHeadersReceived',
|
'onHeadersReceived',
|
||||||
async (listeners) => {
|
async (listeners) => {
|
||||||
return listeners.reduce(
|
return listeners.reduce(async (accumulator, listener) => {
|
||||||
async (accumulator, listener) => {
|
const acc = await accumulator;
|
||||||
const acc = await accumulator;
|
if (acc.cancel) {
|
||||||
if (acc.cancel) {
|
return acc;
|
||||||
return acc;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
const result = await listener.apply();
|
const result = await listener.apply();
|
||||||
return { ...accumulator, ...result };
|
return { ...accumulator, ...result };
|
||||||
},
|
}, Promise.resolve({ cancel: false }));
|
||||||
Promise.resolve({ cancel: false }),
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { BrowserWindow, ipcMain } from 'electron';
|
import { type BrowserWindow, ipcMain } from 'electron';
|
||||||
|
|
||||||
import { deepmerge } from 'deepmerge-ts';
|
import { deepmerge } from 'deepmerge-ts';
|
||||||
import { allPlugins, mainPlugins } from 'virtual:plugins';
|
import { allPlugins, mainPlugins } from 'virtual:plugins';
|
||||||
|
|||||||
@ -1,11 +1,11 @@
|
|||||||
import is from 'electron-is';
|
import is from 'electron-is';
|
||||||
import {
|
import {
|
||||||
app,
|
app,
|
||||||
BrowserWindow,
|
type BrowserWindow,
|
||||||
clipboard,
|
clipboard,
|
||||||
dialog,
|
dialog,
|
||||||
Menu,
|
Menu,
|
||||||
MenuItem,
|
type MenuItem,
|
||||||
shell,
|
shell,
|
||||||
} from 'electron';
|
} from 'electron';
|
||||||
import prompt from 'custom-electron-prompt';
|
import prompt from 'custom-electron-prompt';
|
||||||
|
|||||||
@ -81,26 +81,26 @@ export default createPlugin<
|
|||||||
<>
|
<>
|
||||||
<Show when={showUnDislike()}>
|
<Show when={showUnDislike()}>
|
||||||
<UnDislikeButton
|
<UnDislikeButton
|
||||||
onClick={this.loadFullList}
|
|
||||||
maskSize={unDislikeMaskSize()}
|
maskSize={unDislikeMaskSize()}
|
||||||
|
onClick={this.loadFullList}
|
||||||
/>
|
/>
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={showDislike()}>
|
<Show when={showDislike()}>
|
||||||
<DislikeButton
|
<DislikeButton
|
||||||
onClick={this.loadFullList}
|
|
||||||
maskSize={dislikeMaskSize()}
|
maskSize={dislikeMaskSize()}
|
||||||
|
onClick={this.loadFullList}
|
||||||
/>
|
/>
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={showLike()}>
|
<Show when={showLike()}>
|
||||||
<LikeButton
|
<LikeButton
|
||||||
onClick={this.loadFullList}
|
|
||||||
maskSize={likeMaskSize()}
|
maskSize={likeMaskSize()}
|
||||||
|
onClick={this.loadFullList}
|
||||||
/>
|
/>
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={showUnLike()}>
|
<Show when={showUnLike()}>
|
||||||
<UnLikeButton
|
<UnLikeButton
|
||||||
onClick={this.loadFullList}
|
|
||||||
maskSize={unLikeMaskSize()}
|
maskSize={unLikeMaskSize()}
|
||||||
|
onClick={this.loadFullList}
|
||||||
/>
|
/>
|
||||||
</Show>
|
</Show>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@ -6,22 +6,23 @@ export interface DislikeButtonProps {
|
|||||||
export const DislikeButton = (props: DislikeButtonProps) => (
|
export const DislikeButton = (props: DislikeButtonProps) => (
|
||||||
<div class="style-scope">
|
<div class="style-scope">
|
||||||
<button
|
<button
|
||||||
id="alldislike"
|
|
||||||
data-type="dislike"
|
|
||||||
data-filled="false"
|
|
||||||
class="like-menu yt-spec-button-shape-next yt-spec-button-shape-next--text yt-spec-button-shape-next--mono yt-spec-button-shape-next--size-m yt-spec-button-shape-next--icon-button"
|
|
||||||
aria-pressed="false"
|
|
||||||
aria-label="Dislike all"
|
aria-label="Dislike all"
|
||||||
|
aria-pressed="false"
|
||||||
|
class="like-menu yt-spec-button-shape-next yt-spec-button-shape-next--text yt-spec-button-shape-next--mono yt-spec-button-shape-next--size-m yt-spec-button-shape-next--icon-button"
|
||||||
|
data-filled="false"
|
||||||
|
data-type="dislike"
|
||||||
|
id="alldislike"
|
||||||
onClick={(e) => props.onClick?.(e)}
|
onClick={(e) => props.onClick?.(e)}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
|
aria-hidden="true"
|
||||||
class="yt-spec-button-shape-next__icon"
|
class="yt-spec-button-shape-next__icon"
|
||||||
style={{
|
style={{
|
||||||
'color': 'var(--ytmusic-setting-item-toggle-active)',
|
'color': 'var(--ytmusic-setting-item-toggle-active)',
|
||||||
}}
|
}}
|
||||||
aria-hidden="true"
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
|
aria-hidden="true"
|
||||||
class="yt-spec-button-shape-next__icon"
|
class="yt-spec-button-shape-next__icon"
|
||||||
style={{
|
style={{
|
||||||
'color': 'white',
|
'color': 'white',
|
||||||
@ -32,24 +33,23 @@ export const DislikeButton = (props: DislikeButtonProps) => (
|
|||||||
'z-index': 1,
|
'z-index': 1,
|
||||||
'position': 'absolute',
|
'position': 'absolute',
|
||||||
}}
|
}}
|
||||||
aria-hidden="true"
|
|
||||||
>
|
>
|
||||||
<div style={{ 'width': '24px', 'height': '24px' }}>
|
<div style={{ 'width': '24px', 'height': '24px' }}>
|
||||||
<svg
|
<svg
|
||||||
viewBox="0 0 24 24"
|
|
||||||
preserveAspectRatio="xMidYMid meet"
|
|
||||||
class="style-scope yt-icon"
|
class="style-scope yt-icon"
|
||||||
|
preserveAspectRatio="xMidYMid meet"
|
||||||
style={{
|
style={{
|
||||||
'pointer-events': 'none',
|
'pointer-events': 'none',
|
||||||
'display': 'block',
|
'display': 'block',
|
||||||
'width': '100%',
|
'width': '100%',
|
||||||
'height': '100%',
|
'height': '100%',
|
||||||
}}
|
}}
|
||||||
|
viewBox="0 0 24 24"
|
||||||
>
|
>
|
||||||
<g class="style-scope yt-icon">
|
<g class="style-scope yt-icon">
|
||||||
<path
|
<path
|
||||||
d="M18,4h3v10h-3V4z M5.23,14h4.23l-1.52,4.94C7.62,19.97,8.46,21,9.62,21c0.58,0,1.14-0.24,1.52-0.65L17,14V4H6.57 C5.5,4,4.59,4.67,4.38,5.61l-1.34,6C2.77,12.85,3.82,14,5.23,14z"
|
|
||||||
class="style-scope yt-icon"
|
class="style-scope yt-icon"
|
||||||
|
d="M18,4h3v10h-3V4z M5.23,14h4.23l-1.52,4.94C7.62,19.97,8.46,21,9.62,21c0.58,0,1.14-0.24,1.52-0.65L17,14V4H6.57 C5.5,4,4.59,4.67,4.38,5.61l-1.34,6C2.77,12.85,3.82,14,5.23,14z"
|
||||||
/>
|
/>
|
||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
@ -62,20 +62,20 @@ export const DislikeButton = (props: DislikeButtonProps) => (
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
viewBox="0 0 24 24"
|
|
||||||
preserveAspectRatio="xMidYMid meet"
|
|
||||||
class="style-scope yt-icon"
|
class="style-scope yt-icon"
|
||||||
|
preserveAspectRatio="xMidYMid meet"
|
||||||
style={{
|
style={{
|
||||||
'pointer-events': 'none',
|
'pointer-events': 'none',
|
||||||
'display': 'block',
|
'display': 'block',
|
||||||
'width': '100%',
|
'width': '100%',
|
||||||
'height': '100%',
|
'height': '100%',
|
||||||
}}
|
}}
|
||||||
|
viewBox="0 0 24 24"
|
||||||
>
|
>
|
||||||
<g class="style-scope yt-icon">
|
<g class="style-scope yt-icon">
|
||||||
<path
|
<path
|
||||||
d="M18,4h3v10h-3V4z M5.23,14h4.23l-1.52,4.94C7.62,19.97,8.46,21,9.62,21c0.58,0,1.14-0.24,1.52-0.65L17,14V4H6.57 C5.5,4,4.59,4.67,4.38,5.61l-1.34,6C2.77,12.85,3.82,14,5.23,14z"
|
|
||||||
class="style-scope yt-icon"
|
class="style-scope yt-icon"
|
||||||
|
d="M18,4h3v10h-3V4z M5.23,14h4.23l-1.52,4.94C7.62,19.97,8.46,21,9.62,21c0.58,0,1.14-0.24,1.52-0.65L17,14V4H6.57 C5.5,4,4.59,4.67,4.38,5.61l-1.34,6C2.77,12.85,3.82,14,5.23,14z"
|
||||||
/>
|
/>
|
||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
@ -87,8 +87,8 @@ export const DislikeButton = (props: DislikeButtonProps) => (
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="yt-spec-touch-feedback-shape yt-spec-touch-feedback-shape--touch-response"
|
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
|
class="yt-spec-touch-feedback-shape yt-spec-touch-feedback-shape--touch-response"
|
||||||
>
|
>
|
||||||
<div class="yt-spec-touch-feedback-shape__stroke" />
|
<div class="yt-spec-touch-feedback-shape__stroke" />
|
||||||
<div class="yt-spec-touch-feedback-shape__fill" />
|
<div class="yt-spec-touch-feedback-shape__fill" />
|
||||||
|
|||||||
@ -6,22 +6,23 @@ export interface LikeButtonProps {
|
|||||||
export const LikeButton = (props: LikeButtonProps) => (
|
export const LikeButton = (props: LikeButtonProps) => (
|
||||||
<div class="style-scope">
|
<div class="style-scope">
|
||||||
<button
|
<button
|
||||||
id="alllike"
|
|
||||||
data-type="like"
|
|
||||||
data-filled="false"
|
|
||||||
class="like-menu yt-spec-button-shape-next yt-spec-button-shape-next--text yt-spec-button-shape-next--mono yt-spec-button-shape-next--size-m yt-spec-button-shape-next--icon-button"
|
|
||||||
aria-pressed="false"
|
|
||||||
aria-label="Like all"
|
aria-label="Like all"
|
||||||
|
aria-pressed="false"
|
||||||
|
class="like-menu yt-spec-button-shape-next yt-spec-button-shape-next--text yt-spec-button-shape-next--mono yt-spec-button-shape-next--size-m yt-spec-button-shape-next--icon-button"
|
||||||
|
data-filled="false"
|
||||||
|
data-type="like"
|
||||||
|
id="alllike"
|
||||||
onClick={(e) => props.onClick?.(e)}
|
onClick={(e) => props.onClick?.(e)}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
|
aria-hidden="true"
|
||||||
class="yt-spec-button-shape-next__icon"
|
class="yt-spec-button-shape-next__icon"
|
||||||
style={{
|
style={{
|
||||||
'color': 'var(--ytmusic-setting-item-toggle-active)',
|
'color': 'var(--ytmusic-setting-item-toggle-active)',
|
||||||
}}
|
}}
|
||||||
aria-hidden="true"
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
|
aria-hidden="true"
|
||||||
class="yt-spec-button-shape-next__icon"
|
class="yt-spec-button-shape-next__icon"
|
||||||
style={{
|
style={{
|
||||||
'color': 'white',
|
'color': 'white',
|
||||||
@ -32,24 +33,23 @@ export const LikeButton = (props: LikeButtonProps) => (
|
|||||||
'z-index': 1,
|
'z-index': 1,
|
||||||
'position': 'absolute',
|
'position': 'absolute',
|
||||||
}}
|
}}
|
||||||
aria-hidden="true"
|
|
||||||
>
|
>
|
||||||
<div style={{ 'width': '24px', 'height': '24px' }}>
|
<div style={{ 'width': '24px', 'height': '24px' }}>
|
||||||
<svg
|
<svg
|
||||||
viewBox="0 0 24 24"
|
|
||||||
preserveAspectRatio="xMidYMid meet"
|
|
||||||
class="style-scope yt-icon"
|
class="style-scope yt-icon"
|
||||||
|
preserveAspectRatio="xMidYMid meet"
|
||||||
style={{
|
style={{
|
||||||
'pointer-events': 'none',
|
'pointer-events': 'none',
|
||||||
'display': 'block',
|
'display': 'block',
|
||||||
'width': '100%',
|
'width': '100%',
|
||||||
'height': '100%',
|
'height': '100%',
|
||||||
}}
|
}}
|
||||||
|
viewBox="0 0 24 24"
|
||||||
>
|
>
|
||||||
<g class="style-scope yt-icon">
|
<g class="style-scope yt-icon">
|
||||||
<path
|
<path
|
||||||
d="M3,11h3v10H3V11z M18.77,11h-4.23l1.52-4.94C16.38,5.03,15.54,4,14.38,4c-0.58,0-1.14,0.24-1.52,0.65L7,11v10h10.43 c1.06,0,1.98-0.67,2.19-1.61l1.34-6C21.23,12.15,20.18,11,18.77,11z"
|
|
||||||
class="style-scope yt-icon"
|
class="style-scope yt-icon"
|
||||||
|
d="M3,11h3v10H3V11z M18.77,11h-4.23l1.52-4.94C16.38,5.03,15.54,4,14.38,4c-0.58,0-1.14,0.24-1.52,0.65L7,11v10h10.43 c1.06,0,1.98-0.67,2.19-1.61l1.34-6C21.23,12.15,20.18,11,18.77,11z"
|
||||||
/>
|
/>
|
||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
@ -57,20 +57,20 @@ export const LikeButton = (props: LikeButtonProps) => (
|
|||||||
</div>
|
</div>
|
||||||
<div style={{ 'width': '24px', 'height': '24px' }}>
|
<div style={{ 'width': '24px', 'height': '24px' }}>
|
||||||
<svg
|
<svg
|
||||||
viewBox="0 0 24 24"
|
|
||||||
preserveAspectRatio="xMidYMid meet"
|
|
||||||
class="style-scope yt-icon"
|
class="style-scope yt-icon"
|
||||||
|
preserveAspectRatio="xMidYMid meet"
|
||||||
style={{
|
style={{
|
||||||
'pointer-events': 'none',
|
'pointer-events': 'none',
|
||||||
'display': 'block',
|
'display': 'block',
|
||||||
'width': '100%',
|
'width': '100%',
|
||||||
'height': '100%',
|
'height': '100%',
|
||||||
}}
|
}}
|
||||||
|
viewBox="0 0 24 24"
|
||||||
>
|
>
|
||||||
<g class="style-scope yt-icon">
|
<g class="style-scope yt-icon">
|
||||||
<path
|
<path
|
||||||
d="M3,11h3v10H3V11z M18.77,11h-4.23l1.52-4.94C16.38,5.03,15.54,4,14.38,4c-0.58,0-1.14,0.24-1.52,0.65L7,11v10h10.43 c1.06,0,1.98-0.67,2.19-1.61l1.34-6C21.23,12.15,20.18,11,18.77,11z"
|
|
||||||
class="style-scope yt-icon"
|
class="style-scope yt-icon"
|
||||||
|
d="M3,11h3v10H3V11z M18.77,11h-4.23l1.52-4.94C16.38,5.03,15.54,4,14.38,4c-0.58,0-1.14,0.24-1.52,0.65L7,11v10h10.43 c1.06,0,1.98-0.67,2.19-1.61l1.34-6C21.23,12.15,20.18,11,18.77,11z"
|
||||||
/>
|
/>
|
||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
@ -78,8 +78,8 @@ export const LikeButton = (props: LikeButtonProps) => (
|
|||||||
</div>
|
</div>
|
||||||
<yt-touch-feedback-shape style={{ 'border-radius': 'inherit' }}>
|
<yt-touch-feedback-shape style={{ 'border-radius': 'inherit' }}>
|
||||||
<div
|
<div
|
||||||
class="yt-spec-touch-feedback-shape yt-spec-touch-feedback-shape--touch-response"
|
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
|
class="yt-spec-touch-feedback-shape yt-spec-touch-feedback-shape--touch-response"
|
||||||
>
|
>
|
||||||
<div class="yt-spec-touch-feedback-shape__stroke" />
|
<div class="yt-spec-touch-feedback-shape__stroke" />
|
||||||
<div class="yt-spec-touch-feedback-shape__fill" />
|
<div class="yt-spec-touch-feedback-shape__fill" />
|
||||||
|
|||||||
@ -6,22 +6,23 @@ export interface UnDislikeButtonProps {
|
|||||||
export const UnDislikeButton = (props: UnDislikeButtonProps) => (
|
export const UnDislikeButton = (props: UnDislikeButtonProps) => (
|
||||||
<div class="style-scope">
|
<div class="style-scope">
|
||||||
<button
|
<button
|
||||||
id="allundislike"
|
|
||||||
data-type="dislike"
|
|
||||||
data-filled="true"
|
|
||||||
class="like-menu yt-spec-button-shape-next yt-spec-button-shape-next--text yt-spec-button-shape-next--mono yt-spec-button-shape-next--size-m yt-spec-button-shape-next--icon-button"
|
|
||||||
aria-pressed="false"
|
|
||||||
aria-label="Undislike all"
|
aria-label="Undislike all"
|
||||||
|
aria-pressed="false"
|
||||||
|
class="like-menu yt-spec-button-shape-next yt-spec-button-shape-next--text yt-spec-button-shape-next--mono yt-spec-button-shape-next--size-m yt-spec-button-shape-next--icon-button"
|
||||||
|
data-filled="true"
|
||||||
|
data-type="dislike"
|
||||||
|
id="allundislike"
|
||||||
onClick={(e) => props.onClick?.(e)}
|
onClick={(e) => props.onClick?.(e)}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
|
aria-hidden="true"
|
||||||
class="yt-spec-button-shape-next__icon"
|
class="yt-spec-button-shape-next__icon"
|
||||||
style={{
|
style={{
|
||||||
color: 'var(--ytmusic-setting-item-toggle-active)',
|
color: 'var(--ytmusic-setting-item-toggle-active)',
|
||||||
}}
|
}}
|
||||||
aria-hidden="true"
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
|
aria-hidden="true"
|
||||||
class="yt-spec-button-shape-next__icon"
|
class="yt-spec-button-shape-next__icon"
|
||||||
style={{
|
style={{
|
||||||
'color': 'white',
|
'color': 'white',
|
||||||
@ -32,7 +33,6 @@ export const UnDislikeButton = (props: UnDislikeButtonProps) => (
|
|||||||
'z-index': 1,
|
'z-index': 1,
|
||||||
'position': 'absolute',
|
'position': 'absolute',
|
||||||
}}
|
}}
|
||||||
aria-hidden="true"
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
@ -41,20 +41,20 @@ export const UnDislikeButton = (props: UnDislikeButtonProps) => (
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
viewBox="0 0 24 24"
|
|
||||||
preserveAspectRatio="xMidYMid meet"
|
|
||||||
class="style-scope yt-icon"
|
class="style-scope yt-icon"
|
||||||
|
preserveAspectRatio="xMidYMid meet"
|
||||||
style={{
|
style={{
|
||||||
'pointer-events': 'none',
|
'pointer-events': 'none',
|
||||||
'display': 'block',
|
'display': 'block',
|
||||||
'width': '100%',
|
'width': '100%',
|
||||||
'height': '100%',
|
'height': '100%',
|
||||||
}}
|
}}
|
||||||
|
viewBox="0 0 24 24"
|
||||||
>
|
>
|
||||||
<g class="style-scope yt-icon">
|
<g class="style-scope yt-icon">
|
||||||
<path
|
<path
|
||||||
d="M17,4h-1H6.57C5.5,4,4.59,4.67,4.38,5.61l-1.34,6C2.77,12.85,3.82,14,5.23,14h4.23l-1.52,4.94C7.62,19.97,8.46,21,9.62,21 c0.58,0,1.14-0.24,1.52-0.65L17,14h4V4H17z M10.4,19.67C10.21,19.88,9.92,20,9.62,20c-0.26,0-0.5-0.11-0.63-0.3 c-0.07-0.1-0.15-0.26-0.09-0.47l1.52-4.94l0.4-1.29H9.46H5.23c-0.41,0-0.8-0.17-1.03-0.46c-0.12-0.15-0.25-0.4-0.18-0.72l1.34-6 C5.46,5.35,5.97,5,6.57,5H16v8.61L10.4,19.67z M20,13h-3V5h3V13z"
|
|
||||||
class="style-scope yt-icon"
|
class="style-scope yt-icon"
|
||||||
|
d="M17,4h-1H6.57C5.5,4,4.59,4.67,4.38,5.61l-1.34,6C2.77,12.85,3.82,14,5.23,14h4.23l-1.52,4.94C7.62,19.97,8.46,21,9.62,21 c0.58,0,1.14-0.24,1.52-0.65L17,14h4V4H17z M10.4,19.67C10.21,19.88,9.92,20,9.62,20c-0.26,0-0.5-0.11-0.63-0.3 c-0.07-0.1-0.15-0.26-0.09-0.47l1.52-4.94l0.4-1.29H9.46H5.23c-0.41,0-0.8-0.17-1.03-0.46c-0.12-0.15-0.25-0.4-0.18-0.72l1.34-6 C5.46,5.35,5.97,5,6.57,5H16v8.61L10.4,19.67z M20,13h-3V5h3V13z"
|
||||||
/>
|
/>
|
||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
@ -67,20 +67,20 @@ export const UnDislikeButton = (props: UnDislikeButtonProps) => (
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
viewBox="0 0 24 24"
|
|
||||||
preserveAspectRatio="xMidYMid meet"
|
|
||||||
class="style-scope yt-icon"
|
class="style-scope yt-icon"
|
||||||
|
preserveAspectRatio="xMidYMid meet"
|
||||||
style={{
|
style={{
|
||||||
'pointer-events': 'none',
|
'pointer-events': 'none',
|
||||||
'display': 'block',
|
'display': 'block',
|
||||||
'width': '100%',
|
'width': '100%',
|
||||||
'height': '100%',
|
'height': '100%',
|
||||||
}}
|
}}
|
||||||
|
viewBox="0 0 24 24"
|
||||||
>
|
>
|
||||||
<g class="style-scope yt-icon">
|
<g class="style-scope yt-icon">
|
||||||
<path
|
<path
|
||||||
d="M17,4h-1H6.57C5.5,4,4.59,4.67,4.38,5.61l-1.34,6C2.77,12.85,3.82,14,5.23,14h4.23l-1.52,4.94C7.62,19.97,8.46,21,9.62,21 c0.58,0,1.14-0.24,1.52-0.65L17,14h4V4H17z M10.4,19.67C10.21,19.88,9.92,20,9.62,20c-0.26,0-0.5-0.11-0.63-0.3 c-0.07-0.1-0.15-0.26-0.09-0.47l1.52-4.94l0.4-1.29H9.46H5.23c-0.41,0-0.8-0.17-1.03-0.46c-0.12-0.15-0.25-0.4-0.18-0.72l1.34-6 C5.46,5.35,5.97,5,6.57,5H16v8.61L10.4,19.67z M20,13h-3V5h3V13z"
|
|
||||||
class="style-scope yt-icon"
|
class="style-scope yt-icon"
|
||||||
|
d="M17,4h-1H6.57C5.5,4,4.59,4.67,4.38,5.61l-1.34,6C2.77,12.85,3.82,14,5.23,14h4.23l-1.52,4.94C7.62,19.97,8.46,21,9.62,21 c0.58,0,1.14-0.24,1.52-0.65L17,14h4V4H17z M10.4,19.67C10.21,19.88,9.92,20,9.62,20c-0.26,0-0.5-0.11-0.63-0.3 c-0.07-0.1-0.15-0.26-0.09-0.47l1.52-4.94l0.4-1.29H9.46H5.23c-0.41,0-0.8-0.17-1.03-0.46c-0.12-0.15-0.25-0.4-0.18-0.72l1.34-6 C5.46,5.35,5.97,5,6.57,5H16v8.61L10.4,19.67z M20,13h-3V5h3V13z"
|
||||||
/>
|
/>
|
||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
@ -92,8 +92,8 @@ export const UnDislikeButton = (props: UnDislikeButtonProps) => (
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="yt-spec-touch-feedback-shape yt-spec-touch-feedback-shape--touch-response"
|
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
|
class="yt-spec-touch-feedback-shape yt-spec-touch-feedback-shape--touch-response"
|
||||||
>
|
>
|
||||||
<div class="yt-spec-touch-feedback-shape__stroke" />
|
<div class="yt-spec-touch-feedback-shape__stroke" />
|
||||||
<div class="yt-spec-touch-feedback-shape__fill" />
|
<div class="yt-spec-touch-feedback-shape__fill" />
|
||||||
|
|||||||
@ -6,22 +6,23 @@ export interface UnLikeButtonProps {
|
|||||||
export const UnLikeButton = (props: UnLikeButtonProps) => (
|
export const UnLikeButton = (props: UnLikeButtonProps) => (
|
||||||
<div class="style-scope">
|
<div class="style-scope">
|
||||||
<button
|
<button
|
||||||
id="allunlike"
|
|
||||||
data-type="like"
|
|
||||||
data-filled="true"
|
|
||||||
class="like-menu yt-spec-button-shape-next yt-spec-button-shape-next--text yt-spec-button-shape-next--mono yt-spec-button-shape-next--size-m yt-spec-button-shape-next--icon-button"
|
|
||||||
aria-pressed="false"
|
|
||||||
aria-label="Unlike all"
|
aria-label="Unlike all"
|
||||||
|
aria-pressed="false"
|
||||||
|
class="like-menu yt-spec-button-shape-next yt-spec-button-shape-next--text yt-spec-button-shape-next--mono yt-spec-button-shape-next--size-m yt-spec-button-shape-next--icon-button"
|
||||||
|
data-filled="true"
|
||||||
|
data-type="like"
|
||||||
|
id="allunlike"
|
||||||
onClick={(e) => props.onClick?.(e)}
|
onClick={(e) => props.onClick?.(e)}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
|
aria-hidden="true"
|
||||||
class="yt-spec-button-shape-next__icon"
|
class="yt-spec-button-shape-next__icon"
|
||||||
style={{
|
style={{
|
||||||
'color': 'var(--ytmusic-setting-item-toggle-active)',
|
'color': 'var(--ytmusic-setting-item-toggle-active)',
|
||||||
}}
|
}}
|
||||||
aria-hidden="true"
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
|
aria-hidden="true"
|
||||||
class="yt-spec-button-shape-next__icon"
|
class="yt-spec-button-shape-next__icon"
|
||||||
style={{
|
style={{
|
||||||
'color': 'white',
|
'color': 'white',
|
||||||
@ -32,7 +33,6 @@ export const UnLikeButton = (props: UnLikeButtonProps) => (
|
|||||||
'z-index': 1,
|
'z-index': 1,
|
||||||
'position': 'absolute',
|
'position': 'absolute',
|
||||||
}}
|
}}
|
||||||
aria-hidden="true"
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
@ -41,20 +41,20 @@ export const UnLikeButton = (props: UnLikeButtonProps) => (
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
viewBox="0 0 24 24"
|
|
||||||
preserveAspectRatio="xMidYMid meet"
|
|
||||||
class="style-scope yt-icon"
|
class="style-scope yt-icon"
|
||||||
|
preserveAspectRatio="xMidYMid meet"
|
||||||
style={{
|
style={{
|
||||||
'pointer-events': 'none',
|
'pointer-events': 'none',
|
||||||
'display': 'block',
|
'display': 'block',
|
||||||
'width': '100%',
|
'width': '100%',
|
||||||
'height': '100%',
|
'height': '100%',
|
||||||
}}
|
}}
|
||||||
|
viewBox="0 0 24 24"
|
||||||
>
|
>
|
||||||
<g class="style-scope yt-icon">
|
<g class="style-scope yt-icon">
|
||||||
<path
|
<path
|
||||||
d="M18.77,11h-4.23l1.52-4.94C16.38,5.03,15.54,4,14.38,4c-0.58,0-1.14,0.24-1.52,0.65L7,11H3v10h4h1h9.43 c1.06,0,1.98-0.67,2.19-1.61l1.34-6C21.23,12.15,20.18,11,18.77,11z M7,20H4v-8h3V20z M19.98,13.17l-1.34,6 C18.54,19.65,18.03,20,17.43,20H8v-8.61l5.6-6.06C13.79,5.12,14.08,5,14.38,5c0.26,0,0.5,0.11,0.63,0.3 c0.07,0.1,0.15,0.26,0.09,0.47l-1.52,4.94L13.18,12h1.35h4.23c0.41,0,0.8,0.17,1.03,0.46C19.92,12.61,20.05,12.86,19.98,13.17z"
|
|
||||||
class="style-scope yt-icon"
|
class="style-scope yt-icon"
|
||||||
|
d="M18.77,11h-4.23l1.52-4.94C16.38,5.03,15.54,4,14.38,4c-0.58,0-1.14,0.24-1.52,0.65L7,11H3v10h4h1h9.43 c1.06,0,1.98-0.67,2.19-1.61l1.34-6C21.23,12.15,20.18,11,18.77,11z M7,20H4v-8h3V20z M19.98,13.17l-1.34,6 C18.54,19.65,18.03,20,17.43,20H8v-8.61l5.6-6.06C13.79,5.12,14.08,5,14.38,5c0.26,0,0.5,0.11,0.63,0.3 c0.07,0.1,0.15,0.26,0.09,0.47l-1.52,4.94L13.18,12h1.35h4.23c0.41,0,0.8,0.17,1.03,0.46C19.92,12.61,20.05,12.86,19.98,13.17z"
|
||||||
/>
|
/>
|
||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
@ -67,20 +67,20 @@ export const UnLikeButton = (props: UnLikeButtonProps) => (
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
viewBox="0 0 24 24"
|
|
||||||
preserveAspectRatio="xMidYMid meet"
|
|
||||||
class="style-scope yt-icon"
|
class="style-scope yt-icon"
|
||||||
|
preserveAspectRatio="xMidYMid meet"
|
||||||
style={{
|
style={{
|
||||||
'pointer-events': 'none',
|
'pointer-events': 'none',
|
||||||
'display': 'block',
|
'display': 'block',
|
||||||
'width': '100%',
|
'width': '100%',
|
||||||
'height': '100%',
|
'height': '100%',
|
||||||
}}
|
}}
|
||||||
|
viewBox="0 0 24 24"
|
||||||
>
|
>
|
||||||
<g class="style-scope yt-icon">
|
<g class="style-scope yt-icon">
|
||||||
<path
|
<path
|
||||||
d="M18.77,11h-4.23l1.52-4.94C16.38,5.03,15.54,4,14.38,4c-0.58,0-1.14,0.24-1.52,0.65L7,11H3v10h4h1h9.43 c1.06,0,1.98-0.67,2.19-1.61l1.34-6C21.23,12.15,20.18,11,18.77,11z M7,20H4v-8h3V20z M19.98,13.17l-1.34,6 C18.54,19.65,18.03,20,17.43,20H8v-8.61l5.6-6.06C13.79,5.12,14.08,5,14.38,5c0.26,0,0.5,0.11,0.63,0.3 c0.07,0.1,0.15,0.26,0.09,0.47l-1.52,4.94L13.18,12h1.35h4.23c0.41,0,0.8,0.17,1.03,0.46C19.92,12.61,20.05,12.86,19.98,13.17z"
|
|
||||||
class="style-scope yt-icon"
|
class="style-scope yt-icon"
|
||||||
|
d="M18.77,11h-4.23l1.52-4.94C16.38,5.03,15.54,4,14.38,4c-0.58,0-1.14,0.24-1.52,0.65L7,11H3v10h4h1h9.43 c1.06,0,1.98-0.67,2.19-1.61l1.34-6C21.23,12.15,20.18,11,18.77,11z M7,20H4v-8h3V20z M19.98,13.17l-1.34,6 C18.54,19.65,18.03,20,17.43,20H8v-8.61l5.6-6.06C13.79,5.12,14.08,5,14.38,5c0.26,0,0.5,0.11,0.63,0.3 c0.07,0.1,0.15,0.26,0.09,0.47l-1.52,4.94L13.18,12h1.35h4.23c0.41,0,0.8,0.17,1.03,0.46C19.92,12.61,20.05,12.86,19.98,13.17z"
|
||||||
/>
|
/>
|
||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
@ -92,8 +92,8 @@ export const UnLikeButton = (props: UnLikeButtonProps) => (
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="yt-spec-touch-feedback-shape yt-spec-touch-feedback-shape--touch-response"
|
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
|
class="yt-spec-touch-feedback-shape yt-spec-touch-feedback-shape--touch-response"
|
||||||
>
|
>
|
||||||
<div class="yt-spec-touch-feedback-shape__stroke" />
|
<div class="yt-spec-touch-feedback-shape__stroke" />
|
||||||
<div class="yt-spec-touch-feedback-shape__fill" />
|
<div class="yt-spec-touch-feedback-shape__fill" />
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { FastAverageColor } from 'fast-average-color';
|
import { FastAverageColor } from 'fast-average-color';
|
||||||
import Color, { ColorInstance } from 'color';
|
import Color, { type ColorInstance } from 'color';
|
||||||
|
|
||||||
import style from './style.css?inline';
|
import style from './style.css?inline';
|
||||||
|
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import style from './style.css?inline';
|
|||||||
import { t } from '@/i18n';
|
import { t } from '@/i18n';
|
||||||
import { createPlugin } from '@/utils';
|
import { createPlugin } from '@/utils';
|
||||||
import { menu } from './menu';
|
import { menu } from './menu';
|
||||||
import { AmbientModePluginConfig } from './types';
|
import { type AmbientModePluginConfig } from './types';
|
||||||
import { waitForElement } from '@/utils/wait-for-element';
|
import { waitForElement } from '@/utils/wait-for-element';
|
||||||
|
|
||||||
const defaultConfig: AmbientModePluginConfig = {
|
const defaultConfig: AmbientModePluginConfig = {
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
import { MenuItemConstructorOptions } from 'electron';
|
import { type MenuItemConstructorOptions } from 'electron';
|
||||||
|
|
||||||
import { t } from '@/i18n';
|
import { t } from '@/i18n';
|
||||||
import { MenuContext } from '@/types/contexts';
|
import { type MenuContext } from '@/types/contexts';
|
||||||
import { AmbientModePluginConfig } from './types';
|
import { type AmbientModePluginConfig } from './types';
|
||||||
|
|
||||||
export interface menuParameters {
|
export interface menuParameters {
|
||||||
getConfig: () => AmbientModePluginConfig | Promise<AmbientModePluginConfig>;
|
getConfig: () => AmbientModePluginConfig | Promise<AmbientModePluginConfig>;
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import { type Context, Hono } from 'hono';
|
|||||||
import { cors } from 'hono/cors';
|
import { cors } from 'hono/cors';
|
||||||
import { serve } from '@hono/node-server';
|
import { serve } from '@hono/node-server';
|
||||||
|
|
||||||
import registerCallback, { type SongInfo } from '@/providers/song-info';
|
import { registerCallback, type SongInfo } from '@/providers/song-info';
|
||||||
import { createBackend } from '@/utils';
|
import { createBackend } from '@/utils';
|
||||||
|
|
||||||
import type { AmuseSongInfo } from './types';
|
import type { AmuseSongInfo } from './types';
|
||||||
|
|||||||
1
src/plugins/api-server/backend/api-version.ts
Normal file
1
src/plugins/api-server/backend/api-version.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export const API_VERSION = 'v1';
|
||||||
@ -3,17 +3,22 @@ import { OpenAPIHono as Hono } from '@hono/zod-openapi';
|
|||||||
import { cors } from 'hono/cors';
|
import { cors } from 'hono/cors';
|
||||||
import { swaggerUI } from '@hono/swagger-ui';
|
import { swaggerUI } from '@hono/swagger-ui';
|
||||||
import { serve } from '@hono/node-server';
|
import { serve } from '@hono/node-server';
|
||||||
|
import { createNodeWebSocket } from '@hono/node-ws';
|
||||||
|
|
||||||
import registerCallback from '@/providers/song-info';
|
import { registerCallback } from '@/providers/song-info';
|
||||||
import { createBackend } from '@/utils';
|
import { createBackend } from '@/utils';
|
||||||
|
|
||||||
import { JWTPayloadSchema } from './scheme';
|
import { JWTPayloadSchema } from './scheme';
|
||||||
import { registerAuth, registerControl } from './routes';
|
import { registerAuth, registerControl, registerWebsocket } from './routes';
|
||||||
|
|
||||||
import { type APIServerConfig, AuthStrategy } from '../config';
|
import { type APIServerConfig, AuthStrategy } from '../config';
|
||||||
|
|
||||||
import type { BackendType } from './types';
|
import type { BackendType } from './types';
|
||||||
import type { RepeatMode } from '@/types/datahost-get-state';
|
import type {
|
||||||
|
LikeType,
|
||||||
|
RepeatMode,
|
||||||
|
VolumeState,
|
||||||
|
} from '@/types/datahost-get-state';
|
||||||
|
|
||||||
export const backend = createBackend<BackendType, APIServerConfig>({
|
export const backend = createBackend<BackendType, APIServerConfig>({
|
||||||
async start(ctx) {
|
async start(ctx) {
|
||||||
@ -25,8 +30,10 @@ export const backend = createBackend<BackendType, APIServerConfig>({
|
|||||||
});
|
});
|
||||||
|
|
||||||
ctx.ipc.on('ytmd:player-api-loaded', () => {
|
ctx.ipc.on('ytmd:player-api-loaded', () => {
|
||||||
|
ctx.ipc.send('ytmd:setup-seeked-listener');
|
||||||
ctx.ipc.send('ytmd:setup-time-changed-listener');
|
ctx.ipc.send('ytmd:setup-time-changed-listener');
|
||||||
ctx.ipc.send('ytmd:setup-repeat-changed-listener');
|
ctx.ipc.send('ytmd:setup-repeat-changed-listener');
|
||||||
|
ctx.ipc.send('ytmd:setup-like-changed-listener');
|
||||||
ctx.ipc.send('ytmd:setup-volume-changed-listener');
|
ctx.ipc.send('ytmd:setup-volume-changed-listener');
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -37,7 +44,7 @@ export const backend = createBackend<BackendType, APIServerConfig>({
|
|||||||
|
|
||||||
ctx.ipc.on(
|
ctx.ipc.on(
|
||||||
'ytmd:volume-changed',
|
'ytmd:volume-changed',
|
||||||
(newVolume: number) => (this.volume = newVolume),
|
(newVolumeState: VolumeState) => (this.volumeState = newVolumeState),
|
||||||
);
|
);
|
||||||
|
|
||||||
this.run(config.hostname, config.port);
|
this.run(config.hostname, config.port);
|
||||||
@ -62,6 +69,9 @@ export const backend = createBackend<BackendType, APIServerConfig>({
|
|||||||
// Custom
|
// Custom
|
||||||
init(backendCtx) {
|
init(backendCtx) {
|
||||||
this.app = new Hono();
|
this.app = new Hono();
|
||||||
|
const ws = createNodeWebSocket({
|
||||||
|
app: this.app,
|
||||||
|
});
|
||||||
|
|
||||||
this.app.use('*', cors());
|
this.app.use('*', cors());
|
||||||
|
|
||||||
@ -103,9 +113,14 @@ export const backend = createBackend<BackendType, APIServerConfig>({
|
|||||||
backendCtx,
|
backendCtx,
|
||||||
() => this.songInfo,
|
() => this.songInfo,
|
||||||
() => this.currentRepeatMode,
|
() => this.currentRepeatMode,
|
||||||
() => this.volume,
|
() =>
|
||||||
|
backendCtx.window.webContents.executeJavaScript(
|
||||||
|
'document.querySelector("#like-button-renderer")?.likeStatus',
|
||||||
|
) as Promise<LikeType>,
|
||||||
|
() => this.volumeState,
|
||||||
);
|
);
|
||||||
registerAuth(this.app, backendCtx);
|
registerAuth(this.app, backendCtx);
|
||||||
|
registerWebsocket(this.app, ws);
|
||||||
|
|
||||||
// swagger
|
// swagger
|
||||||
this.app.openAPIRegistry.registerComponent(
|
this.app.openAPIRegistry.registerComponent(
|
||||||
@ -133,6 +148,8 @@ export const backend = createBackend<BackendType, APIServerConfig>({
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.app.get('/swagger', swaggerUI({ url: '/doc' }));
|
this.app.get('/swagger', swaggerUI({ url: '/doc' }));
|
||||||
|
|
||||||
|
this.injectWebSocket = ws.injectWebSocket.bind(this);
|
||||||
},
|
},
|
||||||
run(hostname, port) {
|
run(hostname, port) {
|
||||||
if (!this.app) return;
|
if (!this.app) return;
|
||||||
@ -143,6 +160,10 @@ export const backend = createBackend<BackendType, APIServerConfig>({
|
|||||||
port,
|
port,
|
||||||
hostname,
|
hostname,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (this.injectWebSocket && this.server) {
|
||||||
|
this.injectWebSocket(this.server);
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,8 +1,12 @@
|
|||||||
import { createRoute, z } from '@hono/zod-openapi';
|
import { createRoute, z } from '@hono/zod-openapi';
|
||||||
|
|
||||||
import { ipcMain } from 'electron';
|
import { ipcMain } from 'electron';
|
||||||
|
|
||||||
import getSongControls from '@/providers/song-controls';
|
import getSongControls from '@/providers/song-controls';
|
||||||
|
import {
|
||||||
|
LikeType,
|
||||||
|
type RepeatMode,
|
||||||
|
type VolumeState,
|
||||||
|
} from '@/types/datahost-get-state';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
AddSongToQueueSchema,
|
AddSongToQueueSchema,
|
||||||
@ -19,8 +23,8 @@ import {
|
|||||||
SwitchRepeatSchema,
|
SwitchRepeatSchema,
|
||||||
type ResponseSongInfo,
|
type ResponseSongInfo,
|
||||||
} from '../scheme';
|
} from '../scheme';
|
||||||
|
import { API_VERSION } from '../api-version';
|
||||||
|
|
||||||
import type { RepeatMode } from '@/types/datahost-get-state';
|
|
||||||
import type { SongInfo } from '@/providers/song-info';
|
import type { SongInfo } from '@/providers/song-info';
|
||||||
import type { BackendContext } from '@/types/contexts';
|
import type { BackendContext } from '@/types/contexts';
|
||||||
import type { APIServerConfig } from '../../config';
|
import type { APIServerConfig } from '../../config';
|
||||||
@ -28,8 +32,6 @@ import type { HonoApp } from '../types';
|
|||||||
import type { QueueResponse } from '@/types/youtube-music-desktop-internal';
|
import type { QueueResponse } from '@/types/youtube-music-desktop-internal';
|
||||||
import type { Context } from 'hono';
|
import type { Context } from 'hono';
|
||||||
|
|
||||||
const API_VERSION = 'v1';
|
|
||||||
|
|
||||||
const routes = {
|
const routes = {
|
||||||
previous: createRoute({
|
previous: createRoute({
|
||||||
method: 'post',
|
method: 'post',
|
||||||
@ -87,6 +89,24 @@ const routes = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
getLikeState: createRoute({
|
||||||
|
method: 'get',
|
||||||
|
path: `/api/${API_VERSION}/like-state`,
|
||||||
|
summary: 'get like state',
|
||||||
|
description: 'Get the current like state',
|
||||||
|
responses: {
|
||||||
|
200: {
|
||||||
|
description: 'Success',
|
||||||
|
content: {
|
||||||
|
'application/json': {
|
||||||
|
schema: z.object({
|
||||||
|
state: z.enum(LikeType).nullable(),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
like: createRoute({
|
like: createRoute({
|
||||||
method: 'post',
|
method: 'post',
|
||||||
path: `/api/${API_VERSION}/like`,
|
path: `/api/${API_VERSION}/like`,
|
||||||
@ -274,6 +294,7 @@ const routes = {
|
|||||||
'application/json': {
|
'application/json': {
|
||||||
schema: z.object({
|
schema: z.object({
|
||||||
state: z.number(),
|
state: z.number(),
|
||||||
|
isMuted: z.boolean(),
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -526,12 +547,15 @@ const routes = {
|
|||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type PromiseOrValue<T> = T | Promise<T>;
|
||||||
|
|
||||||
export const register = (
|
export const register = (
|
||||||
app: HonoApp,
|
app: HonoApp,
|
||||||
{ window }: BackendContext<APIServerConfig>,
|
{ window }: BackendContext<APIServerConfig>,
|
||||||
songInfoGetter: () => SongInfo | undefined,
|
songInfoGetter: () => PromiseOrValue<SongInfo | undefined>,
|
||||||
repeatModeGetter: () => RepeatMode | undefined,
|
repeatModeGetter: () => PromiseOrValue<RepeatMode | undefined>,
|
||||||
volumeGetter: () => number | undefined,
|
likeTypeGetter: () => PromiseOrValue<LikeType | undefined>,
|
||||||
|
volumeStateGetter: () => PromiseOrValue<VolumeState | undefined>,
|
||||||
) => {
|
) => {
|
||||||
const controller = getSongControls(window);
|
const controller = getSongControls(window);
|
||||||
|
|
||||||
@ -565,6 +589,10 @@ export const register = (
|
|||||||
ctx.status(204);
|
ctx.status(204);
|
||||||
return ctx.body(null);
|
return ctx.body(null);
|
||||||
});
|
});
|
||||||
|
app.openapi(routes.getLikeState, async (ctx) => {
|
||||||
|
ctx.status(200);
|
||||||
|
return ctx.json({ state: (await likeTypeGetter()) ?? null });
|
||||||
|
});
|
||||||
app.openapi(routes.like, (ctx) => {
|
app.openapi(routes.like, (ctx) => {
|
||||||
controller.like();
|
controller.like();
|
||||||
|
|
||||||
@ -624,9 +652,9 @@ export const register = (
|
|||||||
return ctx.body(null);
|
return ctx.body(null);
|
||||||
});
|
});
|
||||||
|
|
||||||
app.openapi(routes.repeatMode, (ctx) => {
|
app.openapi(routes.repeatMode, async (ctx) => {
|
||||||
ctx.status(200);
|
ctx.status(200);
|
||||||
return ctx.json({ mode: repeatModeGetter() ?? null });
|
return ctx.json({ mode: (await repeatModeGetter()) ?? null });
|
||||||
});
|
});
|
||||||
app.openapi(routes.switchRepeat, (ctx) => {
|
app.openapi(routes.switchRepeat, (ctx) => {
|
||||||
const { iteration } = ctx.req.valid('json');
|
const { iteration } = ctx.req.valid('json');
|
||||||
@ -642,9 +670,11 @@ export const register = (
|
|||||||
ctx.status(204);
|
ctx.status(204);
|
||||||
return ctx.body(null);
|
return ctx.body(null);
|
||||||
});
|
});
|
||||||
app.openapi(routes.getVolumeState, (ctx) => {
|
app.openapi(routes.getVolumeState, async (ctx) => {
|
||||||
ctx.status(200);
|
ctx.status(200);
|
||||||
return ctx.json({ state: volumeGetter() ?? 0 });
|
return ctx.json(
|
||||||
|
(await volumeStateGetter()) ?? { state: 0, isMuted: false },
|
||||||
|
);
|
||||||
});
|
});
|
||||||
app.openapi(routes.setFullscreen, (ctx) => {
|
app.openapi(routes.setFullscreen, (ctx) => {
|
||||||
const { state } = ctx.req.valid('json');
|
const { state } = ctx.req.valid('json');
|
||||||
@ -678,8 +708,8 @@ export const register = (
|
|||||||
return ctx.json({ state: fullscreen });
|
return ctx.json({ state: fullscreen });
|
||||||
});
|
});
|
||||||
|
|
||||||
const songInfo = (ctx: Context) => {
|
const songInfo = async (ctx: Context) => {
|
||||||
const info = songInfoGetter();
|
const info = await songInfoGetter();
|
||||||
|
|
||||||
if (!info) {
|
if (!info) {
|
||||||
ctx.status(204);
|
ctx.status(204);
|
||||||
|
|||||||
@ -1,2 +1,3 @@
|
|||||||
export { register as registerControl } from './control';
|
export { register as registerControl } from './control';
|
||||||
export { register as registerAuth } from './auth';
|
export { register as registerAuth } from './auth';
|
||||||
|
export { register as registerWebsocket } from './websocket';
|
||||||
|
|||||||
137
src/plugins/api-server/backend/routes/websocket.ts
Normal file
137
src/plugins/api-server/backend/routes/websocket.ts
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
import { ipcMain } from 'electron';
|
||||||
|
import { createRoute } from '@hono/zod-openapi';
|
||||||
|
|
||||||
|
import { type NodeWebSocket } from '@hono/node-ws';
|
||||||
|
|
||||||
|
import {
|
||||||
|
registerCallback,
|
||||||
|
type SongInfo,
|
||||||
|
SongInfoEvent,
|
||||||
|
} from '@/providers/song-info';
|
||||||
|
|
||||||
|
import { API_VERSION } from '../api-version';
|
||||||
|
|
||||||
|
import type { WSContext } from 'hono/ws';
|
||||||
|
import type { Context, Next } from 'hono';
|
||||||
|
import type { RepeatMode, VolumeState } from '@/types/datahost-get-state';
|
||||||
|
import type { HonoApp } from '../types';
|
||||||
|
|
||||||
|
enum DataTypes {
|
||||||
|
PlayerInfo = 'PLAYER_INFO',
|
||||||
|
VideoChanged = 'VIDEO_CHANGED',
|
||||||
|
PlayerStateChanged = 'PLAYER_STATE_CHANGED',
|
||||||
|
PositionChanged = 'POSITION_CHANGED',
|
||||||
|
VolumeChanged = 'VOLUME_CHANGED',
|
||||||
|
RepeatChanged = 'REPEAT_CHANGED',
|
||||||
|
}
|
||||||
|
|
||||||
|
type PlayerState = {
|
||||||
|
song?: SongInfo;
|
||||||
|
isPlaying: boolean;
|
||||||
|
muted: boolean;
|
||||||
|
position: number;
|
||||||
|
volume: number;
|
||||||
|
repeat: RepeatMode;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const register = (app: HonoApp, nodeWebSocket: NodeWebSocket) => {
|
||||||
|
let volumeState: VolumeState | undefined = undefined;
|
||||||
|
let repeat: RepeatMode = 'NONE';
|
||||||
|
let lastSongInfo: SongInfo | undefined = undefined;
|
||||||
|
|
||||||
|
const sockets = new Set<WSContext<WebSocket>>();
|
||||||
|
|
||||||
|
const send = (type: DataTypes, state: Partial<PlayerState>) => {
|
||||||
|
sockets.forEach((socket) =>
|
||||||
|
socket.send(JSON.stringify({ type, ...state })),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const createPlayerState = ({
|
||||||
|
songInfo,
|
||||||
|
volumeState,
|
||||||
|
repeat,
|
||||||
|
}: {
|
||||||
|
songInfo?: SongInfo;
|
||||||
|
volumeState?: VolumeState;
|
||||||
|
repeat: RepeatMode;
|
||||||
|
}): PlayerState => ({
|
||||||
|
song: songInfo,
|
||||||
|
isPlaying: songInfo ? !songInfo.isPaused : false,
|
||||||
|
muted: volumeState?.isMuted ?? false,
|
||||||
|
position: songInfo?.elapsedSeconds ?? 0,
|
||||||
|
volume: volumeState?.state ?? 100,
|
||||||
|
repeat,
|
||||||
|
});
|
||||||
|
|
||||||
|
registerCallback((songInfo, event) => {
|
||||||
|
if (event === SongInfoEvent.VideoSrcChanged) {
|
||||||
|
send(DataTypes.VideoChanged, { song: songInfo, position: 0 });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event === SongInfoEvent.PlayOrPaused) {
|
||||||
|
send(DataTypes.PlayerStateChanged, {
|
||||||
|
isPlaying: !(songInfo?.isPaused ?? true),
|
||||||
|
position: songInfo.elapsedSeconds,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event === SongInfoEvent.TimeChanged) {
|
||||||
|
send(DataTypes.PositionChanged, { position: songInfo.elapsedSeconds });
|
||||||
|
}
|
||||||
|
|
||||||
|
lastSongInfo = { ...songInfo };
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.on('ytmd:volume-changed', (_, newVolumeState: VolumeState) => {
|
||||||
|
volumeState = newVolumeState;
|
||||||
|
send(DataTypes.VolumeChanged, {
|
||||||
|
volume: volumeState.state,
|
||||||
|
muted: volumeState.isMuted,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.on('ytmd:repeat-changed', (_, mode: RepeatMode) => {
|
||||||
|
repeat = mode;
|
||||||
|
send(DataTypes.RepeatChanged, { repeat });
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.on('ytmd:seeked', (_, t: number) => {
|
||||||
|
send(DataTypes.PositionChanged, { position: t });
|
||||||
|
});
|
||||||
|
|
||||||
|
app.openapi(
|
||||||
|
createRoute({
|
||||||
|
method: 'get',
|
||||||
|
path: `/api/${API_VERSION}/ws`,
|
||||||
|
summary: 'websocket endpoint',
|
||||||
|
description: 'WebSocket endpoint for real-time updates',
|
||||||
|
responses: {
|
||||||
|
101: {
|
||||||
|
description: 'Switching Protocols',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
nodeWebSocket.upgradeWebSocket(() => ({
|
||||||
|
onOpen(_, ws) {
|
||||||
|
// "Unsafe argument of type `WSContext<WebSocket>` assigned to a parameter of type `WSContext<WebSocket>`. (@typescript-eslint/no-unsafe-argument)" ????? what?
|
||||||
|
sockets.add(ws as WSContext<WebSocket>);
|
||||||
|
|
||||||
|
ws.send(
|
||||||
|
JSON.stringify({
|
||||||
|
type: DataTypes.PlayerInfo,
|
||||||
|
...createPlayerState({
|
||||||
|
songInfo: lastSongInfo,
|
||||||
|
volumeState,
|
||||||
|
repeat,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
onClose(_, ws) {
|
||||||
|
sockets.delete(ws as WSContext<WebSocket>);
|
||||||
|
},
|
||||||
|
})) as (ctx: Context, next: Next) => Promise<Response>,
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -1,9 +1,9 @@
|
|||||||
import { OpenAPIHono as Hono } from '@hono/zod-openapi';
|
import { type OpenAPIHono as Hono } from '@hono/zod-openapi';
|
||||||
import { serve } from '@hono/node-server';
|
import { type serve } from '@hono/node-server';
|
||||||
|
|
||||||
|
import type { RepeatMode, VolumeState } from '@/types/datahost-get-state';
|
||||||
import type { BackendContext } from '@/types/contexts';
|
import type { BackendContext } from '@/types/contexts';
|
||||||
import type { SongInfo } from '@/providers/song-info';
|
import type { SongInfo } from '@/providers/song-info';
|
||||||
import type { RepeatMode } from '@/types/datahost-get-state';
|
|
||||||
import type { APIServerConfig } from '../config';
|
import type { APIServerConfig } from '../config';
|
||||||
|
|
||||||
export type HonoApp = Hono;
|
export type HonoApp = Hono;
|
||||||
@ -13,7 +13,8 @@ export type BackendType = {
|
|||||||
oldConfig?: APIServerConfig;
|
oldConfig?: APIServerConfig;
|
||||||
songInfo?: SongInfo;
|
songInfo?: SongInfo;
|
||||||
currentRepeatMode?: RepeatMode;
|
currentRepeatMode?: RepeatMode;
|
||||||
volume?: number;
|
volumeState?: VolumeState;
|
||||||
|
injectWebSocket?: (server: ReturnType<typeof serve>) => void;
|
||||||
|
|
||||||
init: (ctx: BackendContext<APIServerConfig>) => void;
|
init: (ctx: BackendContext<APIServerConfig>) => void;
|
||||||
run: (hostname: string, port: number) => void;
|
run: (hostname: string, port: number) => void;
|
||||||
|
|||||||
@ -1,26 +1,133 @@
|
|||||||
import { createPlugin } from '@/utils';
|
import { createPlugin } from '@/utils';
|
||||||
import { t } from '@/i18n';
|
import { t } from '@/i18n';
|
||||||
|
import { type YoutubePlayer } from '@/types/youtube-player';
|
||||||
|
|
||||||
|
const lazySafeTry = (...fns: (() => void)[]) => {
|
||||||
|
for (const fn of fns) {
|
||||||
|
try {
|
||||||
|
fn();
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const createCompressorNode = (
|
||||||
|
audioContext: AudioContext,
|
||||||
|
): DynamicsCompressorNode => {
|
||||||
|
const compressor = audioContext.createDynamicsCompressor();
|
||||||
|
|
||||||
|
compressor.threshold.value = -50;
|
||||||
|
compressor.ratio.value = 12;
|
||||||
|
compressor.knee.value = 40;
|
||||||
|
compressor.attack.value = 0;
|
||||||
|
compressor.release.value = 0.25;
|
||||||
|
|
||||||
|
return compressor;
|
||||||
|
};
|
||||||
|
|
||||||
|
class Storage {
|
||||||
|
lastSource: MediaElementAudioSourceNode | null = null;
|
||||||
|
lastContext: AudioContext | null = null;
|
||||||
|
lastCompressor: DynamicsCompressorNode | null = null;
|
||||||
|
|
||||||
|
connected: WeakMap<MediaElementAudioSourceNode, DynamicsCompressorNode> =
|
||||||
|
new WeakMap();
|
||||||
|
|
||||||
|
connectToCompressor = (
|
||||||
|
source: MediaElementAudioSourceNode | null = null,
|
||||||
|
audioContext: AudioContext | null = null,
|
||||||
|
compressor: DynamicsCompressorNode | null = null,
|
||||||
|
): boolean => {
|
||||||
|
if (!(source && audioContext && compressor)) return false;
|
||||||
|
|
||||||
|
const current = this.connected.get(source);
|
||||||
|
if (current === compressor) return false;
|
||||||
|
|
||||||
|
this.lastSource = source;
|
||||||
|
this.lastContext = audioContext;
|
||||||
|
this.lastCompressor = compressor;
|
||||||
|
|
||||||
|
if (current) {
|
||||||
|
lazySafeTry(
|
||||||
|
() => source.disconnect(current),
|
||||||
|
() => current.disconnect(audioContext.destination),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
lazySafeTry(() => source.disconnect(audioContext.destination));
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
source.connect(compressor);
|
||||||
|
compressor.connect(audioContext.destination);
|
||||||
|
this.connected.set(source, compressor);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('connectToCompressor failed', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
disconnectCompressor = (): boolean => {
|
||||||
|
const source = this.lastSource;
|
||||||
|
const audioContext = this.lastContext;
|
||||||
|
if (!(source && audioContext)) return false;
|
||||||
|
const current = this.connected.get(source);
|
||||||
|
if (!current) return false;
|
||||||
|
|
||||||
|
lazySafeTry(
|
||||||
|
() => source.connect(audioContext.destination),
|
||||||
|
() => source.disconnect(current),
|
||||||
|
() => current.disconnect(audioContext.destination),
|
||||||
|
);
|
||||||
|
this.connected.delete(source);
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const storage = new Storage();
|
||||||
|
|
||||||
|
const audioCanPlayHandler = ({
|
||||||
|
detail: { audioSource, audioContext },
|
||||||
|
}: CustomEvent<Compressor>) => {
|
||||||
|
storage.connectToCompressor(
|
||||||
|
audioSource,
|
||||||
|
audioContext,
|
||||||
|
createCompressorNode(audioContext),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ensureAudioContextLoad = (playerApi: YoutubePlayer) => {
|
||||||
|
if (playerApi.getPlayerState() !== 1 || storage.lastContext) return;
|
||||||
|
|
||||||
|
playerApi.loadVideoById(
|
||||||
|
playerApi.getPlayerResponse().videoDetails.videoId,
|
||||||
|
playerApi.getCurrentTime(),
|
||||||
|
playerApi.getUserPlaybackQualityPreference(),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export default createPlugin({
|
export default createPlugin({
|
||||||
name: () => t('plugins.audio-compressor.name'),
|
name: () => t('plugins.audio-compressor.name'),
|
||||||
description: () => t('plugins.audio-compressor.description'),
|
description: () => t('plugins.audio-compressor.description'),
|
||||||
|
|
||||||
renderer() {
|
renderer: {
|
||||||
document.addEventListener(
|
onPlayerApiReady(playerApi) {
|
||||||
'ytmd:audio-can-play',
|
ensureAudioContextLoad(playerApi);
|
||||||
({ detail: { audioSource, audioContext } }) => {
|
},
|
||||||
const compressor = audioContext.createDynamicsCompressor();
|
|
||||||
|
|
||||||
compressor.threshold.value = -50;
|
start() {
|
||||||
compressor.ratio.value = 12;
|
document.addEventListener('ytmd:audio-can-play', audioCanPlayHandler, {
|
||||||
compressor.knee.value = 40;
|
passive: true,
|
||||||
compressor.attack.value = 0;
|
});
|
||||||
compressor.release.value = 0.25;
|
storage.connectToCompressor(
|
||||||
|
storage.lastSource,
|
||||||
|
storage.lastContext,
|
||||||
|
storage.lastCompressor,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
audioSource.connect(compressor);
|
stop() {
|
||||||
compressor.connect(audioContext.destination);
|
document.removeEventListener('ytmd:audio-can-play', audioCanPlayHandler);
|
||||||
},
|
storage.disconnectCompressor();
|
||||||
{ once: true, passive: true },
|
},
|
||||||
);
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,16 +1,16 @@
|
|||||||
import net from 'net';
|
import net from 'net';
|
||||||
|
|
||||||
import { SocksClient, SocksClientOptions } from 'socks';
|
import { SocksClient, type SocksClientOptions } from 'socks';
|
||||||
|
|
||||||
import is from 'electron-is';
|
import is from 'electron-is';
|
||||||
|
|
||||||
import { createBackend, LoggerPrefix } from '@/utils';
|
import { createBackend, LoggerPrefix } from '@/utils';
|
||||||
|
|
||||||
import { BackendType } from './types';
|
import { type BackendType } from './types';
|
||||||
|
|
||||||
import config from '@/config';
|
import config from '@/config';
|
||||||
|
|
||||||
import { AuthProxyConfig, defaultAuthProxyConfig } from '../config';
|
import { type AuthProxyConfig, defaultAuthProxyConfig } from '../config';
|
||||||
|
|
||||||
import type { BackendContext } from '@/types/contexts';
|
import type { BackendContext } from '@/types/contexts';
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
import net from 'net';
|
import type net from 'net';
|
||||||
|
|
||||||
import type { AuthProxyConfig } from '../config';
|
import type { AuthProxyConfig } from '../config';
|
||||||
import type { Server } from 'http';
|
import type { Server } from 'http';
|
||||||
|
|
||||||
|
|||||||
@ -2,7 +2,10 @@ import { createPlugin } from '@/utils';
|
|||||||
import { t } from '@/i18n';
|
import { t } from '@/i18n';
|
||||||
|
|
||||||
import backend from './back';
|
import backend from './back';
|
||||||
import renderer, { CaptionsSelectorConfig, LanguageOptions } from './renderer';
|
import renderer, {
|
||||||
|
type CaptionsSelectorConfig,
|
||||||
|
type LanguageOptions,
|
||||||
|
} from './renderer';
|
||||||
|
|
||||||
import type { YoutubePlayer } from '@/types/youtube-player';
|
import type { YoutubePlayer } from '@/types/youtube-player';
|
||||||
|
|
||||||
|
|||||||
@ -9,10 +9,10 @@ export const CaptionsSettingButton = (props: CaptionsSettingsButtonProps) => (
|
|||||||
aria-label={props.label}
|
aria-label={props.label}
|
||||||
class="player-captions-button style-scope ytmusic-player-bar"
|
class="player-captions-button style-scope ytmusic-player-bar"
|
||||||
icon={'yt-icons:subtitles'}
|
icon={'yt-icons:subtitles'}
|
||||||
|
on:click={(e) => props.onClick(e)}
|
||||||
role={'button'}
|
role={'button'}
|
||||||
tabindex={0}
|
tabindex={0}
|
||||||
title={props.label}
|
title={props.label}
|
||||||
on:click={(e) => props.onClick(e)}
|
|
||||||
>
|
>
|
||||||
<span class="yt-icon-shape style-scope yt-icon yt-spec-icon-shape">
|
<span class="yt-icon-shape style-scope yt-icon yt-spec-icon-shape">
|
||||||
<div
|
<div
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
import { Innertube } from 'youtubei.js';
|
import { Innertube } from 'youtubei.js';
|
||||||
|
|
||||||
import { BrowserWindow } from 'electron';
|
|
||||||
import prompt from 'custom-electron-prompt';
|
import prompt from 'custom-electron-prompt';
|
||||||
|
|
||||||
import { Howl } from 'howler';
|
import { Howl } from 'howler';
|
||||||
@ -12,6 +11,7 @@ import { VolumeFader } from './fader';
|
|||||||
|
|
||||||
import { t } from '@/i18n';
|
import { t } from '@/i18n';
|
||||||
|
|
||||||
|
import type { BrowserWindow } from 'electron';
|
||||||
import type { RendererContext } from '@/types/contexts';
|
import type { RendererContext } from '@/types/contexts';
|
||||||
|
|
||||||
export type CrossfadePluginConfig = {
|
export type CrossfadePluginConfig = {
|
||||||
|
|||||||
54
src/plugins/custom-output-device/index.ts
Normal file
54
src/plugins/custom-output-device/index.ts
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
import prompt from 'custom-electron-prompt';
|
||||||
|
|
||||||
|
import { t } from '@/i18n';
|
||||||
|
import promptOptions from '@/providers/prompt-options';
|
||||||
|
import { createPlugin } from '@/utils';
|
||||||
|
import { renderer } from './renderer';
|
||||||
|
|
||||||
|
export interface CustomOutputPluginConfig {
|
||||||
|
enabled: boolean;
|
||||||
|
output: string;
|
||||||
|
devices: Record<string, string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default createPlugin({
|
||||||
|
name: () => t('plugins.custom-output-device.name'),
|
||||||
|
description: () => t('plugins.custom-output-device.description'),
|
||||||
|
restartNeeded: true,
|
||||||
|
config: {
|
||||||
|
enabled: false,
|
||||||
|
output: 'default',
|
||||||
|
devices: {},
|
||||||
|
} as CustomOutputPluginConfig,
|
||||||
|
menu: ({ setConfig, getConfig, window }) => {
|
||||||
|
const promptDeviceSelector = async () => {
|
||||||
|
const options = await getConfig();
|
||||||
|
|
||||||
|
const response = await prompt(
|
||||||
|
{
|
||||||
|
title: t('plugins.custom-output-device.prompt.device-selector.title'),
|
||||||
|
label: t('plugins.custom-output-device.prompt.device-selector.label'),
|
||||||
|
value: options.output || 'default',
|
||||||
|
type: 'select',
|
||||||
|
selectOptions: options.devices,
|
||||||
|
width: 500,
|
||||||
|
...promptOptions(),
|
||||||
|
},
|
||||||
|
window,
|
||||||
|
).catch(console.error);
|
||||||
|
|
||||||
|
if (!response) return;
|
||||||
|
options.output = response;
|
||||||
|
setConfig(options);
|
||||||
|
};
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
label: t('plugins.custom-output-device.menu.device-selector'),
|
||||||
|
click: promptDeviceSelector,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
},
|
||||||
|
|
||||||
|
renderer,
|
||||||
|
});
|
||||||
76
src/plugins/custom-output-device/renderer.ts
Normal file
76
src/plugins/custom-output-device/renderer.ts
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
import { createRenderer } from '@/utils';
|
||||||
|
|
||||||
|
import type { YoutubePlayer } from '@/types/youtube-player';
|
||||||
|
import type { RendererContext } from '@/types/contexts';
|
||||||
|
import type { CustomOutputPluginConfig } from './index';
|
||||||
|
|
||||||
|
const updateDeviceList = async (
|
||||||
|
context: RendererContext<CustomOutputPluginConfig>,
|
||||||
|
) => {
|
||||||
|
const newDevices: Record<string, string> = {};
|
||||||
|
const devices = await navigator.mediaDevices
|
||||||
|
.enumerateDevices()
|
||||||
|
.then((devices) =>
|
||||||
|
devices.filter((device) => device.kind === 'audiooutput'),
|
||||||
|
);
|
||||||
|
for (const device of devices) {
|
||||||
|
newDevices[device.deviceId] = device.label;
|
||||||
|
}
|
||||||
|
const options = await context.getConfig();
|
||||||
|
options.devices = newDevices;
|
||||||
|
context.setConfig(options);
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateSinkId = async (
|
||||||
|
audioContext?: AudioContext & {
|
||||||
|
setSinkId?: (sinkId: string) => Promise<void>;
|
||||||
|
},
|
||||||
|
sinkId?: string,
|
||||||
|
) => {
|
||||||
|
if (!audioContext || !sinkId) return;
|
||||||
|
if (!('setSinkId' in audioContext)) return;
|
||||||
|
|
||||||
|
if (typeof audioContext.setSinkId === 'function') {
|
||||||
|
await audioContext.setSinkId(sinkId);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const renderer = createRenderer<
|
||||||
|
{
|
||||||
|
options?: CustomOutputPluginConfig;
|
||||||
|
audioContext?: AudioContext;
|
||||||
|
audioCanPlayHandler: (event: CustomEvent<Compressor>) => Promise<void>;
|
||||||
|
},
|
||||||
|
CustomOutputPluginConfig
|
||||||
|
>({
|
||||||
|
async audioCanPlayHandler({ detail: { audioContext } }) {
|
||||||
|
this.audioContext = audioContext;
|
||||||
|
await updateSinkId(audioContext, this.options!.output);
|
||||||
|
},
|
||||||
|
|
||||||
|
async onPlayerApiReady(_: YoutubePlayer, context) {
|
||||||
|
this.options = await context.getConfig();
|
||||||
|
await navigator.mediaDevices.getUserMedia({ audio: true, video: false });
|
||||||
|
navigator.mediaDevices.ondevicechange = async () =>
|
||||||
|
await updateDeviceList(context);
|
||||||
|
|
||||||
|
document.addEventListener('ytmd:audio-can-play', this.audioCanPlayHandler, {
|
||||||
|
once: true,
|
||||||
|
passive: true,
|
||||||
|
});
|
||||||
|
await updateDeviceList(context);
|
||||||
|
},
|
||||||
|
|
||||||
|
stop() {
|
||||||
|
document.removeEventListener(
|
||||||
|
'ytmd:audio-can-play',
|
||||||
|
this.audioCanPlayHandler,
|
||||||
|
);
|
||||||
|
navigator.mediaDevices.ondevicechange = null;
|
||||||
|
},
|
||||||
|
|
||||||
|
async onConfigChange(config) {
|
||||||
|
this.options = config;
|
||||||
|
await updateSinkId(this.audioContext, config.output);
|
||||||
|
},
|
||||||
|
});
|
||||||
@ -23,3 +23,13 @@ export enum TimerKey {
|
|||||||
UpdateTimeout = 'updateTimeout', // Timer for throttled activity updates
|
UpdateTimeout = 'updateTimeout', // Timer for throttled activity updates
|
||||||
DiscordConnectRetry = 'discordConnectRetry', // Timer for Discord connection retries
|
DiscordConnectRetry = 'discordConnectRetry', // Timer for Discord connection retries
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An enum for Discord's activity.status_display_type field, governing which field of the activity should be used after
|
||||||
|
* "Listening to..." in the user's Discord status.
|
||||||
|
*/
|
||||||
|
export const DiscordStatusDisplayType = {
|
||||||
|
YOUTUBE_MUSIC: 0,
|
||||||
|
ARTIST: 1,
|
||||||
|
TITLE: 2,
|
||||||
|
} as const;
|
||||||
|
|||||||
@ -98,8 +98,11 @@ export class DiscordService {
|
|||||||
|
|
||||||
const activityInfo: SetActivity = {
|
const activityInfo: SetActivity = {
|
||||||
type: ActivityType.Listening,
|
type: ActivityType.Listening,
|
||||||
|
statusDisplayType: config.statusDisplayType,
|
||||||
details: truncateString(songInfo.title, 128), // Song title
|
details: truncateString(songInfo.title, 128), // Song title
|
||||||
|
detailsUrl: songInfo.url ?? undefined,
|
||||||
state: truncateString(songInfo.artist, 128), // Artist name
|
state: truncateString(songInfo.artist, 128), // Artist name
|
||||||
|
stateUrl: songInfo.artistUrl,
|
||||||
largeImageKey: songInfo.imageSrc ?? undefined,
|
largeImageKey: songInfo.imageSrc ?? undefined,
|
||||||
largeImageText: songInfo.album
|
largeImageText: songInfo.album
|
||||||
? truncateString(songInfo.album, 128)
|
? truncateString(songInfo.album, 128)
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import { createPlugin } from '@/utils';
|
|||||||
import { backend } from './main';
|
import { backend } from './main';
|
||||||
import { onMenu } from './menu';
|
import { onMenu } from './menu';
|
||||||
import { t } from '@/i18n';
|
import { t } from '@/i18n';
|
||||||
|
import { DiscordStatusDisplayType } from './constants';
|
||||||
|
|
||||||
export type DiscordPluginConfig = {
|
export type DiscordPluginConfig = {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
@ -33,6 +34,10 @@ export type DiscordPluginConfig = {
|
|||||||
* Hide the "duration left" in the rich presence
|
* Hide the "duration left" in the rich presence
|
||||||
*/
|
*/
|
||||||
hideDurationLeft: boolean;
|
hideDurationLeft: boolean;
|
||||||
|
/**
|
||||||
|
* Controls which field is displayed in the Discord status text
|
||||||
|
*/
|
||||||
|
statusDisplayType: (typeof DiscordStatusDisplayType)[keyof typeof DiscordStatusDisplayType];
|
||||||
};
|
};
|
||||||
|
|
||||||
export default createPlugin({
|
export default createPlugin({
|
||||||
@ -47,6 +52,7 @@ export default createPlugin({
|
|||||||
playOnYouTubeMusic: true,
|
playOnYouTubeMusic: true,
|
||||||
hideGitHubButton: false,
|
hideGitHubButton: false,
|
||||||
hideDurationLeft: false,
|
hideDurationLeft: false,
|
||||||
|
statusDisplayType: DiscordStatusDisplayType.ARTIST,
|
||||||
} as DiscordPluginConfig,
|
} as DiscordPluginConfig,
|
||||||
menu: onMenu,
|
menu: onMenu,
|
||||||
backend,
|
backend,
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { app } from 'electron';
|
import { app } from 'electron';
|
||||||
|
|
||||||
import registerCallback, { SongInfoEvent } from '@/providers/song-info';
|
import { registerCallback, SongInfoEvent } from '@/providers/song-info';
|
||||||
import { createBackend } from '@/utils';
|
import { createBackend } from '@/utils';
|
||||||
|
|
||||||
import { DiscordService } from './discord-service';
|
import { DiscordService } from './discord-service';
|
||||||
|
|||||||
@ -8,6 +8,8 @@ import { setMenuOptions } from '@/config/plugins';
|
|||||||
|
|
||||||
import { t } from '@/i18n';
|
import { t } from '@/i18n';
|
||||||
|
|
||||||
|
import { DiscordStatusDisplayType } from './constants';
|
||||||
|
|
||||||
import type { MenuContext } from '@/types/contexts';
|
import type { MenuContext } from '@/types/contexts';
|
||||||
import type { DiscordPluginConfig } from './index';
|
import type { DiscordPluginConfig } from './index';
|
||||||
|
|
||||||
@ -17,6 +19,15 @@ const registerRefreshOnce = singleton((refreshMenu: () => void) => {
|
|||||||
discordService?.registerRefreshCallback(refreshMenu);
|
discordService?.registerRefreshCallback(refreshMenu);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const DiscordStatusDisplayTypeLabels = {
|
||||||
|
[DiscordStatusDisplayType.YOUTUBE_MUSIC]:
|
||||||
|
'plugins.discord.menu.set-status-display-type.submenu.youtube-music',
|
||||||
|
[DiscordStatusDisplayType.ARTIST]:
|
||||||
|
'plugins.discord.menu.set-status-display-type.submenu.artist',
|
||||||
|
[DiscordStatusDisplayType.TITLE]:
|
||||||
|
'plugins.discord.menu.set-status-display-type.submenu.title',
|
||||||
|
};
|
||||||
|
|
||||||
export const onMenu = async ({
|
export const onMenu = async ({
|
||||||
window,
|
window,
|
||||||
getConfig,
|
getConfig,
|
||||||
@ -92,6 +103,21 @@ export const onMenu = async ({
|
|||||||
label: t('plugins.discord.menu.set-inactivity-timeout'),
|
label: t('plugins.discord.menu.set-inactivity-timeout'),
|
||||||
click: () => setInactivityTimeout(window, config),
|
click: () => setInactivityTimeout(window, config),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: t('plugins.discord.menu.set-status-display-type.label'),
|
||||||
|
submenu: Object.values(DiscordStatusDisplayType).map(
|
||||||
|
(statusDisplayType) => ({
|
||||||
|
label: t(DiscordStatusDisplayTypeLabels[statusDisplayType]),
|
||||||
|
type: 'radio',
|
||||||
|
checked: config.statusDisplayType == statusDisplayType,
|
||||||
|
click() {
|
||||||
|
setConfig({
|
||||||
|
statusDisplayType,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
},
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { TimerKey } from './constants';
|
import type { TimerKey } from './constants';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Manages NodeJS Timers, ensuring only one timer exists per key.
|
* Manages NodeJS Timers, ensuring only one timer exists per key.
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { DefaultPresetList, Preset } from './types';
|
import { DefaultPresetList, type Preset } from './types';
|
||||||
|
|
||||||
import style from './style.css?inline';
|
import style from './style.css?inline';
|
||||||
|
|
||||||
|
|||||||
@ -2,12 +2,12 @@ import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
|||||||
import { join } from 'node:path';
|
import { join } from 'node:path';
|
||||||
import { randomBytes } from 'node:crypto';
|
import { randomBytes } from 'node:crypto';
|
||||||
|
|
||||||
import { app, BrowserWindow, dialog, ipcMain } from 'electron';
|
import { app, type BrowserWindow, dialog, ipcMain } from 'electron';
|
||||||
import { Innertube, UniversalCache, Utils, YTNodes } from 'youtubei.js';
|
import { Innertube, UniversalCache, Utils, YTNodes } from 'youtubei.js';
|
||||||
import is from 'electron-is';
|
import is from 'electron-is';
|
||||||
import filenamify from 'filenamify';
|
import filenamify from 'filenamify';
|
||||||
import { Mutex } from 'async-mutex';
|
import { Mutex } from 'async-mutex';
|
||||||
import NodeID3, { TagConstants } from 'node-id3';
|
import * as NodeID3 from 'node-id3';
|
||||||
import { BG, type BgConfig } from 'bgutils-js';
|
import { BG, type BgConfig } from 'bgutils-js';
|
||||||
import { lazy } from 'lazy-var';
|
import { lazy } from 'lazy-var';
|
||||||
|
|
||||||
@ -17,7 +17,8 @@ import {
|
|||||||
sendFeedback as sendFeedback_,
|
sendFeedback as sendFeedback_,
|
||||||
setBadge,
|
setBadge,
|
||||||
} from './utils';
|
} from './utils';
|
||||||
import registerCallback, {
|
import {
|
||||||
|
registerCallback,
|
||||||
cleanupName,
|
cleanupName,
|
||||||
getImage,
|
getImage,
|
||||||
MediaType,
|
MediaType,
|
||||||
@ -590,7 +591,7 @@ async function writeID3(
|
|||||||
tags.image = {
|
tags.image = {
|
||||||
mime: 'image/png',
|
mime: 'image/png',
|
||||||
type: {
|
type: {
|
||||||
id: TagConstants.AttachedPicture.PictureType.FRONT_COVER,
|
id: NodeID3.TagConstants.AttachedPicture.PictureType.FRONT_COVER,
|
||||||
},
|
},
|
||||||
description: 'thumbnail',
|
description: 'thumbnail',
|
||||||
imageBuffer: coverBuffer,
|
imageBuffer: coverBuffer,
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { app, BrowserWindow } from 'electron';
|
import { app, type BrowserWindow } from 'electron';
|
||||||
import is from 'electron-is';
|
import is from 'electron-is';
|
||||||
|
|
||||||
export const getFolder = (customFolder?: string) =>
|
export const getFolder = (customFolder?: string) =>
|
||||||
|
|||||||
@ -5,8 +5,8 @@ export const DownloadButton = (props: {
|
|||||||
<a
|
<a
|
||||||
class="yt-simple-endpoint style-scope ytmusic-menu-navigation-item-renderer"
|
class="yt-simple-endpoint style-scope ytmusic-menu-navigation-item-renderer"
|
||||||
id="navigation-endpoint"
|
id="navigation-endpoint"
|
||||||
tabindex={-1}
|
|
||||||
onClick={props.onClick}
|
onClick={props.onClick}
|
||||||
|
tabindex={-1}
|
||||||
>
|
>
|
||||||
<div class="icon ytmd-menu-item style-scope ytmusic-menu-navigation-item-renderer">
|
<div class="icon ytmd-menu-item style-scope ytmusic-menu-navigation-item-renderer">
|
||||||
<svg
|
<svg
|
||||||
|
|||||||
@ -1,8 +1,15 @@
|
|||||||
import { createPlugin } from '@/utils';
|
import { createPlugin } from '@/utils';
|
||||||
import { t } from '@/i18n';
|
import { t } from '@/i18n';
|
||||||
import { MenuContext } from '@/types/contexts';
|
|
||||||
import { MenuTemplate } from '@/menu';
|
import {
|
||||||
import { defaultPresets, presetConfigs, Preset, FilterConfig } from './presets';
|
defaultPresets,
|
||||||
|
presetConfigs,
|
||||||
|
type Preset,
|
||||||
|
type FilterConfig,
|
||||||
|
} from './presets';
|
||||||
|
|
||||||
|
import type { MenuContext } from '@/types/contexts';
|
||||||
|
import type { MenuTemplate } from '@/menu';
|
||||||
|
|
||||||
export type EqualizerPluginConfig = {
|
export type EqualizerPluginConfig = {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
import { createPlugin } from '@/utils';
|
import { createPlugin } from '@/utils';
|
||||||
import { t } from '@/i18n';
|
import { t } from '@/i18n';
|
||||||
|
|
||||||
|
import type { YoutubePlayer } from '@/types/youtube-player';
|
||||||
|
|
||||||
export default createPlugin({
|
export default createPlugin({
|
||||||
name: () => t('plugins.exponential-volume.name'),
|
name: () => t('plugins.exponential-volume.name'),
|
||||||
description: () => t('plugins.exponential-volume.description'),
|
description: () => t('plugins.exponential-volume.description'),
|
||||||
@ -9,7 +11,16 @@ export default createPlugin({
|
|||||||
enabled: false,
|
enabled: false,
|
||||||
},
|
},
|
||||||
renderer: {
|
renderer: {
|
||||||
onPlayerApiReady() {
|
onPlayerApiReady(playerApi) {
|
||||||
|
const syncVolume = (playerApi: YoutubePlayer) => {
|
||||||
|
if (playerApi.getPlayerState() === 3) {
|
||||||
|
setTimeout(() => syncVolume(playerApi), 0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
playerApi.setVolume(playerApi.getVolume());
|
||||||
|
};
|
||||||
|
|
||||||
// "YouTube Music fix volume ratio 0.4" by Marco Pfeiffer
|
// "YouTube Music fix volume ratio 0.4" by Marco Pfeiffer
|
||||||
// https://greasyfork.org/en/scripts/397686-youtube-music-fix-volume-ratio/
|
// https://greasyfork.org/en/scripts/397686-youtube-music-fix-volume-ratio/
|
||||||
|
|
||||||
@ -48,6 +59,7 @@ export default createPlugin({
|
|||||||
propertyDescriptor?.set?.call(this, lowVolume);
|
propertyDescriptor?.set?.call(this, lowVolume);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
syncVolume(playerApi);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@ -3,10 +3,10 @@ import { register } from 'electron-localshortcut';
|
|||||||
import {
|
import {
|
||||||
BrowserWindow,
|
BrowserWindow,
|
||||||
Menu,
|
Menu,
|
||||||
MenuItem,
|
type MenuItem,
|
||||||
ipcMain,
|
ipcMain,
|
||||||
nativeImage,
|
nativeImage,
|
||||||
WebContents,
|
type WebContents,
|
||||||
} from 'electron';
|
} from 'electron';
|
||||||
|
|
||||||
import type { BackendContext } from '@/types/contexts';
|
import type { BackendContext } from '@/types/contexts';
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import { createSignal } from 'solid-js';
|
|||||||
import { render } from 'solid-js/web';
|
import { render } from 'solid-js/web';
|
||||||
|
|
||||||
import { TitleBar } from './renderer/TitleBar';
|
import { TitleBar } from './renderer/TitleBar';
|
||||||
import { defaultInAppMenuConfig, InAppMenuConfig } from './constants';
|
import { defaultInAppMenuConfig, type InAppMenuConfig } from './constants';
|
||||||
|
|
||||||
import type { RendererContext } from '@/types/contexts';
|
import type { RendererContext } from '@/types/contexts';
|
||||||
|
|
||||||
@ -33,12 +33,12 @@ export const onRendererLoad = async ({
|
|||||||
render(
|
render(
|
||||||
() => (
|
() => (
|
||||||
<TitleBar
|
<TitleBar
|
||||||
ipc={ipc}
|
|
||||||
isMacOS={isMacOS}
|
|
||||||
enableController={
|
enableController={
|
||||||
isNotWindowsOrMacOS && !config().hideDOMWindowControls
|
isNotWindowsOrMacOS && !config().hideDOMWindowControls
|
||||||
}
|
}
|
||||||
initialCollapsed={window.mainConfig.get('options.hideMenu')}
|
initialCollapsed={window.mainConfig.get('options.hideMenu')}
|
||||||
|
ipc={ipc}
|
||||||
|
isMacOS={isMacOS}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
document.body,
|
document.body,
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { JSX } from 'solid-js';
|
import { type JSX } from 'solid-js';
|
||||||
import { css } from 'solid-styled-components';
|
import { css } from 'solid-styled-components';
|
||||||
|
|
||||||
import { cacheNoArgs } from '@/providers/decorators';
|
import { cacheNoArgs } from '@/providers/decorators';
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { JSX, splitProps } from 'solid-js';
|
import { type JSX, splitProps } from 'solid-js';
|
||||||
import { css } from 'solid-styled-components';
|
import { css } from 'solid-styled-components';
|
||||||
|
|
||||||
import { cacheNoArgs } from '@/providers/decorators';
|
import { cacheNoArgs } from '@/providers/decorators';
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { createSignal, JSX, Show, splitProps, mergeProps } from 'solid-js';
|
import { createSignal, type JSX, Show, splitProps, mergeProps } from 'solid-js';
|
||||||
import { Portal } from 'solid-js/web';
|
import { 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';
|
||||||
@ -6,7 +6,7 @@ import {
|
|||||||
autoUpdate,
|
autoUpdate,
|
||||||
flip,
|
flip,
|
||||||
offset,
|
offset,
|
||||||
OffsetOptions,
|
type OffsetOptions,
|
||||||
size,
|
size,
|
||||||
} from '@floating-ui/dom';
|
} from '@floating-ui/dom';
|
||||||
import { useFloating } from 'solid-floating-ui';
|
import { useFloating } from 'solid-floating-ui';
|
||||||
@ -149,17 +149,17 @@ export const Panel = (props: PanelProps) => {
|
|||||||
<Portal>
|
<Portal>
|
||||||
<Transition
|
<Transition
|
||||||
appear
|
appear
|
||||||
enterClass={animationStyle().enter}
|
|
||||||
enterActiveClass={animationStyle().enterActive}
|
enterActiveClass={animationStyle().enterActive}
|
||||||
exitToClass={animationStyle().exitTo}
|
enterClass={animationStyle().enter}
|
||||||
exitActiveClass={animationStyle().exitActive}
|
exitActiveClass={animationStyle().exitActive}
|
||||||
|
exitToClass={animationStyle().exitTo}
|
||||||
>
|
>
|
||||||
<Show when={local.open}>
|
<Show when={local.open}>
|
||||||
<ul
|
<ul
|
||||||
{...leftProps}
|
{...leftProps}
|
||||||
|
class={panelStyle()}
|
||||||
data-ytmd-sub-panel={true}
|
data-ytmd-sub-panel={true}
|
||||||
ref={setPanel}
|
ref={setPanel}
|
||||||
class={panelStyle()}
|
|
||||||
style={{
|
style={{
|
||||||
'--offset-x': `${position.x}px`,
|
'--offset-x': `${position.x}px`,
|
||||||
'--offset-y': `${position.y}px`,
|
'--offset-y': `${position.y}px`,
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { createSignal, Match, Show, Switch } from 'solid-js';
|
import { createSignal, Match, Show, Switch } from 'solid-js';
|
||||||
import { JSX } from 'solid-js/jsx-runtime';
|
import { type JSX } from 'solid-js/jsx-runtime';
|
||||||
import { css } from 'solid-styled-components';
|
import { css } from 'solid-styled-components';
|
||||||
import { Portal } from 'solid-js/web';
|
import { Portal } from 'solid-js/web';
|
||||||
|
|
||||||
@ -290,80 +290,80 @@ export const PanelItem = (props: PanelItemProps) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<li
|
<li
|
||||||
ref={setAnchor}
|
|
||||||
class={itemStyle()}
|
class={itemStyle()}
|
||||||
onMouseEnter={handleHover}
|
|
||||||
onClick={handleClick}
|
|
||||||
data-selected={open()}
|
data-selected={open()}
|
||||||
|
onClick={handleClick}
|
||||||
|
onMouseEnter={handleHover}
|
||||||
|
ref={setAnchor}
|
||||||
>
|
>
|
||||||
<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
|
<svg
|
||||||
class={itemIconStyle()}
|
class={itemIconStyle()}
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
stroke-width="1.5"
|
|
||||||
stroke="currentColor"
|
|
||||||
fill="none"
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
stroke-linecap="round"
|
stroke-linecap="round"
|
||||||
stroke-linejoin="round"
|
stroke-linejoin="round"
|
||||||
|
stroke-width="1.5"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
>
|
>
|
||||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
<path d="M0 0h24v24H0z" fill="none" stroke="none" />
|
||||||
<path d="M5 12l5 5l10 -10" />
|
<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
|
<svg
|
||||||
class={itemIconStyle()}
|
class={itemIconStyle()}
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 20 20"
|
|
||||||
style={{ padding: '6px' }}
|
style={{ padding: '6px' }}
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
>
|
>
|
||||||
<path
|
<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"
|
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"
|
||||||
|
fill="currentColor"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={props.type === 'radio' && !props.checked}>
|
<Match when={props.type === 'radio' && !props.checked}>
|
||||||
<svg
|
<svg
|
||||||
class={itemIconStyle()}
|
class={itemIconStyle()}
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 20 20"
|
|
||||||
style={{ padding: '6px' }}
|
style={{ padding: '6px' }}
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
>
|
>
|
||||||
<path
|
<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"
|
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"
|
||||||
|
fill="currentColor"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</Match>
|
</Match>
|
||||||
</Switch>
|
</Switch>
|
||||||
<span class={itemLabelStyle()}>{props.name}</span>
|
<span class={itemLabelStyle()}>{props.name}</span>
|
||||||
<Show when={props.chip} fallback={<div />}>
|
<Show fallback={<div />} when={props.chip}>
|
||||||
<span class={itemChipStyle()}>{props.chip}</span>
|
<span class={itemChipStyle()}>{props.chip}</span>
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={props.type === 'submenu'}>
|
<Show when={props.type === 'submenu'}>
|
||||||
<svg
|
<svg
|
||||||
class={itemIconStyle()}
|
class={itemIconStyle()}
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
stroke-width="1.5"
|
|
||||||
stroke="currentColor"
|
|
||||||
fill="none"
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
stroke-linecap="round"
|
stroke-linecap="round"
|
||||||
stroke-linejoin="round"
|
stroke-linejoin="round"
|
||||||
|
stroke-width="1.5"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
>
|
>
|
||||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
<path d="M0 0h24v24H0z" fill="none" stroke="none" />
|
||||||
<polyline points="9 6 15 12 9 18" />
|
<polyline points="9 6 15 12 9 18" />
|
||||||
</svg>
|
</svg>
|
||||||
<Panel
|
<Panel
|
||||||
ref={setChild}
|
|
||||||
open={open()}
|
|
||||||
anchor={anchor()}
|
anchor={anchor()}
|
||||||
placement={'right-start'}
|
|
||||||
data-level={props.type === 'submenu' && props.level.join('/')}
|
data-level={props.type === 'submenu' && props.level.join('/')}
|
||||||
offset={{ mainAxis: 8 }}
|
offset={{ mainAxis: 8 }}
|
||||||
|
open={open()}
|
||||||
|
placement={'right-start'}
|
||||||
|
ref={setChild}
|
||||||
>
|
>
|
||||||
{props.type === 'submenu' && props.children}
|
{props.type === 'submenu' && props.children}
|
||||||
</Panel>
|
</Panel>
|
||||||
@ -371,8 +371,8 @@ export const PanelItem = (props: PanelItemProps) => {
|
|||||||
<Show when={props.toolTip}>
|
<Show when={props.toolTip}>
|
||||||
<Portal>
|
<Portal>
|
||||||
<div
|
<div
|
||||||
ref={setToolTip}
|
|
||||||
class={popupStyle()}
|
class={popupStyle()}
|
||||||
|
ref={setToolTip}
|
||||||
style={{
|
style={{
|
||||||
'--offset-x': `${position.x}px`,
|
'--offset-x': `${position.x}px`,
|
||||||
'--offset-y': `${position.y}px`,
|
'--offset-y': `${position.y}px`,
|
||||||
@ -380,10 +380,10 @@ export const PanelItem = (props: PanelItemProps) => {
|
|||||||
>
|
>
|
||||||
<Transition
|
<Transition
|
||||||
appear
|
appear
|
||||||
enterClass={animationStyle().enter}
|
|
||||||
enterActiveClass={animationStyle().enterActive}
|
enterActiveClass={animationStyle().enterActive}
|
||||||
exitToClass={animationStyle().exitTo}
|
enterClass={animationStyle().enter}
|
||||||
exitActiveClass={animationStyle().exitActive}
|
exitActiveClass={animationStyle().exitActive}
|
||||||
|
exitToClass={animationStyle().exitTo}
|
||||||
>
|
>
|
||||||
<Show when={toolTipOpen()}>
|
<Show when={toolTipOpen()}>
|
||||||
<div class={toolTipStyle()}>{props.toolTip}</div>
|
<div class={toolTipStyle()}>{props.toolTip}</div>
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { Menu, MenuItem } from 'electron';
|
import { type Menu, type MenuItem } from 'electron';
|
||||||
import {
|
import {
|
||||||
createEffect,
|
createEffect,
|
||||||
createResource,
|
createResource,
|
||||||
@ -120,22 +120,22 @@ const PanelRenderer = (props: PanelRendererProps) => {
|
|||||||
<Switch>
|
<Switch>
|
||||||
<Match when={subItem().type === 'normal'}>
|
<Match when={subItem().type === 'normal'}>
|
||||||
<PanelItem
|
<PanelItem
|
||||||
type={'normal'}
|
|
||||||
name={subItem().label}
|
|
||||||
chip={subItem().sublabel}
|
chip={subItem().sublabel}
|
||||||
toolTip={subItem().toolTip}
|
|
||||||
commandId={subItem().commandId}
|
commandId={subItem().commandId}
|
||||||
|
name={subItem().label}
|
||||||
onClick={() => props.onClick?.(subItem().commandId)}
|
onClick={() => props.onClick?.(subItem().commandId)}
|
||||||
|
toolTip={subItem().toolTip}
|
||||||
|
type={'normal'}
|
||||||
/>
|
/>
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={subItem().type === 'submenu'}>
|
<Match when={subItem().type === 'submenu'}>
|
||||||
<PanelItem
|
<PanelItem
|
||||||
type={'submenu'}
|
|
||||||
name={subItem().label}
|
|
||||||
chip={subItem().sublabel}
|
chip={subItem().sublabel}
|
||||||
toolTip={subItem().toolTip}
|
|
||||||
level={[...(props.level ?? []), subItem().commandId]}
|
|
||||||
commandId={subItem().commandId}
|
commandId={subItem().commandId}
|
||||||
|
level={[...(props.level ?? []), subItem().commandId]}
|
||||||
|
name={subItem().label}
|
||||||
|
toolTip={subItem().toolTip}
|
||||||
|
type={'submenu'}
|
||||||
>
|
>
|
||||||
<PanelRenderer
|
<PanelRenderer
|
||||||
items={subItem().submenu?.items ?? []}
|
items={subItem().submenu?.items ?? []}
|
||||||
@ -146,26 +146,26 @@ const PanelRenderer = (props: PanelRendererProps) => {
|
|||||||
</Match>
|
</Match>
|
||||||
<Match when={subItem().type === 'checkbox'}>
|
<Match when={subItem().type === 'checkbox'}>
|
||||||
<PanelItem
|
<PanelItem
|
||||||
type={'checkbox'}
|
|
||||||
name={subItem().label}
|
|
||||||
checked={subItem().checked}
|
checked={subItem().checked}
|
||||||
chip={subItem().sublabel}
|
chip={subItem().sublabel}
|
||||||
toolTip={subItem().toolTip}
|
|
||||||
commandId={subItem().commandId}
|
commandId={subItem().commandId}
|
||||||
|
name={subItem().label}
|
||||||
onChange={() => props.onClick?.(subItem().commandId)}
|
onChange={() => props.onClick?.(subItem().commandId)}
|
||||||
|
toolTip={subItem().toolTip}
|
||||||
|
type={'checkbox'}
|
||||||
/>
|
/>
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={subItem().type === 'radio'}>
|
<Match when={subItem().type === 'radio'}>
|
||||||
<PanelItem
|
<PanelItem
|
||||||
type={'radio'}
|
|
||||||
name={subItem().label}
|
|
||||||
checked={subItem().checked}
|
checked={subItem().checked}
|
||||||
chip={subItem().sublabel}
|
chip={subItem().sublabel}
|
||||||
toolTip={subItem().toolTip}
|
|
||||||
commandId={subItem().commandId}
|
commandId={subItem().commandId}
|
||||||
|
name={subItem().label}
|
||||||
onChange={() =>
|
onChange={() =>
|
||||||
props.onClick?.(subItem().commandId, radioGroup())
|
props.onClick?.(subItem().commandId, radioGroup())
|
||||||
}
|
}
|
||||||
|
toolTip={subItem().toolTip}
|
||||||
|
type={'radio'}
|
||||||
/>
|
/>
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={subItem().type === 'separator'}>
|
<Match when={subItem().type === 'separator'}>
|
||||||
@ -325,10 +325,10 @@ export const TitleBar = (props: TitleBarProps) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<nav
|
<nav
|
||||||
data-ytmd-main-panel={true}
|
|
||||||
class={titleStyle()}
|
class={titleStyle()}
|
||||||
data-macos={props.isMacOS}
|
data-macos={props.isMacOS}
|
||||||
data-show={mouseY() < 32}
|
data-show={mouseY() < 32}
|
||||||
|
data-ytmd-main-panel={true}
|
||||||
>
|
>
|
||||||
<IconButton
|
<IconButton
|
||||||
onClick={() => setCollapsed(!collapsed())}
|
onClick={() => setCollapsed(!collapsed())}
|
||||||
@ -336,7 +336,7 @@ export const TitleBar = (props: TitleBarProps) => {
|
|||||||
'border-top-left-radius': '4px',
|
'border-top-left-radius': '4px',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<svg width={16} height={16} viewBox={'0 0 24 24'}>
|
<svg height={16} viewBox={'0 0 24 24'} width={16}>
|
||||||
<path
|
<path
|
||||||
d="M3 17h12a1 1 0 0 1 .117 1.993L15 19H3a1 1 0 0 1-.117-1.993L3 17h12H3Zm0-6h18a1 1 0 0 1 .117 1.993L21 13H3a1 1 0 0 1-.117-1.993L3 11h18H3Zm0-6h15a1 1 0 0 1 .117 1.993L18 7H3a1 1 0 0 1-.117-1.993L3 5h15H3Z"
|
d="M3 17h12a1 1 0 0 1 .117 1.993L15 19H3a1 1 0 0 1-.117-1.993L3 17h12H3Zm0-6h18a1 1 0 0 1 .117 1.993L21 13H3a1 1 0 0 1-.117-1.993L3 11h18H3Zm0-6h15a1 1 0 0 1 .117 1.993L18 7H3a1 1 0 0 1-.117-1.993L3 5h15H3Z"
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
@ -344,26 +344,29 @@ export const TitleBar = (props: TitleBarProps) => {
|
|||||||
</svg>
|
</svg>
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<TransitionGroup
|
<TransitionGroup
|
||||||
enterClass={
|
|
||||||
ignoreTransition()
|
|
||||||
? animationStyle().fakeTarget
|
|
||||||
: animationStyle().enter
|
|
||||||
}
|
|
||||||
enterActiveClass={
|
enterActiveClass={
|
||||||
ignoreTransition()
|
ignoreTransition()
|
||||||
? animationStyle().fake
|
? animationStyle().fake
|
||||||
: animationStyle().enterActive
|
: animationStyle().enterActive
|
||||||
}
|
}
|
||||||
exitToClass={
|
enterClass={
|
||||||
ignoreTransition()
|
ignoreTransition()
|
||||||
? animationStyle().fakeTarget
|
? animationStyle().fakeTarget
|
||||||
: animationStyle().exitTo
|
: animationStyle().enter
|
||||||
}
|
}
|
||||||
exitActiveClass={
|
exitActiveClass={
|
||||||
ignoreTransition()
|
ignoreTransition()
|
||||||
? animationStyle().fake
|
? animationStyle().fake
|
||||||
: animationStyle().exitActive
|
: animationStyle().exitActive
|
||||||
}
|
}
|
||||||
|
exitToClass={
|
||||||
|
ignoreTransition()
|
||||||
|
? animationStyle().fakeTarget
|
||||||
|
: animationStyle().exitTo
|
||||||
|
}
|
||||||
|
onAfterEnter={(element) => {
|
||||||
|
(element as HTMLElement).style.removeProperty('transition-delay');
|
||||||
|
}}
|
||||||
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);
|
||||||
@ -373,9 +376,6 @@ export const TitleBar = (props: TitleBarProps) => {
|
|||||||
`${index * 0.025}s`,
|
`${index * 0.025}s`,
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
onAfterEnter={(element) => {
|
|
||||||
(element as HTMLElement).style.removeProperty('transition-delay');
|
|
||||||
}}
|
|
||||||
onBeforeExit={(element) => {
|
onBeforeExit={(element) => {
|
||||||
if (ignoreTransition()) return;
|
if (ignoreTransition()) return;
|
||||||
const index = Number(element.getAttribute('data-index') ?? 0);
|
const index = Number(element.getAttribute('data-index') ?? 0);
|
||||||
@ -405,18 +405,18 @@ export const TitleBar = (props: TitleBarProps) => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<MenuButton
|
<MenuButton
|
||||||
ref={setAnchor}
|
|
||||||
text={item().label}
|
|
||||||
onClick={handleClick}
|
|
||||||
selected={openTarget() === anchor()}
|
|
||||||
data-index={index}
|
data-index={index}
|
||||||
data-length={data()?.items.length}
|
data-length={data()?.items.length}
|
||||||
|
onClick={handleClick}
|
||||||
|
ref={setAnchor}
|
||||||
|
selected={openTarget() === anchor()}
|
||||||
|
text={item().label}
|
||||||
/>
|
/>
|
||||||
<Panel
|
<Panel
|
||||||
open={openTarget() === anchor()}
|
|
||||||
anchor={anchor()}
|
anchor={anchor()}
|
||||||
placement={'bottom-start'}
|
|
||||||
offset={{ mainAxis: 8 }}
|
offset={{ mainAxis: 8 }}
|
||||||
|
open={openTarget() === anchor()}
|
||||||
|
placement={'bottom-start'}
|
||||||
>
|
>
|
||||||
<PanelRenderer
|
<PanelRenderer
|
||||||
items={item().submenu?.items ?? []}
|
items={item().submenu?.items ?? []}
|
||||||
@ -433,9 +433,9 @@ export const TitleBar = (props: TitleBarProps) => {
|
|||||||
<div style={{ flex: 1 }} />
|
<div style={{ flex: 1 }} />
|
||||||
<WindowController
|
<WindowController
|
||||||
isMaximize={isMaximized()}
|
isMaximize={isMaximized()}
|
||||||
onToggleMaximize={handleToggleMaximize}
|
|
||||||
onMinimize={handleMinimize}
|
|
||||||
onClose={handleClose}
|
onClose={handleClose}
|
||||||
|
onMinimize={handleMinimize}
|
||||||
|
onToggleMaximize={handleToggleMaximize}
|
||||||
/>
|
/>
|
||||||
</Show>
|
</Show>
|
||||||
</nav>
|
</nav>
|
||||||
|
|||||||
@ -32,61 +32,61 @@ export const WindowController = (props: WindowControllerProps) => {
|
|||||||
<div class={containerStyle()}>
|
<div class={containerStyle()}>
|
||||||
<IconButton onClick={props.onMinimize}>
|
<IconButton onClick={props.onMinimize}>
|
||||||
<svg
|
<svg
|
||||||
width={16}
|
|
||||||
height={16}
|
|
||||||
fill="none"
|
fill="none"
|
||||||
|
height={16}
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
|
width={16}
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
>
|
>
|
||||||
<path
|
<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"
|
d="M3.755 12.5h16.492a.75.75 0 0 0 0-1.5H3.755a.75.75 0 0 0 0 1.5Z"
|
||||||
|
fill="currentColor"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<IconButton onClick={props.onToggleMaximize}>
|
<IconButton onClick={props.onToggleMaximize}>
|
||||||
<Show
|
<Show
|
||||||
when={props.isMaximize}
|
|
||||||
fallback={
|
fallback={
|
||||||
<svg
|
<svg
|
||||||
width={16}
|
|
||||||
height={16}
|
|
||||||
fill="none"
|
fill="none"
|
||||||
|
height={16}
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
|
width={16}
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
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"
|
||||||
|
fill="currentColor"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
}
|
}
|
||||||
|
when={props.isMaximize}
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
width={16}
|
|
||||||
height={16}
|
|
||||||
fill="none"
|
fill="none"
|
||||||
|
height={16}
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
|
width={16}
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
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"
|
||||||
|
fill="currentColor"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</Show>
|
</Show>
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<IconButton onClick={props.onClose}>
|
<IconButton onClick={props.onClose}>
|
||||||
<svg
|
<svg
|
||||||
width={16}
|
|
||||||
height={16}
|
|
||||||
fill="none"
|
fill="none"
|
||||||
|
height={16}
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
|
width={16}
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
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"
|
||||||
|
fill="currentColor"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</IconButton>
|
</IconButton>
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { net } from 'electron';
|
import { net } from 'electron';
|
||||||
|
|
||||||
import { createPlugin } from '@/utils';
|
import { createPlugin } from '@/utils';
|
||||||
import registerCallback from '@/providers/song-info';
|
import { registerCallback } from '@/providers/song-info';
|
||||||
import { t } from '@/i18n';
|
import { t } from '@/i18n';
|
||||||
|
|
||||||
type LumiaData = {
|
type LumiaData = {
|
||||||
|
|||||||
@ -1,4 +1,9 @@
|
|||||||
import { DataConnection, Peer, PeerError, PeerErrorType } from 'peerjs';
|
import {
|
||||||
|
type DataConnection,
|
||||||
|
Peer,
|
||||||
|
type PeerError,
|
||||||
|
PeerErrorType,
|
||||||
|
} from 'peerjs';
|
||||||
import delay from 'delay';
|
import delay from 'delay';
|
||||||
|
|
||||||
import type { Permission, Profile, VideoData } from './types';
|
import type { Permission, Profile, VideoData } from './types';
|
||||||
|
|||||||
@ -1,10 +1,9 @@
|
|||||||
import prompt from 'custom-electron-prompt';
|
import prompt from 'custom-electron-prompt';
|
||||||
|
|
||||||
import { DataConnection } from 'peerjs';
|
|
||||||
|
|
||||||
import { t } from '@/i18n';
|
import { t } from '@/i18n';
|
||||||
import { createPlugin } from '@/utils';
|
import { createPlugin } from '@/utils';
|
||||||
import promptOptions from '@/providers/prompt-options';
|
import promptOptions from '@/providers/prompt-options';
|
||||||
|
import { waitForElement } from '@/utils/wait-for-element';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
getDefaultProfile,
|
getDefaultProfile,
|
||||||
@ -21,8 +20,7 @@ import { createSettingPopup } from './ui/setting';
|
|||||||
import settingHTML from './templates/setting.html?raw';
|
import settingHTML from './templates/setting.html?raw';
|
||||||
import style from './style.css?inline';
|
import style from './style.css?inline';
|
||||||
|
|
||||||
import { waitForElement } from '@/utils/wait-for-element';
|
import type { DataConnection } from 'peerjs';
|
||||||
|
|
||||||
import type { YoutubePlayer } from '@/types/youtube-player';
|
import type { YoutubePlayer } from '@/types/youtube-player';
|
||||||
import type { RendererContext } from '@/types/contexts';
|
import type { RendererContext } from '@/types/contexts';
|
||||||
import type { VideoDataChanged } from '@/types/video-data-changed';
|
import type { VideoDataChanged } from '@/types/video-data-changed';
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import {
|
import type {
|
||||||
ItemPlaylistPanelVideoRenderer,
|
ItemPlaylistPanelVideoRenderer,
|
||||||
PlaylistPanelVideoWrapperRenderer,
|
PlaylistPanelVideoWrapperRenderer,
|
||||||
QueueItem,
|
QueueItem,
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { app, BrowserWindow, Notification } from 'electron';
|
import { app, type BrowserWindow, Notification } from 'electron';
|
||||||
|
|
||||||
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';
|
||||||
@ -8,7 +8,8 @@ import previousIcon from '@assets/media-icons-black/previous.png?asset&asarUnpac
|
|||||||
import { notificationImage, secondsToMinutes, ToastStyles } from './utils';
|
import { notificationImage, secondsToMinutes, ToastStyles } from './utils';
|
||||||
|
|
||||||
import getSongControls from '@/providers/song-controls';
|
import getSongControls from '@/providers/song-controls';
|
||||||
import registerCallback, {
|
import {
|
||||||
|
registerCallback,
|
||||||
type SongInfo,
|
type SongInfo,
|
||||||
SongInfoEvent,
|
SongInfoEvent,
|
||||||
} from '@/providers/song-info';
|
} from '@/providers/song-info';
|
||||||
|
|||||||
@ -5,7 +5,8 @@ import is from 'electron-is';
|
|||||||
import { notificationImage } from './utils';
|
import { notificationImage } from './utils';
|
||||||
import interactive from './interactive';
|
import interactive from './interactive';
|
||||||
|
|
||||||
import registerCallback, {
|
import {
|
||||||
|
registerCallback,
|
||||||
type SongInfo,
|
type SongInfo,
|
||||||
SongInfoEvent,
|
SongInfoEvent,
|
||||||
} from '@/providers/song-info';
|
} from '@/providers/song-info';
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import is from 'electron-is';
|
import is from 'electron-is';
|
||||||
import { MenuItem } from 'electron';
|
import { type MenuItem } from 'electron';
|
||||||
|
|
||||||
import { snakeToCamel, ToastStyles, urgencyLevels } from './utils';
|
import { snakeToCamel, ToastStyles, urgencyLevels } from './utils';
|
||||||
|
|
||||||
|
|||||||
@ -1,11 +1,11 @@
|
|||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import fs from 'node:fs';
|
import fs from 'node:fs';
|
||||||
|
|
||||||
import { app, NativeImage } from 'electron';
|
import { app, type NativeImage } from 'electron';
|
||||||
|
|
||||||
import youtubeMusicIcon from '@assets/youtube-music.png?asset&asarUnpack';
|
import youtubeMusicIcon from '@assets/youtube-music.png?asset&asarUnpack';
|
||||||
|
|
||||||
import { SongInfo } from '@/providers/song-info';
|
import { type SongInfo } from '@/providers/song-info';
|
||||||
|
|
||||||
import type { NotificationsPluginConfig } from './index';
|
import type { NotificationsPluginConfig } from './index';
|
||||||
|
|
||||||
|
|||||||
@ -7,19 +7,19 @@ export const PictureInPictureButton = (props: PictureInPictureButtonProps) => (
|
|||||||
<a
|
<a
|
||||||
class="yt-simple-endpoint style-scope ytmusic-menu-navigation-item-renderer"
|
class="yt-simple-endpoint style-scope ytmusic-menu-navigation-item-renderer"
|
||||||
id="navigation-endpoint"
|
id="navigation-endpoint"
|
||||||
tabindex={-1}
|
|
||||||
onClick={(e) => props.onClick?.(e)}
|
onClick={(e) => props.onClick?.(e)}
|
||||||
|
tabindex={-1}
|
||||||
>
|
>
|
||||||
<div class="icon ytmd-menu-item style-scope ytmusic-menu-navigation-item-renderer">
|
<div class="icon ytmd-menu-item style-scope ytmusic-menu-navigation-item-renderer">
|
||||||
<svg
|
<svg
|
||||||
class="style-scope yt-icon"
|
class="style-scope yt-icon"
|
||||||
|
id="Layer_1"
|
||||||
style={{
|
style={{
|
||||||
'pointer-events': 'none',
|
'pointer-events': 'none',
|
||||||
'display': 'block',
|
'display': 'block',
|
||||||
'width': '100%',
|
'width': '100%',
|
||||||
'height': '100%',
|
'height': '100%',
|
||||||
}}
|
}}
|
||||||
id="Layer_1"
|
|
||||||
viewBox="0 0 512 512"
|
viewBox="0 0 512 512"
|
||||||
x="0px"
|
x="0px"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
@ -28,10 +28,10 @@ export const PictureInPictureButton = (props: PictureInPictureButtonProps) => (
|
|||||||
<g class="style-scope yt-icon" id="XMLID_6_">
|
<g class="style-scope yt-icon" id="XMLID_6_">
|
||||||
<path
|
<path
|
||||||
class="style-scope yt-icon"
|
class="style-scope yt-icon"
|
||||||
fill="#aaaaaa"
|
|
||||||
d="M418.5,139.4H232.4v139.8h186.1V139.4z M464.8,46.7H46.3C20.5,46.7,0,68.1,0,93.1v325.9
|
d="M418.5,139.4H232.4v139.8h186.1V139.4z M464.8,46.7H46.3C20.5,46.7,0,68.1,0,93.1v325.9
|
||||||
c0,25.8,21.4,46.3,46.3,46.3h419.4c25.8,0,46.3-20.5,46.3-46.3V93.1C512,67.2,490.6,46.7,464.8,46.7z M464.8,418.9H46.3V92.2h419.4
|
c0,25.8,21.4,46.3,46.3,46.3h419.4c25.8,0,46.3-20.5,46.3-46.3V93.1C512,67.2,490.6,46.7,464.8,46.7z M464.8,418.9H46.3V92.2h419.4
|
||||||
v326.8H464.8z"
|
v326.8H464.8z"
|
||||||
|
fill="#aaaaaa"
|
||||||
id="XMLID_11_"
|
id="XMLID_11_"
|
||||||
/>
|
/>
|
||||||
</g>
|
</g>
|
||||||
|
|||||||
@ -26,10 +26,10 @@ export const PlaybackSpeedSlider = (props: PlaybackSpeedSliderProps) => (
|
|||||||
aria-valuenow={props.speed}
|
aria-valuenow={props.speed}
|
||||||
class="volume-slider style-scope ytmusic-player-bar on-hover"
|
class="volume-slider style-scope ytmusic-player-bar on-hover"
|
||||||
dir="ltr"
|
dir="ltr"
|
||||||
on:immediate-value-changed={(e) => props.onImmediateValueChanged?.(e)}
|
|
||||||
onWheel={(e) => props.onWheel?.(e)}
|
|
||||||
max="2"
|
max="2"
|
||||||
min="0"
|
min="0"
|
||||||
|
on:immediate-value-changed={(e) => props.onImmediateValueChanged?.(e)}
|
||||||
|
onWheel={(e) => props.onWheel?.(e)}
|
||||||
role="slider"
|
role="slider"
|
||||||
step="0.125"
|
step="0.125"
|
||||||
style={{ 'display': 'inherit !important' }}
|
style={{ 'display': 'inherit !important' }}
|
||||||
|
|||||||
@ -43,8 +43,6 @@ export const onPlayerApiReady = () => {
|
|||||||
render(
|
render(
|
||||||
() => (
|
() => (
|
||||||
<PlaybackSpeedSlider
|
<PlaybackSpeedSlider
|
||||||
speed={speed()}
|
|
||||||
title={t('plugins.playback-speed.templates.button')}
|
|
||||||
onImmediateValueChanged={(e) => {
|
onImmediateValueChanged={(e) => {
|
||||||
let targetSpeed = Number(e.detail.value ?? MIN_PLAYBACK_SPEED);
|
let targetSpeed = Number(e.detail.value ?? MIN_PLAYBACK_SPEED);
|
||||||
|
|
||||||
@ -78,6 +76,8 @@ export const onPlayerApiReady = () => {
|
|||||||
|
|
||||||
updatePlayBackSpeed();
|
updatePlayBackSpeed();
|
||||||
}}
|
}}
|
||||||
|
speed={speed()}
|
||||||
|
title={t('plugins.playback-speed.templates.button')}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
sliderContainer,
|
sliderContainer,
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { globalShortcut, MenuItem } from 'electron';
|
import { globalShortcut, type MenuItem } from 'electron';
|
||||||
import prompt, { KeybindOptions } from 'custom-electron-prompt';
|
import prompt, { type KeybindOptions } from 'custom-electron-prompt';
|
||||||
|
|
||||||
import hudStyle from './volume-hud.css?inline';
|
import hudStyle from './volume-hud.css?inline';
|
||||||
import { createPlugin } from '@/utils';
|
import { createPlugin } from '@/utils';
|
||||||
|
|||||||
@ -66,7 +66,7 @@ export const onPlayerApiReady = async (
|
|||||||
injectVolumeHud(noVid);
|
injectVolumeHud(noVid);
|
||||||
if (!noVid) {
|
if (!noVid) {
|
||||||
setupVideoPlayerOnwheel();
|
setupVideoPlayerOnwheel();
|
||||||
if (!await window.mainConfig.plugins.isEnabled('video-toggle')) {
|
if (!(await window.mainConfig.plugins.isEnabled('video-toggle'))) {
|
||||||
// Video-toggle handles hud positioning on its own
|
// Video-toggle handles hud positioning on its own
|
||||||
const videoMode = () =>
|
const videoMode = () =>
|
||||||
api.getPlayerResponse().videoDetails?.musicVideoType !==
|
api.getPlayerResponse().videoDetails?.musicVideoType !==
|
||||||
|
|||||||
@ -9,10 +9,10 @@ export const QualitySettingButton = (props: QualitySettingButtonProps) => (
|
|||||||
aria-label={props.label}
|
aria-label={props.label}
|
||||||
class="player-quality-button style-scope ytmusic-player"
|
class="player-quality-button style-scope ytmusic-player"
|
||||||
icon={'yt-icons:settings'}
|
icon={'yt-icons:settings'}
|
||||||
|
on:click={(e) => props.onClick(e)}
|
||||||
role={'button'}
|
role={'button'}
|
||||||
tabindex={0}
|
tabindex={0}
|
||||||
title={props.label}
|
title={props.label}
|
||||||
on:click={(e) => props.onClick(e)}
|
|
||||||
>
|
>
|
||||||
<span class="yt-icon-shape style-scope yt-icon yt-spec-icon-shape">
|
<span class="yt-icon-shape style-scope yt-icon yt-spec-icon-shape">
|
||||||
<div
|
<div
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { BrowserWindow } from 'electron';
|
import { type BrowserWindow } from 'electron';
|
||||||
|
|
||||||
import registerCallback, {
|
import {
|
||||||
|
registerCallback,
|
||||||
MediaType,
|
MediaType,
|
||||||
type SongInfo,
|
type SongInfo,
|
||||||
SongInfoEvent,
|
SongInfoEvent,
|
||||||
|
|||||||
@ -1,12 +1,12 @@
|
|||||||
import prompt from 'custom-electron-prompt';
|
import prompt from 'custom-electron-prompt';
|
||||||
|
|
||||||
import { BrowserWindow } from 'electron';
|
import { type BrowserWindow } from 'electron';
|
||||||
|
|
||||||
import { t } from '@/i18n';
|
import { t } from '@/i18n';
|
||||||
import promptOptions from '@/providers/prompt-options';
|
import promptOptions from '@/providers/prompt-options';
|
||||||
|
|
||||||
import { ScrobblerPluginConfig } from './index';
|
import { type ScrobblerPluginConfig } from './index';
|
||||||
import { SetConfType, backend } from './main';
|
import { type 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';
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { BrowserWindow, globalShortcut } from 'electron';
|
import { type BrowserWindow, globalShortcut } from 'electron';
|
||||||
import is from 'electron-is';
|
import is from 'electron-is';
|
||||||
import { register as registerElectronLocalShortcut } from 'electron-localshortcut';
|
import { register as registerElectronLocalShortcut } from 'electron-localshortcut';
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import prompt, { KeybindOptions } from 'custom-electron-prompt';
|
import prompt, { type KeybindOptions } from 'custom-electron-prompt';
|
||||||
|
|
||||||
import promptOptions from '@/providers/prompt-options';
|
import promptOptions from '@/providers/prompt-options';
|
||||||
|
|
||||||
|
|||||||
2
src/plugins/shortcuts/mpris-service.d.ts
vendored
2
src/plugins/shortcuts/mpris-service.d.ts
vendored
@ -1,7 +1,7 @@
|
|||||||
declare module '@jellybrick/mpris-service' {
|
declare module '@jellybrick/mpris-service' {
|
||||||
import { EventEmitter } from 'events';
|
import { EventEmitter } from 'events';
|
||||||
|
|
||||||
import { interface as dbusInterface } from '@jellybrick/dbus-next';
|
import { type interface as dbusInterface } from '@jellybrick/dbus-next';
|
||||||
|
|
||||||
interface RootInterfaceOptions {
|
interface RootInterfaceOptions {
|
||||||
identity?: string;
|
identity?: string;
|
||||||
|
|||||||
@ -1,20 +1,21 @@
|
|||||||
import { BrowserWindow, ipcMain } from 'electron';
|
import { type BrowserWindow, ipcMain } from 'electron';
|
||||||
|
|
||||||
import MprisPlayer, {
|
import MprisPlayer, {
|
||||||
LOOP_STATUS_NONE,
|
LOOP_STATUS_NONE,
|
||||||
LOOP_STATUS_PLAYLIST,
|
LOOP_STATUS_PLAYLIST,
|
||||||
LOOP_STATUS_TRACK,
|
LOOP_STATUS_TRACK,
|
||||||
LoopStatus,
|
type LoopStatus,
|
||||||
PLAYBACK_STATUS_PAUSED,
|
PLAYBACK_STATUS_PAUSED,
|
||||||
PLAYBACK_STATUS_PLAYING,
|
PLAYBACK_STATUS_PLAYING,
|
||||||
PLAYBACK_STATUS_STOPPED,
|
PLAYBACK_STATUS_STOPPED,
|
||||||
type PlayBackStatus,
|
type PlayBackStatus,
|
||||||
type PlayerOptions,
|
type PlayerOptions,
|
||||||
type Position,
|
type Position,
|
||||||
Track,
|
type Track,
|
||||||
} from '@jellybrick/mpris-service';
|
} from '@jellybrick/mpris-service';
|
||||||
|
|
||||||
import registerCallback, {
|
import {
|
||||||
|
registerCallback,
|
||||||
type SongInfo,
|
type SongInfo,
|
||||||
SongInfoEvent,
|
SongInfoEvent,
|
||||||
} from '@/providers/song-info';
|
} from '@/providers/song-info';
|
||||||
@ -22,7 +23,7 @@ import getSongControls from '@/providers/song-controls';
|
|||||||
import config from '@/config';
|
import config from '@/config';
|
||||||
import { LoggerPrefix } from '@/utils';
|
import { LoggerPrefix } from '@/utils';
|
||||||
|
|
||||||
import type { RepeatMode } from '@/types/datahost-get-state';
|
import type { RepeatMode, VolumeState } from '@/types/datahost-get-state';
|
||||||
import type { QueueResponse } from '@/types/youtube-music-desktop-internal';
|
import type { QueueResponse } from '@/types/youtube-music-desktop-internal';
|
||||||
|
|
||||||
class YTPlayer extends MprisPlayer {
|
class YTPlayer extends MprisPlayer {
|
||||||
@ -305,8 +306,10 @@ function registerMPRIS(win: BrowserWindow) {
|
|||||||
console.trace(error);
|
console.trace(error);
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.on('ytmd:volume-changed', (_, newVol) => {
|
ipcMain.on('ytmd:volume-changed', (_, newVolumeState: VolumeState) => {
|
||||||
player.volume = Number.parseFloat((newVol / 100).toFixed(2));
|
player.volume = newVolumeState.isMuted
|
||||||
|
? 0
|
||||||
|
: Number.parseFloat((newVolumeState.state / 100).toFixed(2));
|
||||||
});
|
});
|
||||||
|
|
||||||
player.on('volume', async (newVolume: number) => {
|
player.on('volume', async (newVolume: number) => {
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
// Segments are an array [ [start, end], … ]
|
// Segments are an array [ [start, end], … ]
|
||||||
import { Segment } from './types';
|
import type { Segment } from './types';
|
||||||
|
|
||||||
export const sortSegments = (segments: Segment[]) => {
|
export const sortSegments = (segments: Segment[]) => {
|
||||||
segments.sort((segment1, segment2) =>
|
segments.sort((segment1, segment2) =>
|
||||||
|
|||||||
@ -2,8 +2,6 @@ import { createStore } from 'solid-js/store';
|
|||||||
|
|
||||||
import { createMemo } from 'solid-js';
|
import { createMemo } from 'solid-js';
|
||||||
|
|
||||||
import { SongInfo } from '@/providers/song-info';
|
|
||||||
|
|
||||||
import { LRCLib } from './LRCLib';
|
import { LRCLib } from './LRCLib';
|
||||||
import { LyricsGenius } from './LyricsGenius';
|
import { LyricsGenius } from './LyricsGenius';
|
||||||
import { MusixMatch } from './MusixMatch';
|
import { MusixMatch } from './MusixMatch';
|
||||||
@ -12,6 +10,7 @@ import { YTMusic } from './YTMusic';
|
|||||||
import { getSongInfo } from '@/providers/song-info-front';
|
import { getSongInfo } from '@/providers/song-info-front';
|
||||||
|
|
||||||
import type { LyricProvider, LyricResult } from '../types';
|
import type { LyricProvider, LyricResult } from '../types';
|
||||||
|
import type { SongInfo } from '@/providers/song-info';
|
||||||
|
|
||||||
export const providers = {
|
export const providers = {
|
||||||
YTMusic: new YTMusic(),
|
YTMusic: new YTMusic(),
|
||||||
|
|||||||
@ -45,7 +45,6 @@ export const ErrorDisplay = (props: ErrorDisplayProps) => {
|
|||||||
</pre>
|
</pre>
|
||||||
|
|
||||||
<yt-button-renderer
|
<yt-button-renderer
|
||||||
onClick={() => retrySearch(lyricsStore.provider, getSongInfo())}
|
|
||||||
data={{
|
data={{
|
||||||
icon: { iconType: 'REFRESH' },
|
icon: { iconType: 'REFRESH' },
|
||||||
isDisabled: false,
|
isDisabled: false,
|
||||||
@ -54,6 +53,7 @@ export const ErrorDisplay = (props: ErrorDisplayProps) => {
|
|||||||
simpleText: t('plugins.synced-lyrics.refetch-btn.normal')
|
simpleText: t('plugins.synced-lyrics.refetch-btn.normal')
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
|
onClick={() => retrySearch(lyricsStore.provider, getSongInfo())}
|
||||||
style={{
|
style={{
|
||||||
'margin-top': '1em',
|
'margin-top': '1em',
|
||||||
'width': '100%'
|
'width': '100%'
|
||||||
|
|||||||
@ -7,16 +7,16 @@ import {
|
|||||||
Match,
|
Match,
|
||||||
onCleanup,
|
onCleanup,
|
||||||
onMount,
|
onMount,
|
||||||
Setter,
|
type Setter,
|
||||||
Switch,
|
Switch,
|
||||||
} from 'solid-js';
|
} from 'solid-js';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
currentLyrics,
|
currentLyrics,
|
||||||
lyricsStore,
|
lyricsStore,
|
||||||
ProviderName,
|
type ProviderName,
|
||||||
providerNames,
|
providerNames,
|
||||||
ProviderState,
|
type ProviderState,
|
||||||
setLyricsStore,
|
setLyricsStore,
|
||||||
} from '../../providers';
|
} from '../../providers';
|
||||||
|
|
||||||
@ -132,11 +132,11 @@ export const LyricsPicker = (props: {
|
|||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
class="style-scope yt-icon"
|
class="style-scope yt-icon"
|
||||||
|
fill="#FFFFFF"
|
||||||
|
height={'40px'}
|
||||||
preserveAspectRatio="xMidYMid meet"
|
preserveAspectRatio="xMidYMid meet"
|
||||||
viewBox="0 -960 960 960"
|
viewBox="0 -960 960 960"
|
||||||
height={'40px'}
|
|
||||||
width={'40px'}
|
width={'40px'}
|
||||||
fill="#FFFFFF"
|
|
||||||
>
|
>
|
||||||
<g class="style-scope yt-icon">
|
<g class="style-scope yt-icon">
|
||||||
<path
|
<path
|
||||||
@ -156,10 +156,10 @@ export const LyricsPicker = (props: {
|
|||||||
{(provider) => (
|
{(provider) => (
|
||||||
<div
|
<div
|
||||||
class="lyrics-picker-item"
|
class="lyrics-picker-item"
|
||||||
tabindex="-1"
|
|
||||||
style={{
|
style={{
|
||||||
transform: `translateX(${providerIdx() * -100 - 5}%)`,
|
transform: `translateX(${providerIdx() * -100 - 5}%)`,
|
||||||
}}
|
}}
|
||||||
|
tabindex="-1"
|
||||||
>
|
>
|
||||||
<Switch>
|
<Switch>
|
||||||
<Match
|
<Match
|
||||||
@ -170,16 +170,16 @@ export const LyricsPicker = (props: {
|
|||||||
>
|
>
|
||||||
<tp-yt-paper-spinner-lite
|
<tp-yt-paper-spinner-lite
|
||||||
active
|
active
|
||||||
tabindex="-1"
|
|
||||||
class="loading-indicator style-scope"
|
class="loading-indicator style-scope"
|
||||||
style={{ padding: '5px', transform: 'scale(0.5)' }}
|
style={{ padding: '5px', transform: 'scale(0.5)' }}
|
||||||
|
tabindex="-1"
|
||||||
/>
|
/>
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={currentLyrics().state === 'error'}>
|
<Match when={currentLyrics().state === 'error'}>
|
||||||
<yt-icon-button
|
<yt-icon-button
|
||||||
icon={errorIcon}
|
icon={errorIcon}
|
||||||
tabindex="-1"
|
|
||||||
style={{ padding: '5px', transform: 'scale(0.5)' }}
|
style={{ padding: '5px', transform: 'scale(0.5)' }}
|
||||||
|
tabindex="-1"
|
||||||
/>
|
/>
|
||||||
</Match>
|
</Match>
|
||||||
<Match
|
<Match
|
||||||
@ -191,8 +191,8 @@ export const LyricsPicker = (props: {
|
|||||||
>
|
>
|
||||||
<yt-icon-button
|
<yt-icon-button
|
||||||
icon={successIcon}
|
icon={successIcon}
|
||||||
tabindex="-1"
|
|
||||||
style={{ padding: '5px', transform: 'scale(0.5)' }}
|
style={{ padding: '5px', transform: 'scale(0.5)' }}
|
||||||
|
tabindex="-1"
|
||||||
/>
|
/>
|
||||||
</Match>
|
</Match>
|
||||||
<Match
|
<Match
|
||||||
@ -204,8 +204,8 @@ export const LyricsPicker = (props: {
|
|||||||
>
|
>
|
||||||
<yt-icon-button
|
<yt-icon-button
|
||||||
icon={notFoundIcon}
|
icon={notFoundIcon}
|
||||||
tabindex="-1"
|
|
||||||
style={{ padding: '5px', transform: 'scale(0.5)' }}
|
style={{ padding: '5px', transform: 'scale(0.5)' }}
|
||||||
|
tabindex="-1"
|
||||||
/>
|
/>
|
||||||
</Match>
|
</Match>
|
||||||
</Switch>
|
</Switch>
|
||||||
@ -252,11 +252,11 @@ export const LyricsPicker = (props: {
|
|||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
class="style-scope yt-icon"
|
class="style-scope yt-icon"
|
||||||
|
fill="#FFFFFF"
|
||||||
|
height={'40px'}
|
||||||
preserveAspectRatio="xMidYMid meet"
|
preserveAspectRatio="xMidYMid meet"
|
||||||
viewBox="0 -960 960 960"
|
viewBox="0 -960 960 960"
|
||||||
height={'40px'}
|
|
||||||
width={'40px'}
|
width={'40px'}
|
||||||
fill="#FFFFFF"
|
|
||||||
>
|
>
|
||||||
<g class="style-scope yt-icon">
|
<g class="style-scope yt-icon">
|
||||||
<path
|
<path
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
import { createEffect, createMemo, For, Show, createSignal } from 'solid-js';
|
import { createEffect, createMemo, For, Show, createSignal } from 'solid-js';
|
||||||
|
|
||||||
import { VirtualizerHandle } from 'virtua/solid';
|
import { type VirtualizerHandle } from 'virtua/solid';
|
||||||
|
|
||||||
import { LineLyrics } from '@/plugins/synced-lyrics/types';
|
import { type LineLyrics } from '@/plugins/synced-lyrics/types';
|
||||||
|
|
||||||
import { config } from '../renderer';
|
import { config } from '../renderer';
|
||||||
import { _ytAPI } from '..';
|
import { _ytAPI } from '..';
|
||||||
@ -39,7 +39,6 @@ export const SyncedLine = (props: SyncedLineProps) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Show
|
<Show
|
||||||
when={text()}
|
|
||||||
fallback={
|
fallback={
|
||||||
<yt-formatted-string
|
<yt-formatted-string
|
||||||
text={{
|
text={{
|
||||||
@ -47,6 +46,7 @@ export const SyncedLine = (props: SyncedLineProps) => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
when={text()}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class={`synced-line ${props.status}`}
|
class={`synced-line ${props.status}`}
|
||||||
@ -54,7 +54,7 @@ export const SyncedLine = (props: SyncedLineProps) => {
|
|||||||
_ytAPI?.seekTo((props.line.timeInMs + 10) / 1000);
|
_ytAPI?.seekTo((props.line.timeInMs + 10) / 1000);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div dir="auto" class="description ytmusic-description-shelf-renderer">
|
<div class="description ytmusic-description-shelf-renderer" dir="auto">
|
||||||
<yt-formatted-string
|
<yt-formatted-string
|
||||||
text={{
|
text={{
|
||||||
runs: [
|
runs: [
|
||||||
|
|||||||
@ -301,8 +301,8 @@ export const LyricsRenderer = () => {
|
|||||||
return (
|
return (
|
||||||
<SyncedLine
|
<SyncedLine
|
||||||
{...props}
|
{...props}
|
||||||
scroller={scroller()!}
|
|
||||||
index={idx()}
|
index={idx()}
|
||||||
|
scroller={scroller()!}
|
||||||
status={statuses()[idx() - 1]}
|
status={statuses()[idx() - 1]}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,15 +1,11 @@
|
|||||||
import { render } from 'solid-js/web';
|
import { render } from 'solid-js/web';
|
||||||
|
|
||||||
import KuromojiAnalyzer from 'kuroshiro-analyzer-kuromoji';
|
import KuromojiAnalyzer from 'kuroshiro-analyzer-kuromoji';
|
||||||
import Kuroshiro from 'kuroshiro';
|
import Kuroshiro from 'kuroshiro';
|
||||||
|
|
||||||
import { romanize as esHangulRomanize } from 'es-hangul';
|
import { romanize as esHangulRomanize } from 'es-hangul';
|
||||||
import hanja from 'hanja';
|
import hanja from 'hanja';
|
||||||
|
import * as pinyin from 'tiny-pinyin';
|
||||||
import pinyin from 'tiny-pinyin';
|
import { romanize as romanizeThaiFrag } from '@dehoist/romanize-thai';
|
||||||
|
|
||||||
import { lazy } from 'lazy-var';
|
import { lazy } from 'lazy-var';
|
||||||
|
|
||||||
import { detect } from 'tinyld';
|
import { detect } from 'tinyld';
|
||||||
|
|
||||||
import { waitForElement } from '@/utils/wait-for-element';
|
import { waitForElement } from '@/utils/wait-for-element';
|
||||||
@ -155,26 +151,9 @@ const hasKorean = (lines: string[]) =>
|
|||||||
const hasChinese = (lines: string[]) =>
|
const hasChinese = (lines: string[]) =>
|
||||||
lines.some((line) => /[\u4E00-\u9FFF]+/.test(line));
|
lines.some((line) => /[\u4E00-\u9FFF]+/.test(line));
|
||||||
|
|
||||||
export const romanize = async (line: string) => {
|
// https://en.wikipedia.org/wiki/Thai_(Unicode_block)
|
||||||
const lang = detect(line);
|
const hasThai = (lines: string[]) =>
|
||||||
|
lines.some((line) => /[\u0E00-\u0E7F]+/.test(line));
|
||||||
const handlers: Record<string, (line: string) => Promise<string> | string> = {
|
|
||||||
ja: romanizeJapanese,
|
|
||||||
ko: romanizeHangul,
|
|
||||||
zh: romanizeChinese,
|
|
||||||
};
|
|
||||||
|
|
||||||
const NO_OP = (l: string) => l;
|
|
||||||
const handler = handlers[lang] ?? NO_OP;
|
|
||||||
|
|
||||||
line = await handler(line);
|
|
||||||
|
|
||||||
if (hasJapanese([line])) line = await romanizeJapanese(line);
|
|
||||||
if (hasKorean([line])) line = romanizeHangul(line);
|
|
||||||
if (hasChinese([line])) line = romanizeChinese(line);
|
|
||||||
|
|
||||||
return line;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const romanizeJapanese = async (line: string) =>
|
export const romanizeJapanese = async (line: string) =>
|
||||||
(await kuroshiro.get()).convert(line, {
|
(await kuroshiro.get()).convert(line, {
|
||||||
@ -190,3 +169,47 @@ export const romanizeChinese = (line: string) => {
|
|||||||
pinyin.convertToPinyin(match, ' ', true),
|
pinyin.convertToPinyin(match, ' ', true),
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const thaiSegmenter = Intl.Segmenter.supportedLocalesOf('th').includes('th')
|
||||||
|
? new Intl.Segmenter('th', { granularity: 'word' })
|
||||||
|
: null;
|
||||||
|
|
||||||
|
export const romanizeThai = (line: string) => {
|
||||||
|
if (!thaiSegmenter) return romanizeThaiFrag(line);
|
||||||
|
|
||||||
|
const segments = Array.from(thaiSegmenter.segment(line));
|
||||||
|
const latin = segments
|
||||||
|
.map((segment) =>
|
||||||
|
segment.isWordLike
|
||||||
|
? romanizeThaiFrag(segment.segment)
|
||||||
|
: segment.segment.trim(),
|
||||||
|
)
|
||||||
|
.join(' ')
|
||||||
|
.trim();
|
||||||
|
|
||||||
|
return latin;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlers: Record<string, (line: string) => Promise<string> | string> = {
|
||||||
|
ja: romanizeJapanese,
|
||||||
|
ko: romanizeHangul,
|
||||||
|
zh: romanizeChinese,
|
||||||
|
th: romanizeThai,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const romanize = async (line: string) => {
|
||||||
|
const lang = detect(line);
|
||||||
|
|
||||||
|
const handler = handlers[lang];
|
||||||
|
if (handler) {
|
||||||
|
return handler(line);
|
||||||
|
}
|
||||||
|
|
||||||
|
// fallback
|
||||||
|
if (hasJapanese([line])) line = await romanizeJapanese(line);
|
||||||
|
if (hasKorean([line])) line = romanizeHangul(line);
|
||||||
|
if (hasChinese([line])) line = romanizeChinese(line);
|
||||||
|
if (hasThai([line])) line = romanizeThai(line);
|
||||||
|
|
||||||
|
return line;
|
||||||
|
};
|
||||||
|
|||||||
@ -8,17 +8,20 @@ import previousIcon from '@assets/media-icons-black/previous.png?asset&asarUnpac
|
|||||||
|
|
||||||
import { createPlugin } from '@/utils';
|
import { createPlugin } from '@/utils';
|
||||||
import getSongControls from '@/providers/song-controls';
|
import getSongControls from '@/providers/song-controls';
|
||||||
import registerCallback, {
|
import {
|
||||||
|
registerCallback,
|
||||||
type SongInfo,
|
type SongInfo,
|
||||||
SongInfoEvent,
|
SongInfoEvent,
|
||||||
} from '@/providers/song-info';
|
} from '@/providers/song-info';
|
||||||
import { mediaIcons } from '@/types/media-icons';
|
import { type mediaIcons } from '@/types/media-icons';
|
||||||
import { t } from '@/i18n';
|
import { t } from '@/i18n';
|
||||||
|
import { Platform } from '@/types/plugins';
|
||||||
|
|
||||||
export default createPlugin({
|
export default createPlugin({
|
||||||
name: () => t('plugins.taskbar-mediacontrol.name'),
|
name: () => t('plugins.taskbar-mediacontrol.name'),
|
||||||
description: () => t('plugins.taskbar-mediacontrol.description'),
|
description: () => t('plugins.taskbar-mediacontrol.description'),
|
||||||
restartNeeded: true,
|
restartNeeded: true,
|
||||||
|
platform: Platform.Windows,
|
||||||
config: {
|
config: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
},
|
},
|
||||||
|
|||||||
@ -2,15 +2,17 @@ import { nativeImage, type NativeImage, TouchBar } from 'electron';
|
|||||||
|
|
||||||
import { createPlugin } from '@/utils';
|
import { createPlugin } from '@/utils';
|
||||||
import getSongControls from '@/providers/song-controls';
|
import getSongControls from '@/providers/song-controls';
|
||||||
import registerCallback, { SongInfoEvent } from '@/providers/song-info';
|
import { registerCallback, SongInfoEvent } from '@/providers/song-info';
|
||||||
import { t } from '@/i18n';
|
import { t } from '@/i18n';
|
||||||
|
|
||||||
import youtubeMusicIcon from '@assets/youtube-music.png?asset&asarUnpack';
|
import youtubeMusicIcon from '@assets/youtube-music.png?asset&asarUnpack';
|
||||||
|
import { Platform } from '@/types/plugins';
|
||||||
|
|
||||||
export default createPlugin({
|
export default createPlugin({
|
||||||
name: () => t('plugins.touchbar.name'),
|
name: () => t('plugins.touchbar.name'),
|
||||||
description: () => t('plugins.touchbar.description'),
|
description: () => t('plugins.touchbar.description'),
|
||||||
restartNeeded: true,
|
restartNeeded: true,
|
||||||
|
platform: Platform.macOS,
|
||||||
config: {
|
config: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
},
|
},
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import { net } from 'electron';
|
|||||||
import is from 'electron-is';
|
import is from 'electron-is';
|
||||||
|
|
||||||
import { createPlugin } from '@/utils';
|
import { createPlugin } from '@/utils';
|
||||||
import registerCallback from '@/providers/song-info';
|
import { registerCallback } from '@/providers/song-info';
|
||||||
import { t } from '@/i18n';
|
import { t } from '@/i18n';
|
||||||
|
|
||||||
interface Data {
|
interface Data {
|
||||||
|
|||||||
@ -7,9 +7,9 @@ import buttonSwitcherStyle from './button-switcher.css?inline';
|
|||||||
|
|
||||||
import { createPlugin } from '@/utils';
|
import { createPlugin } from '@/utils';
|
||||||
import { moveVolumeHud as preciseVolumeMoveVolumeHud } from '@/plugins/precise-volume/renderer';
|
import { moveVolumeHud as preciseVolumeMoveVolumeHud } from '@/plugins/precise-volume/renderer';
|
||||||
import { ThumbnailElement } from '@/types/get-player-response';
|
import { type ThumbnailElement } from '@/types/get-player-response';
|
||||||
import { t } from '@/i18n';
|
import { t } from '@/i18n';
|
||||||
import { MenuTemplate } from '@/menu';
|
import { type MenuTemplate } from '@/menu';
|
||||||
|
|
||||||
import { VideoSwitchButton } from './templates/video-switch-button';
|
import { VideoSwitchButton } from './templates/video-switch-button';
|
||||||
|
|
||||||
@ -177,12 +177,12 @@ export default createPlugin({
|
|||||||
() => (
|
() => (
|
||||||
<Show when={showButton()}>
|
<Show when={showButton()}>
|
||||||
<VideoSwitchButton
|
<VideoSwitchButton
|
||||||
onClick={(e) => e.stopPropagation()}
|
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const target = e.target as HTMLInputElement;
|
const target = e.target as HTMLInputElement;
|
||||||
|
|
||||||
setVideoState(target.checked);
|
setVideoState(target.checked);
|
||||||
}}
|
}}
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
songButtonText={t('plugins.video-toggle.templates.button-song')}
|
songButtonText={t('plugins.video-toggle.templates.button-song')}
|
||||||
videoButtonText={t('plugins.video-toggle.templates.button-video')}
|
videoButtonText={t('plugins.video-toggle.templates.button-video')}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -14,8 +14,8 @@ export const VideoSwitchButton = (props: VideoSwitchButtonProps) => (
|
|||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
checked={true}
|
checked={true}
|
||||||
id="video-toggle-video-switch-button-checkbox"
|
|
||||||
class="video-switch-button-checkbox"
|
class="video-switch-button-checkbox"
|
||||||
|
id="video-toggle-video-switch-button-checkbox"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
/>
|
/>
|
||||||
<label
|
<label
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import emptyStyle from './empty-player.css?inline';
|
import emptyStyle from './empty-player.css?inline';
|
||||||
import { createPlugin } from '@/utils';
|
import { createPlugin } from '@/utils';
|
||||||
import { Visualizer } from './visualizers/visualizer';
|
import { type Visualizer } from './visualizers/visualizer';
|
||||||
import {
|
import {
|
||||||
ButterchurnVisualizer as butterchurn,
|
ButterchurnVisualizer as butterchurn,
|
||||||
VudioVisualizer as vudio,
|
VudioVisualizer as vudio,
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
contextBridge,
|
contextBridge,
|
||||||
ipcRenderer,
|
ipcRenderer,
|
||||||
IpcRendererEvent,
|
type IpcRendererEvent,
|
||||||
webFrame,
|
webFrame,
|
||||||
} from 'electron';
|
} from 'electron';
|
||||||
import is from 'electron-is';
|
import is from 'electron-is';
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
|
|
||||||
import { app, BrowserWindow } from 'electron';
|
import { app, type BrowserWindow } from 'electron';
|
||||||
|
|
||||||
import getSongControls from './song-controls';
|
import getSongControls from './song-controls';
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
// This is used for to control the songs
|
// This is used for to control the songs
|
||||||
import { BrowserWindow, ipcMain } from 'electron';
|
import { type BrowserWindow, ipcMain } from 'electron';
|
||||||
|
|
||||||
|
import { LikeType } from '@/types/datahost-get-state';
|
||||||
|
|
||||||
// see protocol-handler.ts
|
// see protocol-handler.ts
|
||||||
type ArgsType<T> = T | string[] | undefined;
|
type ArgsType<T> = T | string[] | undefined;
|
||||||
@ -42,8 +44,8 @@ export default (win: BrowserWindow) => {
|
|||||||
play: () => win.webContents.send('ytmd:play'),
|
play: () => win.webContents.send('ytmd:play'),
|
||||||
pause: () => win.webContents.send('ytmd:pause'),
|
pause: () => win.webContents.send('ytmd:pause'),
|
||||||
playPause: () => win.webContents.send('ytmd:toggle-play'),
|
playPause: () => win.webContents.send('ytmd:toggle-play'),
|
||||||
like: () => win.webContents.send('ytmd:update-like', 'LIKE'),
|
like: () => win.webContents.send('ytmd:update-like', LikeType.Like),
|
||||||
dislike: () => win.webContents.send('ytmd:update-like', 'DISLIKE'),
|
dislike: () => win.webContents.send('ytmd:update-like', LikeType.Dislike),
|
||||||
seekTo: (seconds: ArgsType<number>) => {
|
seekTo: (seconds: ArgsType<number>) => {
|
||||||
const secondsNumber = parseNumberFromArgsType(seconds);
|
const secondsNumber = parseNumberFromArgsType(seconds);
|
||||||
if (secondsNumber !== null) {
|
if (secondsNumber !== null) {
|
||||||
|
|||||||
@ -1,7 +1,8 @@
|
|||||||
import { singleton } from './decorators';
|
import { singleton } from './decorators';
|
||||||
|
|
||||||
|
import { LikeType, type GetState } from '@/types/datahost-get-state';
|
||||||
|
|
||||||
import type { YoutubePlayer } from '@/types/youtube-player';
|
import type { YoutubePlayer } from '@/types/youtube-player';
|
||||||
import type { GetState } from '@/types/datahost-get-state';
|
|
||||||
import type {
|
import type {
|
||||||
AlbumDetails,
|
AlbumDetails,
|
||||||
PlayerOverlays,
|
PlayerOverlays,
|
||||||
@ -79,12 +80,52 @@ export const setupRepeatChangedListener = singleton(() => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const mapLikeStatus = (status: string | null): LikeType =>
|
||||||
|
Object.values(LikeType).includes(status as LikeType)
|
||||||
|
? (status as LikeType)
|
||||||
|
: LikeType.Indifferent;
|
||||||
|
|
||||||
|
const LIKE_STATUS_ATTRIBUTE = 'like-status';
|
||||||
|
|
||||||
|
export const setupLikeChangedListener = singleton(() => {
|
||||||
|
const likeDislikeObserver = new MutationObserver((mutations) => {
|
||||||
|
window.ipcRenderer.send(
|
||||||
|
'ytmd:like-changed',
|
||||||
|
mapLikeStatus(
|
||||||
|
(mutations[0].target as HTMLElement)?.getAttribute?.(
|
||||||
|
LIKE_STATUS_ATTRIBUTE,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
const likeButtonRenderer = document.querySelector('#like-button-renderer');
|
||||||
|
if (likeButtonRenderer) {
|
||||||
|
likeDislikeObserver.observe(likeButtonRenderer, {
|
||||||
|
attributes: true,
|
||||||
|
attributeFilter: [LIKE_STATUS_ATTRIBUTE],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Emit the initial value as well; as it's persistent between launches.
|
||||||
|
window.ipcRenderer.send(
|
||||||
|
'ytmd:like-changed',
|
||||||
|
mapLikeStatus(likeButtonRenderer.getAttribute?.(LIKE_STATUS_ATTRIBUTE)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
export const setupVolumeChangedListener = singleton((api: YoutubePlayer) => {
|
export const setupVolumeChangedListener = singleton((api: YoutubePlayer) => {
|
||||||
document.querySelector('video')?.addEventListener('volumechange', () => {
|
document.querySelector('video')?.addEventListener('volumechange', () => {
|
||||||
window.ipcRenderer.send('ytmd:volume-changed', api.getVolume());
|
window.ipcRenderer.send('ytmd:volume-changed', {
|
||||||
|
state: api.getVolume(),
|
||||||
|
isMuted: api.isMuted(),
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Emit the initial value as well; as it's persistent between launches.
|
// Emit the initial value as well; as it's persistent between launches.
|
||||||
window.ipcRenderer.send('ytmd:volume-changed', api.getVolume());
|
window.ipcRenderer.send('ytmd:volume-changed', {
|
||||||
|
state: api.getVolume(),
|
||||||
|
isMuted: api.isMuted(),
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
export const setupShuffleChangedListener = singleton(() => {
|
export const setupShuffleChangedListener = singleton(() => {
|
||||||
@ -153,6 +194,10 @@ export default (api: YoutubePlayer) => {
|
|||||||
setupTimeChangedListener();
|
setupTimeChangedListener();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
window.ipcRenderer.on('ytmd:setup-like-changed-listener', () => {
|
||||||
|
setupLikeChangedListener();
|
||||||
|
});
|
||||||
|
|
||||||
window.ipcRenderer.on('ytmd:setup-repeat-changed-listener', () => {
|
window.ipcRenderer.on('ytmd:setup-repeat-changed-listener', () => {
|
||||||
setupRepeatChangedListener();
|
setupRepeatChangedListener();
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { BrowserWindow, ipcMain, nativeImage, net } from 'electron';
|
import { type BrowserWindow, ipcMain, nativeImage, net } from 'electron';
|
||||||
|
|
||||||
import { Mutex } from 'async-mutex';
|
import { Mutex } from 'async-mutex';
|
||||||
|
|
||||||
@ -30,6 +30,7 @@ export interface SongInfo {
|
|||||||
title: string;
|
title: string;
|
||||||
alternativeTitle?: string;
|
alternativeTitle?: string;
|
||||||
artist: string;
|
artist: string;
|
||||||
|
artistUrl?: string;
|
||||||
views: number;
|
views: number;
|
||||||
uploadDate?: string;
|
uploadDate?: string;
|
||||||
imageSrc?: string | null;
|
imageSrc?: string | null;
|
||||||
@ -72,6 +73,7 @@ const handleData = async (
|
|||||||
title: '',
|
title: '',
|
||||||
alternativeTitle: '',
|
alternativeTitle: '',
|
||||||
artist: '',
|
artist: '',
|
||||||
|
artistUrl: '',
|
||||||
views: 0,
|
views: 0,
|
||||||
uploadDate: '',
|
uploadDate: '',
|
||||||
imageSrc: '',
|
imageSrc: '',
|
||||||
@ -93,6 +95,9 @@ const handleData = async (
|
|||||||
songInfo.url = microformat.urlCanonical?.split('&')[0];
|
songInfo.url = microformat.urlCanonical?.split('&')[0];
|
||||||
songInfo.playlistId =
|
songInfo.playlistId =
|
||||||
new URL(microformat.urlCanonical).searchParams.get('list') ?? '';
|
new URL(microformat.urlCanonical).searchParams.get('list') ?? '';
|
||||||
|
if (microformat.pageOwnerDetails?.externalChannelId) {
|
||||||
|
songInfo.artistUrl = `https://music.youtube.com/channel/${microformat.pageOwnerDetails.externalChannelId}`;
|
||||||
|
}
|
||||||
// Used for options.resumeOnStart
|
// Used for options.resumeOnStart
|
||||||
config.set('url', microformat.urlCanonical);
|
config.set('url', microformat.urlCanonical);
|
||||||
songInfo.alternativeTitle = microformat.linkAlternates.find(
|
songInfo.alternativeTitle = microformat.linkAlternates.find(
|
||||||
@ -110,7 +115,7 @@ const handleData = async (
|
|||||||
songInfo.elapsedSeconds = videoDetails.elapsedSeconds;
|
songInfo.elapsedSeconds = videoDetails.elapsedSeconds;
|
||||||
songInfo.isPaused = videoDetails.isPaused;
|
songInfo.isPaused = videoDetails.isPaused;
|
||||||
songInfo.videoId = videoDetails.videoId;
|
songInfo.videoId = videoDetails.videoId;
|
||||||
songInfo.album = data?.videoDetails?.album; // Will be undefined if video exist
|
songInfo.album = videoDetails.album; // Will be undefined if video exist
|
||||||
|
|
||||||
switch (videoDetails?.musicVideoType) {
|
switch (videoDetails?.musicVideoType) {
|
||||||
case 'MUSIC_VIDEO_TYPE_ATV':
|
case 'MUSIC_VIDEO_TYPE_ATV':
|
||||||
@ -179,7 +184,7 @@ export type SongInfoCallback = (
|
|||||||
const callbacks: Set<SongInfoCallback> = new Set();
|
const callbacks: Set<SongInfoCallback> = new Set();
|
||||||
|
|
||||||
// This function will allow plugins to register callback that will be triggered when data changes
|
// This function will allow plugins to register callback that will be triggered when data changes
|
||||||
const registerCallback = (callback: SongInfoCallback) => {
|
export const registerCallback = (callback: SongInfoCallback) => {
|
||||||
callbacks.add(callback);
|
callbacks.add(callback);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -277,5 +282,4 @@ export function cleanupName(name: string): string {
|
|||||||
return name;
|
return name;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default registerCallback;
|
|
||||||
export const setupSongInfo = registerProvider;
|
export const setupSongInfo = registerProvider;
|
||||||
|
|||||||
2
src/reset.d.ts
vendored
2
src/reset.d.ts
vendored
@ -19,6 +19,8 @@ declare global {
|
|||||||
'videodatachange': CustomEvent<VideoDataChanged>;
|
'videodatachange': CustomEvent<VideoDataChanged>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
declare var electronIs: typeof import('electron-is');
|
||||||
|
|
||||||
interface Window {
|
interface Window {
|
||||||
trustedTypes?: typeof trustedTypes;
|
trustedTypes?: typeof trustedTypes;
|
||||||
ipcRenderer: typeof electronIpcRenderer;
|
ipcRenderer: typeof electronIpcRenderer;
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user