mirror of
https://github.com/th-ch/youtube-music.git
synced 2026-01-15 04:11:47 +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 ]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
@ -28,14 +28,14 @@ jobs:
|
||||
|
||||
- name: Setup NodeJS
|
||||
if: startsWith(matrix.os, 'macOS') != true
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Setup NodeJS for macOS
|
||||
if: startsWith(matrix.os, 'macOS')
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
|
||||
@ -91,7 +91,7 @@ jobs:
|
||||
if: github.repository == 'th-ch/youtube-music' && github.ref == 'refs/heads/master'
|
||||
needs: build
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
@ -103,14 +103,14 @@ jobs:
|
||||
|
||||
- name: Setup NodeJS
|
||||
if: startsWith(matrix.os, 'macOS') != true
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Setup NodeJS for macOS
|
||||
if: startsWith(matrix.os, 'macOS')
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v5
|
||||
with:
|
||||
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
|
||||
steps:
|
||||
- name: "Checkout Repository"
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: "Dependency Review"
|
||||
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>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="footer-copyright">© 2024 th-ch</div>
|
||||
<div class="footer-copyright">© 2025 th-ch</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -31,11 +31,19 @@ export default tsEslint.config(
|
||||
rules: {
|
||||
'stylistic/arrow-parens': ['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' }],
|
||||
'@typescript-eslint/no-floating-promises': 'off',
|
||||
'@typescript-eslint/no-misused-promises': ['off', { checksVoidReturn: false }],
|
||||
'@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }],
|
||||
'@typescript-eslint/no-non-null-assertion': 'off',
|
||||
'@typescript-eslint/consistent-type-imports': ['error', {
|
||||
fixStyle: 'inline-type-imports',
|
||||
prefer: 'type-imports',
|
||||
disallowTypeAnnotations: false,
|
||||
}],
|
||||
'importPlugin/first': 'error',
|
||||
'importPlugin/newline-after-import': 'off',
|
||||
'importPlugin/no-default-export': 'off',
|
||||
|
||||
78
package.json
78
package.json
@ -14,16 +14,16 @@
|
||||
"url": "https://github.com/th-ch/youtube-music"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "playwright test",
|
||||
"test:debug": "cross-env DEBUG=pw:*,-pw:test:protocol playwright test",
|
||||
"build": "electron-vite build",
|
||||
"test": "pnpm playwright test",
|
||||
"test:debug": "pnpm cross-env DEBUG=pw:*,-pw:test:protocol playwright test",
|
||||
"build": "pnpm electron-vite build",
|
||||
"vite:inspect": "pnpm clean && electron-vite build --mode development && pnpm exec serve .vite-inspect",
|
||||
"start": "electron-vite preview",
|
||||
"start:debug": "cross-env ELECTRON_ENABLE_LOGGING=1 pnpm start",
|
||||
"dev": "cross-env NODE_OPTIONS=--enable-source-maps electron-vite dev --watch",
|
||||
"dev:renderer": "cross-env NODE_OPTIONS=--enable-source-maps electron-vite dev",
|
||||
"dev:debug": "cross-env ELECTRON_ENABLE_LOGGING=1 pnpm dev",
|
||||
"clean": "del-cli dist && del-cli pack && del-cli .vite-inspect",
|
||||
"start": "pnpm electron-vite preview",
|
||||
"start:debug": "pnpm cross-env ELECTRON_ENABLE_LOGGING=1 pnpm start",
|
||||
"dev": "pnpm cross-env NODE_OPTIONS=--enable-source-maps electron-vite dev --watch",
|
||||
"dev:renderer": "pnpm cross-env NODE_OPTIONS=--enable-source-maps electron-vite dev",
|
||||
"dev:debug": "pnpm cross-env ELECTRON_ENABLE_LOGGING=1 pnpm dev",
|
||||
"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: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",
|
||||
@ -32,12 +32,12 @@
|
||||
"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:x64": "pnpm clean && pnpm build && pnpm electron-builder --win nsis-web:x64 -p never",
|
||||
"lint": "eslint .",
|
||||
"changelog": "npx --yes auto-changelog",
|
||||
"lint": "pnpm eslint ./src",
|
||||
"changelog": "pnpm dlx --yes auto-changelog",
|
||||
"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: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": {
|
||||
"node": ">=22",
|
||||
@ -45,30 +45,34 @@
|
||||
},
|
||||
"pnpm": {
|
||||
"overrides": {
|
||||
"vite": "npm:rolldown-vite@7.1.2",
|
||||
"node-gyp": "11.3.0",
|
||||
"vite": "npm:rolldown-vite@7.1.5",
|
||||
"node-gyp": "11.4.2",
|
||||
"xml2js": "0.6.2",
|
||||
"node-fetch": "3.3.2",
|
||||
"@electron/universal": "3.0.1",
|
||||
"@babel/runtime": "7.28.3"
|
||||
"@babel/runtime": "7.28.4"
|
||||
},
|
||||
"patchedDependencies": {
|
||||
"vudio@2.1.1": "patches/vudio@2.1.1.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",
|
||||
"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": []
|
||||
},
|
||||
"dependencies": {
|
||||
"@dehoist/romanize-thai": "1.0.0",
|
||||
"@electron-toolkit/tsconfig": "1.0.1",
|
||||
"@electron/remote": "2.1.3",
|
||||
"@ffmpeg.wasm/core-mt": "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",
|
||||
"@ghostery/adblocker-electron": "2.11.3",
|
||||
"@ghostery/adblocker-electron-preload": "2.11.3",
|
||||
"@hono/node-server": "1.18.2",
|
||||
"@ghostery/adblocker-electron": "2.11.6",
|
||||
"@ghostery/adblocker-electron-preload": "2.11.6",
|
||||
"@hono/node-server": "1.19.1",
|
||||
"@hono/node-ws": "1.2.0",
|
||||
"@hono/swagger-ui": "0.5.2",
|
||||
"@hono/zod-openapi": "1.1.0",
|
||||
"@hono/zod-validator": "0.7.2",
|
||||
@ -100,10 +104,10 @@
|
||||
"filenamify": "6.0.0",
|
||||
"hanja": "1.1.5",
|
||||
"happy-dom": "18.0.1",
|
||||
"hono": "4.9.2",
|
||||
"hono": "4.9.6",
|
||||
"howler": "2.2.4",
|
||||
"html-to-text": "9.0.5",
|
||||
"i18next": "25.3.6",
|
||||
"i18next": "25.5.2",
|
||||
"jimp": "1.6.0",
|
||||
"keyboardevent-from-electron-accelerator": "2.0.0",
|
||||
"keyboardevents-areequal": "0.2.2",
|
||||
@ -115,7 +119,7 @@
|
||||
"node-id3": "0.2.9",
|
||||
"peerjs": "1.5.5",
|
||||
"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",
|
||||
"socks": "2.8.7",
|
||||
"solid-element": "1.9.1",
|
||||
@ -125,35 +129,35 @@
|
||||
"solid-transition-group": "0.3.0",
|
||||
"tiny-pinyin": "1.3.2",
|
||||
"tinyld": "1.3.4",
|
||||
"virtua": "0.41.5",
|
||||
"virtua": "0.42.2",
|
||||
"vudio": "2.1.1",
|
||||
"x11": "2.3.0",
|
||||
"youtubei.js": "15.0.1",
|
||||
"zod": "4.0.17"
|
||||
"zod": "4.1.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@electron-toolkit/tsconfig": "1.0.1",
|
||||
"@eslint/js": "9.33.0",
|
||||
"@eslint/js": "9.35.0",
|
||||
"@malept/flatpak-bundler": "0.4.0",
|
||||
"@playwright/test": "1.54.2",
|
||||
"@stylistic/eslint-plugin": "5.2.3",
|
||||
"@playwright/test": "1.55.0",
|
||||
"@stylistic/eslint-plugin": "5.3.1",
|
||||
"@total-typescript/ts-reset": "0.6.1",
|
||||
"@types/electron-localshortcut": "3.1.3",
|
||||
"@types/howler": "2.2.12",
|
||||
"@types/html-to-text": "9.0.4",
|
||||
"@types/semver": "7.7.0",
|
||||
"@types/semver": "7.7.1",
|
||||
"@types/trusted-types": "2.0.7",
|
||||
"bufferutil": "4.0.9",
|
||||
"builtin-modules": "5.0.0",
|
||||
"cross-env": "10.0.0",
|
||||
"del-cli": "6.0.0",
|
||||
"discord-api-types": "0.38.20",
|
||||
"electron": "37.3.0",
|
||||
"discord-api-types": "0.38.23",
|
||||
"electron": "38.0.0",
|
||||
"electron-builder": "26.0.12",
|
||||
"electron-builder-squirrel-windows": "26.0.12",
|
||||
"electron-devtools-installer": "4.0.0",
|
||||
"electron-vite": "4.0.0",
|
||||
"eslint": "9.33.0",
|
||||
"eslint": "9.35.0",
|
||||
"eslint-config-prettier": "10.1.8",
|
||||
"eslint-import-resolver-exports": "1.0.0-beta.5",
|
||||
"eslint-import-resolver-typescript": "4.4.4",
|
||||
@ -161,14 +165,14 @@
|
||||
"eslint-plugin-prettier": "5.5.4",
|
||||
"eslint-plugin-solid": "0.14.5",
|
||||
"glob": "11.0.3",
|
||||
"node-gyp": "11.3.0",
|
||||
"playwright": "1.54.2",
|
||||
"node-gyp": "11.4.2",
|
||||
"playwright": "1.55.0",
|
||||
"ts-morph": "26.0.0",
|
||||
"typescript": "5.9.2",
|
||||
"typescript-eslint": "8.39.1",
|
||||
"typescript-eslint": "8.42.0",
|
||||
"utf-8-validate": "6.0.5",
|
||||
"vite": "npm:rolldown-vite@7.1.2",
|
||||
"vite-plugin-inspect": "11.3.2",
|
||||
"vite": "npm:rolldown-vite@7.1.5",
|
||||
"vite-plugin-inspect": "11.3.3",
|
||||
"vite-plugin-resolve": "2.5.2",
|
||||
"vite-plugin-solid": "2.11.8",
|
||||
"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 store, { IStore } from './store';
|
||||
import store, { type IStore } from './store';
|
||||
import plugins from './plugins';
|
||||
|
||||
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' {
|
||||
import { BrowserWindow } from 'electron';
|
||||
import { type BrowserWindow } from 'electron';
|
||||
|
||||
export type SelectOptions = Record<string, string>;
|
||||
|
||||
|
||||
@ -150,6 +150,13 @@
|
||||
"visual-tweaks": {
|
||||
"label": "Οπτικές προσαρμογές",
|
||||
"submenu": {
|
||||
"custom-window-title": {
|
||||
"label": "Προσαρμοσμένος τίτλος παραθύρου",
|
||||
"prompt": {
|
||||
"label": "Εισαγωγή προσαρμοσμένου τίτλου παραθύρου: (κενό για απενεργοποίηση)",
|
||||
"placeholder": "Παράδειγμα: YouTube Music"
|
||||
}
|
||||
},
|
||||
"like-buttons": {
|
||||
"default": "Προεπιλογή",
|
||||
"force-show": "Επιβολή εμφάνισης",
|
||||
@ -381,6 +388,11 @@
|
||||
},
|
||||
"templates": {
|
||||
"title": "Ανοίξτε τον επιλογέα λεζάντας"
|
||||
},
|
||||
"toast": {
|
||||
"caption-changed": "Λεζάντα άλλαξε σε {{language}}",
|
||||
"caption-disabled": "Λεζάντες απενεργοποιήθηκαν",
|
||||
"no-captions": "Λεζάντες μη διαθέσιμες για αυτό το τραγούδι"
|
||||
}
|
||||
},
|
||||
"compact-sidebar": {
|
||||
@ -600,7 +612,15 @@
|
||||
},
|
||||
"navigation": {
|
||||
"description": "Βέλη πλοήγησης Επόμενο/Πίσω ενσωματωμένα απευθείας στο περιβάλλον εργασίας, όπως στο αγαπημένο σας πρόγραμμα περιήγησης",
|
||||
"name": "Πλοήγηση"
|
||||
"name": "Πλοήγηση",
|
||||
"templates": {
|
||||
"back": {
|
||||
"title": "Μετάβαση στην προηγούμενη σελίδα"
|
||||
},
|
||||
"forward": {
|
||||
"title": "Μετάβαση στην επόμενη σελίδα"
|
||||
}
|
||||
}
|
||||
},
|
||||
"no-google-login": {
|
||||
"description": "Αφαίρεση των κουμπιών και των συνδέσμων σύνδεσης Google από το περιβάλλον εργασίας",
|
||||
@ -692,7 +712,12 @@
|
||||
}
|
||||
},
|
||||
"description": "Επιτρέπει την αλλαγή της ποιότητας βίντεο με ένα κουμπί στην επικάλυψη βίντεο",
|
||||
"name": "Αλλαγή ποιότητας βίντεο"
|
||||
"name": "Αλλαγή ποιότητας βίντεο",
|
||||
"renderer": {
|
||||
"quality-settings-button": {
|
||||
"label": "Άνοιγμα ρυθμίσεων ποιότητας αναπαραγωγέα"
|
||||
}
|
||||
}
|
||||
},
|
||||
"scrobbler": {
|
||||
"description": "Προσθήκη υποστήριξης scrobbling (κ.λπ. last.fm, Listenbrainz)",
|
||||
@ -859,7 +884,8 @@
|
||||
},
|
||||
"name": "Εναλλαγή βίντεο",
|
||||
"templates": {
|
||||
"button-song": "Τραγούδι"
|
||||
"button-song": "Τραγούδι",
|
||||
"button-video": "Βίντεο"
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
"description": "Makes song start in \"paused\" mode",
|
||||
"menu": {
|
||||
@ -444,7 +457,15 @@
|
||||
"hide-duration-left": "Hide duration left",
|
||||
"hide-github-button": "Hide GitHub link Button",
|
||||
"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",
|
||||
"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": {
|
||||
"description": "Hace que la canción comience en modo \"pausado\"",
|
||||
"menu": {
|
||||
|
||||
@ -2,14 +2,14 @@
|
||||
"common": {
|
||||
"console": {
|
||||
"plugins": {
|
||||
"execute-failed": "Misslyckades med att köra plugin {{pluginName}}::{{contextName}}",
|
||||
"executed-at-ms": "Plugin {{pluginName}}::{{contextName}} kördes på {{ms}} ms",
|
||||
"initialize-failed": "Misslyckades med att initialisera pluginen \"{{pluginName}}\"",
|
||||
"load-all": "Laddar alla pluginer",
|
||||
"load-failed": "Misslyckades med att ladda pluginen \"{{pluginName}}\"",
|
||||
"loaded": "Pluginen \"{{pluginName}}\" laddad",
|
||||
"unload-failed": "Misslyckades med att avlasta pluginen \"{{pluginName}}\"",
|
||||
"unloaded": "Pluginen \"{{pluginName}}\" avlastad"
|
||||
"execute-failed": "Misslyckades med att köra tillägget {{pluginName}}::{{contextName}}",
|
||||
"executed-at-ms": "Tillägget {{pluginName}}::{{contextName}} kördes på {{ms}}ms",
|
||||
"initialize-failed": "Misslyckades med att initialisera tillägget \"{{pluginName}}\"",
|
||||
"load-all": "Laddar alla tillägg",
|
||||
"load-failed": "Misslyckades med att ladda tillägget \"{{pluginName}}\"",
|
||||
"loaded": "Tillägget \"{{pluginName}}\" laddades in",
|
||||
"unload-failed": "Kunde inte inaktivera {{pluginName}}-tillägget",
|
||||
"unloaded": "{{pluginName}}-tillägget inaktiverat"
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -21,7 +21,7 @@
|
||||
"main": {
|
||||
"console": {
|
||||
"did-finish-load": {
|
||||
"dev-tools": "Laddning klar. DevTools öppnad"
|
||||
"dev-tools": "Laddning slutförd. Utvecklarverktyg öppnad"
|
||||
},
|
||||
"i18n": {
|
||||
"loaded": "i18n laddad"
|
||||
@ -45,16 +45,16 @@
|
||||
"dialog": {
|
||||
"hide-menu-enabled": {
|
||||
"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",
|
||||
"title": "Dölj Meny aktiverad"
|
||||
"message": "Dölj meny är aktiverad",
|
||||
"title": "Dölj meny aktiverad"
|
||||
},
|
||||
"need-to-restart": {
|
||||
"buttons": {
|
||||
"later": "Senare",
|
||||
"restart-now": "Starta om nu"
|
||||
},
|
||||
"detail": "\"{{pluginName}}\" pluginen kräver en omstart för att träda i kraft",
|
||||
"message": "\"{{pluginName}}\" behöver startas om",
|
||||
"detail": "\"{{pluginName}}\"-tillägget kräver en omstart för att träda i kraft",
|
||||
"message": "\"{{pluginName}}\"-tillägget behöver startas om",
|
||||
"title": "Omstart krävs"
|
||||
},
|
||||
"unresponsive": {
|
||||
@ -84,17 +84,17 @@
|
||||
"label": "Navigering",
|
||||
"submenu": {
|
||||
"copy-current-url": "Kopiera nuvarande länk",
|
||||
"go-back": "Föregående",
|
||||
"go-forward": "Nästa",
|
||||
"go-back": "Gå tillbaka",
|
||||
"go-forward": "Gå framåt",
|
||||
"quit": "Lämna",
|
||||
"restart": "Starta om appen"
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"label": "Valmöjligheter",
|
||||
"label": "Alternativ",
|
||||
"submenu": {
|
||||
"advanced-options": {
|
||||
"label": "Avancerade valmöjligheter",
|
||||
"label": "Avancerade alternativ",
|
||||
"submenu": {
|
||||
"auto-reset-app-cache": "Nollställ appcache när appen startar",
|
||||
"disable-hardware-acceleration": "Stäng av hårdvaruacceleration",
|
||||
@ -114,60 +114,589 @@
|
||||
},
|
||||
"always-on-top": "Alltid överst",
|
||||
"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": {
|
||||
"dialog": {
|
||||
"message": "Språket ändras efter omstart",
|
||||
"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": {
|
||||
"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": {
|
||||
"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": {
|
||||
"name": "Inget Google Login"
|
||||
"description": "Ta bort Google-inloggningsknappar och länkar från gränssnittet",
|
||||
"name": "Ingen Google-inloggning"
|
||||
},
|
||||
"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"
|
||||
},
|
||||
"performance-improvement": {
|
||||
"description": "Förbättra prestanda genom att aktivera experimentella skript",
|
||||
"name": "Prestandaförbättring [Beta]"
|
||||
},
|
||||
"picture-in-picture": {
|
||||
"description": "Tillåter appen att växla till bild-i-bild-läge",
|
||||
"menu": {
|
||||
"always-on-top": "Alltid överst",
|
||||
"hotkey": {
|
||||
"label": "Snabbkommando",
|
||||
"prompt": {
|
||||
"keybind-options": {
|
||||
"hotkey": "Snabbkommando"
|
||||
},
|
||||
"label": "Välj ett snabbkommando för att växla bild-i-bild-läge",
|
||||
"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": {
|
||||
"button": "Bild-i-bild"
|
||||
}
|
||||
},
|
||||
"playback-speed": {
|
||||
"description": "Lägger till ett reglage för att ändra uppspelningshastighet",
|
||||
"name": "Uppspelningshastighet",
|
||||
"templates": {
|
||||
"button": "Hasighet"
|
||||
"button": "Hastighet"
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
"global-shortcuts": {
|
||||
"keybind-options": {
|
||||
"decrease": "Minska Volym",
|
||||
"increase": "Öka Volym"
|
||||
}
|
||||
"decrease": "Sänk volymen",
|
||||
"increase": "Öka volymen"
|
||||
},
|
||||
"label": "Välj globala kortkommandon för volym:",
|
||||
"title": "Globala kortkommandon för volym"
|
||||
},
|
||||
"volume-steps": {
|
||||
"label": "Välj volymsteg för ökning/minskning",
|
||||
"title": "Volymsteg"
|
||||
}
|
||||
}
|
||||
@ -176,54 +705,195 @@
|
||||
"backend": {
|
||||
"dialog": {
|
||||
"quality-changer": {
|
||||
"detail": "Nuvarande kvalité: {{quality}}",
|
||||
"message": "Välj Video Kvalité:",
|
||||
"title": "Välj Video Kvalité"
|
||||
"detail": "Nuvarande kvalitet: {{quality}}",
|
||||
"message": "Välj videokvalitet:",
|
||||
"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": {
|
||||
"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": {
|
||||
"lastfm": {
|
||||
"api-key": "Last.fm API nyckel"
|
||||
"api-key": "Last.fm API nyckel",
|
||||
"api-secret": "Last.fm API-hemlighet"
|
||||
},
|
||||
"listenbrainz": {
|
||||
"token": {
|
||||
"label": "Ange din ListenBrainz användartoken:",
|
||||
"title": "ListenBrainz token"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
"keybind": {
|
||||
"keybind-options": {
|
||||
"next": "Nästa",
|
||||
"play-pause": "Spela / Pausa",
|
||||
"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": {
|
||||
"description": "Lägger till en knapp för att växla mellan video/musik-läge. Kan också valfritt ta bort hela videofliken",
|
||||
"menu": {
|
||||
"align": {
|
||||
"label": "Justering",
|
||||
"submenu": {
|
||||
"left": "Vänster",
|
||||
"middle": "Mitten",
|
||||
"right": "Höger"
|
||||
}
|
||||
},
|
||||
"force-hide": "Tvinga borttagning av videoflik",
|
||||
"mode": {
|
||||
"label": "Läge",
|
||||
"submenu": {
|
||||
"disabled": "Inaktiverad"
|
||||
"custom": "Anpassad växling",
|
||||
"disabled": "Inaktiverad",
|
||||
"native": "Inbyggd växling"
|
||||
}
|
||||
}
|
||||
},
|
||||
"name": "Video PÅ/AV",
|
||||
"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-github-button": "ซ่อนปุ่มลิงก์ GitHub",
|
||||
"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": "แสดงกิจกรรมบนดิสคอร์ด",
|
||||
"prompt": {
|
||||
|
||||
40
src/index.ts
40
src/index.ts
@ -15,7 +15,7 @@ import {
|
||||
type BrowserWindowConstructorOptions,
|
||||
} from 'electron';
|
||||
import enhanceWebRequest, {
|
||||
BetterSession,
|
||||
type BetterSession,
|
||||
} from '@jellybrick/electron-better-web-request';
|
||||
import is from 'electron-is';
|
||||
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 type { PluginConfig } from '@/types/plugins';
|
||||
|
||||
if (!is.macOS()) {
|
||||
delete (await allPlugins())['touchbar'];
|
||||
}
|
||||
if (!is.windows()) {
|
||||
delete (await allPlugins())['taskbar-mediacontrol'];
|
||||
}
|
||||
import { type PluginConfig } from '@/types/plugins';
|
||||
|
||||
// Catch errors and log them
|
||||
unhandled({
|
||||
@ -345,8 +338,8 @@ async function createMainWindow() {
|
||||
titleBarStyle: useInlineMenu
|
||||
? 'hidden'
|
||||
: is.macOS()
|
||||
? 'hiddenInset'
|
||||
: 'default',
|
||||
? 'hiddenInset'
|
||||
: 'default',
|
||||
autoHideMenuBar: config.get('options.hideMenu'),
|
||||
};
|
||||
|
||||
@ -360,6 +353,8 @@ async function createMainWindow() {
|
||||
icon,
|
||||
width: windowSize.width,
|
||||
height: windowSize.height,
|
||||
minWidth: 325,
|
||||
minHeight: 425,
|
||||
backgroundColor: '#000',
|
||||
show: false,
|
||||
webPreferences: {
|
||||
@ -534,8 +529,8 @@ app.once('browser-window-created', (_event, win) => {
|
||||
const updatedUserAgent = is.macOS()
|
||||
? userAgents.mac
|
||||
: is.windows()
|
||||
? userAgents.windows
|
||||
: userAgents.linux;
|
||||
? userAgents.windows
|
||||
: userAgents.linux;
|
||||
|
||||
win.webContents.userAgent = updatedUserAgent;
|
||||
app.userAgentFallback = updatedUserAgent;
|
||||
@ -956,18 +951,15 @@ function removeContentSecurityPolicy(
|
||||
betterSession.webRequest.setResolver(
|
||||
'onHeadersReceived',
|
||||
async (listeners) => {
|
||||
return listeners.reduce(
|
||||
async (accumulator, listener) => {
|
||||
const acc = await accumulator;
|
||||
if (acc.cancel) {
|
||||
return acc;
|
||||
}
|
||||
return listeners.reduce(async (accumulator, listener) => {
|
||||
const acc = await accumulator;
|
||||
if (acc.cancel) {
|
||||
return acc;
|
||||
}
|
||||
|
||||
const result = await listener.apply();
|
||||
return { ...accumulator, ...result };
|
||||
},
|
||||
Promise.resolve({ cancel: false }),
|
||||
);
|
||||
const result = await listener.apply();
|
||||
return { ...accumulator, ...result };
|
||||
}, 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 { allPlugins, mainPlugins } from 'virtual:plugins';
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
import is from 'electron-is';
|
||||
import {
|
||||
app,
|
||||
BrowserWindow,
|
||||
type BrowserWindow,
|
||||
clipboard,
|
||||
dialog,
|
||||
Menu,
|
||||
MenuItem,
|
||||
type MenuItem,
|
||||
shell,
|
||||
} from 'electron';
|
||||
import prompt from 'custom-electron-prompt';
|
||||
|
||||
@ -81,26 +81,26 @@ export default createPlugin<
|
||||
<>
|
||||
<Show when={showUnDislike()}>
|
||||
<UnDislikeButton
|
||||
onClick={this.loadFullList}
|
||||
maskSize={unDislikeMaskSize()}
|
||||
onClick={this.loadFullList}
|
||||
/>
|
||||
</Show>
|
||||
<Show when={showDislike()}>
|
||||
<DislikeButton
|
||||
onClick={this.loadFullList}
|
||||
maskSize={dislikeMaskSize()}
|
||||
onClick={this.loadFullList}
|
||||
/>
|
||||
</Show>
|
||||
<Show when={showLike()}>
|
||||
<LikeButton
|
||||
onClick={this.loadFullList}
|
||||
maskSize={likeMaskSize()}
|
||||
onClick={this.loadFullList}
|
||||
/>
|
||||
</Show>
|
||||
<Show when={showUnLike()}>
|
||||
<UnLikeButton
|
||||
onClick={this.loadFullList}
|
||||
maskSize={unLikeMaskSize()}
|
||||
onClick={this.loadFullList}
|
||||
/>
|
||||
</Show>
|
||||
</>
|
||||
|
||||
@ -6,22 +6,23 @@ export interface DislikeButtonProps {
|
||||
export const DislikeButton = (props: DislikeButtonProps) => (
|
||||
<div class="style-scope">
|
||||
<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-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)}
|
||||
>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
class="yt-spec-button-shape-next__icon"
|
||||
style={{
|
||||
'color': 'var(--ytmusic-setting-item-toggle-active)',
|
||||
}}
|
||||
aria-hidden="true"
|
||||
>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
class="yt-spec-button-shape-next__icon"
|
||||
style={{
|
||||
'color': 'white',
|
||||
@ -32,24 +33,23 @@ export const DislikeButton = (props: DislikeButtonProps) => (
|
||||
'z-index': 1,
|
||||
'position': 'absolute',
|
||||
}}
|
||||
aria-hidden="true"
|
||||
>
|
||||
<div style={{ 'width': '24px', 'height': '24px' }}>
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
preserveAspectRatio="xMidYMid meet"
|
||||
class="style-scope yt-icon"
|
||||
preserveAspectRatio="xMidYMid meet"
|
||||
style={{
|
||||
'pointer-events': 'none',
|
||||
'display': 'block',
|
||||
'width': '100%',
|
||||
'height': '100%',
|
||||
}}
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<g class="style-scope yt-icon">
|
||||
<path
|
||||
d="M18,4h3v10h-3V4z M5.23,14h4.23l-1.52,4.94C7.62,19.97,8.46,21,9.62,21c0.58,0,1.14-0.24,1.52-0.65L17,14V4H6.57 C5.5,4,4.59,4.67,4.38,5.61l-1.34,6C2.77,12.85,3.82,14,5.23,14z"
|
||||
class="style-scope yt-icon"
|
||||
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>
|
||||
</svg>
|
||||
@ -62,20 +62,20 @@ export const DislikeButton = (props: DislikeButtonProps) => (
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
preserveAspectRatio="xMidYMid meet"
|
||||
class="style-scope yt-icon"
|
||||
preserveAspectRatio="xMidYMid meet"
|
||||
style={{
|
||||
'pointer-events': 'none',
|
||||
'display': 'block',
|
||||
'width': '100%',
|
||||
'height': '100%',
|
||||
}}
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<g class="style-scope yt-icon">
|
||||
<path
|
||||
d="M18,4h3v10h-3V4z M5.23,14h4.23l-1.52,4.94C7.62,19.97,8.46,21,9.62,21c0.58,0,1.14-0.24,1.52-0.65L17,14V4H6.57 C5.5,4,4.59,4.67,4.38,5.61l-1.34,6C2.77,12.85,3.82,14,5.23,14z"
|
||||
class="style-scope yt-icon"
|
||||
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>
|
||||
</svg>
|
||||
@ -87,8 +87,8 @@ export const DislikeButton = (props: DislikeButtonProps) => (
|
||||
}}
|
||||
>
|
||||
<div
|
||||
class="yt-spec-touch-feedback-shape yt-spec-touch-feedback-shape--touch-response"
|
||||
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__fill" />
|
||||
|
||||
@ -6,22 +6,23 @@ export interface LikeButtonProps {
|
||||
export const LikeButton = (props: LikeButtonProps) => (
|
||||
<div class="style-scope">
|
||||
<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-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)}
|
||||
>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
class="yt-spec-button-shape-next__icon"
|
||||
style={{
|
||||
'color': 'var(--ytmusic-setting-item-toggle-active)',
|
||||
}}
|
||||
aria-hidden="true"
|
||||
>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
class="yt-spec-button-shape-next__icon"
|
||||
style={{
|
||||
'color': 'white',
|
||||
@ -32,24 +33,23 @@ export const LikeButton = (props: LikeButtonProps) => (
|
||||
'z-index': 1,
|
||||
'position': 'absolute',
|
||||
}}
|
||||
aria-hidden="true"
|
||||
>
|
||||
<div style={{ 'width': '24px', 'height': '24px' }}>
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
preserveAspectRatio="xMidYMid meet"
|
||||
class="style-scope yt-icon"
|
||||
preserveAspectRatio="xMidYMid meet"
|
||||
style={{
|
||||
'pointer-events': 'none',
|
||||
'display': 'block',
|
||||
'width': '100%',
|
||||
'height': '100%',
|
||||
}}
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<g class="style-scope yt-icon">
|
||||
<path
|
||||
d="M3,11h3v10H3V11z M18.77,11h-4.23l1.52-4.94C16.38,5.03,15.54,4,14.38,4c-0.58,0-1.14,0.24-1.52,0.65L7,11v10h10.43 c1.06,0,1.98-0.67,2.19-1.61l1.34-6C21.23,12.15,20.18,11,18.77,11z"
|
||||
class="style-scope yt-icon"
|
||||
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>
|
||||
</svg>
|
||||
@ -57,20 +57,20 @@ export const LikeButton = (props: LikeButtonProps) => (
|
||||
</div>
|
||||
<div style={{ 'width': '24px', 'height': '24px' }}>
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
preserveAspectRatio="xMidYMid meet"
|
||||
class="style-scope yt-icon"
|
||||
preserveAspectRatio="xMidYMid meet"
|
||||
style={{
|
||||
'pointer-events': 'none',
|
||||
'display': 'block',
|
||||
'width': '100%',
|
||||
'height': '100%',
|
||||
}}
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<g class="style-scope yt-icon">
|
||||
<path
|
||||
d="M3,11h3v10H3V11z M18.77,11h-4.23l1.52-4.94C16.38,5.03,15.54,4,14.38,4c-0.58,0-1.14,0.24-1.52,0.65L7,11v10h10.43 c1.06,0,1.98-0.67,2.19-1.61l1.34-6C21.23,12.15,20.18,11,18.77,11z"
|
||||
class="style-scope yt-icon"
|
||||
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>
|
||||
</svg>
|
||||
@ -78,8 +78,8 @@ export const LikeButton = (props: LikeButtonProps) => (
|
||||
</div>
|
||||
<yt-touch-feedback-shape style={{ 'border-radius': 'inherit' }}>
|
||||
<div
|
||||
class="yt-spec-touch-feedback-shape yt-spec-touch-feedback-shape--touch-response"
|
||||
aria-hidden="true"
|
||||
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__fill" />
|
||||
|
||||
@ -6,22 +6,23 @@ export interface UnDislikeButtonProps {
|
||||
export const UnDislikeButton = (props: UnDislikeButtonProps) => (
|
||||
<div class="style-scope">
|
||||
<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-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)}
|
||||
>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
class="yt-spec-button-shape-next__icon"
|
||||
style={{
|
||||
color: 'var(--ytmusic-setting-item-toggle-active)',
|
||||
}}
|
||||
aria-hidden="true"
|
||||
>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
class="yt-spec-button-shape-next__icon"
|
||||
style={{
|
||||
'color': 'white',
|
||||
@ -32,7 +33,6 @@ export const UnDislikeButton = (props: UnDislikeButtonProps) => (
|
||||
'z-index': 1,
|
||||
'position': 'absolute',
|
||||
}}
|
||||
aria-hidden="true"
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
@ -41,20 +41,20 @@ export const UnDislikeButton = (props: UnDislikeButtonProps) => (
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
preserveAspectRatio="xMidYMid meet"
|
||||
class="style-scope yt-icon"
|
||||
preserveAspectRatio="xMidYMid meet"
|
||||
style={{
|
||||
'pointer-events': 'none',
|
||||
'display': 'block',
|
||||
'width': '100%',
|
||||
'height': '100%',
|
||||
}}
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<g class="style-scope yt-icon">
|
||||
<path
|
||||
d="M17,4h-1H6.57C5.5,4,4.59,4.67,4.38,5.61l-1.34,6C2.77,12.85,3.82,14,5.23,14h4.23l-1.52,4.94C7.62,19.97,8.46,21,9.62,21 c0.58,0,1.14-0.24,1.52-0.65L17,14h4V4H17z M10.4,19.67C10.21,19.88,9.92,20,9.62,20c-0.26,0-0.5-0.11-0.63-0.3 c-0.07-0.1-0.15-0.26-0.09-0.47l1.52-4.94l0.4-1.29H9.46H5.23c-0.41,0-0.8-0.17-1.03-0.46c-0.12-0.15-0.25-0.4-0.18-0.72l1.34-6 C5.46,5.35,5.97,5,6.57,5H16v8.61L10.4,19.67z M20,13h-3V5h3V13z"
|
||||
class="style-scope yt-icon"
|
||||
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>
|
||||
</svg>
|
||||
@ -67,20 +67,20 @@ export const UnDislikeButton = (props: UnDislikeButtonProps) => (
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
preserveAspectRatio="xMidYMid meet"
|
||||
class="style-scope yt-icon"
|
||||
preserveAspectRatio="xMidYMid meet"
|
||||
style={{
|
||||
'pointer-events': 'none',
|
||||
'display': 'block',
|
||||
'width': '100%',
|
||||
'height': '100%',
|
||||
}}
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<g class="style-scope yt-icon">
|
||||
<path
|
||||
d="M17,4h-1H6.57C5.5,4,4.59,4.67,4.38,5.61l-1.34,6C2.77,12.85,3.82,14,5.23,14h4.23l-1.52,4.94C7.62,19.97,8.46,21,9.62,21 c0.58,0,1.14-0.24,1.52-0.65L17,14h4V4H17z M10.4,19.67C10.21,19.88,9.92,20,9.62,20c-0.26,0-0.5-0.11-0.63-0.3 c-0.07-0.1-0.15-0.26-0.09-0.47l1.52-4.94l0.4-1.29H9.46H5.23c-0.41,0-0.8-0.17-1.03-0.46c-0.12-0.15-0.25-0.4-0.18-0.72l1.34-6 C5.46,5.35,5.97,5,6.57,5H16v8.61L10.4,19.67z M20,13h-3V5h3V13z"
|
||||
class="style-scope yt-icon"
|
||||
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>
|
||||
</svg>
|
||||
@ -92,8 +92,8 @@ export const UnDislikeButton = (props: UnDislikeButtonProps) => (
|
||||
}}
|
||||
>
|
||||
<div
|
||||
class="yt-spec-touch-feedback-shape yt-spec-touch-feedback-shape--touch-response"
|
||||
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__fill" />
|
||||
|
||||
@ -6,22 +6,23 @@ export interface UnLikeButtonProps {
|
||||
export const UnLikeButton = (props: UnLikeButtonProps) => (
|
||||
<div class="style-scope">
|
||||
<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-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)}
|
||||
>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
class="yt-spec-button-shape-next__icon"
|
||||
style={{
|
||||
'color': 'var(--ytmusic-setting-item-toggle-active)',
|
||||
}}
|
||||
aria-hidden="true"
|
||||
>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
class="yt-spec-button-shape-next__icon"
|
||||
style={{
|
||||
'color': 'white',
|
||||
@ -32,7 +33,6 @@ export const UnLikeButton = (props: UnLikeButtonProps) => (
|
||||
'z-index': 1,
|
||||
'position': 'absolute',
|
||||
}}
|
||||
aria-hidden="true"
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
@ -41,20 +41,20 @@ export const UnLikeButton = (props: UnLikeButtonProps) => (
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
preserveAspectRatio="xMidYMid meet"
|
||||
class="style-scope yt-icon"
|
||||
preserveAspectRatio="xMidYMid meet"
|
||||
style={{
|
||||
'pointer-events': 'none',
|
||||
'display': 'block',
|
||||
'width': '100%',
|
||||
'height': '100%',
|
||||
}}
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<g class="style-scope yt-icon">
|
||||
<path
|
||||
d="M18.77,11h-4.23l1.52-4.94C16.38,5.03,15.54,4,14.38,4c-0.58,0-1.14,0.24-1.52,0.65L7,11H3v10h4h1h9.43 c1.06,0,1.98-0.67,2.19-1.61l1.34-6C21.23,12.15,20.18,11,18.77,11z M7,20H4v-8h3V20z M19.98,13.17l-1.34,6 C18.54,19.65,18.03,20,17.43,20H8v-8.61l5.6-6.06C13.79,5.12,14.08,5,14.38,5c0.26,0,0.5,0.11,0.63,0.3 c0.07,0.1,0.15,0.26,0.09,0.47l-1.52,4.94L13.18,12h1.35h4.23c0.41,0,0.8,0.17,1.03,0.46C19.92,12.61,20.05,12.86,19.98,13.17z"
|
||||
class="style-scope yt-icon"
|
||||
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>
|
||||
</svg>
|
||||
@ -67,20 +67,20 @@ export const UnLikeButton = (props: UnLikeButtonProps) => (
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
preserveAspectRatio="xMidYMid meet"
|
||||
class="style-scope yt-icon"
|
||||
preserveAspectRatio="xMidYMid meet"
|
||||
style={{
|
||||
'pointer-events': 'none',
|
||||
'display': 'block',
|
||||
'width': '100%',
|
||||
'height': '100%',
|
||||
}}
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<g class="style-scope yt-icon">
|
||||
<path
|
||||
d="M18.77,11h-4.23l1.52-4.94C16.38,5.03,15.54,4,14.38,4c-0.58,0-1.14,0.24-1.52,0.65L7,11H3v10h4h1h9.43 c1.06,0,1.98-0.67,2.19-1.61l1.34-6C21.23,12.15,20.18,11,18.77,11z M7,20H4v-8h3V20z M19.98,13.17l-1.34,6 C18.54,19.65,18.03,20,17.43,20H8v-8.61l5.6-6.06C13.79,5.12,14.08,5,14.38,5c0.26,0,0.5,0.11,0.63,0.3 c0.07,0.1,0.15,0.26,0.09,0.47l-1.52,4.94L13.18,12h1.35h4.23c0.41,0,0.8,0.17,1.03,0.46C19.92,12.61,20.05,12.86,19.98,13.17z"
|
||||
class="style-scope yt-icon"
|
||||
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>
|
||||
</svg>
|
||||
@ -92,8 +92,8 @@ export const UnLikeButton = (props: UnLikeButtonProps) => (
|
||||
}}
|
||||
>
|
||||
<div
|
||||
class="yt-spec-touch-feedback-shape yt-spec-touch-feedback-shape--touch-response"
|
||||
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__fill" />
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { FastAverageColor } from 'fast-average-color';
|
||||
import Color, { ColorInstance } from 'color';
|
||||
import Color, { type ColorInstance } from 'color';
|
||||
|
||||
import style from './style.css?inline';
|
||||
|
||||
|
||||
@ -3,7 +3,7 @@ import style from './style.css?inline';
|
||||
import { t } from '@/i18n';
|
||||
import { createPlugin } from '@/utils';
|
||||
import { menu } from './menu';
|
||||
import { AmbientModePluginConfig } from './types';
|
||||
import { type AmbientModePluginConfig } from './types';
|
||||
import { waitForElement } from '@/utils/wait-for-element';
|
||||
|
||||
const defaultConfig: AmbientModePluginConfig = {
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import { MenuItemConstructorOptions } from 'electron';
|
||||
import { type MenuItemConstructorOptions } from 'electron';
|
||||
|
||||
import { t } from '@/i18n';
|
||||
import { MenuContext } from '@/types/contexts';
|
||||
import { AmbientModePluginConfig } from './types';
|
||||
import { type MenuContext } from '@/types/contexts';
|
||||
import { type AmbientModePluginConfig } from './types';
|
||||
|
||||
export interface menuParameters {
|
||||
getConfig: () => AmbientModePluginConfig | Promise<AmbientModePluginConfig>;
|
||||
|
||||
@ -4,7 +4,7 @@ import { type Context, Hono } from 'hono';
|
||||
import { cors } from 'hono/cors';
|
||||
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 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 { swaggerUI } from '@hono/swagger-ui';
|
||||
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 { JWTPayloadSchema } from './scheme';
|
||||
import { registerAuth, registerControl } from './routes';
|
||||
import { registerAuth, registerControl, registerWebsocket } from './routes';
|
||||
|
||||
import { type APIServerConfig, AuthStrategy } from '../config';
|
||||
|
||||
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>({
|
||||
async start(ctx) {
|
||||
@ -25,8 +30,10 @@ export const backend = createBackend<BackendType, APIServerConfig>({
|
||||
});
|
||||
|
||||
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-repeat-changed-listener');
|
||||
ctx.ipc.send('ytmd:setup-like-changed-listener');
|
||||
ctx.ipc.send('ytmd:setup-volume-changed-listener');
|
||||
});
|
||||
|
||||
@ -37,7 +44,7 @@ export const backend = createBackend<BackendType, APIServerConfig>({
|
||||
|
||||
ctx.ipc.on(
|
||||
'ytmd:volume-changed',
|
||||
(newVolume: number) => (this.volume = newVolume),
|
||||
(newVolumeState: VolumeState) => (this.volumeState = newVolumeState),
|
||||
);
|
||||
|
||||
this.run(config.hostname, config.port);
|
||||
@ -62,6 +69,9 @@ export const backend = createBackend<BackendType, APIServerConfig>({
|
||||
// Custom
|
||||
init(backendCtx) {
|
||||
this.app = new Hono();
|
||||
const ws = createNodeWebSocket({
|
||||
app: this.app,
|
||||
});
|
||||
|
||||
this.app.use('*', cors());
|
||||
|
||||
@ -103,9 +113,14 @@ export const backend = createBackend<BackendType, APIServerConfig>({
|
||||
backendCtx,
|
||||
() => this.songInfo,
|
||||
() => this.currentRepeatMode,
|
||||
() => this.volume,
|
||||
() =>
|
||||
backendCtx.window.webContents.executeJavaScript(
|
||||
'document.querySelector("#like-button-renderer")?.likeStatus',
|
||||
) as Promise<LikeType>,
|
||||
() => this.volumeState,
|
||||
);
|
||||
registerAuth(this.app, backendCtx);
|
||||
registerWebsocket(this.app, ws);
|
||||
|
||||
// swagger
|
||||
this.app.openAPIRegistry.registerComponent(
|
||||
@ -133,6 +148,8 @@ export const backend = createBackend<BackendType, APIServerConfig>({
|
||||
});
|
||||
|
||||
this.app.get('/swagger', swaggerUI({ url: '/doc' }));
|
||||
|
||||
this.injectWebSocket = ws.injectWebSocket.bind(this);
|
||||
},
|
||||
run(hostname, port) {
|
||||
if (!this.app) return;
|
||||
@ -143,6 +160,10 @@ export const backend = createBackend<BackendType, APIServerConfig>({
|
||||
port,
|
||||
hostname,
|
||||
});
|
||||
|
||||
if (this.injectWebSocket && this.server) {
|
||||
this.injectWebSocket(this.server);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
|
||||
@ -1,8 +1,12 @@
|
||||
import { createRoute, z } from '@hono/zod-openapi';
|
||||
|
||||
import { ipcMain } from 'electron';
|
||||
|
||||
import getSongControls from '@/providers/song-controls';
|
||||
import {
|
||||
LikeType,
|
||||
type RepeatMode,
|
||||
type VolumeState,
|
||||
} from '@/types/datahost-get-state';
|
||||
|
||||
import {
|
||||
AddSongToQueueSchema,
|
||||
@ -19,8 +23,8 @@ import {
|
||||
SwitchRepeatSchema,
|
||||
type ResponseSongInfo,
|
||||
} 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 { BackendContext } from '@/types/contexts';
|
||||
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 { Context } from 'hono';
|
||||
|
||||
const API_VERSION = 'v1';
|
||||
|
||||
const routes = {
|
||||
previous: createRoute({
|
||||
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({
|
||||
method: 'post',
|
||||
path: `/api/${API_VERSION}/like`,
|
||||
@ -274,6 +294,7 @@ const routes = {
|
||||
'application/json': {
|
||||
schema: z.object({
|
||||
state: z.number(),
|
||||
isMuted: z.boolean(),
|
||||
}),
|
||||
},
|
||||
},
|
||||
@ -526,12 +547,15 @@ const routes = {
|
||||
}),
|
||||
};
|
||||
|
||||
type PromiseOrValue<T> = T | Promise<T>;
|
||||
|
||||
export const register = (
|
||||
app: HonoApp,
|
||||
{ window }: BackendContext<APIServerConfig>,
|
||||
songInfoGetter: () => SongInfo | undefined,
|
||||
repeatModeGetter: () => RepeatMode | undefined,
|
||||
volumeGetter: () => number | undefined,
|
||||
songInfoGetter: () => PromiseOrValue<SongInfo | undefined>,
|
||||
repeatModeGetter: () => PromiseOrValue<RepeatMode | undefined>,
|
||||
likeTypeGetter: () => PromiseOrValue<LikeType | undefined>,
|
||||
volumeStateGetter: () => PromiseOrValue<VolumeState | undefined>,
|
||||
) => {
|
||||
const controller = getSongControls(window);
|
||||
|
||||
@ -565,6 +589,10 @@ export const register = (
|
||||
ctx.status(204);
|
||||
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) => {
|
||||
controller.like();
|
||||
|
||||
@ -624,9 +652,9 @@ export const register = (
|
||||
return ctx.body(null);
|
||||
});
|
||||
|
||||
app.openapi(routes.repeatMode, (ctx) => {
|
||||
app.openapi(routes.repeatMode, async (ctx) => {
|
||||
ctx.status(200);
|
||||
return ctx.json({ mode: repeatModeGetter() ?? null });
|
||||
return ctx.json({ mode: (await repeatModeGetter()) ?? null });
|
||||
});
|
||||
app.openapi(routes.switchRepeat, (ctx) => {
|
||||
const { iteration } = ctx.req.valid('json');
|
||||
@ -642,9 +670,11 @@ export const register = (
|
||||
ctx.status(204);
|
||||
return ctx.body(null);
|
||||
});
|
||||
app.openapi(routes.getVolumeState, (ctx) => {
|
||||
app.openapi(routes.getVolumeState, async (ctx) => {
|
||||
ctx.status(200);
|
||||
return ctx.json({ state: volumeGetter() ?? 0 });
|
||||
return ctx.json(
|
||||
(await volumeStateGetter()) ?? { state: 0, isMuted: false },
|
||||
);
|
||||
});
|
||||
app.openapi(routes.setFullscreen, (ctx) => {
|
||||
const { state } = ctx.req.valid('json');
|
||||
@ -678,8 +708,8 @@ export const register = (
|
||||
return ctx.json({ state: fullscreen });
|
||||
});
|
||||
|
||||
const songInfo = (ctx: Context) => {
|
||||
const info = songInfoGetter();
|
||||
const songInfo = async (ctx: Context) => {
|
||||
const info = await songInfoGetter();
|
||||
|
||||
if (!info) {
|
||||
ctx.status(204);
|
||||
|
||||
@ -1,2 +1,3 @@
|
||||
export { register as registerControl } from './control';
|
||||
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 { serve } from '@hono/node-server';
|
||||
import { type OpenAPIHono as Hono } from '@hono/zod-openapi';
|
||||
import { type serve } from '@hono/node-server';
|
||||
|
||||
import type { RepeatMode, VolumeState } from '@/types/datahost-get-state';
|
||||
import type { BackendContext } from '@/types/contexts';
|
||||
import type { SongInfo } from '@/providers/song-info';
|
||||
import type { RepeatMode } from '@/types/datahost-get-state';
|
||||
import type { APIServerConfig } from '../config';
|
||||
|
||||
export type HonoApp = Hono;
|
||||
@ -13,7 +13,8 @@ export type BackendType = {
|
||||
oldConfig?: APIServerConfig;
|
||||
songInfo?: SongInfo;
|
||||
currentRepeatMode?: RepeatMode;
|
||||
volume?: number;
|
||||
volumeState?: VolumeState;
|
||||
injectWebSocket?: (server: ReturnType<typeof serve>) => void;
|
||||
|
||||
init: (ctx: BackendContext<APIServerConfig>) => void;
|
||||
run: (hostname: string, port: number) => void;
|
||||
|
||||
@ -1,26 +1,133 @@
|
||||
import { createPlugin } from '@/utils';
|
||||
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({
|
||||
name: () => t('plugins.audio-compressor.name'),
|
||||
description: () => t('plugins.audio-compressor.description'),
|
||||
|
||||
renderer() {
|
||||
document.addEventListener(
|
||||
'ytmd:audio-can-play',
|
||||
({ detail: { audioSource, audioContext } }) => {
|
||||
const compressor = audioContext.createDynamicsCompressor();
|
||||
renderer: {
|
||||
onPlayerApiReady(playerApi) {
|
||||
ensureAudioContextLoad(playerApi);
|
||||
},
|
||||
|
||||
compressor.threshold.value = -50;
|
||||
compressor.ratio.value = 12;
|
||||
compressor.knee.value = 40;
|
||||
compressor.attack.value = 0;
|
||||
compressor.release.value = 0.25;
|
||||
start() {
|
||||
document.addEventListener('ytmd:audio-can-play', audioCanPlayHandler, {
|
||||
passive: true,
|
||||
});
|
||||
storage.connectToCompressor(
|
||||
storage.lastSource,
|
||||
storage.lastContext,
|
||||
storage.lastCompressor,
|
||||
);
|
||||
},
|
||||
|
||||
audioSource.connect(compressor);
|
||||
compressor.connect(audioContext.destination);
|
||||
},
|
||||
{ once: true, passive: true },
|
||||
);
|
||||
stop() {
|
||||
document.removeEventListener('ytmd:audio-can-play', audioCanPlayHandler);
|
||||
storage.disconnectCompressor();
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@ -1,16 +1,16 @@
|
||||
import net from 'net';
|
||||
|
||||
import { SocksClient, SocksClientOptions } from 'socks';
|
||||
import { SocksClient, type SocksClientOptions } from 'socks';
|
||||
|
||||
import is from 'electron-is';
|
||||
|
||||
import { createBackend, LoggerPrefix } from '@/utils';
|
||||
|
||||
import { BackendType } from './types';
|
||||
import { type BackendType } from './types';
|
||||
|
||||
import config from '@/config';
|
||||
|
||||
import { AuthProxyConfig, defaultAuthProxyConfig } from '../config';
|
||||
import { type AuthProxyConfig, defaultAuthProxyConfig } from '../config';
|
||||
|
||||
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 { Server } from 'http';
|
||||
|
||||
|
||||
@ -2,7 +2,10 @@ import { createPlugin } from '@/utils';
|
||||
import { t } from '@/i18n';
|
||||
|
||||
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';
|
||||
|
||||
|
||||
@ -9,10 +9,10 @@ export const CaptionsSettingButton = (props: CaptionsSettingsButtonProps) => (
|
||||
aria-label={props.label}
|
||||
class="player-captions-button style-scope ytmusic-player-bar"
|
||||
icon={'yt-icons:subtitles'}
|
||||
on:click={(e) => props.onClick(e)}
|
||||
role={'button'}
|
||||
tabindex={0}
|
||||
title={props.label}
|
||||
on:click={(e) => props.onClick(e)}
|
||||
>
|
||||
<span class="yt-icon-shape style-scope yt-icon yt-spec-icon-shape">
|
||||
<div
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
import { Innertube } from 'youtubei.js';
|
||||
|
||||
import { BrowserWindow } from 'electron';
|
||||
import prompt from 'custom-electron-prompt';
|
||||
|
||||
import { Howl } from 'howler';
|
||||
@ -12,6 +11,7 @@ import { VolumeFader } from './fader';
|
||||
|
||||
import { t } from '@/i18n';
|
||||
|
||||
import type { BrowserWindow } from 'electron';
|
||||
import type { RendererContext } from '@/types/contexts';
|
||||
|
||||
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
|
||||
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 = {
|
||||
type: ActivityType.Listening,
|
||||
statusDisplayType: config.statusDisplayType,
|
||||
details: truncateString(songInfo.title, 128), // Song title
|
||||
detailsUrl: songInfo.url ?? undefined,
|
||||
state: truncateString(songInfo.artist, 128), // Artist name
|
||||
stateUrl: songInfo.artistUrl,
|
||||
largeImageKey: songInfo.imageSrc ?? undefined,
|
||||
largeImageText: songInfo.album
|
||||
? truncateString(songInfo.album, 128)
|
||||
|
||||
@ -2,6 +2,7 @@ import { createPlugin } from '@/utils';
|
||||
import { backend } from './main';
|
||||
import { onMenu } from './menu';
|
||||
import { t } from '@/i18n';
|
||||
import { DiscordStatusDisplayType } from './constants';
|
||||
|
||||
export type DiscordPluginConfig = {
|
||||
enabled: boolean;
|
||||
@ -33,6 +34,10 @@ export type DiscordPluginConfig = {
|
||||
* Hide the "duration left" in the rich presence
|
||||
*/
|
||||
hideDurationLeft: boolean;
|
||||
/**
|
||||
* Controls which field is displayed in the Discord status text
|
||||
*/
|
||||
statusDisplayType: (typeof DiscordStatusDisplayType)[keyof typeof DiscordStatusDisplayType];
|
||||
};
|
||||
|
||||
export default createPlugin({
|
||||
@ -47,6 +52,7 @@ export default createPlugin({
|
||||
playOnYouTubeMusic: true,
|
||||
hideGitHubButton: false,
|
||||
hideDurationLeft: false,
|
||||
statusDisplayType: DiscordStatusDisplayType.ARTIST,
|
||||
} as DiscordPluginConfig,
|
||||
menu: onMenu,
|
||||
backend,
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { app } from 'electron';
|
||||
|
||||
import registerCallback, { SongInfoEvent } from '@/providers/song-info';
|
||||
import { registerCallback, SongInfoEvent } from '@/providers/song-info';
|
||||
import { createBackend } from '@/utils';
|
||||
|
||||
import { DiscordService } from './discord-service';
|
||||
|
||||
@ -8,6 +8,8 @@ import { setMenuOptions } from '@/config/plugins';
|
||||
|
||||
import { t } from '@/i18n';
|
||||
|
||||
import { DiscordStatusDisplayType } from './constants';
|
||||
|
||||
import type { MenuContext } from '@/types/contexts';
|
||||
import type { DiscordPluginConfig } from './index';
|
||||
|
||||
@ -17,6 +19,15 @@ const registerRefreshOnce = singleton((refreshMenu: () => void) => {
|
||||
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 ({
|
||||
window,
|
||||
getConfig,
|
||||
@ -92,6 +103,21 @@ export const onMenu = async ({
|
||||
label: t('plugins.discord.menu.set-inactivity-timeout'),
|
||||
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.
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { DefaultPresetList, Preset } from './types';
|
||||
import { DefaultPresetList, type Preset } from './types';
|
||||
|
||||
import style from './style.css?inline';
|
||||
|
||||
|
||||
@ -2,12 +2,12 @@ import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
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 is from 'electron-is';
|
||||
import filenamify from 'filenamify';
|
||||
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 { lazy } from 'lazy-var';
|
||||
|
||||
@ -17,7 +17,8 @@ import {
|
||||
sendFeedback as sendFeedback_,
|
||||
setBadge,
|
||||
} from './utils';
|
||||
import registerCallback, {
|
||||
import {
|
||||
registerCallback,
|
||||
cleanupName,
|
||||
getImage,
|
||||
MediaType,
|
||||
@ -590,7 +591,7 @@ async function writeID3(
|
||||
tags.image = {
|
||||
mime: 'image/png',
|
||||
type: {
|
||||
id: TagConstants.AttachedPicture.PictureType.FRONT_COVER,
|
||||
id: NodeID3.TagConstants.AttachedPicture.PictureType.FRONT_COVER,
|
||||
},
|
||||
description: 'thumbnail',
|
||||
imageBuffer: coverBuffer,
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { app, BrowserWindow } from 'electron';
|
||||
import { app, type BrowserWindow } from 'electron';
|
||||
import is from 'electron-is';
|
||||
|
||||
export const getFolder = (customFolder?: string) =>
|
||||
|
||||
@ -5,8 +5,8 @@ export const DownloadButton = (props: {
|
||||
<a
|
||||
class="yt-simple-endpoint style-scope ytmusic-menu-navigation-item-renderer"
|
||||
id="navigation-endpoint"
|
||||
tabindex={-1}
|
||||
onClick={props.onClick}
|
||||
tabindex={-1}
|
||||
>
|
||||
<div class="icon ytmd-menu-item style-scope ytmusic-menu-navigation-item-renderer">
|
||||
<svg
|
||||
|
||||
@ -1,8 +1,15 @@
|
||||
import { createPlugin } from '@/utils';
|
||||
import { t } from '@/i18n';
|
||||
import { MenuContext } from '@/types/contexts';
|
||||
import { MenuTemplate } from '@/menu';
|
||||
import { defaultPresets, presetConfigs, Preset, FilterConfig } from './presets';
|
||||
|
||||
import {
|
||||
defaultPresets,
|
||||
presetConfigs,
|
||||
type Preset,
|
||||
type FilterConfig,
|
||||
} from './presets';
|
||||
|
||||
import type { MenuContext } from '@/types/contexts';
|
||||
import type { MenuTemplate } from '@/menu';
|
||||
|
||||
export type EqualizerPluginConfig = {
|
||||
enabled: boolean;
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
import { createPlugin } from '@/utils';
|
||||
import { t } from '@/i18n';
|
||||
|
||||
import type { YoutubePlayer } from '@/types/youtube-player';
|
||||
|
||||
export default createPlugin({
|
||||
name: () => t('plugins.exponential-volume.name'),
|
||||
description: () => t('plugins.exponential-volume.description'),
|
||||
@ -9,7 +11,16 @@ export default createPlugin({
|
||||
enabled: false,
|
||||
},
|
||||
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
|
||||
// https://greasyfork.org/en/scripts/397686-youtube-music-fix-volume-ratio/
|
||||
|
||||
@ -48,6 +59,7 @@ export default createPlugin({
|
||||
propertyDescriptor?.set?.call(this, lowVolume);
|
||||
},
|
||||
});
|
||||
syncVolume(playerApi);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@ -3,10 +3,10 @@ import { register } from 'electron-localshortcut';
|
||||
import {
|
||||
BrowserWindow,
|
||||
Menu,
|
||||
MenuItem,
|
||||
type MenuItem,
|
||||
ipcMain,
|
||||
nativeImage,
|
||||
WebContents,
|
||||
type WebContents,
|
||||
} from 'electron';
|
||||
|
||||
import type { BackendContext } from '@/types/contexts';
|
||||
|
||||
@ -2,7 +2,7 @@ import { createSignal } from 'solid-js';
|
||||
import { render } from 'solid-js/web';
|
||||
|
||||
import { TitleBar } from './renderer/TitleBar';
|
||||
import { defaultInAppMenuConfig, InAppMenuConfig } from './constants';
|
||||
import { defaultInAppMenuConfig, type InAppMenuConfig } from './constants';
|
||||
|
||||
import type { RendererContext } from '@/types/contexts';
|
||||
|
||||
@ -33,12 +33,12 @@ export const onRendererLoad = async ({
|
||||
render(
|
||||
() => (
|
||||
<TitleBar
|
||||
ipc={ipc}
|
||||
isMacOS={isMacOS}
|
||||
enableController={
|
||||
isNotWindowsOrMacOS && !config().hideDOMWindowControls
|
||||
}
|
||||
initialCollapsed={window.mainConfig.get('options.hideMenu')}
|
||||
ipc={ipc}
|
||||
isMacOS={isMacOS}
|
||||
/>
|
||||
),
|
||||
document.body,
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { JSX } from 'solid-js';
|
||||
import { type JSX } from 'solid-js';
|
||||
import { css } from 'solid-styled-components';
|
||||
|
||||
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 { 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 { css } from 'solid-styled-components';
|
||||
import { Transition } from 'solid-transition-group';
|
||||
@ -6,7 +6,7 @@ import {
|
||||
autoUpdate,
|
||||
flip,
|
||||
offset,
|
||||
OffsetOptions,
|
||||
type OffsetOptions,
|
||||
size,
|
||||
} from '@floating-ui/dom';
|
||||
import { useFloating } from 'solid-floating-ui';
|
||||
@ -149,17 +149,17 @@ export const Panel = (props: PanelProps) => {
|
||||
<Portal>
|
||||
<Transition
|
||||
appear
|
||||
enterClass={animationStyle().enter}
|
||||
enterActiveClass={animationStyle().enterActive}
|
||||
exitToClass={animationStyle().exitTo}
|
||||
enterClass={animationStyle().enter}
|
||||
exitActiveClass={animationStyle().exitActive}
|
||||
exitToClass={animationStyle().exitTo}
|
||||
>
|
||||
<Show when={local.open}>
|
||||
<ul
|
||||
{...leftProps}
|
||||
class={panelStyle()}
|
||||
data-ytmd-sub-panel={true}
|
||||
ref={setPanel}
|
||||
class={panelStyle()}
|
||||
style={{
|
||||
'--offset-x': `${position.x}px`,
|
||||
'--offset-y': `${position.y}px`,
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
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 { Portal } from 'solid-js/web';
|
||||
|
||||
@ -290,80 +290,80 @@ export const PanelItem = (props: PanelItemProps) => {
|
||||
|
||||
return (
|
||||
<li
|
||||
ref={setAnchor}
|
||||
class={itemStyle()}
|
||||
onMouseEnter={handleHover}
|
||||
onClick={handleClick}
|
||||
data-selected={open()}
|
||||
onClick={handleClick}
|
||||
onMouseEnter={handleHover}
|
||||
ref={setAnchor}
|
||||
>
|
||||
<Switch fallback={<div class={itemIconStyle()} />}>
|
||||
<Match when={props.type === 'checkbox' && props.checked}>
|
||||
<svg
|
||||
class={itemIconStyle()}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="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" />
|
||||
</svg>
|
||||
</Match>
|
||||
<Match when={props.type === 'radio' && props.checked}>
|
||||
<svg
|
||||
class={itemIconStyle()}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
style={{ padding: '6px' }}
|
||||
viewBox="0 0 20 20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<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"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</Match>
|
||||
<Match when={props.type === 'radio' && !props.checked}>
|
||||
<svg
|
||||
class={itemIconStyle()}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
style={{ padding: '6px' }}
|
||||
viewBox="0 0 20 20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<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"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</Match>
|
||||
</Switch>
|
||||
<span class={itemLabelStyle()}>{props.name}</span>
|
||||
<Show when={props.chip} fallback={<div />}>
|
||||
<Show fallback={<div />} when={props.chip}>
|
||||
<span class={itemChipStyle()}>{props.chip}</span>
|
||||
</Show>
|
||||
<Show when={props.type === 'submenu'}>
|
||||
<svg
|
||||
class={itemIconStyle()}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="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" />
|
||||
</svg>
|
||||
<Panel
|
||||
ref={setChild}
|
||||
open={open()}
|
||||
anchor={anchor()}
|
||||
placement={'right-start'}
|
||||
data-level={props.type === 'submenu' && props.level.join('/')}
|
||||
offset={{ mainAxis: 8 }}
|
||||
open={open()}
|
||||
placement={'right-start'}
|
||||
ref={setChild}
|
||||
>
|
||||
{props.type === 'submenu' && props.children}
|
||||
</Panel>
|
||||
@ -371,8 +371,8 @@ export const PanelItem = (props: PanelItemProps) => {
|
||||
<Show when={props.toolTip}>
|
||||
<Portal>
|
||||
<div
|
||||
ref={setToolTip}
|
||||
class={popupStyle()}
|
||||
ref={setToolTip}
|
||||
style={{
|
||||
'--offset-x': `${position.x}px`,
|
||||
'--offset-y': `${position.y}px`,
|
||||
@ -380,10 +380,10 @@ export const PanelItem = (props: PanelItemProps) => {
|
||||
>
|
||||
<Transition
|
||||
appear
|
||||
enterClass={animationStyle().enter}
|
||||
enterActiveClass={animationStyle().enterActive}
|
||||
exitToClass={animationStyle().exitTo}
|
||||
enterClass={animationStyle().enter}
|
||||
exitActiveClass={animationStyle().exitActive}
|
||||
exitToClass={animationStyle().exitTo}
|
||||
>
|
||||
<Show when={toolTipOpen()}>
|
||||
<div class={toolTipStyle()}>{props.toolTip}</div>
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { Menu, MenuItem } from 'electron';
|
||||
import { type Menu, type MenuItem } from 'electron';
|
||||
import {
|
||||
createEffect,
|
||||
createResource,
|
||||
@ -120,22 +120,22 @@ const PanelRenderer = (props: PanelRendererProps) => {
|
||||
<Switch>
|
||||
<Match when={subItem().type === 'normal'}>
|
||||
<PanelItem
|
||||
type={'normal'}
|
||||
name={subItem().label}
|
||||
chip={subItem().sublabel}
|
||||
toolTip={subItem().toolTip}
|
||||
commandId={subItem().commandId}
|
||||
name={subItem().label}
|
||||
onClick={() => props.onClick?.(subItem().commandId)}
|
||||
toolTip={subItem().toolTip}
|
||||
type={'normal'}
|
||||
/>
|
||||
</Match>
|
||||
<Match when={subItem().type === 'submenu'}>
|
||||
<PanelItem
|
||||
type={'submenu'}
|
||||
name={subItem().label}
|
||||
chip={subItem().sublabel}
|
||||
toolTip={subItem().toolTip}
|
||||
level={[...(props.level ?? []), subItem().commandId]}
|
||||
commandId={subItem().commandId}
|
||||
level={[...(props.level ?? []), subItem().commandId]}
|
||||
name={subItem().label}
|
||||
toolTip={subItem().toolTip}
|
||||
type={'submenu'}
|
||||
>
|
||||
<PanelRenderer
|
||||
items={subItem().submenu?.items ?? []}
|
||||
@ -146,26 +146,26 @@ const PanelRenderer = (props: PanelRendererProps) => {
|
||||
</Match>
|
||||
<Match when={subItem().type === 'checkbox'}>
|
||||
<PanelItem
|
||||
type={'checkbox'}
|
||||
name={subItem().label}
|
||||
checked={subItem().checked}
|
||||
chip={subItem().sublabel}
|
||||
toolTip={subItem().toolTip}
|
||||
commandId={subItem().commandId}
|
||||
name={subItem().label}
|
||||
onChange={() => props.onClick?.(subItem().commandId)}
|
||||
toolTip={subItem().toolTip}
|
||||
type={'checkbox'}
|
||||
/>
|
||||
</Match>
|
||||
<Match when={subItem().type === 'radio'}>
|
||||
<PanelItem
|
||||
type={'radio'}
|
||||
name={subItem().label}
|
||||
checked={subItem().checked}
|
||||
chip={subItem().sublabel}
|
||||
toolTip={subItem().toolTip}
|
||||
commandId={subItem().commandId}
|
||||
name={subItem().label}
|
||||
onChange={() =>
|
||||
props.onClick?.(subItem().commandId, radioGroup())
|
||||
}
|
||||
toolTip={subItem().toolTip}
|
||||
type={'radio'}
|
||||
/>
|
||||
</Match>
|
||||
<Match when={subItem().type === 'separator'}>
|
||||
@ -325,10 +325,10 @@ export const TitleBar = (props: TitleBarProps) => {
|
||||
|
||||
return (
|
||||
<nav
|
||||
data-ytmd-main-panel={true}
|
||||
class={titleStyle()}
|
||||
data-macos={props.isMacOS}
|
||||
data-show={mouseY() < 32}
|
||||
data-ytmd-main-panel={true}
|
||||
>
|
||||
<IconButton
|
||||
onClick={() => setCollapsed(!collapsed())}
|
||||
@ -336,7 +336,7 @@ export const TitleBar = (props: TitleBarProps) => {
|
||||
'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
|
||||
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"
|
||||
@ -344,26 +344,29 @@ export const TitleBar = (props: TitleBarProps) => {
|
||||
</svg>
|
||||
</IconButton>
|
||||
<TransitionGroup
|
||||
enterClass={
|
||||
ignoreTransition()
|
||||
? animationStyle().fakeTarget
|
||||
: animationStyle().enter
|
||||
}
|
||||
enterActiveClass={
|
||||
ignoreTransition()
|
||||
? animationStyle().fake
|
||||
: animationStyle().enterActive
|
||||
}
|
||||
exitToClass={
|
||||
enterClass={
|
||||
ignoreTransition()
|
||||
? animationStyle().fakeTarget
|
||||
: animationStyle().exitTo
|
||||
: animationStyle().enter
|
||||
}
|
||||
exitActiveClass={
|
||||
ignoreTransition()
|
||||
? animationStyle().fake
|
||||
: animationStyle().exitActive
|
||||
}
|
||||
exitToClass={
|
||||
ignoreTransition()
|
||||
? animationStyle().fakeTarget
|
||||
: animationStyle().exitTo
|
||||
}
|
||||
onAfterEnter={(element) => {
|
||||
(element as HTMLElement).style.removeProperty('transition-delay');
|
||||
}}
|
||||
onBeforeEnter={(element) => {
|
||||
if (ignoreTransition()) return;
|
||||
const index = Number(element.getAttribute('data-index') ?? 0);
|
||||
@ -373,9 +376,6 @@ export const TitleBar = (props: TitleBarProps) => {
|
||||
`${index * 0.025}s`,
|
||||
);
|
||||
}}
|
||||
onAfterEnter={(element) => {
|
||||
(element as HTMLElement).style.removeProperty('transition-delay');
|
||||
}}
|
||||
onBeforeExit={(element) => {
|
||||
if (ignoreTransition()) return;
|
||||
const index = Number(element.getAttribute('data-index') ?? 0);
|
||||
@ -405,18 +405,18 @@ export const TitleBar = (props: TitleBarProps) => {
|
||||
return (
|
||||
<>
|
||||
<MenuButton
|
||||
ref={setAnchor}
|
||||
text={item().label}
|
||||
onClick={handleClick}
|
||||
selected={openTarget() === anchor()}
|
||||
data-index={index}
|
||||
data-length={data()?.items.length}
|
||||
onClick={handleClick}
|
||||
ref={setAnchor}
|
||||
selected={openTarget() === anchor()}
|
||||
text={item().label}
|
||||
/>
|
||||
<Panel
|
||||
open={openTarget() === anchor()}
|
||||
anchor={anchor()}
|
||||
placement={'bottom-start'}
|
||||
offset={{ mainAxis: 8 }}
|
||||
open={openTarget() === anchor()}
|
||||
placement={'bottom-start'}
|
||||
>
|
||||
<PanelRenderer
|
||||
items={item().submenu?.items ?? []}
|
||||
@ -433,9 +433,9 @@ export const TitleBar = (props: TitleBarProps) => {
|
||||
<div style={{ flex: 1 }} />
|
||||
<WindowController
|
||||
isMaximize={isMaximized()}
|
||||
onToggleMaximize={handleToggleMaximize}
|
||||
onMinimize={handleMinimize}
|
||||
onClose={handleClose}
|
||||
onMinimize={handleMinimize}
|
||||
onToggleMaximize={handleToggleMaximize}
|
||||
/>
|
||||
</Show>
|
||||
</nav>
|
||||
|
||||
@ -32,61 +32,61 @@ export const WindowController = (props: WindowControllerProps) => {
|
||||
<div class={containerStyle()}>
|
||||
<IconButton onClick={props.onMinimize}>
|
||||
<svg
|
||||
width={16}
|
||||
height={16}
|
||||
fill="none"
|
||||
height={16}
|
||||
viewBox="0 0 24 24"
|
||||
width={16}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M3.755 12.5h16.492a.75.75 0 0 0 0-1.5H3.755a.75.75 0 0 0 0 1.5Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</IconButton>
|
||||
<IconButton onClick={props.onToggleMaximize}>
|
||||
<Show
|
||||
when={props.isMaximize}
|
||||
fallback={
|
||||
<svg
|
||||
width={16}
|
||||
height={16}
|
||||
fill="none"
|
||||
height={16}
|
||||
viewBox="0 0 24 24"
|
||||
width={16}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<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"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
}
|
||||
when={props.isMaximize}
|
||||
>
|
||||
<svg
|
||||
width={16}
|
||||
height={16}
|
||||
fill="none"
|
||||
height={16}
|
||||
viewBox="0 0 24 24"
|
||||
width={16}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<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"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</Show>
|
||||
</IconButton>
|
||||
<IconButton onClick={props.onClose}>
|
||||
<svg
|
||||
width={16}
|
||||
height={16}
|
||||
fill="none"
|
||||
height={16}
|
||||
viewBox="0 0 24 24"
|
||||
width={16}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<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"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</IconButton>
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { net } from 'electron';
|
||||
|
||||
import { createPlugin } from '@/utils';
|
||||
import registerCallback from '@/providers/song-info';
|
||||
import { registerCallback } from '@/providers/song-info';
|
||||
import { t } from '@/i18n';
|
||||
|
||||
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 type { Permission, Profile, VideoData } from './types';
|
||||
|
||||
@ -1,10 +1,9 @@
|
||||
import prompt from 'custom-electron-prompt';
|
||||
|
||||
import { DataConnection } from 'peerjs';
|
||||
|
||||
import { t } from '@/i18n';
|
||||
import { createPlugin } from '@/utils';
|
||||
import promptOptions from '@/providers/prompt-options';
|
||||
import { waitForElement } from '@/utils/wait-for-element';
|
||||
|
||||
import {
|
||||
getDefaultProfile,
|
||||
@ -21,8 +20,7 @@ import { createSettingPopup } from './ui/setting';
|
||||
import settingHTML from './templates/setting.html?raw';
|
||||
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 { RendererContext } from '@/types/contexts';
|
||||
import type { VideoDataChanged } from '@/types/video-data-changed';
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import {
|
||||
import type {
|
||||
ItemPlaylistPanelVideoRenderer,
|
||||
PlaylistPanelVideoWrapperRenderer,
|
||||
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 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 getSongControls from '@/providers/song-controls';
|
||||
import registerCallback, {
|
||||
import {
|
||||
registerCallback,
|
||||
type SongInfo,
|
||||
SongInfoEvent,
|
||||
} from '@/providers/song-info';
|
||||
|
||||
@ -5,7 +5,8 @@ import is from 'electron-is';
|
||||
import { notificationImage } from './utils';
|
||||
import interactive from './interactive';
|
||||
|
||||
import registerCallback, {
|
||||
import {
|
||||
registerCallback,
|
||||
type SongInfo,
|
||||
SongInfoEvent,
|
||||
} from '@/providers/song-info';
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import is from 'electron-is';
|
||||
import { MenuItem } from 'electron';
|
||||
import { type MenuItem } from 'electron';
|
||||
|
||||
import { snakeToCamel, ToastStyles, urgencyLevels } from './utils';
|
||||
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
import path from 'node:path';
|
||||
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 { SongInfo } from '@/providers/song-info';
|
||||
import { type SongInfo } from '@/providers/song-info';
|
||||
|
||||
import type { NotificationsPluginConfig } from './index';
|
||||
|
||||
|
||||
@ -7,19 +7,19 @@ export const PictureInPictureButton = (props: PictureInPictureButtonProps) => (
|
||||
<a
|
||||
class="yt-simple-endpoint style-scope ytmusic-menu-navigation-item-renderer"
|
||||
id="navigation-endpoint"
|
||||
tabindex={-1}
|
||||
onClick={(e) => props.onClick?.(e)}
|
||||
tabindex={-1}
|
||||
>
|
||||
<div class="icon ytmd-menu-item style-scope ytmusic-menu-navigation-item-renderer">
|
||||
<svg
|
||||
class="style-scope yt-icon"
|
||||
id="Layer_1"
|
||||
style={{
|
||||
'pointer-events': 'none',
|
||||
'display': 'block',
|
||||
'width': '100%',
|
||||
'height': '100%',
|
||||
}}
|
||||
id="Layer_1"
|
||||
viewBox="0 0 512 512"
|
||||
x="0px"
|
||||
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_">
|
||||
<path
|
||||
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
|
||||
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"
|
||||
fill="#aaaaaa"
|
||||
id="XMLID_11_"
|
||||
/>
|
||||
</g>
|
||||
|
||||
@ -26,10 +26,10 @@ export const PlaybackSpeedSlider = (props: PlaybackSpeedSliderProps) => (
|
||||
aria-valuenow={props.speed}
|
||||
class="volume-slider style-scope ytmusic-player-bar on-hover"
|
||||
dir="ltr"
|
||||
on:immediate-value-changed={(e) => props.onImmediateValueChanged?.(e)}
|
||||
onWheel={(e) => props.onWheel?.(e)}
|
||||
max="2"
|
||||
min="0"
|
||||
on:immediate-value-changed={(e) => props.onImmediateValueChanged?.(e)}
|
||||
onWheel={(e) => props.onWheel?.(e)}
|
||||
role="slider"
|
||||
step="0.125"
|
||||
style={{ 'display': 'inherit !important' }}
|
||||
|
||||
@ -43,8 +43,6 @@ export const onPlayerApiReady = () => {
|
||||
render(
|
||||
() => (
|
||||
<PlaybackSpeedSlider
|
||||
speed={speed()}
|
||||
title={t('plugins.playback-speed.templates.button')}
|
||||
onImmediateValueChanged={(e) => {
|
||||
let targetSpeed = Number(e.detail.value ?? MIN_PLAYBACK_SPEED);
|
||||
|
||||
@ -78,6 +76,8 @@ export const onPlayerApiReady = () => {
|
||||
|
||||
updatePlayBackSpeed();
|
||||
}}
|
||||
speed={speed()}
|
||||
title={t('plugins.playback-speed.templates.button')}
|
||||
/>
|
||||
),
|
||||
sliderContainer,
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { globalShortcut, MenuItem } from 'electron';
|
||||
import prompt, { KeybindOptions } from 'custom-electron-prompt';
|
||||
import { globalShortcut, type MenuItem } from 'electron';
|
||||
import prompt, { type KeybindOptions } from 'custom-electron-prompt';
|
||||
|
||||
import hudStyle from './volume-hud.css?inline';
|
||||
import { createPlugin } from '@/utils';
|
||||
|
||||
@ -66,7 +66,7 @@ export const onPlayerApiReady = async (
|
||||
injectVolumeHud(noVid);
|
||||
if (!noVid) {
|
||||
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
|
||||
const videoMode = () =>
|
||||
api.getPlayerResponse().videoDetails?.musicVideoType !==
|
||||
|
||||
@ -9,10 +9,10 @@ export const QualitySettingButton = (props: QualitySettingButtonProps) => (
|
||||
aria-label={props.label}
|
||||
class="player-quality-button style-scope ytmusic-player"
|
||||
icon={'yt-icons:settings'}
|
||||
on:click={(e) => props.onClick(e)}
|
||||
role={'button'}
|
||||
tabindex={0}
|
||||
title={props.label}
|
||||
on:click={(e) => props.onClick(e)}
|
||||
>
|
||||
<span class="yt-icon-shape style-scope yt-icon yt-spec-icon-shape">
|
||||
<div
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { BrowserWindow } from 'electron';
|
||||
import { type BrowserWindow } from 'electron';
|
||||
|
||||
import registerCallback, {
|
||||
import {
|
||||
registerCallback,
|
||||
MediaType,
|
||||
type SongInfo,
|
||||
SongInfoEvent,
|
||||
|
||||
@ -1,12 +1,12 @@
|
||||
import prompt from 'custom-electron-prompt';
|
||||
|
||||
import { BrowserWindow } from 'electron';
|
||||
import { type BrowserWindow } from 'electron';
|
||||
|
||||
import { t } from '@/i18n';
|
||||
import promptOptions from '@/providers/prompt-options';
|
||||
|
||||
import { ScrobblerPluginConfig } from './index';
|
||||
import { SetConfType, backend } from './main';
|
||||
import { type ScrobblerPluginConfig } from './index';
|
||||
import { type SetConfType, backend } from './main';
|
||||
|
||||
import type { MenuContext } from '@/types/contexts';
|
||||
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 { 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';
|
||||
|
||||
|
||||
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' {
|
||||
import { EventEmitter } from 'events';
|
||||
|
||||
import { interface as dbusInterface } from '@jellybrick/dbus-next';
|
||||
import { type interface as dbusInterface } from '@jellybrick/dbus-next';
|
||||
|
||||
interface RootInterfaceOptions {
|
||||
identity?: string;
|
||||
|
||||
@ -1,20 +1,21 @@
|
||||
import { BrowserWindow, ipcMain } from 'electron';
|
||||
import { type BrowserWindow, ipcMain } from 'electron';
|
||||
|
||||
import MprisPlayer, {
|
||||
LOOP_STATUS_NONE,
|
||||
LOOP_STATUS_PLAYLIST,
|
||||
LOOP_STATUS_TRACK,
|
||||
LoopStatus,
|
||||
type LoopStatus,
|
||||
PLAYBACK_STATUS_PAUSED,
|
||||
PLAYBACK_STATUS_PLAYING,
|
||||
PLAYBACK_STATUS_STOPPED,
|
||||
type PlayBackStatus,
|
||||
type PlayerOptions,
|
||||
type Position,
|
||||
Track,
|
||||
type Track,
|
||||
} from '@jellybrick/mpris-service';
|
||||
|
||||
import registerCallback, {
|
||||
import {
|
||||
registerCallback,
|
||||
type SongInfo,
|
||||
SongInfoEvent,
|
||||
} from '@/providers/song-info';
|
||||
@ -22,7 +23,7 @@ import getSongControls from '@/providers/song-controls';
|
||||
import config from '@/config';
|
||||
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';
|
||||
|
||||
class YTPlayer extends MprisPlayer {
|
||||
@ -305,8 +306,10 @@ function registerMPRIS(win: BrowserWindow) {
|
||||
console.trace(error);
|
||||
});
|
||||
|
||||
ipcMain.on('ytmd:volume-changed', (_, newVol) => {
|
||||
player.volume = Number.parseFloat((newVol / 100).toFixed(2));
|
||||
ipcMain.on('ytmd:volume-changed', (_, newVolumeState: VolumeState) => {
|
||||
player.volume = newVolumeState.isMuted
|
||||
? 0
|
||||
: Number.parseFloat((newVolumeState.state / 100).toFixed(2));
|
||||
});
|
||||
|
||||
player.on('volume', async (newVolume: number) => {
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
// Segments are an array [ [start, end], … ]
|
||||
import { Segment } from './types';
|
||||
import type { Segment } from './types';
|
||||
|
||||
export const sortSegments = (segments: Segment[]) => {
|
||||
segments.sort((segment1, segment2) =>
|
||||
|
||||
@ -2,8 +2,6 @@ import { createStore } from 'solid-js/store';
|
||||
|
||||
import { createMemo } from 'solid-js';
|
||||
|
||||
import { SongInfo } from '@/providers/song-info';
|
||||
|
||||
import { LRCLib } from './LRCLib';
|
||||
import { LyricsGenius } from './LyricsGenius';
|
||||
import { MusixMatch } from './MusixMatch';
|
||||
@ -12,6 +10,7 @@ import { YTMusic } from './YTMusic';
|
||||
import { getSongInfo } from '@/providers/song-info-front';
|
||||
|
||||
import type { LyricProvider, LyricResult } from '../types';
|
||||
import type { SongInfo } from '@/providers/song-info';
|
||||
|
||||
export const providers = {
|
||||
YTMusic: new YTMusic(),
|
||||
|
||||
@ -45,7 +45,6 @@ export const ErrorDisplay = (props: ErrorDisplayProps) => {
|
||||
</pre>
|
||||
|
||||
<yt-button-renderer
|
||||
onClick={() => retrySearch(lyricsStore.provider, getSongInfo())}
|
||||
data={{
|
||||
icon: { iconType: 'REFRESH' },
|
||||
isDisabled: false,
|
||||
@ -54,6 +53,7 @@ export const ErrorDisplay = (props: ErrorDisplayProps) => {
|
||||
simpleText: t('plugins.synced-lyrics.refetch-btn.normal')
|
||||
},
|
||||
}}
|
||||
onClick={() => retrySearch(lyricsStore.provider, getSongInfo())}
|
||||
style={{
|
||||
'margin-top': '1em',
|
||||
'width': '100%'
|
||||
|
||||
@ -7,16 +7,16 @@ import {
|
||||
Match,
|
||||
onCleanup,
|
||||
onMount,
|
||||
Setter,
|
||||
type Setter,
|
||||
Switch,
|
||||
} from 'solid-js';
|
||||
|
||||
import {
|
||||
currentLyrics,
|
||||
lyricsStore,
|
||||
ProviderName,
|
||||
type ProviderName,
|
||||
providerNames,
|
||||
ProviderState,
|
||||
type ProviderState,
|
||||
setLyricsStore,
|
||||
} from '../../providers';
|
||||
|
||||
@ -132,11 +132,11 @@ export const LyricsPicker = (props: {
|
||||
>
|
||||
<svg
|
||||
class="style-scope yt-icon"
|
||||
fill="#FFFFFF"
|
||||
height={'40px'}
|
||||
preserveAspectRatio="xMidYMid meet"
|
||||
viewBox="0 -960 960 960"
|
||||
height={'40px'}
|
||||
width={'40px'}
|
||||
fill="#FFFFFF"
|
||||
>
|
||||
<g class="style-scope yt-icon">
|
||||
<path
|
||||
@ -156,10 +156,10 @@ export const LyricsPicker = (props: {
|
||||
{(provider) => (
|
||||
<div
|
||||
class="lyrics-picker-item"
|
||||
tabindex="-1"
|
||||
style={{
|
||||
transform: `translateX(${providerIdx() * -100 - 5}%)`,
|
||||
}}
|
||||
tabindex="-1"
|
||||
>
|
||||
<Switch>
|
||||
<Match
|
||||
@ -170,16 +170,16 @@ export const LyricsPicker = (props: {
|
||||
>
|
||||
<tp-yt-paper-spinner-lite
|
||||
active
|
||||
tabindex="-1"
|
||||
class="loading-indicator style-scope"
|
||||
style={{ padding: '5px', transform: 'scale(0.5)' }}
|
||||
tabindex="-1"
|
||||
/>
|
||||
</Match>
|
||||
<Match when={currentLyrics().state === 'error'}>
|
||||
<yt-icon-button
|
||||
icon={errorIcon}
|
||||
tabindex="-1"
|
||||
style={{ padding: '5px', transform: 'scale(0.5)' }}
|
||||
tabindex="-1"
|
||||
/>
|
||||
</Match>
|
||||
<Match
|
||||
@ -191,8 +191,8 @@ export const LyricsPicker = (props: {
|
||||
>
|
||||
<yt-icon-button
|
||||
icon={successIcon}
|
||||
tabindex="-1"
|
||||
style={{ padding: '5px', transform: 'scale(0.5)' }}
|
||||
tabindex="-1"
|
||||
/>
|
||||
</Match>
|
||||
<Match
|
||||
@ -204,8 +204,8 @@ export const LyricsPicker = (props: {
|
||||
>
|
||||
<yt-icon-button
|
||||
icon={notFoundIcon}
|
||||
tabindex="-1"
|
||||
style={{ padding: '5px', transform: 'scale(0.5)' }}
|
||||
tabindex="-1"
|
||||
/>
|
||||
</Match>
|
||||
</Switch>
|
||||
@ -252,11 +252,11 @@ export const LyricsPicker = (props: {
|
||||
>
|
||||
<svg
|
||||
class="style-scope yt-icon"
|
||||
fill="#FFFFFF"
|
||||
height={'40px'}
|
||||
preserveAspectRatio="xMidYMid meet"
|
||||
viewBox="0 -960 960 960"
|
||||
height={'40px'}
|
||||
width={'40px'}
|
||||
fill="#FFFFFF"
|
||||
>
|
||||
<g class="style-scope yt-icon">
|
||||
<path
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
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 { _ytAPI } from '..';
|
||||
@ -39,7 +39,6 @@ export const SyncedLine = (props: SyncedLineProps) => {
|
||||
|
||||
return (
|
||||
<Show
|
||||
when={text()}
|
||||
fallback={
|
||||
<yt-formatted-string
|
||||
text={{
|
||||
@ -47,6 +46,7 @@ export const SyncedLine = (props: SyncedLineProps) => {
|
||||
}}
|
||||
/>
|
||||
}
|
||||
when={text()}
|
||||
>
|
||||
<div
|
||||
class={`synced-line ${props.status}`}
|
||||
@ -54,7 +54,7 @@ export const SyncedLine = (props: SyncedLineProps) => {
|
||||
_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
|
||||
text={{
|
||||
runs: [
|
||||
|
||||
@ -301,8 +301,8 @@ export const LyricsRenderer = () => {
|
||||
return (
|
||||
<SyncedLine
|
||||
{...props}
|
||||
scroller={scroller()!}
|
||||
index={idx()}
|
||||
scroller={scroller()!}
|
||||
status={statuses()[idx() - 1]}
|
||||
/>
|
||||
);
|
||||
|
||||
@ -1,15 +1,11 @@
|
||||
import { render } from 'solid-js/web';
|
||||
|
||||
import KuromojiAnalyzer from 'kuroshiro-analyzer-kuromoji';
|
||||
import Kuroshiro from 'kuroshiro';
|
||||
|
||||
import { romanize as esHangulRomanize } from 'es-hangul';
|
||||
import hanja from 'hanja';
|
||||
|
||||
import pinyin from 'tiny-pinyin';
|
||||
|
||||
import * as pinyin from 'tiny-pinyin';
|
||||
import { romanize as romanizeThaiFrag } from '@dehoist/romanize-thai';
|
||||
import { lazy } from 'lazy-var';
|
||||
|
||||
import { detect } from 'tinyld';
|
||||
|
||||
import { waitForElement } from '@/utils/wait-for-element';
|
||||
@ -155,26 +151,9 @@ const hasKorean = (lines: string[]) =>
|
||||
const hasChinese = (lines: string[]) =>
|
||||
lines.some((line) => /[\u4E00-\u9FFF]+/.test(line));
|
||||
|
||||
export const romanize = async (line: string) => {
|
||||
const lang = detect(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;
|
||||
};
|
||||
// https://en.wikipedia.org/wiki/Thai_(Unicode_block)
|
||||
const hasThai = (lines: string[]) =>
|
||||
lines.some((line) => /[\u0E00-\u0E7F]+/.test(line));
|
||||
|
||||
export const romanizeJapanese = async (line: string) =>
|
||||
(await kuroshiro.get()).convert(line, {
|
||||
@ -190,3 +169,47 @@ export const romanizeChinese = (line: string) => {
|
||||
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 getSongControls from '@/providers/song-controls';
|
||||
import registerCallback, {
|
||||
import {
|
||||
registerCallback,
|
||||
type SongInfo,
|
||||
SongInfoEvent,
|
||||
} from '@/providers/song-info';
|
||||
import { mediaIcons } from '@/types/media-icons';
|
||||
import { type mediaIcons } from '@/types/media-icons';
|
||||
import { t } from '@/i18n';
|
||||
import { Platform } from '@/types/plugins';
|
||||
|
||||
export default createPlugin({
|
||||
name: () => t('plugins.taskbar-mediacontrol.name'),
|
||||
description: () => t('plugins.taskbar-mediacontrol.description'),
|
||||
restartNeeded: true,
|
||||
platform: Platform.Windows,
|
||||
config: {
|
||||
enabled: false,
|
||||
},
|
||||
|
||||
@ -2,15 +2,17 @@ import { nativeImage, type NativeImage, TouchBar } from 'electron';
|
||||
|
||||
import { createPlugin } from '@/utils';
|
||||
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 youtubeMusicIcon from '@assets/youtube-music.png?asset&asarUnpack';
|
||||
import { Platform } from '@/types/plugins';
|
||||
|
||||
export default createPlugin({
|
||||
name: () => t('plugins.touchbar.name'),
|
||||
description: () => t('plugins.touchbar.description'),
|
||||
restartNeeded: true,
|
||||
platform: Platform.macOS,
|
||||
config: {
|
||||
enabled: false,
|
||||
},
|
||||
|
||||
@ -3,7 +3,7 @@ import { net } from 'electron';
|
||||
import is from 'electron-is';
|
||||
|
||||
import { createPlugin } from '@/utils';
|
||||
import registerCallback from '@/providers/song-info';
|
||||
import { registerCallback } from '@/providers/song-info';
|
||||
import { t } from '@/i18n';
|
||||
|
||||
interface Data {
|
||||
|
||||
@ -7,9 +7,9 @@ import buttonSwitcherStyle from './button-switcher.css?inline';
|
||||
|
||||
import { createPlugin } from '@/utils';
|
||||
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 { MenuTemplate } from '@/menu';
|
||||
import { type MenuTemplate } from '@/menu';
|
||||
|
||||
import { VideoSwitchButton } from './templates/video-switch-button';
|
||||
|
||||
@ -177,12 +177,12 @@ export default createPlugin({
|
||||
() => (
|
||||
<Show when={showButton()}>
|
||||
<VideoSwitchButton
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onChange={(e) => {
|
||||
const target = e.target as HTMLInputElement;
|
||||
|
||||
setVideoState(target.checked);
|
||||
}}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
songButtonText={t('plugins.video-toggle.templates.button-song')}
|
||||
videoButtonText={t('plugins.video-toggle.templates.button-video')}
|
||||
/>
|
||||
|
||||
@ -14,8 +14,8 @@ export const VideoSwitchButton = (props: VideoSwitchButtonProps) => (
|
||||
>
|
||||
<input
|
||||
checked={true}
|
||||
id="video-toggle-video-switch-button-checkbox"
|
||||
class="video-switch-button-checkbox"
|
||||
id="video-toggle-video-switch-button-checkbox"
|
||||
type="checkbox"
|
||||
/>
|
||||
<label
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import emptyStyle from './empty-player.css?inline';
|
||||
import { createPlugin } from '@/utils';
|
||||
import { Visualizer } from './visualizers/visualizer';
|
||||
import { type Visualizer } from './visualizers/visualizer';
|
||||
import {
|
||||
ButterchurnVisualizer as butterchurn,
|
||||
VudioVisualizer as vudio,
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import {
|
||||
contextBridge,
|
||||
ipcRenderer,
|
||||
IpcRendererEvent,
|
||||
type IpcRendererEvent,
|
||||
webFrame,
|
||||
} from 'electron';
|
||||
import is from 'electron-is';
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import path from 'node:path';
|
||||
|
||||
import { app, BrowserWindow } from 'electron';
|
||||
import { app, type BrowserWindow } from 'electron';
|
||||
|
||||
import getSongControls from './song-controls';
|
||||
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
// 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
|
||||
type ArgsType<T> = T | string[] | undefined;
|
||||
@ -42,8 +44,8 @@ export default (win: BrowserWindow) => {
|
||||
play: () => win.webContents.send('ytmd:play'),
|
||||
pause: () => win.webContents.send('ytmd:pause'),
|
||||
playPause: () => win.webContents.send('ytmd:toggle-play'),
|
||||
like: () => win.webContents.send('ytmd:update-like', 'LIKE'),
|
||||
dislike: () => win.webContents.send('ytmd:update-like', 'DISLIKE'),
|
||||
like: () => win.webContents.send('ytmd:update-like', LikeType.Like),
|
||||
dislike: () => win.webContents.send('ytmd:update-like', LikeType.Dislike),
|
||||
seekTo: (seconds: ArgsType<number>) => {
|
||||
const secondsNumber = parseNumberFromArgsType(seconds);
|
||||
if (secondsNumber !== null) {
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
import { singleton } from './decorators';
|
||||
|
||||
import { LikeType, type GetState } from '@/types/datahost-get-state';
|
||||
|
||||
import type { YoutubePlayer } from '@/types/youtube-player';
|
||||
import type { GetState } from '@/types/datahost-get-state';
|
||||
import type {
|
||||
AlbumDetails,
|
||||
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) => {
|
||||
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.
|
||||
window.ipcRenderer.send('ytmd:volume-changed', api.getVolume());
|
||||
window.ipcRenderer.send('ytmd:volume-changed', {
|
||||
state: api.getVolume(),
|
||||
isMuted: api.isMuted(),
|
||||
});
|
||||
});
|
||||
|
||||
export const setupShuffleChangedListener = singleton(() => {
|
||||
@ -153,6 +194,10 @@ export default (api: YoutubePlayer) => {
|
||||
setupTimeChangedListener();
|
||||
});
|
||||
|
||||
window.ipcRenderer.on('ytmd:setup-like-changed-listener', () => {
|
||||
setupLikeChangedListener();
|
||||
});
|
||||
|
||||
window.ipcRenderer.on('ytmd:setup-repeat-changed-listener', () => {
|
||||
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';
|
||||
|
||||
@ -30,6 +30,7 @@ export interface SongInfo {
|
||||
title: string;
|
||||
alternativeTitle?: string;
|
||||
artist: string;
|
||||
artistUrl?: string;
|
||||
views: number;
|
||||
uploadDate?: string;
|
||||
imageSrc?: string | null;
|
||||
@ -72,6 +73,7 @@ const handleData = async (
|
||||
title: '',
|
||||
alternativeTitle: '',
|
||||
artist: '',
|
||||
artistUrl: '',
|
||||
views: 0,
|
||||
uploadDate: '',
|
||||
imageSrc: '',
|
||||
@ -93,6 +95,9 @@ const handleData = async (
|
||||
songInfo.url = microformat.urlCanonical?.split('&')[0];
|
||||
songInfo.playlistId =
|
||||
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
|
||||
config.set('url', microformat.urlCanonical);
|
||||
songInfo.alternativeTitle = microformat.linkAlternates.find(
|
||||
@ -110,7 +115,7 @@ const handleData = async (
|
||||
songInfo.elapsedSeconds = videoDetails.elapsedSeconds;
|
||||
songInfo.isPaused = videoDetails.isPaused;
|
||||
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) {
|
||||
case 'MUSIC_VIDEO_TYPE_ATV':
|
||||
@ -179,7 +184,7 @@ export type SongInfoCallback = (
|
||||
const callbacks: Set<SongInfoCallback> = new Set();
|
||||
|
||||
// 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);
|
||||
};
|
||||
|
||||
@ -277,5 +282,4 @@ export function cleanupName(name: string): string {
|
||||
return name;
|
||||
}
|
||||
|
||||
export default registerCallback;
|
||||
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>;
|
||||
}
|
||||
|
||||
declare var electronIs: typeof import('electron-is');
|
||||
|
||||
interface Window {
|
||||
trustedTypes?: typeof trustedTypes;
|
||||
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