Compare commits

..

1 Commits

Author SHA1 Message Date
2382ada8fe fix(deps): update dependency solid-js to v1.9.10 2025-12-31 16:47:54 +00:00
40 changed files with 2093 additions and 2208 deletions

View File

@ -1,16 +0,0 @@
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
// README at: https://github.com/devcontainers/templates/tree/main/src/typescript-node
{
"name": "Pear Desktop - Dev Container",
// Keep in sync with `.github/workflows/build.yml`
"image": "mcr.microsoft.com/devcontainers/typescript-node:24",
// Features to add to the dev container. More info: https://containers.dev/features.
"features": {},
"postCreateCommand": "pnpm install --frozen-lockfile"
// Use 'forwardPorts' to make a list of ports inside the container available locally.
// "forwardPorts": [],
// Configure tool-specific properties.
// "customizations": {},
// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
// "remoteUser": "root"
}

View File

@ -16,7 +16,7 @@ jobs:
strategy: strategy:
fail-fast: true fail-fast: true
matrix: matrix:
os: [ macos-26, ubuntu-latest, windows-latest ] os: [ macos-latest, ubuntu-latest, windows-latest ]
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v5
@ -29,14 +29,14 @@ jobs:
- name: Setup NodeJS - name: Setup NodeJS
if: startsWith(matrix.os, 'macOS') != true if: startsWith(matrix.os, 'macOS') != true
uses: actions/setup-node@v6 uses: actions/setup-node@v5
with: with:
node-version: ${{ env.NODE_VERSION }} node-version: ${{ env.NODE_VERSION }}
cache: 'pnpm' cache: 'pnpm'
- name: Setup NodeJS for macOS - name: Setup NodeJS for macOS
if: startsWith(matrix.os, 'macOS') if: startsWith(matrix.os, 'macOS')
uses: actions/setup-node@v6 uses: actions/setup-node@v5
with: with:
node-version: ${{ env.NODE_VERSION }} node-version: ${{ env.NODE_VERSION }}
@ -104,14 +104,14 @@ jobs:
- name: Setup NodeJS - name: Setup NodeJS
if: startsWith(matrix.os, 'macOS') != true if: startsWith(matrix.os, 'macOS') != true
uses: actions/setup-node@v6 uses: actions/setup-node@v5
with: with:
node-version: ${{ env.NODE_VERSION }} node-version: ${{ env.NODE_VERSION }}
cache: 'pnpm' cache: 'pnpm'
- name: Setup NodeJS for macOS - name: Setup NodeJS for macOS
if: startsWith(matrix.os, 'macOS') if: startsWith(matrix.os, 'macOS')
uses: actions/setup-node@v6 uses: actions/setup-node@v5
with: with:
node-version: ${{ env.NODE_VERSION }} node-version: ${{ env.NODE_VERSION }}

View File

@ -43,14 +43,14 @@ jobs:
- name: Setup NodeJS - name: Setup NodeJS
if: startsWith(matrix.os, 'macOS') != true if: startsWith(matrix.os, 'macOS') != true
uses: actions/setup-node@v6 uses: actions/setup-node@v5
with: with:
node-version: ${{ env.NODE_VERSION }} node-version: ${{ env.NODE_VERSION }}
cache: 'pnpm' cache: 'pnpm'
- name: Setup NodeJS for macOS - name: Setup NodeJS for macOS
if: startsWith(matrix.os, 'macOS') if: startsWith(matrix.os, 'macOS')
uses: actions/setup-node@v6 uses: actions/setup-node@v5
with: with:
node-version: ${{ env.NODE_VERSION }} node-version: ${{ env.NODE_VERSION }}
@ -103,7 +103,7 @@ jobs:
pull-requests: write pull-requests: write
steps: steps:
- name: Create comment - name: Create comment
uses: actions/github-script@v8 uses: actions/github-script@v7
with: with:
script: | script: |
const runId = context.runId; const runId = context.runId;

View File

@ -26,7 +26,7 @@ jobs:
run_install: false run_install: false
- name: Setup NodeJS - name: Setup NodeJS
uses: actions/setup-node@v6 uses: actions/setup-node@v5
with: with:
node-version: ${{ env.NODE_VERSION }} node-version: ${{ env.NODE_VERSION }}
cache: 'pnpm' cache: 'pnpm'

1
.gitignore vendored
View File

@ -5,7 +5,6 @@ node_modules
.idea .idea
.pnp.* .pnp.*
.pnpm-store
.yarn/* .yarn/*
!.yarn/patches !.yarn/patches
!.yarn/plugins !.yarn/plugins

15
.vscode/launch.json vendored
View File

@ -1,15 +0,0 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "node-terminal",
"name": "Run Script: dev (pear-desktop)",
"request": "launch",
"command": "pnpm run dev",
"cwd": "${workspaceFolder}"
}
]
}

View File

@ -1,17 +1,3 @@
<div align="center" markdown="1">
<sup>Special thanks to:</sup>
<br>
<br>
<a href="https://go.warp.dev/pear-desktop">
<img alt="Warp sponsorship" width="400" src="https://github.com/user-attachments/assets/8307ea56-e872-494a-8a9c-de0e296a06ed" />
</a>
### [Warp, built for coding with multiple AI agents](https://go.warp.dev/pear-desktop)
[Available for macOS, Linux, & Windows](https://go.warp.dev/pear-desktop)<br>
</div>
<hr>
<div align="center"> <div align="center">
# :pear: Pear Desktop # :pear: Pear Desktop
@ -69,7 +55,7 @@
You can help with translation on [Hosted Weblate](https://bit.ly/48n5YF7). You can help with translation on [Hosted Weblate](https://bit.ly/48n5YF7).
<a href="https://bit.ly/48n5YF7"> <a href="https://bit.ly/48n5YF7/">
<img src="https://bit.ly/4q83L6S" alt="translation status" /> <img src="https://bit.ly/4q83L6S" alt="translation status" />
<img src="https://bit.ly/4h3zBxo" alt="translation status 2" /> <img src="https://bit.ly/4h3zBxo" alt="translation status 2" />
</a> </a>
@ -86,10 +72,10 @@ this [wiki page](https://wiki.archlinux.org/index.php/Arch_User_Repository#Insta
### macOS ### macOS
You can install the app using Homebrew (see the [cask definition](https://github.com/pear-devs/homebrew-pear)): You can install the app using Homebrew (see the [cask definition](https://github.com/pear-devs/pear-desktop-homebrew)):
```bash ```bash
brew install pear-devs/pear/pear-desktop brew install pear-devs/pear-desktop
``` ```
If you install the app manually and get an error "is damaged and cant be opened." when launching the app, run the following in the Terminal: If you install the app manually and get an error "is damaged and cant be opened." when launching the app, run the following in the Terminal:
@ -144,10 +130,6 @@ pnpm install --frozen-lockfile
pnpm dev pnpm dev
``` ```
Instead of installing pnpm on your system, you can also use [devcontainers](https://containers.dev/). You can use devcontainers either as a development environment in VS Code, or as a way to easily build the project without installing dependencies on your host system.
Note that this has it's own limitations (for example, GUI doesn't work on, at least some, Linux hosts).
## Build your own plugins ## Build your own plugins
Using plugins, you can: Using plugins, you can:
@ -283,16 +265,6 @@ export default createPlugin({
Builds the app for macOS, Linux, and Windows, Builds the app for macOS, Linux, and Windows,
using [electron-builder](https://github.com/electron-userland/electron-builder). using [electron-builder](https://github.com/electron-userland/electron-builder).
### Building in devcontainer
1. Clone the repo;
2. Open the folder in VS Code;
3. Reopen in container when prompted;
4. Run `pnpm build` as above (choosing the desired target);
5. Collect the built files from the `dist` folder.
Since devcontainer uses a mount for the workspace, the built files will be available on the host system as well.
## Production Preview ## Production Preview
```bash ```bash

View File

@ -109,7 +109,7 @@ deb:
- libgbm1 - libgbm1
rpm: rpm:
depends: depends:
- libuuid - /usr/lib64/libuuid.so.1
fpm: fpm:
- '--rpm-rpmbuild-define' - '--rpm-rpmbuild-define'
- _build_id_links none - _build_id_links none

View File

@ -1,7 +1,7 @@
import { dirname, join, resolve } from 'node:path'; import { dirname, join, resolve } from 'node:path';
import { fileURLToPath } from 'node:url'; import { fileURLToPath } from 'node:url';
import { defineConfig } from 'electron-vite'; import { defineConfig, defineViteConfig } from 'electron-vite';
import builtinModules from 'builtin-modules'; import builtinModules from 'builtin-modules';
import Inspect from 'vite-plugin-inspect'; import Inspect from 'vite-plugin-inspect';
@ -21,10 +21,9 @@ const resolveAlias = {
'@assets': resolve(__dirname, './assets'), '@assets': resolve(__dirname, './assets'),
}; };
export default defineConfig(({ mode }) => { export default defineConfig({
const isDev = mode === 'development'; main: defineViteConfig(({ mode }) => {
const commonConfig: UserConfig = {
const mainConfig: UserConfig = {
experimental: { experimental: {
enableNativePlugin: true, enableNativePlugin: true,
}, },
@ -37,8 +36,8 @@ export default defineConfig(({ mode }) => {
], ],
publicDir: 'assets', publicDir: 'assets',
define: { define: {
__dirname: 'import.meta.dirname', '__dirname': 'import.meta.dirname',
__filename: 'import.meta.filename', '__filename': 'import.meta.filename',
}, },
build: { build: {
lib: { lib: {
@ -50,16 +49,34 @@ export default defineConfig(({ mode }) => {
external: ['electron', 'custom-electron-prompt', ...builtinModules], external: ['electron', 'custom-electron-prompt', ...builtinModules],
input: './src/index.ts', input: './src/index.ts',
}, },
minify: !isDev,
cssMinify: !isDev,
sourcemap: isDev ? 'inline' : undefined,
}, },
resolve: { resolve: {
alias: resolveAlias, alias: resolveAlias,
}, },
}; };
const preloadConfig: UserConfig = { if (mode === 'development') {
commonConfig.build!.sourcemap = 'inline';
commonConfig.plugins?.push(
Inspect({
build: true,
outputDir: join(__dirname, '.vite-inspect/backend'),
}),
);
return commonConfig;
}
return {
...commonConfig,
build: {
...commonConfig.build,
minify: true,
cssMinify: true,
},
};
}),
preload: defineViteConfig(({ mode }) => {
const commonConfig: UserConfig = {
experimental: { experimental: {
enableNativePlugin: true, enableNativePlugin: true,
}, },
@ -83,18 +100,36 @@ export default defineConfig(({ mode }) => {
external: ['electron', 'custom-electron-prompt', ...builtinModules], external: ['electron', 'custom-electron-prompt', ...builtinModules],
input: './src/preload.ts', input: './src/preload.ts',
}, },
minify: !isDev,
cssMinify: !isDev,
sourcemap: isDev ? 'inline' : undefined,
}, },
resolve: { resolve: {
alias: resolveAlias, alias: resolveAlias,
}, },
}; };
const rendererConfig: UserConfig = { if (mode === 'development') {
commonConfig.build!.sourcemap = 'inline';
commonConfig.plugins?.push(
Inspect({
build: true,
outputDir: join(__dirname, '.vite-inspect/preload'),
}),
);
return commonConfig;
}
return {
...commonConfig,
build: {
...commonConfig.build,
minify: true,
cssMinify: true,
},
};
}),
renderer: defineViteConfig(({ mode }) => {
const commonConfig: UserConfig = {
experimental: { experimental: {
enableNativePlugin: !isDev, // Disable native plugin in development mode to avoid issues with HMR (bug in rolldown-vite) enableNativePlugin: mode !== 'development', // Disable native plugin in development mode to avoid issues with HMR (bug in rolldown-vite)
}, },
plugins: [ plugins: [
pluginLoader('renderer'), pluginLoader('renderer'),
@ -118,44 +153,36 @@ export default defineConfig(({ mode }) => {
external: ['electron', ...builtinModules], external: ['electron', ...builtinModules],
input: './src/index.html', input: './src/index.html',
}, },
minify: !isDev,
cssMinify: !isDev,
sourcemap: isDev ? 'inline' : undefined,
}, },
resolve: { resolve: {
alias: resolveAlias, alias: resolveAlias,
}, },
server: { server: {
cors: { cors: {
origin: 'https://music.\u0079\u006f\u0075\u0074\u0075\u0062\u0065.com', origin:
'https://music.\u0079\u006f\u0075\u0074\u0075\u0062\u0065.com',
}, },
}, },
}; };
if (isDev) { if (mode === 'development') {
mainConfig.plugins?.push( commonConfig.build!.sourcemap = 'inline';
Inspect({ commonConfig.plugins?.push(
build: true,
outputDir: join(__dirname, '.vite-inspect/backend'),
}),
);
preloadConfig.plugins?.push(
Inspect({
build: true,
outputDir: join(__dirname, '.vite-inspect/preload'),
}),
);
rendererConfig.plugins?.push(
Inspect({ Inspect({
build: true, build: true,
outputDir: join(__dirname, '.vite-inspect/renderer'), outputDir: join(__dirname, '.vite-inspect/renderer'),
}), }),
); );
return commonConfig;
} }
return { return {
main: mainConfig, ...commonConfig,
preload: preloadConfig, build: {
renderer: rendererConfig, ...commonConfig.build,
minify: true,
cssMinify: true,
},
}; };
}),
}); });

View File

@ -45,12 +45,12 @@
}, },
"pnpm": { "pnpm": {
"overrides": { "overrides": {
"vite": "npm:rolldown-vite@7.3.1", "vite": "npm:rolldown-vite@7.3.0",
"node-gyp": "11.5.0", "node-gyp": "11.4.2",
"xml2js": "0.6.2", "xml2js": "0.6.2",
"node-fetch": "3.3.2", "node-fetch": "3.3.2",
"@electron/universal": "3.0.2", "@electron/universal": "3.0.2",
"@babel/runtime": "7.28.6" "@babel/runtime": "7.28.4"
}, },
"patchedDependencies": { "patchedDependencies": {
"vudio@2.1.1": "patches/vudio@2.1.1.patch", "vudio@2.1.1": "patches/vudio@2.1.1.patch",
@ -67,14 +67,14 @@
"@electron-toolkit/tsconfig": "1.0.1", "@electron-toolkit/tsconfig": "1.0.1",
"@electron/remote": "2.1.3", "@electron/remote": "2.1.3",
"@ffmpeg.wasm/core-mt": "0.12.0", "@ffmpeg.wasm/core-mt": "0.12.0",
"@ffmpeg.wasm/main": "0.13.1", "@ffmpeg.wasm/main": "0.12.0",
"@floating-ui/dom": "1.7.5", "@floating-ui/dom": "1.7.4",
"@foobar404/wave": "2.0.5", "@foobar404/wave": "2.0.5",
"@ghostery/adblocker-electron": "2.13.4", "@ghostery/adblocker-electron": "2.11.6",
"@ghostery/adblocker-electron-preload": "2.13.4", "@ghostery/adblocker-electron-preload": "2.11.6",
"@hono/node-server": "1.19.9", "@hono/node-server": "1.19.7",
"@hono/node-ws": "1.3.0", "@hono/node-ws": "1.2.0",
"@hono/swagger-ui": "0.5.3", "@hono/swagger-ui": "0.5.2",
"@hono/zod-openapi": "1.2.0", "@hono/zod-openapi": "1.2.0",
"@hono/zod-validator": "0.7.6", "@hono/zod-validator": "0.7.6",
"@jellybrick/dbus-next": "0.10.3", "@jellybrick/dbus-next": "0.10.3",
@ -90,7 +90,7 @@
"butterchurn-presets": "3.0.0-beta.4", "butterchurn-presets": "3.0.0-beta.4",
"color": "5.0.3", "color": "5.0.3",
"conf": "14.0.0", "conf": "14.0.0",
"custom-electron-prompt": "1.6.1", "custom-electron-prompt": "1.5.8",
"deepmerge-ts": "7.1.5", "deepmerge-ts": "7.1.5",
"delay": "6.0.0", "delay": "6.0.0",
"electron-debug": "4.1.0", "electron-debug": "4.1.0",
@ -98,15 +98,15 @@
"electron-localshortcut": "3.2.1", "electron-localshortcut": "3.2.1",
"electron-store": "10.1.0", "electron-store": "10.1.0",
"electron-unhandled": "5.0.0", "electron-unhandled": "5.0.0",
"electron-updater": "6.7.3", "electron-updater": "6.6.2",
"es-hangul": "2.3.8", "es-hangul": "2.3.8",
"fast-average-color": "9.5.0", "fast-average-color": "9.5.0",
"fast-equals": "5.4.0", "fast-equals": "5.2.2",
"fflate": "0.8.2", "fflate": "0.8.2",
"filenamify": "6.0.0", "filenamify": "6.0.0",
"hanja": "1.1.5", "hanja": "1.1.5",
"happy-dom": "20.4.0", "happy-dom": "20.0.11",
"hono": "4.11.7", "hono": "4.10.3",
"howler": "2.2.4", "howler": "2.2.4",
"html-to-text": "9.0.5", "html-to-text": "9.0.5",
"i18next": "25.5.2", "i18next": "25.5.2",
@ -118,7 +118,7 @@
"kuroshiro-analyzer-kuromoji": "1.1.0", "kuroshiro-analyzer-kuromoji": "1.1.0",
"lazy-var": "2.2.2", "lazy-var": "2.2.2",
"mdui": "2.1.4", "mdui": "2.1.4",
"node-html-parser": "7.0.2", "node-html-parser": "7.0.1",
"node-id3": "0.2.9", "node-id3": "0.2.9",
"peerjs": "1.5.5", "peerjs": "1.5.5",
"semver": "7.7.3", "semver": "7.7.3",
@ -126,12 +126,12 @@
"socks": "2.8.7", "socks": "2.8.7",
"solid-element": "1.9.1", "solid-element": "1.9.1",
"solid-floating-ui": "0.3.1", "solid-floating-ui": "0.3.1",
"solid-js": "1.9.11", "solid-js": "1.9.10",
"solid-styled-components": "0.28.5", "solid-styled-components": "0.28.5",
"solid-transition-group": "0.3.0", "solid-transition-group": "0.3.0",
"tiny-pinyin": "1.3.2", "tiny-pinyin": "1.3.2",
"tinyld": "1.3.4", "tinyld": "1.3.4",
"virtua": "0.48.5", "virtua": "0.48.2",
"vudio": "2.1.1", "vudio": "2.1.1",
"x11": "2.3.0", "x11": "2.3.0",
"youtubei.js": "^16.0.1", "youtubei.js": "^16.0.1",
@ -139,44 +139,44 @@
}, },
"devDependencies": { "devDependencies": {
"@electron-toolkit/tsconfig": "1.0.1", "@electron-toolkit/tsconfig": "1.0.1",
"@eslint/js": "9.39.2", "@eslint/js": "9.35.0",
"@malept/flatpak-bundler": "0.4.0", "@malept/flatpak-bundler": "0.4.0",
"@playwright/test": "1.58.0", "@playwright/test": "1.55.0",
"@stylistic/eslint-plugin": "5.7.1", "@stylistic/eslint-plugin": "5.3.1",
"@total-typescript/ts-reset": "0.6.1", "@total-typescript/ts-reset": "0.6.1",
"@types/electron-localshortcut": "3.1.3", "@types/electron-localshortcut": "3.1.3",
"@types/howler": "2.2.12", "@types/howler": "2.2.12",
"@types/html-to-text": "9.0.4", "@types/html-to-text": "9.0.4",
"@types/semver": "7.7.1", "@types/semver": "7.7.1",
"@types/trusted-types": "2.0.7", "@types/trusted-types": "2.0.7",
"bufferutil": "4.1.0", "bufferutil": "4.0.9",
"builtin-modules": "5.0.0", "builtin-modules": "5.0.0",
"cross-env": "10.1.0", "cross-env": "10.0.0",
"del-cli": "6.0.0", "del-cli": "6.0.0",
"discord-api-types": "0.38.37", "discord-api-types": "0.38.37",
"electron": "38.8.0", "electron": "38.7.2",
"electron-builder": "26.4.0", "electron-builder": "26.4.0",
"electron-builder-squirrel-windows": "26.4.0", "electron-builder-squirrel-windows": "26.0.12",
"electron-devtools-installer": "4.0.0", "electron-devtools-installer": "4.0.0",
"electron-vite": "5.0.0", "electron-vite": "4.0.1",
"eslint": "9.39.2", "eslint": "9.35.0",
"eslint-config-prettier": "10.1.8", "eslint-config-prettier": "10.1.8",
"eslint-import-resolver-exports": "1.0.0-beta.5", "eslint-import-resolver-exports": "1.0.0-beta.5",
"eslint-import-resolver-typescript": "4.4.4", "eslint-import-resolver-typescript": "4.4.4",
"eslint-plugin-import": "2.32.0", "eslint-plugin-import": "2.32.0",
"eslint-plugin-prettier": "5.5.5", "eslint-plugin-prettier": "5.5.4",
"eslint-plugin-solid": "0.14.5", "eslint-plugin-solid": "0.14.5",
"glob": "11.1.0", "glob": "11.1.0",
"node-gyp": "11.5.0", "node-gyp": "11.4.2",
"ts-morph": "27.0.2", "ts-morph": "27.0.2",
"typescript": "5.9.3", "typescript": "5.9.3",
"typescript-eslint": "8.53.1", "typescript-eslint": "8.43.0",
"utf-8-validate": "6.0.6", "utf-8-validate": "6.0.6",
"vite": "npm:rolldown-vite@7.3.1", "vite": "npm:rolldown-vite@7.3.0",
"vite-plugin-inspect": "11.3.3", "vite-plugin-inspect": "11.3.3",
"vite-plugin-resolve": "2.5.2", "vite-plugin-resolve": "2.5.2",
"vite-plugin-solid": "2.11.10", "vite-plugin-solid": "2.11.10",
"ws": "8.19.0" "ws": "8.18.3"
}, },
"auto-changelog": { "auto-changelog": {
"hideCredit": true, "hideCredit": true,

2913
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -29,7 +29,6 @@ export interface DefaultConfig {
autoResetAppCache: boolean; autoResetAppCache: boolean;
resumeOnStart: boolean; resumeOnStart: boolean;
likeButtons: string; likeButtons: string;
swapLikeButtonsOrder: boolean;
proxy: string; proxy: string;
startingPage: string; startingPage: string;
backgroundMaterial?: 'none' | 'mica' | 'acrylic' | 'tabbed'; backgroundMaterial?: 'none' | 'mica' | 'acrylic' | 'tabbed';
@ -67,7 +66,6 @@ export const defaultConfig: DefaultConfig = {
autoResetAppCache: false, autoResetAppCache: false,
resumeOnStart: true, resumeOnStart: true,
likeButtons: '', likeButtons: '',
swapLikeButtonsOrder: false,
proxy: '', proxy: '',
startingPage: '', startingPage: '',
overrideUserAgent: false, overrideUserAgent: false,

View File

@ -321,22 +321,6 @@
"hostname": { "hostname": {
"label": "Nom del host" "label": "Nom del host"
}, },
"https": {
"label": "HTTPS i Certificats",
"submenu": {
"cert": {
"dialogTitle": "Seleccionar arxiu de certificat HTTPS",
"label": "Arxiu de certificat (.crt/.pem)"
},
"enable-https": {
"label": "Activa HTTPS"
},
"key": {
"dialogTitle": "Selecciona arxiu de clau HTTPS privada",
"label": "Arxiu de clau privada (.key/.pem)"
}
}
},
"port": { "port": {
"label": "Port" "label": "Port"
} }
@ -478,8 +462,8 @@
"set-status-display-type": { "set-status-display-type": {
"label": "Text d'estat", "label": "Text d'estat",
"submenu": { "submenu": {
"application": "Escoltant {{applicationName}}",
"artist": "Escoltant {artist}", "artist": "Escoltant {artist}",
"application": "Escoltant {{applicationName}}",
"title": "Escoltant {song title}" "title": "Escoltant {song title}"
} }
} }

View File

@ -883,12 +883,12 @@
}, },
"name": "Synchronisierte Texte", "name": "Synchronisierte Texte",
"refetch-btn": { "refetch-btn": {
"fetching": "Laden...", "fetching": "Hole Songtext...",
"normal": "Songtext neu laden" "normal": "Songtext neu holen"
}, },
"warnings": { "warnings": {
"duration-mismatch": "⚠️ - Es kann sein, dass die Synchronization nicht stimmt, da die Songdauer nicht übereinstimmt.", "duration-mismatch": "⚠️ - Es kann sein, dass die Synchronization nicht stimmt, da die Songdauer nicht übereinstimmt.",
"inexact": "⚠️ - Es ist möglich, dass der Songtext für diesen Song nicht übereinstimmt.", "inexact": "⚠️ - Der Songtext stimmt möglicherweise nicht überein",
"instrumental": "⚠️ - Das ist ein instrumentales Lied" "instrumental": "⚠️ - Das ist ein instrumentales Lied"
} }
}, },

View File

@ -154,7 +154,6 @@
"default": "Default", "default": "Default",
"force-show": "Force show", "force-show": "Force show",
"hide": "Hide", "hide": "Hide",
"swap": "Swap like buttons order",
"label": "Like buttons" "label": "Like buttons"
}, },
"custom-window-title": { "custom-window-title": {
@ -413,17 +412,6 @@
"no-captions": "No captions available for this song" "no-captions": "No captions available for this song"
} }
}, },
"clock": {
"description": "Add a clock to the navigation bar",
"name": "Clock",
"menu": {
"format": {
"label": "Format",
"display-seconds": "Display Seconds",
"24-hour-format": "24-Hour Format"
}
}
},
"compact-sidebar": { "compact-sidebar": {
"description": "Always set the sidebar in compact mode", "description": "Always set the sidebar in compact mode",
"name": "Compact Sidebar" "name": "Compact Sidebar"

View File

@ -128,7 +128,7 @@
}, },
"label": "Keel", "label": "Keel",
"submenu": { "submenu": {
"to-help-translate": "Soovid aidata tõlkimisel? Klõpsa siin" "to-help-translate": "Soovid aidata tõlkimisel? Klõpsi siin"
} }
}, },
"resume-on-start": "Rakenduse käivitamisel jätka viimatiesitatud loo esitamist", "resume-on-start": "Rakenduse käivitamisel jätka viimatiesitatud loo esitamist",
@ -139,7 +139,7 @@
"unset": "Määramata" "unset": "Määramata"
}, },
"tray": { "tray": {
"label": "Tasku", "label": "Trey",
"submenu": { "submenu": {
"disabled": "Välja lülitatud", "disabled": "Välja lülitatud",
"enabled-and-hide-app": "Sisse lülitatud ja rakendus peidetud", "enabled-and-hide-app": "Sisse lülitatud ja rakendus peidetud",
@ -227,7 +227,7 @@
}, },
"album-actions": { "album-actions": {
"description": "Lisab Undislike, Ebameeldiv, Meeldiv ja Unlike nupud selle rakendamiseks kõikidele loendisse või albumisse kuuluvatele lauludele.", "description": "Lisab Undislike, Ebameeldiv, Meeldiv ja Unlike nupud selle rakendamiseks kõikidele loendisse või albumisse kuuluvatele lauludele.",
"name": "Albumi toimingud" "name": "Albumi aktsioonid"
}, },
"album-color-theme": { "album-color-theme": {
"description": "Rakendab dünaamilist teemat ja visuaalseid efekte, mis põhinevad albumi värvipalettil", "description": "Rakendab dünaamilist teemat ja visuaalseid efekte, mis põhinevad albumi värvipalettil",
@ -237,8 +237,7 @@
"submenu": { "submenu": {
"percent": "{{suhe}}%" "percent": "{{suhe}}%"
} }
}, }
"enable-seekbar": "Luba kerimisriba kujundamine"
}, },
"name": "Albumi värviteema" "name": "Albumi värviteema"
}, },
@ -246,19 +245,9 @@
"description": "Rakendab valgusefekti, projitseerides videost õrnad värvid ekraani taustale", "description": "Rakendab valgusefekti, projitseerides videost õrnad värvid ekraani taustale",
"menu": { "menu": {
"blur-amount": { "blur-amount": {
"label": "Hägusus", "label": "Hägusus"
"submenu": {
"pixels": "{{blurAmount}} pikslit"
}
},
"buffer": {
"label": "Puhver",
"submenu": {
"buffer": "{{buffer}}"
}
}, },
"opacity": { "opacity": {
"label": "Läbipaistmatus",
"submenu": { "submenu": {
"percent": "{{opacity}}%" "percent": "{{opacity}}%"
} }
@ -274,15 +263,8 @@
"submenu": { "submenu": {
"percent": "{{size}}%" "percent": "{{size}}%"
} }
},
"smoothness-transition": {
"label": "Sujuv üleminek"
},
"use-fullscreen": {
"label": "Kasutamas täisekraani"
} }
}, }
"name": "Ümbritsev režiim"
}, },
"blur-nav-bar": { "blur-nav-bar": {
"description": "Muudab navigatsiooniriba läbipaistavaks ja hägusaks", "description": "Muudab navigatsiooniriba läbipaistavaks ja hägusaks",

View File

@ -237,8 +237,7 @@
"submenu": { "submenu": {
"percent": "{{ratio}}%" "percent": "{{ratio}}%"
} }
}, }
"enable-seekbar": "Abilita tematizzazione della seekbar"
}, },
"name": "Tema abbinato a colore album" "name": "Tema abbinato a colore album"
}, },
@ -321,22 +320,6 @@
"hostname": { "hostname": {
"label": "Hostname" "label": "Hostname"
}, },
"https": {
"label": "HTTPS & Certificati",
"submenu": {
"cert": {
"dialogTitle": "Seleziona file di certificato HTTPS",
"label": "File di certificato (.crt/.pem)"
},
"enable-https": {
"label": "Abilita HTTPS"
},
"key": {
"dialogTitle": "Seleziona il file della chiave privata HTTPS",
"label": "File della chiave privata (.key/.pem)"
}
}
},
"port": { "port": {
"label": "Porta" "label": "Porta"
} }
@ -478,9 +461,9 @@
"set-status-display-type": { "set-status-display-type": {
"label": "Testo dello status", "label": "Testo dello status",
"submenu": { "submenu": {
"application": "Ascoltando {{applicationName}}",
"artist": "Stai ascoltando {artist}", "artist": "Stai ascoltando {artist}",
"title": "Stai ascoltando {song title}" "title": "Stai ascoltando {song title}",
"application": "Ascoltando {{applicationName}}"
} }
} }
}, },
@ -883,7 +866,7 @@
}, },
"name": "Testi sincronizzati", "name": "Testi sincronizzati",
"refetch-btn": { "refetch-btn": {
"fetching": "Caricamento...", "fetching": "Sto recuperando...",
"normal": "Recupera i testi" "normal": "Recupera i testi"
}, },
"warnings": { "warnings": {

View File

@ -117,7 +117,7 @@
"hide-menu": { "hide-menu": {
"dialog": { "dialog": {
"message": "მენიუ შემდეგი გაშვებისას დაიმალება, მის საჩვენებლად გამოიყენეთ [Alt] (ან თუ აპლიკაციის მენიუს იყენებთ, უკან დააწკაპუნეთ [`])", "message": "მენიუ შემდეგი გაშვებისას დაიმალება, მის საჩვენებლად გამოიყენეთ [Alt] (ან თუ აპლიკაციის მენიუს იყენებთ, უკან დააწკაპუნეთ [`])",
"title": "მენიუს დამალვა ჩართ" "title": "მენიუს დამალვა ჩართულია"
}, },
"label": "მენიუს დამალვა" "label": "მენიუს დამალვა"
}, },

View File

@ -171,7 +171,7 @@
"remove": "제거" "remove": "제거"
}, },
"remove-theme": "사용자 정의 테마를 제거하시겠습니까?", "remove-theme": "사용자 정의 테마를 제거하시겠습니까?",
"remove-theme-message": "이 설정을 변경하면 커스텀 테마가 삭제됩니다" "remove-theme-message": "사용자 정의 테마를 제거하시겠습니까?"
}, },
"label": "테마", "label": "테마",
"submenu": { "submenu": {
@ -289,7 +289,7 @@
}, },
"amuse": { "amuse": {
"description": "6K Labs Amuse의 'now playing' 위젯에 {{applicationName}} 지원 추가", "description": "6K Labs Amuse의 'now playing' 위젯에 {{applicationName}} 지원 추가",
"name": "Amuseio AB", "name": "Amuse",
"response": { "response": {
"query": "Amuse API 서버가 실행 중입니다. GET /query로 노래 정보를 가져오세요." "query": "Amuse API 서버가 실행 중입니다. GET /query로 노래 정보를 가져오세요."
} }
@ -321,22 +321,6 @@
"hostname": { "hostname": {
"label": "호스트 명" "label": "호스트 명"
}, },
"https": {
"label": "HTTPS 및 인증서",
"submenu": {
"cert": {
"dialogTitle": "HTTPS 인증서 파일을 선택해 주세요",
"label": "인증서 파일(.crt/.pem)"
},
"enable-https": {
"label": "HTTPS 활성화"
},
"key": {
"dialogTitle": "HTTPS 개인 키 파일을 선택해 주세요",
"label": "개인 키 파일(.key/.pem)"
}
}
},
"port": { "port": {
"label": "포트" "label": "포트"
} }
@ -473,13 +457,13 @@
"disconnected": "연결 해제 됨", "disconnected": "연결 해제 됨",
"hide-duration-left": "남은 재생 시간 숨기기", "hide-duration-left": "남은 재생 시간 숨기기",
"hide-github-button": "GitHub 링크 버튼 숨기기", "hide-github-button": "GitHub 링크 버튼 숨기기",
"play-on-application": "{{applicationName}} 에서 재생", "play-on-application": "유튜브 뮤직에서 재생",
"set-inactivity-timeout": "비활성 시간 제한 설정", "set-inactivity-timeout": "비활성 시간 제한 설정",
"set-status-display-type": { "set-status-display-type": {
"label": "상태 텍스트", "label": "상태 텍스트",
"submenu": { "submenu": {
"application": "{{applicationName}} 듣는 중",
"artist": "{아티스트} 듣는 중", "artist": "{아티스트} 듣는 중",
"application": "{{applicationName}} 듣는 중",
"title": "{곡 제목} 듣는 중" "title": "{곡 제목} 듣는 중"
} }
} }

View File

@ -151,9 +151,7 @@
"label": "Vizualiniai patobulinimai", "label": "Vizualiniai patobulinimai",
"submenu": { "submenu": {
"custom-window-title": { "custom-window-title": {
"label": "Pasirinktinis lango pavadinimas",
"prompt": { "prompt": {
"label": "Įveskite pasirinktinį lango pavadinimą: (palikite tuščią, jei norite išjungti)",
"placeholder": "Pavyzdys: {{applicationName}}" "placeholder": "Pavyzdys: {{applicationName}}"
} }
}, },
@ -433,9 +431,9 @@
"set-inactivity-timeout": "Nustatyti neveiklumo laiką", "set-inactivity-timeout": "Nustatyti neveiklumo laiką",
"set-status-display-type": { "set-status-display-type": {
"submenu": { "submenu": {
"application": "Klausosi {{applicationName}}",
"artist": "Klausosi {artist]", "artist": "Klausosi {artist]",
"title": "Klausosi {song title}" "title": "Klausosi {song title}",
"application": "Klausosi {{applicationName}}"
} }
} }
}, },

View File

@ -237,8 +237,7 @@
"submenu": { "submenu": {
"percent": "{{ratio}}%" "percent": "{{ratio}}%"
} }
}, }
"enable-seekbar": "Schakel thema's voor de schuifbalk in"
}, },
"name": "Albumkleurthema" "name": "Albumkleurthema"
}, },
@ -321,22 +320,6 @@
"hostname": { "hostname": {
"label": "Hostnaam" "label": "Hostnaam"
}, },
"https": {
"label": "HTTPS & Certificaten",
"submenu": {
"cert": {
"dialogTitle": "Selecteer HTTPS certificaat",
"label": "Certificaatbestand (.crt/.pem)"
},
"enable-https": {
"label": "HTTPS aanzetten"
},
"key": {
"dialogTitle": "Selecteer HTTPS privésleutel",
"label": "Privésleutelbestand (.key/.pem)"
}
}
},
"port": { "port": {
"label": "Poort" "label": "Poort"
} }

View File

@ -209,7 +209,7 @@
"show": "Pokaż okno", "show": "Pokaż okno",
"tooltip": { "tooltip": {
"default": "{{applicationName}}", "default": "{{applicationName}}",
"with-song-info": "{{title}} (autorstwa {{artist}}) - {{applicationName}}" "with-song-info": "{{artist}} - (autorstwa {{artist}}) - {{applicationName}}"
} }
} }
}, },
@ -295,7 +295,7 @@
} }
}, },
"api-server": { "api-server": {
"description": "Steruj odtwarzaczem przez specjalny serwer API", "description": "Pozwala na kontrolowanie {{applicationName}} poprzez podłączenie specjalnego serwera API",
"dialog": { "dialog": {
"request": { "request": {
"buttons": { "buttons": {

View File

@ -321,22 +321,6 @@
"hostname": { "hostname": {
"label": "Nome do anfitrião" "label": "Nome do anfitrião"
}, },
"https": {
"label": "HTTPS & Certificados",
"submenu": {
"cert": {
"dialogTitle": "Selecione o certificado do HTTPS",
"label": "Arquivo de certificado (.crt/.pem)"
},
"enable-https": {
"label": "Habilitar HTTPS"
},
"key": {
"dialogTitle": "Selecione a chave privada do HTTPS",
"label": "Arquivo de chave privada (.key/.pem)"
}
}
},
"port": { "port": {
"label": "Porta" "label": "Porta"
} }
@ -478,8 +462,8 @@
"set-status-display-type": { "set-status-display-type": {
"label": "Texto de status", "label": "Texto de status",
"submenu": { "submenu": {
"application": "Ouvindo {{applicationName}}",
"artist": "Ouvindo {artist}", "artist": "Ouvindo {artist}",
"application": "Ouvindo {{applicationName}}",
"title": "Ouvindo {song title}" "title": "Ouvindo {song title}"
} }
} }

View File

@ -2,13 +2,13 @@
"common": { "common": {
"console": { "console": {
"plugins": { "plugins": {
"execute-failed": "Ошибка при выполнении плагина {{pluginName}}::{{contextName}}", "execute-failed": "Ошибка загрузки плагина {{pluginName}}::{{contextName}}",
"executed-at-ms": "Плагин {{pluginName}}::{{contextName}} загружен за {{ms}}мс", "executed-at-ms": "Плагин {{pluginName}}::{{contextName}} загружен за {{ms}}мс",
"initialize-failed": "Ошибка инициализации плагина \"{{pluginName}}\"", "initialize-failed": "Ошибка инициализации плагина \"{{pluginName}}\"",
"load-all": "Загружаем все плагины", "load-all": "Загружаем все плагины",
"load-failed": "Ошибка загрузки плагина \"{{pluginName}}\"", "load-failed": "Ошибка загрузки плагина \"{{pluginName}}\"",
"loaded": "Плагин \"{{pluginName}}\" загружен", "loaded": "Плагин \"{{pluginName}}\" загружен",
"unload-failed": "Ошибка при выгрузке плагина \"{{pluginName}}\"", "unload-failed": "Ошибка выгрузки плагина \"{{pluginName}}\"",
"unloaded": "Плагин \"{{pluginName}}\" выгружен" "unloaded": "Плагин \"{{pluginName}}\" выгружен"
} }
} }
@ -44,7 +44,7 @@
}, },
"dialog": { "dialog": {
"hide-menu-enabled": { "hide-menu-enabled": {
"detail": "Меню скрыто, используйте 'Alt' чтобы показать его ('Escape' если используете внутреннее меню приложения)", "detail": "Меню скрыто, 'Alt' чтобы показать его ('Escape' если используете внутреннее меню приложения)",
"message": "Скрытие меню включено", "message": "Скрытие меню включено",
"title": "Включено скрытие меню" "title": "Включено скрытие меню"
}, },
@ -53,8 +53,8 @@
"later": "Позже", "later": "Позже",
"restart-now": "Перезапустить сейчас" "restart-now": "Перезапустить сейчас"
}, },
"detail": "Для вступления изменений в силу плагину \"{{pluginName}}\" требуется перезапуск", "detail": "Перезапустите приложение для включения плагина {{pluginName}}",
"message": "Требуется перезапуск плагина \"{{pluginName}}\"", "message": "Перезапуск для применения плагина {{pluginName}}",
"title": "Нужен перезапуск" "title": "Нужен перезапуск"
}, },
"unresponsive": { "unresponsive": {
@ -100,7 +100,7 @@
"disable-hardware-acceleration": "Отключить аппаратное ускорение", "disable-hardware-acceleration": "Отключить аппаратное ускорение",
"edit-config-json": "Редактировать config.json", "edit-config-json": "Редактировать config.json",
"override-user-agent": "Переопределить User-Agent", "override-user-agent": "Переопределить User-Agent",
"restart-on-config-changes": "Перезапускать при изменениях конфигурации", "restart-on-config-changes": "Перезапускать при изменениях конфига",
"set-proxy": { "set-proxy": {
"label": "Задать прокси", "label": "Задать прокси",
"prompt": { "prompt": {
@ -237,8 +237,7 @@
"submenu": { "submenu": {
"percent": "{{ratio}}%" "percent": "{{ratio}}%"
} }
}, }
"enable-seekbar": "Включить тематическое оформление полосы прокрутки"
}, },
"name": "Цветовая тема альбома" "name": "Цветовая тема альбома"
}, },
@ -288,7 +287,7 @@
"name": "Режим Ambient" "name": "Режим Ambient"
}, },
"amuse": { "amuse": {
"description": "Добавляет {{applicationName}} поддержку виджета Amuse „сейчас играет“ от 6K Labs", "description": "Добавляет поддержку виджета Amuse „сейчас играет“ от 6K Labs",
"name": "Amuse", "name": "Amuse",
"response": { "response": {
"query": "Сервер Amuse API запущен. GET /query чтобы получить информацию о треке." "query": "Сервер Amuse API запущен. GET /query чтобы получить информацию о треке."
@ -321,22 +320,6 @@
"hostname": { "hostname": {
"label": "Имя хоста" "label": "Имя хоста"
}, },
"https": {
"label": "HTTPS и сертификаты",
"submenu": {
"cert": {
"dialogTitle": "Выберите файл сертификата HTTPS",
"label": "Файл сертификата (.crt/.pem)"
},
"enable-https": {
"label": "Включить HTTPS"
},
"key": {
"dialogTitle": "Выберите файл приватного ключа HTTPS",
"label": "Файл приватного ключа (.key/.pem)"
}
}
},
"port": { "port": {
"label": "Порт" "label": "Порт"
} }

View File

@ -321,22 +321,6 @@
"hostname": { "hostname": {
"label": "Ana bilgisayar adı" "label": "Ana bilgisayar adı"
}, },
"https": {
"label": "HTTPS & Sertifikalar",
"submenu": {
"cert": {
"dialogTitle": "HTTPS sertifika dosyası seç",
"label": "Sertifika dosyası (.crt/.pem)"
},
"enable-https": {
"label": "HTTPS'i aktifleştir"
},
"key": {
"dialogTitle": "HTTPS özel anahtar dosyası seç",
"label": "Özel anahtar dosyası (.key/.pem)"
}
}
},
"port": { "port": {
"label": "Port" "label": "Port"
} }
@ -478,8 +462,8 @@
"set-status-display-type": { "set-status-display-type": {
"label": "Durum metni", "label": "Durum metni",
"submenu": { "submenu": {
"application": "{{applicationName}} Dinleniyor",
"artist": "{artist} Dinleniyor", "artist": "{artist} Dinleniyor",
"application": "{{applicationName}} Dinleniyor",
"title": "{song title} Dinleniyor" "title": "{song title} Dinleniyor"
} }
} }

View File

@ -320,13 +320,6 @@
"hostname": { "hostname": {
"label": "Tên máy chủ" "label": "Tên máy chủ"
}, },
"https": {
"submenu": {
"enable-https": {
"label": "Bật HTTPS"
}
}
},
"port": { "port": {
"label": "Cổng" "label": "Cổng"
} }
@ -468,9 +461,9 @@
"set-status-display-type": { "set-status-display-type": {
"label": "Văn bản trạng thái", "label": "Văn bản trạng thái",
"submenu": { "submenu": {
"application": "Đang nghe {{applicationName}}",
"artist": "Đang nghe nhạc của {artist}", "artist": "Đang nghe nhạc của {artist}",
"title": "Đang nghe nhạc {song title}" "title": "Đang nghe nhạc {song title}",
"application": "Đang nghe {{applicationName}}"
} }
} }
}, },

View File

@ -285,19 +285,6 @@ export const mainMenuTemplate = async (
config.set('options.likeButtons', 'hide'); config.set('options.likeButtons', 'hide');
}, },
}, },
{
label: t(
'main.menu.options.submenu.visual-tweaks.submenu.like-buttons.swap',
),
type: 'checkbox',
checked: config.get('options.swapLikeButtonsOrder'),
click(item: MenuItem) {
config.setMenuOption(
'options.swapLikeButtonsOrder',
item.checked,
);
},
},
], ],
}, },
{ {

View File

@ -1,109 +0,0 @@
import { render } from 'solid-js/web';
import { createSignal, onMount } from 'solid-js';
import style from './style.css?inline';
import { createPlugin } from '@/utils';
import { type MenuTemplate } from '@/menu';
import { t } from '@/i18n';
import { type ClockPluginConfig } from './types';
const defaultConfig: ClockPluginConfig = {
enabled: false,
displaySeconds: false,
hour12: false,
};
export default createPlugin({
name: () => t('plugins.clock.name'),
description: () => t('plugins.clock.description'),
restartNeeded: false,
config: defaultConfig,
stylesheets: [style],
menu: async ({ getConfig, setConfig }): Promise<MenuTemplate> => {
const config = await getConfig();
return [
{
label: t('plugins.clock.menu.format.label'),
submenu: [
{
label: t('plugins.clock.menu.format.display-seconds'),
type: 'checkbox',
checked: config.displaySeconds,
click(item) {
setConfig({ displaySeconds: item.checked });
},
},
{
label: t('plugins.clock.menu.format.24-hour-format'),
type: 'checkbox',
checked: !config.hour12,
click(item) {
setConfig({ hour12: !item.checked });
},
},
],
},
];
},
renderer: {
displaySeconds: defaultConfig.displaySeconds,
hour12: defaultConfig.hour12,
interval: null as NodeJS.Timeout | null,
clockContainer: document.createElement('div'),
updateTime: null as unknown as () => void,
async start({ getConfig }) {
const config = await getConfig();
this.displaySeconds = config.displaySeconds;
this.hour12 = config.hour12;
if (!this.clockContainer) {
this.clockContainer = document.createElement('div');
}
const [time, setTime] = createSignal<string>();
const updateTime = () => {
const timeFormat: Intl.DateTimeFormatOptions = {
hour12: this.hour12,
hour: 'numeric',
minute: 'numeric',
second: this.displaySeconds ? 'numeric' : undefined,
};
const now = new Date();
setTime(now.toLocaleTimeString('en', timeFormat));
};
this.updateTime = updateTime;
onMount(() => {
this.interval = setInterval(updateTime, 1000);
});
render(
() => (
<>
<h1 class="clock"> {time()} </h1>
</>
),
this.clockContainer,
);
const menu = document.querySelector('.center-content');
menu?.append(this.clockContainer);
},
onConfigChange(newConfig) {
this.displaySeconds = newConfig.displaySeconds;
this.hour12 = newConfig.hour12;
this.updateTime();
},
stop() {
this.clockContainer.remove();
this.clockContainer.replaceChildren();
if (this.interval) {
clearInterval(this.interval);
}
},
},
});

View File

@ -1,6 +0,0 @@
.clock {
position: absolute;
left: 50%;
transform: translateX(-50%);
align-self: center;
}

View File

@ -1,5 +0,0 @@
export type ClockPluginConfig = {
enabled: boolean;
displaySeconds: boolean;
hour12: boolean;
};

View File

@ -9,7 +9,7 @@ import { TimerManager } from './timer-manager';
import { import {
buildDiscordButtons, buildDiscordButtons,
padHangulFields, padHangulFields,
sanitizeActivityText, truncateString,
isSeek, isSeek,
} from './utils'; } from './utils';
@ -22,7 +22,7 @@ export class DiscordService {
/** /**
* Discord RPC client instance. * Discord RPC client instance.
*/ */
rpc!: DiscordClient; rpc = new DiscordClient({ clientId });
/** /**
* Indicates if the service is ready to send activity updates. * Indicates if the service is ready to send activity updates.
*/ */
@ -62,21 +62,6 @@ export class DiscordService {
this.mainWindow = mainWindow; this.mainWindow = mainWindow;
this.autoReconnect = config?.autoReconnect ?? true; // Default autoReconnect to true this.autoReconnect = config?.autoReconnect ?? true; // Default autoReconnect to true
this.initializeRpc();
}
private initializeRpc() {
if (this.rpc) {
try {
this.rpc.destroy();
} catch {
// ignored
}
this.rpc.removeAllListeners();
}
this.rpc = new DiscordClient({ clientId });
this.rpc.on('connected', () => { this.rpc.on('connected', () => {
if (dev()) { if (dev()) {
console.log(LoggerPrefix, t('plugins.discord.backend.connected')); console.log(LoggerPrefix, t('plugins.discord.backend.connected'));
@ -114,17 +99,13 @@ export class DiscordService {
const activityInfo: SetActivity = { const activityInfo: SetActivity = {
type: ActivityType.Listening, type: ActivityType.Listening,
statusDisplayType: config.statusDisplayType, statusDisplayType: config.statusDisplayType,
details: sanitizeActivityText( details: truncateString(songInfo.alternativeTitle ?? songInfo.title, 128), // Song title
songInfo.alternativeTitle ?? songInfo.title
), // Song title
detailsUrl: songInfo.url ?? undefined, detailsUrl: songInfo.url ?? undefined,
state: sanitizeActivityText( state: truncateString(songInfo.tags?.at(0) ?? songInfo.artist, 128), // Artist name
songInfo.tags?.at(0) ?? songInfo.artist
), // Artist name
stateUrl: songInfo.artistUrl, stateUrl: songInfo.artistUrl,
largeImageKey: songInfo.imageSrc ?? undefined, largeImageKey: songInfo.imageSrc ?? undefined,
largeImageText: songInfo.album largeImageText: songInfo.album
? sanitizeActivityText(songInfo.album) ? truncateString(songInfo.album, 128)
: undefined, : undefined,
buttons: buildDiscordButtons(config, songInfo), buttons: buildDiscordButtons(config, songInfo),
}; };
@ -211,7 +192,6 @@ export class DiscordService {
resolve(); resolve();
}) })
.catch(() => { .catch(() => {
this.initializeRpc();
this.connectRecursive(); this.connectRecursive();
}); });
}, },
@ -256,9 +236,6 @@ export class DiscordService {
this.resetInfo(); this.resetInfo();
if (this.autoReconnect) { if (this.autoReconnect) {
// For some reason @xhayper/discord-rpc leaves a dangling listener on connection failure
// so we destroy and recreate the RPC client before reconnecting.
this.initializeRpc();
this.connectRecursive(); this.connectRecursive();
} else if (showErrorDialog && this.mainWindow) { } else if (showErrorDialog && this.mainWindow) {
// connection failed // connection failed
@ -273,12 +250,13 @@ export class DiscordService {
this.autoReconnect = false; this.autoReconnect = false;
this.timerManager.clear(TimerKey.DiscordConnectRetry); this.timerManager.clear(TimerKey.DiscordConnectRetry);
this.timerManager.clear(TimerKey.ClearActivity); this.timerManager.clear(TimerKey.ClearActivity);
if (this.rpc.isConnected) {
try { try {
this.rpc.removeAllListeners();
this.rpc.destroy(); this.rpc.destroy();
} catch { } catch {
// ignored // ignored
} }
}
this.resetInfo(); // Reset internal state this.resetInfo(); // Reset internal state
} }

View File

@ -19,27 +19,6 @@ export const truncateString = (str: string, length: number): string => {
return str; return str;
}; };
/**
* Sanitizes a string for Discord Rich Presence activity, ensuring it meets length requirements.
* @param input - The string to sanitize.
* @param fallback - A fallback string to use if the input is empty or whitespace. Defaults to 'undefined'.
* @returns The sanitized string, compliant with Discord's requirements.
*/
export function sanitizeActivityText(input: string | undefined, fallback: string = 'undefined'): string {
const text = (input && input.trim()) ? input.trim() : fallback.trim();
let safeString = truncateString(text, 128);
if (safeString.length === 0) {
return fallback;
}
if (safeString.length < 2) {
safeString = safeString.padEnd(2, ''); // change if necessary
}
return safeString;
}
/** /**
* Builds the array of buttons for the Discord Rich Presence activity. * Builds the array of buttons for the Discord Rich Presence activity.
* @param config - The plugin configuration. * @param config - The plugin configuration.

View File

@ -1,144 +0,0 @@
import { test, expect } from '@playwright/test';
import { LRC } from './lrc';
test('empty string', () => {
const lrc = LRC.parse('');
expect(lrc).toStrictEqual({ lines: [], tags: [] });
});
test('chorus', () => {
const lrc = LRC.parse(`\
[00:12.00]Line 1 lyrics
[00:17.20]Line 2 lyrics
[00:21.10][00:45.10]Repeating lyrics (e.g. chorus)
[mm:ss.xx]Last lyrics line\
`);
expect(lrc).toStrictEqual({
lines: [
{ duration: 12000, text: '', words: [], time: '00:00:00', timeInMs: 0 },
{
duration: 5020,
text: 'Line 1 lyrics',
words: [],
time: '00:12:00',
timeInMs: 12000,
},
{
duration: 3990,
text: 'Line 2 lyrics',
words: [],
time: '00:17:20',
timeInMs: 17020,
},
{
duration: 24000,
text: 'Repeating lyrics (e.g. chorus)',
words: [],
time: '00:21:10',
timeInMs: 21010,
},
{
duration: Infinity,
text: 'Repeating lyrics (e.g. chorus)',
words: [],
time: '00:45:10',
timeInMs: 45010,
},
],
tags: [],
});
});
test('attributes', () => {
const lrc = LRC.parse(
`[ar:Chubby Checker oppure Beatles, The]
[al:Hits Of The 60's - Vol. 2 Oldies]
[ti:Let's Twist Again]
[au:Written by Kal Mann / Dave Appell, 1961]
[length: 2:23]
[00:12.00]Naku Penda Piya-Naku Taka Piya-Mpenziwe
[00:15.30]Some more lyrics ...`,
);
expect(lrc).toStrictEqual({
lines: [
{ duration: 12000, text: '', words: [], time: '00:00:00', timeInMs: 0 },
{
duration: 3030,
text: 'Naku Penda Piya-Naku Taka Piya-Mpenziwe',
words: [],
time: '00:12:00',
timeInMs: 12000,
},
{
duration: Infinity,
text: 'Some more lyrics ...',
words: [],
time: '00:15:30',
timeInMs: 15030,
},
],
tags: [
{ tag: 'ar', value: 'Chubby Checker oppure Beatles, The' },
{ tag: 'al', value: "Hits Of The 60's - Vol. 2 Oldies" },
{ tag: 'ti', value: "Let's Twist Again" },
{ tag: 'au', value: 'Written by Kal Mann / Dave Appell, 1961' },
{ tag: 'length', value: '2:23' },
],
});
});
test('karaoke', () => {
const lrc = LRC.parse(
'[00:00.00] <00:00.04> When <00:00.16> the <00:00.82> truth <00:01.29> is <00:01.63> found <00:03.09> to <00:03.37> be <00:05.92> lies',
);
expect(lrc).toStrictEqual({
lines: [
{
duration: Infinity,
text: 'When the truth is found to be lies',
time: '00:00:00',
timeInMs: 0,
words: [
{
timeInMs: 4,
word: 'When',
},
{
timeInMs: 16,
word: 'the',
},
{
timeInMs: 82,
word: 'truth',
},
{
timeInMs: 1029,
word: 'is',
},
{
timeInMs: 1063,
word: 'found',
},
{
timeInMs: 3009,
word: 'to',
},
{
timeInMs: 3037,
word: 'be',
},
{
timeInMs: 5092,
word: 'lies',
},
],
},
],
tags: [],
});
});

View File

@ -8,7 +8,6 @@ interface LRCLine {
timeInMs: number; timeInMs: number;
duration: number; duration: number;
text: string; text: string;
words: { timeInMs: number; word: string }[];
} }
interface LRC { interface LRC {
@ -18,10 +17,7 @@ interface LRC {
const tagRegex = /^\[(?<tag>\w+):\s*(?<value>.+?)\s*\]$/; const tagRegex = /^\[(?<tag>\w+):\s*(?<value>.+?)\s*\]$/;
// prettier-ignore // prettier-ignore
const timestampRegex = /^\[(?<minutes>\d+):(?<seconds>\d+)\.(?<milliseconds>\d+)\]/m; const lyricRegex = /^\[(?<minutes>\d+):(?<seconds>\d+)\.(?<milliseconds>\d+)\](?<text>.+)$/;
// prettier-ignore
const wordRegex = /<(?<minutes>\d+):(?<seconds>\d+)\.(?<milliseconds>\d+)> *(?<word>\w+)/g;
export const LRC = { export const LRC = {
parse: (text: string): LRC => { parse: (text: string): LRC => {
@ -31,29 +27,13 @@ export const LRC = {
}; };
let offset = 0; let offset = 0;
let previousLine: LRCLine | null = null;
for (let line of text.split('\n')) { for (const line of text.split('\n')) {
line = line.trim(); if (!line.trim().startsWith('[')) continue;
if (!line.startsWith('[')) continue;
const timestamps = []; const lyric = line.match(lyricRegex)?.groups;
let match: Record<string, string> | undefined; if (!lyric) {
while ((match = line.match(timestampRegex)?.groups)) {
const { minutes, seconds, milliseconds } = match;
const timeInMs =
parseInt(minutes) * 60 * 1000 +
parseInt(seconds) * 1000 +
parseInt(milliseconds);
timestamps.push({
time: `${minutes}:${seconds}:${milliseconds}`,
timeInMs,
});
line = line.replace(timestampRegex, '');
}
if (!timestamps.length) {
const tag = line.match(tagRegex)?.groups; const tag = line.match(tagRegex)?.groups;
if (tag) { if (tag) {
if (tag.tag === 'offset') { if (tag.tag === 'offset') {
@ -69,52 +49,38 @@ export const LRC = {
continue; continue;
} }
let text = line.trim(); const { minutes, seconds, milliseconds, text } = lyric;
const words = Array.from(text.matchAll(wordRegex), ({ groups }) => {
const { minutes, seconds, milliseconds, word } = groups!;
const timeInMs = const timeInMs =
parseInt(minutes) * 60 * 1000 + parseInt(minutes) * 60 * 1000 +
parseInt(seconds) * 1000 + parseInt(seconds) * 1000 +
parseInt(milliseconds); parseInt(milliseconds);
return { timeInMs, word }; const currentLine: LRCLine = {
}); time: `${minutes}:${seconds}:${milliseconds}`,
if (words.length) {
text = words.map(({ word }) => word).join(' ');
}
for (const { time, timeInMs } of timestamps) {
lrc.lines.push({
time,
timeInMs, timeInMs,
text, text: text.trim(),
words,
duration: Infinity, duration: Infinity,
}); };
}
if (previousLine) {
previousLine.duration = timeInMs - previousLine.timeInMs;
} }
lrc.lines.sort(({ timeInMs: timeA }, { timeInMs: timeB }) => timeA - timeB); previousLine = currentLine;
for (let i = 0; i < lrc.lines.length; i++) { lrc.lines.push(currentLine);
const current = lrc.lines[i];
const next = lrc.lines[i + 1];
current.timeInMs += offset;
if (next) {
current.duration = next.timeInMs - current.timeInMs;
} }
for (const line of lrc.lines) {
line.timeInMs += offset;
} }
const first = lrc.lines.at(0); const first = lrc.lines.at(0);
if (first && first.timeInMs > 300) { if (first && first.timeInMs > 300) {
lrc.lines.unshift({ lrc.lines.unshift({
time: '00:00:00', time: '0:0:0',
timeInMs: 0, timeInMs: 0,
duration: first.timeInMs, duration: first.timeInMs,
text: '', text: '',
words: [],
}); });
} }

View File

@ -18,6 +18,7 @@ export type VisualizerPluginConfig = {
type: 'butterchurn' | 'vudio' | 'wave'; type: 'butterchurn' | 'vudio' | 'wave';
butterchurn: { butterchurn: {
preset: string; preset: string;
renderingFrequencyInMs: number;
blendTimeInSeconds: number; blendTimeInSeconds: number;
}; };
vudio: { vudio: {
@ -56,23 +57,17 @@ export type VisualizerPluginConfig = {
}; };
}; };
type RenderProps = {
visualizerInstance: Visualizer | null;
audioContext: AudioContext | null;
audioSource: MediaElementAudioSourceNode | null;
observer: ResizeObserver | null;
};
export default createPlugin({ export default createPlugin({
name: () => t('plugins.visualizer.name'), name: () => t('plugins.visualizer.name'),
description: () => t('plugins.visualizer.description'), description: () => t('plugins.visualizer.description'),
restartNeeded: false, restartNeeded: true,
config: { config: {
enabled: false, enabled: false,
type: 'butterchurn', type: 'butterchurn',
// Config per visualizer // Config per visualizer
butterchurn: { butterchurn: {
preset: 'martin [shadow harlequins shape code] - fata morgana', preset: 'martin [shadow harlequins shape code] - fata morgana',
renderingFrequencyInMs: 500,
blendTimeInSeconds: 2.7, blendTimeInSeconds: 2.7,
}, },
vudio: { vudio: {
@ -153,23 +148,21 @@ export default createPlugin({
}, },
renderer: { renderer: {
props: { async onPlayerApiReady(_, { getConfig }) {
visualizerInstance: null, const config = await getConfig();
audioContext: null,
audioSource: null,
observer: null,
} as RenderProps,
createVisualizer( // eslint-disable-next-line @typescript-eslint/no-explicit-any
this: { props: RenderProps }, let visualizerType: { new (...args: any[]): Visualizer<unknown> } = vudio;
config: VisualizerPluginConfig,
) {
this.props.visualizerInstance?.destroy();
this.props.visualizerInstance = null;
if (!this.props.audioContext || !this.props.audioSource) return; if (config.type === 'wave') {
if (!config.enabled) return; visualizerType = wave;
} else if (config.type === 'butterchurn') {
visualizerType = butterchurn;
}
document.addEventListener(
'peard:audio-can-play',
(e) => {
const video = document.querySelector< const video = document.querySelector<
HTMLVideoElement & { captureStream(): MediaStream } HTMLVideoElement & { captureStream(): MediaStream }
>('video'); >('video');
@ -190,54 +183,46 @@ export default createPlugin({
visualizerContainer?.prepend(canvas); visualizerContainer?.prepend(canvas);
} }
const gainNode = this.props.audioContext.createGain(); const resizeCanvas = () => {
gainNode.gain.value = 1.25; if (canvas) {
this.props.audioSource.connect(gainNode); canvas.width = visualizerContainer.clientWidth;
canvas.height = visualizerContainer.clientHeight;
let visualizerType: {
new (...args: ConstructorParameters<typeof vudio>): Visualizer;
} = vudio;
if (config.type === 'wave') {
visualizerType = wave;
} else if (config.type === 'butterchurn') {
visualizerType = butterchurn;
} }
this.props.visualizerInstance = new visualizerType( };
this.props.audioContext,
this.props.audioSource, resizeCanvas();
const gainNode = e.detail.audioContext.createGain();
gainNode.gain.value = 1.25;
e.detail.audioSource.connect(gainNode);
const visualizer = new visualizerType(
e.detail.audioContext,
e.detail.audioSource,
visualizerContainer,
canvas, canvas,
gainNode, gainNode,
video.captureStream(), video.captureStream(),
config, config,
); );
const resizeVisualizer = () => { const resizeVisualizer = (width: number, height: number) => {
if (canvas && visualizerContainer) { resizeCanvas();
const { width, height } = visualizer.resize(width, height);
window.getComputedStyle(visualizerContainer);
canvas.width = Math.ceil(parseFloat(width));
canvas.height = Math.ceil(parseFloat(height));
}
this.props.visualizerInstance?.resize(canvas.width, canvas.height);
}; };
resizeVisualizer();
this.props.observer?.disconnect(); resizeVisualizer(canvas.width, canvas.height);
this.props.observer = new ResizeObserver(resizeVisualizer); const visualizerContainerObserver = new ResizeObserver((entries) => {
this.props.observer.observe(visualizerContainer); for (const entry of entries) {
}, resizeVisualizer(
entry.contentRect.width,
entry.contentRect.height,
);
}
});
visualizerContainerObserver.observe(visualizerContainer);
onConfigChange(newConfig) { visualizer.render();
this.createVisualizer(newConfig);
},
onPlayerApiReady(_, { getConfig }) {
document.addEventListener(
'peard:audio-can-play',
async (e) => {
this.props.audioContext = e.detail.audioContext;
this.props.audioSource = e.detail.audioSource;
this.createVisualizer(await getConfig());
}, },
{ passive: true }, { passive: true },
); );

View File

@ -5,49 +5,54 @@ import { Visualizer } from './visualizer';
import type { VisualizerPluginConfig } from '../index'; import type { VisualizerPluginConfig } from '../index';
class ButterchurnVisualizer extends Visualizer { class ButterchurnVisualizer extends Visualizer<Butterchurn> {
private readonly visualizer: ReturnType<typeof Butterchurn.createVisualizer>; name = 'butterchurn';
private destroyed: boolean = false;
private animFrameHandle: number | null; visualizer: ReturnType<typeof Butterchurn.createVisualizer>;
private readonly renderingFrequencyInMs: number;
constructor( constructor(
audioContext: AudioContext, audioContext: AudioContext,
audioSource: MediaElementAudioSourceNode, audioSource: MediaElementAudioSourceNode,
visualizerContainer: HTMLElement,
canvas: HTMLCanvasElement, canvas: HTMLCanvasElement,
audioNode: GainNode, audioNode: GainNode,
_stream: MediaStream, stream: MediaStream,
config: VisualizerPluginConfig, options: VisualizerPluginConfig,
) { ) {
super(audioSource, audioNode); super(
audioContext,
const preset = ButterchurnPresets[config.butterchurn.preset]; audioSource,
const renderVisualizer = () => { visualizerContainer,
if (this.destroyed) return; canvas,
this.visualizer.render(); audioNode,
this.animFrameHandle = requestAnimationFrame(renderVisualizer); stream,
}; options,
);
this.visualizer = Butterchurn.createVisualizer(audioContext, canvas, { this.visualizer = Butterchurn.createVisualizer(audioContext, canvas, {
width: canvas.width, width: canvas.width,
height: canvas.height, height: canvas.height,
}); });
this.visualizer.loadPreset(preset, config.butterchurn.blendTimeInSeconds);
const preset = ButterchurnPresets[options.butterchurn.preset];
this.visualizer.loadPreset(preset, options.butterchurn.blendTimeInSeconds);
this.visualizer.connectAudio(audioNode); this.visualizer.connectAudio(audioNode);
// Start animation request loop. Do not use setInterval! this.renderingFrequencyInMs = options.butterchurn.renderingFrequencyInMs;
this.animFrameHandle = requestAnimationFrame(renderVisualizer);
} }
resize(width: number, height: number) { resize(width: number, height: number) {
this.visualizer.setRendererSize(width, height); this.visualizer.setRendererSize(width, height);
} }
destroy() { render() {
if (this.animFrameHandle) cancelAnimationFrame(this.animFrameHandle); const renderVisualizer = () => {
this.destroyed = true; requestAnimationFrame(renderVisualizer);
try { this.visualizer.render();
this.audioSource.disconnect(this.audioNode); };
} catch {} setTimeout(renderVisualizer, this.renderingFrequencyInMs);
} }
} }

View File

@ -1,15 +1,22 @@
export abstract class Visualizer { import type { VisualizerPluginConfig } from '../index';
protected audioNode: GainNode;
protected audioSource: MediaElementAudioSourceNode; export abstract class Visualizer<T> {
/**
* The name must be the same as the file name.
*/
abstract name: string;
abstract visualizer: T;
protected constructor( protected constructor(
_audioContext: AudioContext,
_audioSource: MediaElementAudioSourceNode, _audioSource: MediaElementAudioSourceNode,
_visualizerContainer: HTMLElement,
_canvas: HTMLCanvasElement,
_audioNode: GainNode, _audioNode: GainNode,
) { _stream: MediaStream,
this.audioNode = _audioNode; _options: VisualizerPluginConfig,
this.audioSource = _audioSource; ) {}
}
abstract resize(width: number, height: number): void; abstract resize(width: number, height: number): void;
abstract destroy(): void; abstract render(): void;
} }

View File

@ -4,24 +4,35 @@ import { Visualizer } from './visualizer';
import type { VisualizerPluginConfig } from '../index'; import type { VisualizerPluginConfig } from '../index';
class VudioVisualizer extends Visualizer { class VudioVisualizer extends Visualizer<Vudio> {
private readonly visualizer: Vudio; name = 'vudio';
visualizer: Vudio;
constructor( constructor(
_audioContext: AudioContext, audioContext: AudioContext,
audioSource: MediaElementAudioSourceNode, audioSource: MediaElementAudioSourceNode,
visualizerContainer: HTMLElement,
canvas: HTMLCanvasElement, canvas: HTMLCanvasElement,
audioNode: GainNode, audioNode: GainNode,
stream: MediaStream, stream: MediaStream,
config: VisualizerPluginConfig, options: VisualizerPluginConfig,
) { ) {
super(audioSource, audioNode); super(
audioContext,
audioSource,
visualizerContainer,
canvas,
audioNode,
stream,
options,
);
this.visualizer = new Vudio(stream, canvas, { this.visualizer = new Vudio(stream, canvas, {
width: canvas.width, width: canvas.width,
height: canvas.height, height: canvas.height,
// Visualizer config // Visualizer config
...config, ...options,
}); });
this.visualizer.dance(); this.visualizer.dance();
@ -34,12 +45,7 @@ class VudioVisualizer extends Visualizer {
}); });
} }
destroy() { render() {}
this.visualizer.pause();
try {
this.audioSource.disconnect(this.audioNode);
} catch {}
}
} }
export default VudioVisualizer; export default VudioVisualizer;

View File

@ -4,24 +4,35 @@ import { Visualizer } from './visualizer';
import type { VisualizerPluginConfig } from '../index'; import type { VisualizerPluginConfig } from '../index';
class WaveVisualizer extends Visualizer { class WaveVisualizer extends Visualizer<Wave> {
private readonly visualizer: Wave; name = 'wave';
visualizer: Wave;
constructor( constructor(
audioContext: AudioContext, audioContext: AudioContext,
audioSource: MediaElementAudioSourceNode, audioSource: MediaElementAudioSourceNode,
visualizerContainer: HTMLElement,
canvas: HTMLCanvasElement, canvas: HTMLCanvasElement,
audioNode: GainNode, audioNode: GainNode,
_stream: MediaStream, stream: MediaStream,
config: VisualizerPluginConfig, options: VisualizerPluginConfig,
) { ) {
super(audioSource, audioNode); super(
audioContext,
audioSource,
visualizerContainer,
canvas,
audioNode,
stream,
options,
);
this.visualizer = new Wave( this.visualizer = new Wave(
{ context: audioContext, source: audioSource }, { context: audioContext, source: audioSource },
canvas, canvas,
); );
for (const animation of config.wave.animations) { for (const animation of options.wave.animations) {
const TargetVisualizer = const TargetVisualizer =
this.visualizer.animations[ this.visualizer.animations[
animation.type as keyof typeof this.visualizer.animations animation.type as keyof typeof this.visualizer.animations
@ -35,12 +46,7 @@ class WaveVisualizer extends Visualizer {
resize(_: number, __: number) {} resize(_: number, __: number) {}
destroy() { render() {}
this.visualizer.clearAnimations();
try {
this.audioSource.disconnect(this.audioNode);
} catch {}
}
} }
export default WaveVisualizer; export default WaveVisualizer;

View File

@ -406,18 +406,6 @@ async function onApiLoaded() {
document.head.appendChild(style); document.head.appendChild(style);
} }
// Swap like button order
if (window.mainConfig.get('options.swapLikeButtonsOrder')) {
const style = document.createElement('style');
style.textContent = `
#like-button-renderer {
display: inline-flex;
flex-direction: row-reverse;
}`;
document.head.appendChild(style);
}
} }
/** /**