mirror of
https://github.com/th-ch/youtube-music.git
synced 2026-01-11 18:41:47 +00:00
Compare commits
80 Commits
snyk-fix-b
...
v3.11.0
| Author | SHA1 | Date | |
|---|---|---|---|
| 721271d902 | |||
| a2151930ec | |||
| bdc9f42681 | |||
| 0b3c6f9e1f | |||
| ebe373bdc6 | |||
| ab91e6d735 | |||
| be3ae4d789 | |||
| 336b7fe5e9 | |||
| 5ecd39f324 | |||
| eb50596961 | |||
| aede0cd643 | |||
| 25f51784c3 | |||
| 7acac6d11c | |||
| 1972ef70b5 | |||
| c4455ed3d2 | |||
| c1a06ab955 | |||
| 26fa1f85b2 | |||
| 555817e2f5 | |||
| f8654dfdb9 | |||
| 96c0fc412c | |||
| a70a4106df | |||
| 895210cbb6 | |||
| 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 |
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>
|
||||
|
||||
@ -45,7 +45,7 @@ export default defineConfig({
|
||||
formats: ['es'],
|
||||
},
|
||||
outDir: 'dist/main',
|
||||
rollupOptions: {
|
||||
rolldownOptions: {
|
||||
external: ['electron', 'custom-electron-prompt', ...builtinModules],
|
||||
input: './src/index.ts',
|
||||
},
|
||||
@ -96,7 +96,7 @@ export default defineConfig({
|
||||
commonjsOptions: {
|
||||
ignoreDynamicRequires: true,
|
||||
},
|
||||
rollupOptions: {
|
||||
rolldownOptions: {
|
||||
external: ['electron', 'custom-electron-prompt', ...builtinModules],
|
||||
input: './src/preload.ts',
|
||||
},
|
||||
@ -149,7 +149,7 @@ export default defineConfig({
|
||||
name: 'renderer',
|
||||
},
|
||||
outDir: 'dist/renderer',
|
||||
rollupOptions: {
|
||||
rolldownOptions: {
|
||||
external: ['electron', ...builtinModules],
|
||||
input: './src/index.html',
|
||||
},
|
||||
|
||||
@ -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',
|
||||
|
||||
76
package.json
76
package.json
@ -2,7 +2,7 @@
|
||||
"name": "youtube-music",
|
||||
"desktopName": "com.github.th_ch.youtube_music",
|
||||
"productName": "YouTube Music",
|
||||
"version": "3.10.0",
|
||||
"version": "3.11.0",
|
||||
"description": "YouTube Music Desktop App - including custom plugins",
|
||||
"main": "./dist/main/index.js",
|
||||
"type": "module",
|
||||
@ -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_ENV=development NODE_OPTIONS=--enable-source-maps electron-vite dev --watch",
|
||||
"dev:renderer": "pnpm cross-env NODE_ENV=development 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 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",
|
||||
@ -103,7 +107,7 @@
|
||||
"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",
|
||||
@ -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
@ -31,6 +31,7 @@ export interface DefaultConfig {
|
||||
likeButtons: string;
|
||||
proxy: string;
|
||||
startingPage: string;
|
||||
backgroundMaterial?: 'none' | 'mica' | 'acrylic' | 'tabbed';
|
||||
overrideUserAgent: boolean;
|
||||
usePodcastParticipantAsArtist: boolean;
|
||||
themes: string[];
|
||||
@ -39,7 +40,7 @@ export interface DefaultConfig {
|
||||
'plugins': Record<string, unknown>;
|
||||
}
|
||||
|
||||
const defaultConfig: DefaultConfig = {
|
||||
export const defaultConfig: DefaultConfig = {
|
||||
'window-size': {
|
||||
width: 1100,
|
||||
height: 550,
|
||||
@ -73,5 +74,3 @@ const defaultConfig: DefaultConfig = {
|
||||
},
|
||||
'plugins': {},
|
||||
};
|
||||
|
||||
export default defaultConfig;
|
||||
|
||||
@ -1,30 +1,36 @@
|
||||
import { deepmergeCustom } from 'deepmerge-ts';
|
||||
|
||||
import defaultConfig from './defaults';
|
||||
|
||||
import store, { IStore } from './store';
|
||||
import plugins from './plugins';
|
||||
|
||||
import { store, type IStore } from './store';
|
||||
import { restart } from '@/providers/app-controls';
|
||||
|
||||
import type { defaultConfig } from './defaults';
|
||||
|
||||
const deepmerge = deepmergeCustom({
|
||||
mergeArrays: false,
|
||||
});
|
||||
|
||||
const set = (key: string, value: unknown) => {
|
||||
export { defaultConfig } from './defaults';
|
||||
export * as plugins from './plugins';
|
||||
|
||||
export const set = (key: string, value: unknown) => {
|
||||
store.set(key, value);
|
||||
};
|
||||
const setPartial = (key: string, value: object, defaultValue?: object) => {
|
||||
|
||||
export const setPartial = (
|
||||
key: string,
|
||||
value: object,
|
||||
defaultValue?: object,
|
||||
) => {
|
||||
const newValue = deepmerge(defaultValue ?? {}, store.get(key) ?? {}, value);
|
||||
store.set(key, newValue);
|
||||
};
|
||||
|
||||
function setMenuOption(key: string, value: unknown) {
|
||||
export const setMenuOption = (key: string, value: unknown) => {
|
||||
set(key, value);
|
||||
if (store.get('options.restartOnConfigChanges')) {
|
||||
restart();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// MAGIC OF TYPESCRIPT
|
||||
|
||||
@ -74,18 +80,11 @@ type PathValue<T, K extends string> =
|
||||
? PathValue<T[A], B>
|
||||
: T;
|
||||
|
||||
const get = <Key extends Paths<typeof defaultConfig>>(key: Key) =>
|
||||
export const get = <Key extends Paths<typeof defaultConfig>>(key: Key) =>
|
||||
store.get(key) as PathValue<typeof defaultConfig, typeof key>;
|
||||
|
||||
export default {
|
||||
defaultConfig,
|
||||
get,
|
||||
set,
|
||||
setPartial,
|
||||
setMenuOption,
|
||||
edit: () => store.openInEditor(),
|
||||
watch(cb: Parameters<IStore['onDidAnyChange']>[0]) {
|
||||
store.onDidAnyChange(cb);
|
||||
},
|
||||
plugins,
|
||||
export const edit = () => store.openInEditor();
|
||||
|
||||
export const watch = (cb: Parameters<IStore['onDidAnyChange']>[0]) => {
|
||||
store.onDidAnyChange(cb);
|
||||
};
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { deepmerge } from 'deepmerge-ts';
|
||||
import { allPlugins } from 'virtual:plugins';
|
||||
|
||||
import store from './store';
|
||||
import { store } from './store';
|
||||
|
||||
import { restart } from '@/providers/app-controls';
|
||||
|
||||
@ -68,13 +68,3 @@ export function enable(plugin: string) {
|
||||
export function disable(plugin: string) {
|
||||
setMenuOptions(plugin, { enabled: false }, []);
|
||||
}
|
||||
|
||||
export default {
|
||||
isEnabled,
|
||||
getPlugins,
|
||||
enable,
|
||||
disable,
|
||||
setOptions,
|
||||
setMenuOptions,
|
||||
getOptions,
|
||||
};
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import Store from 'electron-store';
|
||||
|
||||
import defaults from './defaults';
|
||||
import { defaultConfig as defaults } from './defaults';
|
||||
|
||||
import { DefaultPresetList, type Preset } from '@/plugins/downloader/types';
|
||||
|
||||
@ -257,7 +257,7 @@ const migrations = {
|
||||
},
|
||||
};
|
||||
|
||||
export default new Store({
|
||||
export const store = new Store({
|
||||
defaults: {
|
||||
...defaults,
|
||||
// README: 'plugin' uses deepmerge to populate the default values, so it is not necessary to include it here
|
||||
|
||||
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>;
|
||||
|
||||
|
||||
@ -853,6 +853,26 @@
|
||||
"description": "Fügt ein TouchBar-Widget für macOS-Benutzer hinzu",
|
||||
"name": "TouchBar"
|
||||
},
|
||||
"transparent-player": {
|
||||
"description": "Macht das Player-Fenster transparent",
|
||||
"name": "Transparent Player",
|
||||
"menu": {
|
||||
"opacity": {
|
||||
"label": "Hintergrund-Sichtbarkeit",
|
||||
"submenu": {
|
||||
"percent": "{{opacity}}%"
|
||||
}
|
||||
},
|
||||
"type": {
|
||||
"label": "Typ",
|
||||
"submenu": {
|
||||
"acrylic": "Acrylic",
|
||||
"mica": "Mica",
|
||||
"tabbed": "Tabbed"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tuna-obs": {
|
||||
"description": "Integration mit dem OBS-Plugin Tuna",
|
||||
"name": "Tuna OBS"
|
||||
|
||||
@ -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": {
|
||||
@ -737,6 +758,7 @@
|
||||
"token": "Enter ListenBrainz user token"
|
||||
},
|
||||
"scrobble-alternative-title": "Use alternative titles",
|
||||
"scrobble-alternative-artist": "Use alternative artists",
|
||||
"scrobble-other-media": "Scrobble other media"
|
||||
},
|
||||
"name": "Scrobbler",
|
||||
@ -791,6 +813,14 @@
|
||||
"not-found": "⚠️ No lyrics found for this song."
|
||||
},
|
||||
"menu": {
|
||||
"preferred-provider": {
|
||||
"label": "Preferred Provider",
|
||||
"tooltip": "Choose the default provider to use",
|
||||
"none": {
|
||||
"label": "None",
|
||||
"tooltip": "No preferred provider"
|
||||
}
|
||||
},
|
||||
"default-text-string": {
|
||||
"label": "Default character between lyrics",
|
||||
"tooltip": "Choose the default character to use for the gap between lyrics"
|
||||
@ -853,6 +883,27 @@
|
||||
"description": "Adds a TouchBar widget for macOS users",
|
||||
"name": "TouchBar"
|
||||
},
|
||||
"transparent-player": {
|
||||
"description": "Makes the app window transparent",
|
||||
"name": "Transparent Player",
|
||||
"menu": {
|
||||
"opacity": {
|
||||
"label": "Opacity",
|
||||
"submenu": {
|
||||
"percent": "{{opacity}}%"
|
||||
}
|
||||
},
|
||||
"type": {
|
||||
"label": "Type",
|
||||
"submenu": {
|
||||
"acrylic": "Acrylic",
|
||||
"mica": "Mica",
|
||||
"tabbed": "Tabbed",
|
||||
"none": "None"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tuna-obs": {
|
||||
"description": "Integration with OBS's plugin Tuna",
|
||||
"name": "Tuna OBS"
|
||||
|
||||
@ -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": {
|
||||
|
||||
@ -421,6 +421,19 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"custom-output-device": {
|
||||
"description": "미디어 출력 장치 구성",
|
||||
"menu": {
|
||||
"device-selector": "장치 선택"
|
||||
},
|
||||
"name": "출력 장치 커스텀",
|
||||
"prompt": {
|
||||
"device-selector": {
|
||||
"label": "사용할 미디어 출력 장치를 선택하세요",
|
||||
"title": "출력 장치 선택"
|
||||
}
|
||||
}
|
||||
},
|
||||
"disable-autoplay": {
|
||||
"description": "노래를 '일시 정지' 모드로 시작하게 합니다",
|
||||
"menu": {
|
||||
@ -444,7 +457,15 @@
|
||||
"hide-duration-left": "남은 재생 시간 숨기기",
|
||||
"hide-github-button": "GitHub 링크 버튼 숨기기",
|
||||
"play-on-youtube-music": "유튜브 뮤직에서 재생",
|
||||
"set-inactivity-timeout": "비활성 시간 제한 설정"
|
||||
"set-inactivity-timeout": "비활성 시간 제한 설정",
|
||||
"set-status-display-type": {
|
||||
"label": "상태 텍스트",
|
||||
"submenu": {
|
||||
"artist": "{아티스트} 듣는 중",
|
||||
"title": "{곡 제목} 듣는 중",
|
||||
"youtube-music": "YouTube Music 듣는 중"
|
||||
}
|
||||
}
|
||||
},
|
||||
"name": "디스코드 활동 상태",
|
||||
"prompt": {
|
||||
@ -736,6 +757,7 @@
|
||||
"listenbrainz": {
|
||||
"token": "ListenBrainz 유저 토큰 입력"
|
||||
},
|
||||
"scrobble-alternative-artist": "대체 아티스트 명 사용",
|
||||
"scrobble-alternative-title": "대체 제목 사용하기",
|
||||
"scrobble-other-media": "다른 미디어 스크로블하기"
|
||||
},
|
||||
@ -821,6 +843,14 @@
|
||||
"label": "가사를 최대한 정교하게 동기화",
|
||||
"tooltip": "다음 줄의 표시를 밀리초 단위로 계산합니다 (성능에 약간의 영향을 미칠 수 있음)"
|
||||
},
|
||||
"preferred-provider": {
|
||||
"label": "선호하는 가사 제공자",
|
||||
"none": {
|
||||
"label": "없음",
|
||||
"tooltip": "선호하는 가사 제공자 없음"
|
||||
},
|
||||
"tooltip": "사용할 기본 가사 제공자를 선택하세요"
|
||||
},
|
||||
"romanization": {
|
||||
"label": "가사 로마자 변환",
|
||||
"tooltip": "가사가 영어가 아닌 언어로 되어있는 경우, 로마자 표기를 표시합니다."
|
||||
@ -853,6 +883,27 @@
|
||||
"description": "macOS 사용자를 위한 TouchBar 위젯을 추가합니다",
|
||||
"name": "TouchBar"
|
||||
},
|
||||
"transparent-player": {
|
||||
"description": "애플리케이션 창을 투명하게 만듭니다",
|
||||
"menu": {
|
||||
"opacity": {
|
||||
"label": "불투명도",
|
||||
"submenu": {
|
||||
"percent": "{{opacity}}%"
|
||||
}
|
||||
},
|
||||
"type": {
|
||||
"label": "종류",
|
||||
"submenu": {
|
||||
"acrylic": "아크릴",
|
||||
"mica": "미카",
|
||||
"none": "없음",
|
||||
"tabbed": "탭"
|
||||
}
|
||||
}
|
||||
},
|
||||
"name": "투명 플레이어"
|
||||
},
|
||||
"tuna-obs": {
|
||||
"description": "OBS의 확장인 Tuna와의 통합을 활성화합니다",
|
||||
"name": "Tuna OBS"
|
||||
|
||||
@ -421,6 +421,19 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"custom-output-device": {
|
||||
"description": "Configure um dispositivo de saída de mídia personalizado para músicas",
|
||||
"menu": {
|
||||
"device-selector": "Selecionar dispositivo"
|
||||
},
|
||||
"name": "Dispositivo de saída personalizado",
|
||||
"prompt": {
|
||||
"device-selector": {
|
||||
"label": "Escolha o dispositivo de saída de mídia que será usado",
|
||||
"title": "Selecionar dispositivo de saída"
|
||||
}
|
||||
}
|
||||
},
|
||||
"disable-autoplay": {
|
||||
"description": "Faz a música começar no modo \"pausado\"",
|
||||
"menu": {
|
||||
@ -444,7 +457,15 @@
|
||||
"hide-duration-left": "Ocultar duração restante",
|
||||
"hide-github-button": "Ocultar botão do GitHub",
|
||||
"play-on-youtube-music": "Reproduzir no YouTube Music",
|
||||
"set-inactivity-timeout": "Definir tempo limite de inatividade"
|
||||
"set-inactivity-timeout": "Definir tempo limite de inatividade",
|
||||
"set-status-display-type": {
|
||||
"label": "Texto de status",
|
||||
"submenu": {
|
||||
"artist": "Ouvindo {artist}",
|
||||
"title": "Ouvindo {song title}",
|
||||
"youtube-music": "Ouvindo YouTube Music"
|
||||
}
|
||||
}
|
||||
},
|
||||
"name": "Rich Presence do Discord",
|
||||
"prompt": {
|
||||
|
||||
@ -421,6 +421,19 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"custom-output-device": {
|
||||
"description": "Настройка устройства вывода медиа для песен",
|
||||
"menu": {
|
||||
"device-selector": "Выберите устройство"
|
||||
},
|
||||
"name": "Пользовательское устройство вывода",
|
||||
"prompt": {
|
||||
"device-selector": {
|
||||
"label": "Выберите устройство вывода медиа, которое будет использоваться",
|
||||
"title": "Выберите устройство вывода"
|
||||
}
|
||||
}
|
||||
},
|
||||
"disable-autoplay": {
|
||||
"description": "Запускает песню сразу на паузе",
|
||||
"menu": {
|
||||
@ -444,7 +457,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": "Discord Rich Presence",
|
||||
"prompt": {
|
||||
@ -736,6 +757,7 @@
|
||||
"listenbrainz": {
|
||||
"token": "Введите токен пользователя ListenBrainz"
|
||||
},
|
||||
"scrobble-alternative-artist": "Использовать альтернативных исполнителей",
|
||||
"scrobble-alternative-title": "Использовать альтернативные названия",
|
||||
"scrobble-other-media": "Скробблинг других медиа"
|
||||
},
|
||||
@ -853,6 +875,27 @@
|
||||
"description": "Добавляет виджет тачбара для пользователей macOS",
|
||||
"name": "Тачбар"
|
||||
},
|
||||
"transparent-player": {
|
||||
"description": "Делает окно приложения прозрачным",
|
||||
"menu": {
|
||||
"opacity": {
|
||||
"label": "Непрозрачность",
|
||||
"submenu": {
|
||||
"percent": "{{opacity}}%"
|
||||
}
|
||||
},
|
||||
"type": {
|
||||
"label": "Тип",
|
||||
"submenu": {
|
||||
"acrylic": "Acrylic",
|
||||
"mica": "Mica",
|
||||
"none": "Отключено",
|
||||
"tabbed": "Tabbed"
|
||||
}
|
||||
}
|
||||
},
|
||||
"name": "Прозрачный плеер"
|
||||
},
|
||||
"tuna-obs": {
|
||||
"description": "Интеграция с плагином Tuna от OBS",
|
||||
"name": "Tuna OBS"
|
||||
|
||||
@ -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",
|
||||
@ -116,10 +116,10 @@
|
||||
"auto-update": "Uppdatera automatiskt",
|
||||
"hide-menu": {
|
||||
"dialog": {
|
||||
"message": "Menyn kommer vara dold nästa gång du startar, tryck [Alt] för att visa den (eller [´] om du använder inbyggda app-menyn)",
|
||||
"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": "Göm menyn"
|
||||
"label": "Dölj meny"
|
||||
},
|
||||
"language": {
|
||||
"dialog": {
|
||||
@ -132,7 +132,7 @@
|
||||
}
|
||||
},
|
||||
"resume-on-start": "Fortsätt spela när appen öppnas",
|
||||
"single-instance-lock": "Lås för enskild instans",
|
||||
"single-instance-lock": "Lås enskild instans",
|
||||
"start-at-login": "Starta vid inloggning",
|
||||
"starting-page": {
|
||||
"label": "Startsidа",
|
||||
@ -153,14 +153,14 @@
|
||||
"custom-window-title": {
|
||||
"label": "Anpassad titel på fönstret",
|
||||
"prompt": {
|
||||
"label": "Skriv in en egen fönstertitel (lämna tomt om du inte vill använda någon)",
|
||||
"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": "Göm",
|
||||
"hide": "Dölj",
|
||||
"label": "Gilla-knappar"
|
||||
},
|
||||
"remove-upgrade-button": "Ta bort knappen för uppgradering",
|
||||
@ -175,7 +175,7 @@
|
||||
},
|
||||
"label": "Tema",
|
||||
"submenu": {
|
||||
"import-css-file": "Ladda in en anpassad CSS-fil",
|
||||
"import-css-file": "Importera anpassad CSS-fil",
|
||||
"no-theme": "Inget tema"
|
||||
}
|
||||
}
|
||||
@ -185,8 +185,8 @@
|
||||
},
|
||||
"plugins": {
|
||||
"enabled": "Aktiverad",
|
||||
"label": "Plugins",
|
||||
"new": "Ny"
|
||||
"label": "Tillägg",
|
||||
"new": "NY"
|
||||
},
|
||||
"view": {
|
||||
"label": "Visa",
|
||||
@ -226,7 +226,7 @@
|
||||
"name": "Annonsblockerare"
|
||||
},
|
||||
"album-actions": {
|
||||
"description": "Lägger till knappar för Undislike, Dislike, Like och Unlike för att använda detta på alla låtar i en spellista eller ett album",
|
||||
"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": {
|
||||
@ -245,7 +245,7 @@
|
||||
"description": "Ger en ljuseffekt genom att försiktigt kasta färger från videon på skärmens bakgrund",
|
||||
"menu": {
|
||||
"blur-amount": {
|
||||
"label": "Osärpa",
|
||||
"label": "Oskärpa",
|
||||
"submenu": {
|
||||
"pixels": "{{blurAmount}} pixlar"
|
||||
}
|
||||
@ -281,52 +281,422 @@
|
||||
}
|
||||
},
|
||||
"use-fullscreen": {
|
||||
"label": "använder helskärm"
|
||||
"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"
|
||||
}
|
||||
}
|
||||
@ -335,56 +705,194 @@
|
||||
"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": {
|
||||
|
||||
@ -421,6 +421,19 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"custom-output-device": {
|
||||
"description": "Şarkılar için özel bir medya çıkış aygıtı ayarlayın",
|
||||
"menu": {
|
||||
"device-selector": "Aygıt Seçin"
|
||||
},
|
||||
"name": "Özel Çıkış Aygıtı",
|
||||
"prompt": {
|
||||
"device-selector": {
|
||||
"label": "Kullanılacak medya çıkış aygıtını seçin",
|
||||
"title": "Çıkış Aygıtını Seçin"
|
||||
}
|
||||
}
|
||||
},
|
||||
"disable-autoplay": {
|
||||
"description": "Şarkıların otomatik olarak duraklatılmasını sağlar",
|
||||
"menu": {
|
||||
@ -444,7 +457,15 @@
|
||||
"hide-duration-left": "Kalan süreyi gizle",
|
||||
"hide-github-button": "GitHub bağlantısını gizle",
|
||||
"play-on-youtube-music": "YouTube Music de oynat",
|
||||
"set-inactivity-timeout": "Hareketsizlik zaman aşımını ayarla"
|
||||
"set-inactivity-timeout": "Hareketsizlik zaman aşımını ayarla",
|
||||
"set-status-display-type": {
|
||||
"label": "Durum metni",
|
||||
"submenu": {
|
||||
"artist": "{artist} Dinleniyor",
|
||||
"title": "{song title} Dinleniyor",
|
||||
"youtube-music": "YouTube Müzik Dinleniyor"
|
||||
}
|
||||
}
|
||||
},
|
||||
"name": "Discord Etkinlik Durumu",
|
||||
"prompt": {
|
||||
@ -736,6 +757,7 @@
|
||||
"listenbrainz": {
|
||||
"token": "ListenBrainz kullanıcı kimliğinizi girin"
|
||||
},
|
||||
"scrobble-alternative-artist": "Alternatif sanatçıları kullan",
|
||||
"scrobble-alternative-title": "Alternatif başlıklar kullan",
|
||||
"scrobble-other-media": "Diğer medya ortamlarında listele"
|
||||
},
|
||||
@ -853,6 +875,27 @@
|
||||
"description": "macOS kullanıcıları için bir TouchBar widget'ı ekler",
|
||||
"name": "TouchBar"
|
||||
},
|
||||
"transparent-player": {
|
||||
"description": "Uygulama penceresini şeffaf yapar",
|
||||
"menu": {
|
||||
"opacity": {
|
||||
"label": "Opaklık",
|
||||
"submenu": {
|
||||
"percent": "%{{opacity}}"
|
||||
}
|
||||
},
|
||||
"type": {
|
||||
"label": "Tür",
|
||||
"submenu": {
|
||||
"acrylic": "Akrilik",
|
||||
"mica": "Mika",
|
||||
"none": "Hiçbiri",
|
||||
"tabbed": "Sekmeli"
|
||||
}
|
||||
}
|
||||
},
|
||||
"name": "Şeffaf Oynatıcı"
|
||||
},
|
||||
"tuna-obs": {
|
||||
"description": "OBS eklentisi Tuna ile entegrasyon sağlar",
|
||||
"name": "Tuna OBS"
|
||||
|
||||
@ -421,6 +421,19 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"custom-output-device": {
|
||||
"description": "Cài đặt cho thiết bị đầu ra tùy chỉnh cho bài hát",
|
||||
"menu": {
|
||||
"device-selector": "Chọn thiết bị"
|
||||
},
|
||||
"name": "Thiết bị đầu ra tùy chỉnh",
|
||||
"prompt": {
|
||||
"device-selector": {
|
||||
"label": "Chọn thiết bị phát làm đầu ra để dùng",
|
||||
"title": "Chọn thiết bị đầu ra"
|
||||
}
|
||||
}
|
||||
},
|
||||
"disable-autoplay": {
|
||||
"description": "Làm nhạc bắt đầu ở chế độ \"tạm dừng\". Ngoài ra có thể dừng nhạc khi khởi động ứng dụng (nếu có bật tính năng \"Tiếp tục bài hát cuối cùng khi ứng dụng khởi động\")",
|
||||
"menu": {
|
||||
@ -444,9 +457,17 @@
|
||||
"hide-duration-left": "Ẩn thời lượng còn lại",
|
||||
"hide-github-button": "Ẩn nút liên kết GitHub",
|
||||
"play-on-youtube-music": "Phát trong Youtube Music",
|
||||
"set-inactivity-timeout": "Đặt thời gian chờ không hoạt động"
|
||||
"set-inactivity-timeout": "Đặt thời gian chờ không hoạt động",
|
||||
"set-status-display-type": {
|
||||
"label": "Văn bản trạng thái",
|
||||
"submenu": {
|
||||
"artist": "Đang nghe nhạc của {artist}",
|
||||
"title": "Đang nghe nhạc {song title}",
|
||||
"youtube-music": "Đang nghe Youtube Music"
|
||||
}
|
||||
}
|
||||
},
|
||||
"name": "Discord Rich Presence",
|
||||
"name": "Tích hợp trạng thái Discord",
|
||||
"prompt": {
|
||||
"set-inactivity-timeout": {
|
||||
"label": "Nhập thời gian chờ không hoạt động tính bằng giây:",
|
||||
@ -736,6 +757,7 @@
|
||||
"listenbrainz": {
|
||||
"token": "Nhập mã người dùng ListenBrainz"
|
||||
},
|
||||
"scrobble-alternative-artist": "Dùng nghệ sĩ thay thế",
|
||||
"scrobble-alternative-title": "Dùng tiêu đề thay thế",
|
||||
"scrobble-other-media": "Scrobber nội dung khác"
|
||||
},
|
||||
@ -853,6 +875,27 @@
|
||||
"description": "Thêm tiện ích TouchBar cho người dùng macOS",
|
||||
"name": "TouchBar"
|
||||
},
|
||||
"transparent-player": {
|
||||
"description": "Làm cho cửa sổ ứng dụng có hiệu ứng trong suốt",
|
||||
"menu": {
|
||||
"opacity": {
|
||||
"label": "Độ mờ",
|
||||
"submenu": {
|
||||
"percent": "{{opacity}}%"
|
||||
}
|
||||
},
|
||||
"type": {
|
||||
"label": "Kiểu nền",
|
||||
"submenu": {
|
||||
"acrylic": "Acrylic",
|
||||
"mica": "Mica",
|
||||
"none": "Không có",
|
||||
"tabbed": "Tabbed"
|
||||
}
|
||||
}
|
||||
},
|
||||
"name": "Trình phát trong suốt"
|
||||
},
|
||||
"tuna-obs": {
|
||||
"description": "Tích hợp với plugin Tuna của OBS",
|
||||
"name": "Tuna OBS"
|
||||
|
||||
@ -421,6 +421,19 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"custom-output-device": {
|
||||
"description": "配置歌曲的自定义输出媒体设备",
|
||||
"menu": {
|
||||
"device-selector": "选择设备"
|
||||
},
|
||||
"name": "自定义输出设备",
|
||||
"prompt": {
|
||||
"device-selector": {
|
||||
"label": "选择要使用的输出媒体设备",
|
||||
"title": "选择输出设备"
|
||||
}
|
||||
}
|
||||
},
|
||||
"disable-autoplay": {
|
||||
"description": "让曲目开始时处于 “暂停” 模式",
|
||||
"menu": {
|
||||
@ -444,7 +457,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": "在听 {artist}",
|
||||
"title": "在听 {song title}",
|
||||
"youtube-music": "在听 YouTube Music"
|
||||
}
|
||||
}
|
||||
},
|
||||
"name": "Discord Rich Presence 状态显示",
|
||||
"prompt": {
|
||||
@ -736,6 +757,7 @@
|
||||
"listenbrainz": {
|
||||
"token": "输入 ListenBrainz 用户令牌"
|
||||
},
|
||||
"scrobble-alternative-artist": "使用替代艺术家",
|
||||
"scrobble-alternative-title": "使用替代标题",
|
||||
"scrobble-other-media": "记录其他媒体文件"
|
||||
},
|
||||
@ -853,6 +875,27 @@
|
||||
"description": "为 macOS 用户启用 TouchBar 支持",
|
||||
"name": "TouchBar"
|
||||
},
|
||||
"transparent-player": {
|
||||
"description": "把应用窗口变透明",
|
||||
"menu": {
|
||||
"opacity": {
|
||||
"label": "不透明",
|
||||
"submenu": {
|
||||
"percent": "{{opacity}}%"
|
||||
}
|
||||
},
|
||||
"type": {
|
||||
"label": "类型",
|
||||
"submenu": {
|
||||
"acrylic": "亚克力",
|
||||
"mica": "云母",
|
||||
"none": "无",
|
||||
"tabbed": "标签"
|
||||
}
|
||||
}
|
||||
},
|
||||
"name": "透明播放器"
|
||||
},
|
||||
"tuna-obs": {
|
||||
"description": "与 OBS 的 Tuna 插件集成",
|
||||
"name": "Tuna OBS"
|
||||
|
||||
20
src/index.ts
20
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';
|
||||
@ -29,7 +29,7 @@ import { allPlugins, mainPlugins } from 'virtual:plugins';
|
||||
|
||||
import { languageResources } from 'virtual:i18n';
|
||||
|
||||
import config from '@/config';
|
||||
import * as config from '@/config';
|
||||
|
||||
import { refreshMenu, setApplicationMenu } from '@/menu';
|
||||
import { fileExists, injectCSS, injectCSSAsFile } from '@/plugins/utils/main';
|
||||
@ -61,13 +61,6 @@ 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'];
|
||||
}
|
||||
|
||||
// Catch errors and log them
|
||||
unhandled({
|
||||
logger: console.error,
|
||||
@ -356,10 +349,12 @@ async function createMainWindow() {
|
||||
delete decorations.titleBarStyle;
|
||||
}
|
||||
|
||||
const win = new BrowserWindow({
|
||||
const electronWindowSettings: Electron.BrowserWindowConstructorOptions = {
|
||||
icon,
|
||||
width: windowSize.width,
|
||||
height: windowSize.height,
|
||||
minWidth: 325,
|
||||
minHeight: 425,
|
||||
backgroundColor: '#000',
|
||||
show: false,
|
||||
webPreferences: {
|
||||
@ -374,7 +369,10 @@ async function createMainWindow() {
|
||||
}),
|
||||
},
|
||||
...decorations,
|
||||
});
|
||||
};
|
||||
|
||||
const win = new BrowserWindow(electronWindowSettings);
|
||||
|
||||
await initHook(win);
|
||||
initTheme(win);
|
||||
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
import { BrowserWindow, ipcMain } from 'electron';
|
||||
import { type BrowserWindow, ipcMain } from 'electron';
|
||||
|
||||
import { deepmerge } from 'deepmerge-ts';
|
||||
import { allPlugins, mainPlugins } from 'virtual:plugins';
|
||||
|
||||
import config from '@/config';
|
||||
import * as config from '@/config';
|
||||
import { LoggerPrefix, startPlugin, stopPlugin } from '@/utils';
|
||||
|
||||
import { t } from '@/i18n';
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { deepmerge } from 'deepmerge-ts';
|
||||
import { allPlugins } from 'virtual:plugins';
|
||||
|
||||
import config from '@/config';
|
||||
import * as config from '@/config';
|
||||
import { setApplicationMenu } from '@/menu';
|
||||
|
||||
import { LoggerPrefix } from '@/utils';
|
||||
|
||||
@ -3,7 +3,7 @@ import { allPlugins, preloadPlugins } from 'virtual:plugins';
|
||||
|
||||
import { LoggerPrefix, startPlugin, stopPlugin } from '@/utils';
|
||||
|
||||
import config from '@/config';
|
||||
import * as config from '@/config';
|
||||
|
||||
import { t } from '@/i18n';
|
||||
|
||||
|
||||
@ -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';
|
||||
@ -15,7 +15,7 @@ import { allPlugins } from 'virtual:plugins';
|
||||
|
||||
import { languageResources } from 'virtual:i18n';
|
||||
|
||||
import config from './config';
|
||||
import * as config from './config';
|
||||
|
||||
import { restart } from './providers/app-controls';
|
||||
import { startingPages } from './providers/extracted-data';
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -31,7 +31,7 @@ export default createPlugin<
|
||||
alpha?: number,
|
||||
ratioMultiply?: number,
|
||||
): string;
|
||||
updateColor(): void;
|
||||
updateColor(alpha: number): void;
|
||||
},
|
||||
{
|
||||
enabled: boolean;
|
||||
@ -143,7 +143,16 @@ export default createPlugin<
|
||||
document.documentElement.style.setProperty(DARK_COLOR_KEY, '0, 0, 0');
|
||||
}
|
||||
|
||||
this.updateColor();
|
||||
let alpha: number | null = null;
|
||||
if (await window.mainConfig.plugins.isEnabled('transparent-player')) {
|
||||
const value: unknown = window.mainConfig.get(
|
||||
'plugins.transparent-player.opacity',
|
||||
);
|
||||
if (typeof value === 'number' && value >= 0 && value <= 1) {
|
||||
alpha = value;
|
||||
}
|
||||
}
|
||||
this.updateColor(alpha ?? 1);
|
||||
});
|
||||
},
|
||||
onConfigChange(config) {
|
||||
@ -163,7 +172,7 @@ export default createPlugin<
|
||||
}
|
||||
return `color-mix(in srgb, ${color} ${originalRatio}, ${keyColor} ${colorRatio})`;
|
||||
},
|
||||
updateColor() {
|
||||
updateColor(alpha: number) {
|
||||
const variableMap = {
|
||||
'--ytmusic-color-black1': '#212121',
|
||||
'--ytmusic-color-black2': '#181818',
|
||||
@ -202,19 +211,20 @@ export default createPlugin<
|
||||
Object.entries(variableMap).map(([variable, color]) => {
|
||||
document.documentElement.style.setProperty(
|
||||
variable,
|
||||
this.getMixedColor(color, COLOR_KEY),
|
||||
this.getMixedColor(color, COLOR_KEY, alpha),
|
||||
'important',
|
||||
);
|
||||
});
|
||||
|
||||
document.body.style.setProperty(
|
||||
'background',
|
||||
this.getMixedColor('#030303', COLOR_KEY),
|
||||
this.getMixedColor('rgba(3, 3, 3)', DARK_COLOR_KEY, alpha),
|
||||
'important',
|
||||
);
|
||||
document.documentElement.style.setProperty(
|
||||
'--ytmusic-background',
|
||||
this.getMixedColor('#030303', DARK_COLOR_KEY),
|
||||
// #030303
|
||||
this.getMixedColor('rgba(3, 3, 3)', DARK_COLOR_KEY, alpha),
|
||||
'important',
|
||||
);
|
||||
},
|
||||
|
||||
@ -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,9 +30,12 @@ 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');
|
||||
ctx.ipc.send('ytmd:setup-shuffle-changed-listener');
|
||||
});
|
||||
|
||||
ctx.ipc.on(
|
||||
@ -37,7 +45,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);
|
||||
@ -63,6 +71,10 @@ export const backend = createBackend<BackendType, APIServerConfig>({
|
||||
init(backendCtx) {
|
||||
this.app = new Hono();
|
||||
|
||||
const ws = createNodeWebSocket({
|
||||
app: this.app,
|
||||
});
|
||||
|
||||
this.app.use('*', cors());
|
||||
|
||||
// for web remote control
|
||||
@ -103,9 +115,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, backendCtx, ws);
|
||||
|
||||
// swagger
|
||||
this.app.openAPIRegistry.registerComponent(
|
||||
@ -133,6 +150,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 +162,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 { 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';
|
||||
|
||||
154
src/plugins/api-server/backend/routes/websocket.ts
Normal file
154
src/plugins/api-server/backend/routes/websocket.ts
Normal file
@ -0,0 +1,154 @@
|
||||
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';
|
||||
import type { BackendContext } from '@/types/contexts';
|
||||
import type { APIServerConfig } from '@/plugins/api-server/config';
|
||||
|
||||
enum DataTypes {
|
||||
PlayerInfo = 'PLAYER_INFO',
|
||||
VideoChanged = 'VIDEO_CHANGED',
|
||||
PlayerStateChanged = 'PLAYER_STATE_CHANGED',
|
||||
PositionChanged = 'POSITION_CHANGED',
|
||||
VolumeChanged = 'VOLUME_CHANGED',
|
||||
RepeatChanged = 'REPEAT_CHANGED',
|
||||
ShuffleChanged = 'SHUFFLE_CHANGED',
|
||||
}
|
||||
|
||||
type PlayerState = {
|
||||
song?: SongInfo;
|
||||
isPlaying: boolean;
|
||||
muted: boolean;
|
||||
position: number;
|
||||
volume: number;
|
||||
repeat: RepeatMode;
|
||||
shuffle: boolean;
|
||||
};
|
||||
|
||||
export const register = (
|
||||
app: HonoApp,
|
||||
{ ipc }: BackendContext<APIServerConfig>,
|
||||
{ upgradeWebSocket }: NodeWebSocket,
|
||||
) => {
|
||||
let volumeState: VolumeState | undefined = undefined;
|
||||
let repeat: RepeatMode = 'NONE';
|
||||
let shuffle = false;
|
||||
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,
|
||||
shuffle,
|
||||
}: {
|
||||
songInfo?: SongInfo;
|
||||
volumeState?: VolumeState;
|
||||
repeat: RepeatMode;
|
||||
shuffle: boolean;
|
||||
}): PlayerState => ({
|
||||
song: songInfo,
|
||||
isPlaying: songInfo ? !songInfo.isPaused : false,
|
||||
muted: volumeState?.isMuted ?? false,
|
||||
position: songInfo?.elapsedSeconds ?? 0,
|
||||
volume: volumeState?.state ?? 100,
|
||||
repeat,
|
||||
shuffle,
|
||||
});
|
||||
|
||||
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 };
|
||||
});
|
||||
|
||||
ipc.on('ytmd:volume-changed', (newVolumeState: VolumeState) => {
|
||||
volumeState = newVolumeState;
|
||||
send(DataTypes.VolumeChanged, {
|
||||
volume: volumeState.state,
|
||||
muted: volumeState.isMuted,
|
||||
});
|
||||
});
|
||||
|
||||
ipc.on('ytmd:repeat-changed', (mode: RepeatMode) => {
|
||||
repeat = mode;
|
||||
send(DataTypes.RepeatChanged, { repeat });
|
||||
});
|
||||
|
||||
ipc.on('ytmd:seeked', (t: number) => {
|
||||
send(DataTypes.PositionChanged, { position: t });
|
||||
});
|
||||
|
||||
ipc.on('ytmd:shuffle-changed', (newShuffle: boolean) => {
|
||||
shuffle = newShuffle;
|
||||
send(DataTypes.ShuffleChanged, { shuffle });
|
||||
});
|
||||
|
||||
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',
|
||||
},
|
||||
},
|
||||
}),
|
||||
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,
|
||||
shuffle,
|
||||
}),
|
||||
}),
|
||||
);
|
||||
},
|
||||
|
||||
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,17 +1,15 @@
|
||||
import net from 'net';
|
||||
|
||||
import { SocksClient, SocksClientOptions } from 'socks';
|
||||
import * as net from 'node:net';
|
||||
|
||||
import { SocksClient, type SocksClientOptions } from 'socks';
|
||||
import is from 'electron-is';
|
||||
|
||||
import { createBackend, LoggerPrefix } from '@/utils';
|
||||
|
||||
import { BackendType } from './types';
|
||||
import * as config from '@/config';
|
||||
|
||||
import config from '@/config';
|
||||
|
||||
import { AuthProxyConfig, defaultAuthProxyConfig } from '../config';
|
||||
import { type AuthProxyConfig, defaultAuthProxyConfig } from '../config';
|
||||
|
||||
import type { BackendType } from './types';
|
||||
import type { BackendContext } from '@/types/contexts';
|
||||
|
||||
// Parsing the upstream authentication SOCK proxy URL
|
||||
|
||||
@ -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);
|
||||
},
|
||||
});
|
||||
@ -98,8 +98,11 @@ export class DiscordService {
|
||||
|
||||
const activityInfo: SetActivity = {
|
||||
type: ActivityType.Listening,
|
||||
details: truncateString(songInfo.title, 128), // Song title
|
||||
state: truncateString(songInfo.artist, 128), // Artist name
|
||||
statusDisplayType: config.statusDisplayType,
|
||||
details: truncateString(songInfo.alternativeTitle ?? songInfo.title, 128), // Song title
|
||||
detailsUrl: songInfo.url ?? undefined,
|
||||
state: truncateString(songInfo.tags?.at(0) ?? songInfo.artist, 128), // Artist name
|
||||
stateUrl: songInfo.artistUrl,
|
||||
largeImageKey: songInfo.imageSrc ?? undefined,
|
||||
largeImageText: songInfo.album
|
||||
? truncateString(songInfo.album, 128)
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
import { StatusDisplayType } from 'discord-api-types/v10';
|
||||
|
||||
import { createPlugin } from '@/utils';
|
||||
import { backend } from './main';
|
||||
import { onMenu } from './menu';
|
||||
@ -33,6 +35,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 StatusDisplayType)[keyof typeof StatusDisplayType];
|
||||
};
|
||||
|
||||
export default createPlugin({
|
||||
@ -47,6 +53,7 @@ export default createPlugin({
|
||||
playOnYouTubeMusic: true,
|
||||
hideGitHubButton: false,
|
||||
hideDurationLeft: false,
|
||||
statusDisplayType: StatusDisplayType.Details,
|
||||
} 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';
|
||||
|
||||
@ -1,22 +1,30 @@
|
||||
import prompt from 'custom-electron-prompt';
|
||||
|
||||
import { discordService } from './main';
|
||||
import { StatusDisplayType } from 'discord-api-types/v10';
|
||||
|
||||
import { discordService } from './main';
|
||||
import { singleton } from '@/providers/decorators';
|
||||
import promptOptions from '@/providers/prompt-options';
|
||||
import { setMenuOptions } from '@/config/plugins';
|
||||
|
||||
import { t } from '@/i18n';
|
||||
|
||||
import type { MenuContext } from '@/types/contexts';
|
||||
import type { DiscordPluginConfig } from './index';
|
||||
|
||||
import type { MenuTemplate } from '@/menu';
|
||||
|
||||
const registerRefreshOnce = singleton((refreshMenu: () => void) => {
|
||||
discordService?.registerRefreshCallback(refreshMenu);
|
||||
});
|
||||
|
||||
const DiscordStatusDisplayTypeLabels: Record<StatusDisplayType, string> = {
|
||||
[StatusDisplayType.Name]:
|
||||
'plugins.discord.menu.set-status-display-type.submenu.youtube-music',
|
||||
[StatusDisplayType.State]:
|
||||
'plugins.discord.menu.set-status-display-type.submenu.artist',
|
||||
[StatusDisplayType.Details]:
|
||||
'plugins.discord.menu.set-status-display-type.submenu.title',
|
||||
};
|
||||
|
||||
export const onMenu = async ({
|
||||
window,
|
||||
getConfig,
|
||||
@ -92,6 +100,27 @@ 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(StatusDisplayType)
|
||||
.filter(
|
||||
(v) => typeof StatusDisplayType[v as StatusDisplayType] !== 'number',
|
||||
)
|
||||
.map((statusDisplayType) => ({
|
||||
label: t(
|
||||
DiscordStatusDisplayTypeLabels[
|
||||
statusDisplayType as StatusDisplayType
|
||||
],
|
||||
),
|
||||
type: 'radio',
|
||||
checked: config.statusDisplayType === statusDisplayType,
|
||||
click() {
|
||||
setConfig({
|
||||
statusDisplayType: statusDisplayType as 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) =>
|
||||
|
||||
@ -2,11 +2,14 @@ import { createSignal } from 'solid-js';
|
||||
|
||||
import { render } from 'solid-js/web';
|
||||
|
||||
import defaultConfig from '@/config/defaults';
|
||||
import { defaultConfig } from '@/config/defaults';
|
||||
import { getSongMenu } from '@/providers/dom-elements';
|
||||
import { getSongInfo } from '@/providers/song-info-front';
|
||||
import { t } from '@/i18n';
|
||||
import { isMusicOrVideoTrack } from '@/plugins/utils/renderer/check';
|
||||
import {
|
||||
isAlbumOrPlaylist,
|
||||
isMusicOrVideoTrack,
|
||||
} from '@/plugins/utils/renderer/check';
|
||||
|
||||
import { DownloadButton } from './templates/download';
|
||||
|
||||
@ -25,7 +28,7 @@ const menuObserver = new MutationObserver(() => {
|
||||
if (
|
||||
!menu ||
|
||||
menu.contains(buttonContainer) ||
|
||||
!isMusicOrVideoTrack() ||
|
||||
!(isMusicOrVideoTrack() || isAlbumOrPlaylist()) ||
|
||||
!buttonContainer
|
||||
) {
|
||||
return;
|
||||
|
||||
@ -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,11 @@ 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}
|
||||
id={'ytmd-title-bar-main-panel'}
|
||||
>
|
||||
<IconButton
|
||||
onClick={() => setCollapsed(!collapsed())}
|
||||
@ -336,7 +337,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 +345,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 +377,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 +406,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 +434,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';
|
||||
@ -7,8 +7,9 @@ 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 { getSongControls } from '@/providers/song-controls';
|
||||
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
|
||||
|
||||
@ -13,11 +13,17 @@ export interface ScrobblerPluginConfig {
|
||||
*/
|
||||
scrobbleOtherMedia: boolean;
|
||||
/**
|
||||
* Use alternative titles for scrobbling (Useful for non-roman song titles)
|
||||
* Use alternative titles for scrobbling (Useful for non-roman song titles, e.g. (Not) A Devil -> デビルじゃないもん)
|
||||
*
|
||||
* @default false
|
||||
* @default true
|
||||
*/
|
||||
alternativeTitles: boolean;
|
||||
/**
|
||||
* Use alternative artist for scrobbling (e.g., DECO27 & (or) PinocchioP -> DECO27 / marasy -> まらしぃ)
|
||||
*
|
||||
* @default true
|
||||
*/
|
||||
alternativeArtist: boolean;
|
||||
scrobblers: {
|
||||
lastfm: {
|
||||
/**
|
||||
@ -77,7 +83,8 @@ export interface ScrobblerPluginConfig {
|
||||
export const defaultConfig: ScrobblerPluginConfig = {
|
||||
enabled: false,
|
||||
scrobbleOtherMedia: true,
|
||||
alternativeTitles: false,
|
||||
alternativeTitles: true,
|
||||
alternativeArtist: true,
|
||||
scrobblers: {
|
||||
lastfm: {
|
||||
enabled: false,
|
||||
|
||||
@ -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';
|
||||
@ -105,6 +105,15 @@ export const onMenu = async ({
|
||||
setConfig(config);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: t('plugins.scrobbler.menu.scrobble-alternative-artist'),
|
||||
type: 'checkbox',
|
||||
checked: Boolean(config.alternativeArtist),
|
||||
click(item) {
|
||||
config.alternativeArtist = item.checked;
|
||||
setConfig(config);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Last.fm',
|
||||
submenu: [
|
||||
|
||||
@ -132,10 +132,15 @@ export class LastFmScrobbler extends ScrobblerBase {
|
||||
? songInfo.alternativeTitle
|
||||
: songInfo.title;
|
||||
|
||||
const artist =
|
||||
config.alternativeArtist && songInfo.tags?.at(0) !== undefined
|
||||
? songInfo.tags?.at(0)
|
||||
: songInfo.artist;
|
||||
|
||||
const postData: LastFmSongData = {
|
||||
track: title,
|
||||
duration: songInfo.songDuration,
|
||||
artist: songInfo.artist,
|
||||
artist: artist,
|
||||
...(songInfo.album ? { album: songInfo.album } : undefined), // Will be undefined if current song is a video
|
||||
api_key: config.scrobblers.lastfm.apiKey,
|
||||
sk: config.scrobblers.lastfm.sessionKey,
|
||||
|
||||
@ -81,8 +81,13 @@ function createRequestBody(
|
||||
? songInfo.alternativeTitle
|
||||
: songInfo.title;
|
||||
|
||||
const artist =
|
||||
config.alternativeArtist && songInfo.tags?.at(0) !== undefined
|
||||
? songInfo.tags?.at(0)
|
||||
: songInfo.artist;
|
||||
|
||||
const trackMetadata = {
|
||||
artist_name: songInfo.artist,
|
||||
artist_name: artist,
|
||||
track_name: title,
|
||||
release_name: songInfo.album ?? undefined,
|
||||
additional_info: {
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
import { BrowserWindow, globalShortcut } from 'electron';
|
||||
import { type BrowserWindow, globalShortcut } from 'electron';
|
||||
import is from 'electron-is';
|
||||
import { register as registerElectronLocalShortcut } from 'electron-localshortcut';
|
||||
|
||||
import registerMPRIS from './mpris';
|
||||
import getSongControls from '@/providers/song-controls';
|
||||
import { registerMPRIS } from './mpris';
|
||||
import { getSongControls } from '@/providers/song-controls';
|
||||
|
||||
import type { ShortcutMappingType, ShortcutsPluginConfig } from './index';
|
||||
|
||||
|
||||
@ -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,28 +1,29 @@
|
||||
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';
|
||||
import getSongControls from '@/providers/song-controls';
|
||||
import config from '@/config';
|
||||
import { getSongControls } from '@/providers/song-controls';
|
||||
import * as 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 {
|
||||
@ -83,7 +84,7 @@ function setupMPRIS() {
|
||||
return instance;
|
||||
}
|
||||
|
||||
function registerMPRIS(win: BrowserWindow) {
|
||||
export function registerMPRIS(win: BrowserWindow) {
|
||||
const songControls = getSongControls(win);
|
||||
const {
|
||||
playPause,
|
||||
@ -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) => {
|
||||
@ -360,5 +363,3 @@ function registerMPRIS(win: BrowserWindow) {
|
||||
console.trace(error);
|
||||
}
|
||||
}
|
||||
|
||||
export default registerMPRIS;
|
||||
|
||||
@ -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) =>
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
import { t } from '@/i18n';
|
||||
|
||||
import { providerNames } from './providers';
|
||||
|
||||
import type { MenuItemConstructorOptions } from 'electron';
|
||||
import type { MenuContext } from '@/types/contexts';
|
||||
import type { SyncedLyricsPluginConfig } from './types';
|
||||
@ -10,6 +12,35 @@ export const menu = async (
|
||||
const config = await ctx.getConfig();
|
||||
|
||||
return [
|
||||
{
|
||||
label: t('plugins.synced-lyrics.menu.preferred-provider.label'),
|
||||
toolTip: t('plugins.synced-lyrics.menu.preferred-provider.tooltip'),
|
||||
type: 'submenu',
|
||||
submenu: [
|
||||
{
|
||||
label: t('plugins.synced-lyrics.menu.preferred-provider.none.label'),
|
||||
toolTip: t(
|
||||
'plugins.synced-lyrics.menu.preferred-provider.none.tooltip',
|
||||
),
|
||||
type: 'radio',
|
||||
checked: config.preferredProvider === undefined,
|
||||
click() {
|
||||
ctx.setConfig({ preferredProvider: undefined });
|
||||
},
|
||||
},
|
||||
...providerNames.map(
|
||||
(provider) =>
|
||||
({
|
||||
label: provider,
|
||||
type: 'radio',
|
||||
checked: config.preferredProvider === provider,
|
||||
click() {
|
||||
ctx.setConfig({ preferredProvider: provider });
|
||||
},
|
||||
}) as const,
|
||||
),
|
||||
],
|
||||
},
|
||||
{
|
||||
label: t('plugins.synced-lyrics.menu.precise-timing.label'),
|
||||
toolTip: t('plugins.synced-lyrics.menu.precise-timing.tooltip'),
|
||||
@ -93,47 +124,23 @@ export const menu = async (
|
||||
toolTip: t('plugins.synced-lyrics.menu.default-text-string.tooltip'),
|
||||
type: 'submenu',
|
||||
submenu: [
|
||||
{
|
||||
label: '♪',
|
||||
type: 'radio',
|
||||
checked: config.defaultTextString === '♪',
|
||||
click() {
|
||||
ctx.setConfig({
|
||||
defaultTextString: '♪',
|
||||
});
|
||||
},
|
||||
{ label: '♪', value: '♪' },
|
||||
{ label: '" "', value: ' ' },
|
||||
{ label: '...', value: ['.', '..', '...'] },
|
||||
{ label: '•••', value: ['•', '••', '•••'] },
|
||||
{ label: '———', value: '———' },
|
||||
].map(({ label, value }) => ({
|
||||
label,
|
||||
type: 'radio',
|
||||
checked:
|
||||
typeof value === 'string'
|
||||
? config.defaultTextString === value
|
||||
: JSON.stringify(config.defaultTextString) ===
|
||||
JSON.stringify(value),
|
||||
click() {
|
||||
ctx.setConfig({ defaultTextString: value });
|
||||
},
|
||||
{
|
||||
label: '" "',
|
||||
type: 'radio',
|
||||
checked: config.defaultTextString === ' ',
|
||||
click() {
|
||||
ctx.setConfig({
|
||||
defaultTextString: ' ',
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '...',
|
||||
type: 'radio',
|
||||
checked: config.defaultTextString === '...',
|
||||
click() {
|
||||
ctx.setConfig({
|
||||
defaultTextString: '...',
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '———',
|
||||
type: 'radio',
|
||||
checked: config.defaultTextString === '———',
|
||||
click() {
|
||||
ctx.setConfig({
|
||||
defaultTextString: '———',
|
||||
});
|
||||
},
|
||||
},
|
||||
],
|
||||
})),
|
||||
},
|
||||
{
|
||||
label: t('plugins.synced-lyrics.menu.romanization.label'),
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { z } from 'zod';
|
||||
import * as z from 'zod';
|
||||
|
||||
import { LRC } from '../parsers/lrc';
|
||||
import { netFetch } from '../renderer';
|
||||
|
||||
@ -1,192 +1,21 @@
|
||||
import { createStore } from 'solid-js/store';
|
||||
import * as z from 'zod';
|
||||
|
||||
import { createMemo } from 'solid-js';
|
||||
import type { LyricResult } from '../types';
|
||||
|
||||
import { SongInfo } from '@/providers/song-info';
|
||||
export enum ProviderNames {
|
||||
YTMusic = 'YTMusic',
|
||||
LRCLib = 'LRCLib',
|
||||
MusixMatch = 'MusixMatch',
|
||||
LyricsGenius = 'LyricsGenius',
|
||||
// Megalobiz = 'Megalobiz',
|
||||
}
|
||||
|
||||
import { LRCLib } from './LRCLib';
|
||||
import { LyricsGenius } from './LyricsGenius';
|
||||
import { MusixMatch } from './MusixMatch';
|
||||
import { YTMusic } from './YTMusic';
|
||||
|
||||
import { getSongInfo } from '@/providers/song-info-front';
|
||||
|
||||
import type { LyricProvider, LyricResult } from '../types';
|
||||
|
||||
export const providers = {
|
||||
YTMusic: new YTMusic(),
|
||||
LRCLib: new LRCLib(),
|
||||
MusixMatch: new MusixMatch(),
|
||||
LyricsGenius: new LyricsGenius(),
|
||||
// Megalobiz: new Megalobiz(), // Disabled because it is too unstable and slow
|
||||
} as const;
|
||||
|
||||
export type ProviderName = keyof typeof providers;
|
||||
export const providerNames = Object.keys(providers) as ProviderName[];
|
||||
export const ProviderNameSchema = z.enum(ProviderNames);
|
||||
export type ProviderName = z.infer<typeof ProviderNameSchema>;
|
||||
export const providerNames = ProviderNameSchema.options;
|
||||
|
||||
export type ProviderState = {
|
||||
state: 'fetching' | 'done' | 'error';
|
||||
data: LyricResult | null;
|
||||
error: Error | null;
|
||||
};
|
||||
|
||||
type LyricsStore = {
|
||||
provider: ProviderName;
|
||||
current: ProviderState;
|
||||
lyrics: Record<ProviderName, ProviderState>;
|
||||
};
|
||||
|
||||
const initialData = () =>
|
||||
providerNames.reduce(
|
||||
(acc, name) => {
|
||||
acc[name] = { state: 'fetching', data: null, error: null };
|
||||
return acc;
|
||||
},
|
||||
{} as LyricsStore['lyrics'],
|
||||
);
|
||||
|
||||
export const [lyricsStore, setLyricsStore] = createStore<LyricsStore>({
|
||||
provider: providerNames[0],
|
||||
lyrics: initialData(),
|
||||
get current(): ProviderState {
|
||||
return this.lyrics[this.provider];
|
||||
},
|
||||
});
|
||||
|
||||
export const currentLyrics = createMemo(() => {
|
||||
const provider = lyricsStore.provider;
|
||||
return lyricsStore.lyrics[provider];
|
||||
});
|
||||
|
||||
type VideoId = string;
|
||||
|
||||
type SearchCacheData = Record<ProviderName, ProviderState>;
|
||||
interface SearchCache {
|
||||
state: 'loading' | 'done';
|
||||
data: SearchCacheData;
|
||||
}
|
||||
|
||||
// TODO: Maybe use localStorage for the cache.
|
||||
const searchCache = new Map<VideoId, SearchCache>();
|
||||
export const fetchLyrics = (info: SongInfo) => {
|
||||
if (searchCache.has(info.videoId)) {
|
||||
const cache = searchCache.get(info.videoId)!;
|
||||
|
||||
if (cache.state === 'loading') {
|
||||
setTimeout(() => {
|
||||
fetchLyrics(info);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (getSongInfo().videoId === info.videoId) {
|
||||
setLyricsStore('lyrics', () => {
|
||||
// weird bug with solid-js
|
||||
return JSON.parse(JSON.stringify(cache.data)) as typeof cache.data;
|
||||
});
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const cache: SearchCache = {
|
||||
state: 'loading',
|
||||
data: initialData(),
|
||||
};
|
||||
|
||||
searchCache.set(info.videoId, cache);
|
||||
if (getSongInfo().videoId === info.videoId) {
|
||||
setLyricsStore('lyrics', () => {
|
||||
// weird bug with solid-js
|
||||
return JSON.parse(JSON.stringify(cache.data)) as typeof cache.data;
|
||||
});
|
||||
}
|
||||
|
||||
const tasks: Promise<void>[] = [];
|
||||
|
||||
// prettier-ignore
|
||||
for (
|
||||
const [providerName, provider] of Object.entries(providers) as [
|
||||
ProviderName,
|
||||
LyricProvider,
|
||||
][]
|
||||
) {
|
||||
const pCache = cache.data[providerName];
|
||||
|
||||
tasks.push(
|
||||
provider
|
||||
.search(info)
|
||||
.then((res) => {
|
||||
pCache.state = 'done';
|
||||
pCache.data = res;
|
||||
|
||||
if (getSongInfo().videoId === info.videoId) {
|
||||
setLyricsStore('lyrics', (old) => {
|
||||
return {
|
||||
...old,
|
||||
[providerName]: {
|
||||
state: 'done',
|
||||
data: res ? { ...res } : null,
|
||||
error: null,
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch((error: Error) => {
|
||||
pCache.state = 'error';
|
||||
pCache.error = error;
|
||||
|
||||
console.error(error);
|
||||
|
||||
if (getSongInfo().videoId === info.videoId) {
|
||||
setLyricsStore('lyrics', (old) => {
|
||||
return {
|
||||
...old,
|
||||
[providerName]: { state: 'error', error, data: null },
|
||||
};
|
||||
});
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
Promise.allSettled(tasks).then(() => {
|
||||
cache.state = 'done';
|
||||
searchCache.set(info.videoId, cache);
|
||||
});
|
||||
};
|
||||
|
||||
export const retrySearch = (provider: ProviderName, info: SongInfo) => {
|
||||
setLyricsStore('lyrics', (old) => {
|
||||
const pCache = {
|
||||
state: 'fetching',
|
||||
data: null,
|
||||
error: null,
|
||||
};
|
||||
|
||||
return {
|
||||
...old,
|
||||
[provider]: pCache,
|
||||
};
|
||||
});
|
||||
|
||||
providers[provider]
|
||||
.search(info)
|
||||
.then((res) => {
|
||||
setLyricsStore('lyrics', (old) => {
|
||||
return {
|
||||
...old,
|
||||
[provider]: { state: 'done', data: res, error: null },
|
||||
};
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
setLyricsStore('lyrics', (old) => {
|
||||
return {
|
||||
...old,
|
||||
[provider]: { state: 'error', data: null, error },
|
||||
};
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user