Compare commits

..

1 Commits

Author SHA1 Message Date
63f9c7138d fix(deps): update dependency @ffmpeg.wasm/core-mt to v0.13.2 2025-12-31 16:52:14 +00:00
47 changed files with 2385 additions and 2491 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": "12.2.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",
@ -64,24 +64,24 @@
}, },
"dependencies": { "dependencies": {
"@dehoist/romanize-thai": "1.0.0", "@dehoist/romanize-thai": "1.0.0",
"@electron-toolkit/tsconfig": "2.0.0", "@electron-toolkit/tsconfig": "1.0.1",
"@electron/remote": "2.1.3", "@electron/remote": "2.1.3",
"@ffmpeg.wasm/core-mt": "0.12.0", "@ffmpeg.wasm/core-mt": "0.13.2",
"@ffmpeg.wasm/main": "0.12.0", "@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.1", "@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",
"@jellybrick/electron-better-web-request": "2.0.0", "@jellybrick/electron-better-web-request": "2.0.0",
"@jellybrick/mpris-service": "2.1.5", "@jellybrick/mpris-service": "2.1.5",
"@jimp/plugin-color": "1.6.0", "@jimp/plugin-color": "1.6.0",
"@mdui/icons": "1.0.3", "@mdui/icons": "^1.0.3",
"@skyra/jaro-winkler": "1.1.1", "@skyra/jaro-winkler": "1.1.1",
"@xhayper/discord-rpc": "1.3.0", "@xhayper/discord-rpc": "1.3.0",
"async-mutex": "0.5.0", "async-mutex": "0.5.0",
@ -89,26 +89,27 @@
"butterchurn": "3.0.0-beta.5", "butterchurn": "3.0.0-beta.5",
"butterchurn-presets": "3.0.0-beta.4", "butterchurn-presets": "3.0.0-beta.4",
"color": "5.0.3", "color": "5.0.3",
"conf": "15.0.2", "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",
"electron-debug": "4.1.0", "electron-debug": "4.1.0",
"electron-is": "3.0.0", "electron-is": "3.0.0",
"electron-localshortcut": "3.2.1", "electron-localshortcut": "3.2.1",
"electron-store": "11.0.2", "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": "6.0.0", "fast-equals": "5.2.2",
"fflate": "0.8.2", "fflate": "0.8.2",
"filenamify": "7.0.1", "filenamify": "6.0.0",
"hanja": "1.1.5", "hanja": "1.1.5",
"happy-dom": "20.5.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.8.3", "i18next": "25.5.2",
"jimp": "1.6.0", "jimp": "1.6.0",
"keyboardevent-from-electron-accelerator": "2.0.0", "keyboardevent-from-electron-accelerator": "2.0.0",
"keyboardevents-areequal": "0.2.2", "keyboardevents-areequal": "0.2.2",
@ -117,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",
@ -125,57 +126,57 @@
"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.9",
"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",
"zod": "4.3.6" "zod": "4.2.1"
}, },
"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.1", "@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": "7.0.0", "del-cli": "6.0.0",
"discord-api-types": "0.38.38", "discord-api-types": "0.38.37",
"electron": "40.1.0", "electron": "38.7.2",
"electron-builder": "26.7.0", "electron-builder": "26.4.0",
"electron-builder-squirrel-windows": "26.7.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": "13.0.0", "glob": "11.1.0",
"node-gyp": "12.2.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.54.0", "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,

3254
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

@ -161,8 +161,7 @@
"default": "Par défaut", "default": "Par défaut",
"force-show": "Forcer à apparaître", "force-show": "Forcer à apparaître",
"hide": "Cacher", "hide": "Cacher",
"label": "Boutons « J'aime »", "label": "Boutons « J'aime »"
"swap": "Inverser l'order des boutons like"
}, },
"remove-upgrade-button": "Supprimer le bouton de mise à niveau", "remove-upgrade-button": "Supprimer le bouton de mise à niveau",
"theme": { "theme": {
@ -413,17 +412,6 @@
"no-captions": "Aucun sous-titres disponibles pour cette chanson" "no-captions": "Aucun sous-titres disponibles pour cette chanson"
} }
}, },
"clock": {
"description": "Ajoute une horloge a la barre de navigation",
"menu": {
"format": {
"24-hour-format": "Format 24 heures",
"display-seconds": "Afficher les secondes",
"label": "Format"
}
},
"name": "Horloge"
},
"compact-sidebar": { "compact-sidebar": {
"description": "Toujours définir la barre latérale en mode compact", "description": "Toujours définir la barre latérale en mode compact",
"name": "Barre latérale compacte" "name": "Barre latérale compacte"

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

@ -2,13 +2,13 @@
"common": { "common": {
"console": { "console": {
"plugins": { "plugins": {
"execute-failed": "확장 {{pluginName}}::{{contextName}}을(를) 실행 실패함", "execute-failed": "확장 {{pluginName}}::{{contextName}}을(를) 실행하지 못했습니다",
"executed-at-ms": "확장 {{pluginName}}::{{contextName}}이 {{ms}}ms 만에 실행됨", "executed-at-ms": "확장 {{pluginName}}::{{contextName}}이 {{ms}}ms 만에 실행됨",
"initialize-failed": "확장 \"{{pluginName}}\"을(를) 초기화 실패함", "initialize-failed": "확장 \"{{pluginName}}\"을(를) 초기화하지 못했습니다",
"load-all": "모든 확장 로드 중", "load-all": "모든 확장 로드 중",
"load-failed": "확장 \"{{pluginName}}\"을(를) 로드하지 못했습니다", "load-failed": "확장 \"{{pluginName}}\"을(를) 로드하지 못했습니다",
"loaded": "확장 \"{{pluginName}}\" 로드됨", "loaded": "확장 \"{{pluginName}}\" 로드됨",
"unload-failed": "확장 \"{{pluginName}}\"을(를) 언로드 실패함", "unload-failed": "확장 \"{{pluginName}}\"을(를) 언로드하지 못했습니다",
"unloaded": "확장 \"{{pluginName}}\" 언로드 됨" "unloaded": "확장 \"{{pluginName}}\" 언로드 됨"
} }
} }
@ -21,7 +21,7 @@
"main": { "main": {
"console": { "console": {
"did-finish-load": { "did-finish-load": {
"dev-tools": "로드 완료. 개발자 도구 실행됨" "dev-tools": "로드 완료되었습니다. 개발자 도구가 열렸습니다"
}, },
"i18n": { "i18n": {
"loaded": "국제화 로드됨" "loaded": "국제화 로드됨"
@ -30,32 +30,32 @@
"receive-command": "프로토콜을 통해 명령을 받았습니다: \"{{command}}\"" "receive-command": "프로토콜을 통해 명령을 받았습니다: \"{{command}}\""
}, },
"theme": { "theme": {
"css-file-not-found": "CSS 파일 \"{{cssFile}}\"이(가) 존재하지 않. 무시" "css-file-not-found": "CSS 파일 \"{{cssFile}}\"이(가) 존재하지 않습니다. 무시합니다"
}, },
"unresponsive": { "unresponsive": {
"details": "응답 없음 오류!\n{{error}}" "details": "응답 없음 오류!\n{{error}}"
}, },
"when-ready": { "when-ready": {
"clearing-cache-after-20s": "앱 캐시 지우는 중" "clearing-cache-after-20s": "앱 캐시 지우"
}, },
"window": { "window": {
"tried-to-render-offscreen": "창이 오프스크린 렌더링을 시도했습니다, windowSize={{windowSize}}, displaySize={{displaySize}}, position={{position}}" "tried-to-render-offscreen": "창이 오프스크린 렌더링을 시도했습니다. windowSize={{windowSize}}, displaySize={{displaySize}}, position={{position}}"
} }
}, },
"dialog": { "dialog": {
"hide-menu-enabled": { "hide-menu-enabled": {
"detail": "'Alt' 키를 눌러 숨겨진 메뉴를 표시할 수 있습니다 (인앱 메뉴를 사용하는 경우 'Esc' 키를 사용)", "detail": "'Alt' 키를 눌러 숨겨진 메뉴를 표시할 수 있습니다 (인앱 메뉴를 사용하는 경우 'Esc' 키를 사용)",
"message": "메뉴 숨기기가 활성화되어 있", "message": "메뉴 숨기기가 활성화되어 있습니다",
"title": "메뉴 숨기기 활성화됨" "title": "메뉴 숨기기 활성화됨"
}, },
"need-to-restart": { "need-to-restart": {
"buttons": { "buttons": {
"later": "나중에", "later": "나중에 하기",
"restart-now": "지금 재시작하기" "restart-now": "지금 재시작하기"
}, },
"detail": "\"{{pluginName}}\" 확장을 적용하려면 재시작해야 합니다", "detail": "\"{{pluginName}}\" 확장을 적용하려면 재시작해야 합니다",
"message": "\"{{pluginName}}\"은(는) 재시작이 필요합니다", "message": "\"{{pluginName}}\"은(는) 재시작이 필요합니다",
"title": "재시작 필요" "title": "재시작 필요"
}, },
"unresponsive": { "unresponsive": {
"buttons": { "buttons": {
@ -63,17 +63,17 @@
"relaunch": "재시작", "relaunch": "재시작",
"wait": "기다리기" "wait": "기다리기"
}, },
"detail": "불편을 드려 죄송합니다! 다음 중 하나를 선택해 주세요.", "detail": "불편을 드려 죄송합니다! 방법을 선택해 주세요:",
"message": "애플리케이션이 응답하지 않습니다", "message": "애플리케이션이 응답하지 않습니다",
"title": "창이 응답하지 않음" "title": "창이 응답하지 않음"
}, },
"update-available": { "update-available": {
"buttons": { "buttons": {
"disable": "업데이트 비활성화하기", "disable": "업데이트 비활성화",
"download": "다운로드", "download": "다운로드",
"ok": "확인" "ok": "확인"
}, },
"detail": "새 버전 {{downloadLink}}에서 설치할 수 있습니다", "detail": "새 버전이 출시되었습니다. {{downloadLink}}에서 다운로드할 수 있습니다",
"message": "새 버전을 사용할 수 있습니다", "message": "새 버전을 사용할 수 있습니다",
"title": "업데이트 사용 가능" "title": "업데이트 사용 가능"
} }
@ -96,23 +96,23 @@
"advanced-options": { "advanced-options": {
"label": "고급 설정", "label": "고급 설정",
"submenu": { "submenu": {
"auto-reset-app-cache": "앱 시작 시 앱 캐시 초기화하기", "auto-reset-app-cache": "앱 시작 시 앱 캐시 초기화",
"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": {
"label": "프록시 주소를 입력하세요: (비어있을 시 비활성화됨)", "label": "프록시 주소를 입력하세요: (비워두면 비활성화됨)",
"placeholder": "예제: SOCKS5://127.0.0.1:9999", "placeholder": "예제: SOCKS5://127.0.0.1:9999",
"title": "프록시 설정하기" "title": "프록시 설정"
} }
}, },
"toggle-dev-tools": "DevTools 열기" "toggle-dev-tools": "DevTools 열기"
} }
}, },
"always-on-top": "항상 최상단에 표시하기", "always-on-top": "항상 최상단에 표시",
"auto-update": "자동 업데이트", "auto-update": "자동 업데이트",
"hide-menu": { "hide-menu": {
"dialog": { "dialog": {
@ -153,16 +153,15 @@
"custom-window-title": { "custom-window-title": {
"label": "사용자 정의 앱 제목", "label": "사용자 정의 앱 제목",
"prompt": { "prompt": {
"label": "앱 제목으로 표시할 내용: (빈칸일 시 비활성화)", "label": "앱 제목으로 표시할 내용 : (빈 칸 일시 비활성화)",
"placeholder": "예: {{applicationName}}" "placeholder": "예: {{applicationName}}"
} }
}, },
"like-buttons": { "like-buttons": {
"default": "기본", "default": "기본",
"force-show": "강제로 표시하기", "force-show": "강제로 표시",
"hide": "숨기기", "hide": "숨기기",
"label": "좋아요 버튼", "label": "좋아요 버튼"
"swap": "\"좋아요\" 버튼 순서 변경하기"
}, },
"remove-upgrade-button": "업그레이드 버튼 제거", "remove-upgrade-button": "업그레이드 버튼 제거",
"theme": { "theme": {
@ -172,7 +171,7 @@
"remove": "제거" "remove": "제거"
}, },
"remove-theme": "사용자 정의 테마를 제거하시겠습니까?", "remove-theme": "사용자 정의 테마를 제거하시겠습니까?",
"remove-theme-message": "이 설정을 변경하면 커스텀 테마가 삭제됩니다" "remove-theme-message": "사용자 정의 테마를 제거하시겠습니까?"
}, },
"label": "테마", "label": "테마",
"submenu": { "submenu": {
@ -187,7 +186,7 @@
"plugins": { "plugins": {
"enabled": "활성화", "enabled": "활성화",
"label": "확장", "label": "확장",
"new": "새 플러그인" "new": "NEW"
}, },
"view": { "view": {
"label": "보기", "label": "보기",
@ -216,7 +215,7 @@
}, },
"plugins": { "plugins": {
"ad-speedup": { "ad-speedup": {
"description": "광고가 재생될 때 소리를 음소거고 재생 속도 16배로 설정니다", "description": "광고가 재생될 때, 오디오가 음소거고 재생 속도 16배로 설정니다",
"name": "광고 배속" "name": "광고 배속"
}, },
"adblocker": { "adblocker": {
@ -244,7 +243,7 @@
"name": "앨범 컬러 기반 테마" "name": "앨범 컬러 기반 테마"
}, },
"ambient-mode": { "ambient-mode": {
"description": "영상의 간접 조명을 화면 배경에 투사하여 조명 효과를 적용합니다", "description": "영상의 간접 조명을 화면 배경에 투사합니다",
"menu": { "menu": {
"blur-amount": { "blur-amount": {
"label": "흐림 효과 강도", "label": "흐림 효과 강도",
@ -253,7 +252,7 @@
} }
}, },
"buffer": { "buffer": {
"label": "버퍼", "label": "버퍼",
"submenu": { "submenu": {
"buffer": "{{buffer}}" "buffer": "{{buffer}}"
} }
@ -290,7 +289,7 @@
}, },
"amuse": { "amuse": {
"description": "6K Labs Amuse의 'now playing' 위젯에 {{applicationName}} 지원 추가", "description": "6K Labs Amuse의 'now playing' 위젯에 {{applicationName}} 지원 추가",
"name": "Amuse (어뮤즈)", "name": "Amuse",
"response": { "response": {
"query": "Amuse API 서버가 실행 중입니다. GET /query로 노래 정보를 가져오세요." "query": "Amuse API 서버가 실행 중입니다. GET /query로 노래 정보를 가져오세요."
} }
@ -322,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": "포트"
} }
@ -359,10 +342,10 @@
"name": "오디오 컴프레서" "name": "오디오 컴프레서"
}, },
"auth-proxy-adapter": { "auth-proxy-adapter": {
"description": "인증이 필요한 프록시를 지원합니다", "description": "아이디/패스워드가 필요한 프록시를 지원합니다",
"menu": { "menu": {
"disable": "프록시 어댑터 비활성화", "disable": "프록시 어댑터 차단",
"enable": "프록시 어댑터 활성화", "enable": "프록시 어댑터 허용",
"hostname": { "hostname": {
"label": "호스트 명" "label": "호스트 명"
}, },
@ -373,11 +356,11 @@
"name": "권한 프록시 어댑터", "name": "권한 프록시 어댑터",
"prompt": { "prompt": {
"hostname": { "hostname": {
"label": "로컬 프록시 서버의 호스트명을 입력해주세요 (재시이 필요합니다):", "label": "로컬 프록시 서버의 호스트명 을 입력 해주세요 (재시이 필요합니다):",
"title": "프록시 호스트명" "title": "프록시 호스트명"
}, },
"port": { "port": {
"label": "로컬 프록시 서버의 포트를 입력 해주세요 (재시이 필요합니다):", "label": "로컬 프록시 서버의 포트를 입력 해주세요 (재시이 필요합니다):",
"title": "프록시 포트" "title": "프록시 포트"
} }
} }
@ -387,7 +370,7 @@
"name": "탐색 바 흐림 효과" "name": "탐색 바 흐림 효과"
}, },
"bypass-age-restrictions": { "bypass-age-restrictions": {
"description": "플레이어의 연령 확인 인증 우회", "description": "음악 플레이어의 연령 확인 우회합니다",
"name": "나이 제한 우회" "name": "나이 제한 우회"
}, },
"captions-selector": { "captions-selector": {
@ -409,21 +392,10 @@
}, },
"toast": { "toast": {
"caption-changed": "자막 언어가 {{language}}(으)로 변경되었습니다", "caption-changed": "자막 언어가 {{language}}(으)로 변경되었습니다",
"caption-disabled": "자막 비활성화", "caption-disabled": "자막 비활성화 되었습니다",
"no-captions": "이 곡에는 자막이 없습니다" "no-captions": "이 곡에는 자막이 없습니다"
} }
}, },
"clock": {
"description": "네비게이션 바 옆 시계 추가하기",
"menu": {
"format": {
"24-hour-format": "24시간 형식",
"display-seconds": "초 표시하기",
"label": "형식"
}
},
"name": "시계"
},
"compact-sidebar": { "compact-sidebar": {
"description": "사이드바를 항상 컴팩트 모드로 설정합니다", "description": "사이드바를 항상 컴팩트 모드로 설정합니다",
"name": "컴팩트 사이드바" "name": "컴팩트 사이드바"
@ -451,9 +423,9 @@
} }
}, },
"custom-output-device": { "custom-output-device": {
"description": "미디어 출력할­ 장치 구성하기", "description": "미디어 출력 장치 구성",
"menu": { "menu": {
"device-selector": "장치 선택하세요" "device-selector": "장치 선택"
}, },
"name": "출력 장치 커스텀", "name": "출력 장치 커스텀",
"prompt": { "prompt": {
@ -473,30 +445,30 @@
"discord": { "discord": {
"backend": { "backend": {
"already-connected": "활성화 된 연결에 연결을 시도했습니다", "already-connected": "활성화 된 연결에 연결을 시도했습니다",
"connected": "Discord에 연결됨", "connected": "디스코드에 연결됨",
"disconnected": "Discord에서 연결이 끊김" "disconnected": "디스코드에서 연결이 끊김"
}, },
"description": "Rich Presence를 사용하여 친구들에게 내가 듣는 음악을 보여주세요", "description": "활동 상태를 사용하여 친구들에게 내가 듣는 음악을 보여주세요",
"menu": { "menu": {
"auto-reconnect": "자동 연결", "auto-reconnect": "자동 연결",
"clear-activity": "활동 제거", "clear-activity": "활동 제거",
"clear-activity-after-timeout": "시간 초과 시 활동 제거", "clear-activity-after-timeout": "시간 초과 시 활동 제거",
"connected": "연결됨", "connected": "연결 됨",
"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": "{곡 제목} 듣는 중"
} }
} }
}, },
"name": "Discord 활동 상태", "name": "디스코드 활동 상태",
"prompt": { "prompt": {
"set-inactivity-timeout": { "set-inactivity-timeout": {
"label": "비활성 시간 제한을 초 단위로 입력하세요:", "label": "비활성 시간 제한을 초 단위로 입력하세요:",
@ -511,7 +483,7 @@
"buttons": { "buttons": {
"ok": "확인" "ok": "확인"
}, },
"message": "앗! 죄송합니다. 다운로드가 실패했습니다…", "message": "죄송합니다. 다운로드가 실패했습니다…",
"title": "다운로드 중 오류 발생!" "title": "다운로드 중 오류 발생!"
}, },
"start-download-playlist": { "start-download-playlist": {
@ -524,7 +496,7 @@
} }
}, },
"feedback": { "feedback": {
"conversion-progress": "변환: {{percent}}%", "conversion-progress": "변환: {{percent}}%",
"converting": "변환 중…", "converting": "변환 중…",
"done": "완료: {{filePath}}", "done": "완료: {{filePath}}",
"download-info": "{{artist}} - {{title}} [{{videoId}} 다운로드 중", "download-info": "{{artist}} - {{title}} [{{videoId}} 다운로드 중",
@ -539,7 +511,7 @@
"playlist-has-only-one-song": "재생목록에 한 항목만 존재합니다. 직접 다운로드합니다", "playlist-has-only-one-song": "재생목록에 한 항목만 존재합니다. 직접 다운로드합니다",
"playlist-id-not-found": "재생목록 ID를 찾을 수 없습니다", "playlist-id-not-found": "재생목록 ID를 찾을 수 없습니다",
"playlist-is-empty": "재생목록이 비어있습니다", "playlist-is-empty": "재생목록이 비어있습니다",
"playlist-is-mix-or-private": "재생목록 정보 가져오는 중 오류 발생: 비공개 재생목록 또는 \"나만을 위한 맞춤 믹스\" 재생목록이 아닌지 확인하세요\n\n{{error}}", "playlist-is-mix-or-private": "재생목록 정보 가져오는 중 오류 발생: 비공개 재생목록 또는 '유튜브 Mix' 재생목록이 아닌지 확인하세요\n\n{{error}}",
"preparing-file": "파일 준비 중…", "preparing-file": "파일 준비 중…",
"saving": "저장 중…", "saving": "저장 중…",
"trying-to-get-playlist-id": "재생목록 ID를 가져오는 중: {{playlistId}}", "trying-to-get-playlist-id": "재생목록 ID를 가져오는 중: {{playlistId}}",

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

@ -161,8 +161,7 @@
"default": "Padrão", "default": "Padrão",
"force-show": "Forçar exibir", "force-show": "Forçar exibir",
"hide": "Ocultar", "hide": "Ocultar",
"label": "Botões de 'Curtir'", "label": "Botões de 'Curtir'"
"swap": "Inverter ordem dos botões de curtir"
}, },
"remove-upgrade-button": "Remover botão de atualização", "remove-upgrade-button": "Remover botão de atualização",
"theme": { "theme": {
@ -322,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"
} }
@ -413,17 +396,6 @@
"no-captions": "Sem legendas disponíveis para essa música" "no-captions": "Sem legendas disponíveis para essa música"
} }
}, },
"clock": {
"description": "Adicionar relógio na barra de navegação",
"menu": {
"format": {
"24-hour-format": "Formato 24 horas",
"display-seconds": "Mostrar segundos",
"label": "Formato"
}
},
"name": "Relógio"
},
"compact-sidebar": { "compact-sidebar": {
"description": "Sempre definir a barra lateral no modo compacto", "description": "Sempre definir a barra lateral no modo compacto",
"name": "Barra lateral compacta" "name": "Barra lateral compacta"
@ -490,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

@ -1,12 +0,0 @@
{
"common": {
"console": {
"plugins": {
"execute-failed": "Плагины толорор кыаҕа суох {{pluginName}}::{{contextName}}",
"executed-at-ms": "Плагин {{pluginName}}::{{contextName}} толоруллубут {{ms}}мс",
"initialize-failed": "\"{{pluginName}}\" плагины инициализациялааһын кыаллыбата",
"load-all": "Плагиннары загрузкалыбыт"
}
}
}
}

View File

@ -396,17 +396,6 @@
"no-captions": "Pre túto skladbu nie sú dostupné žiadne titulky" "no-captions": "Pre túto skladbu nie sú dostupné žiadne titulky"
} }
}, },
"clock": {
"description": "Pridať hodiny do navigačnej lišty",
"menu": {
"format": {
"24-hour-format": "24-hodinový formát",
"display-seconds": "Zobraz sekundy",
"label": "Formát"
}
},
"name": "Hodiny"
},
"compact-sidebar": { "compact-sidebar": {
"description": "Vždy nastaviť bočný panel do kompaktného režimu", "description": "Vždy nastaviť bočný panel do kompaktného režimu",
"name": "Kompaktný bočný panel" "name": "Kompaktný bočný panel"
@ -473,8 +462,8 @@
"set-status-display-type": { "set-status-display-type": {
"label": "Text stavu", "label": "Text stavu",
"submenu": { "submenu": {
"application": "Počúvať {{applicationName}}",
"artist": "Aktuálne si prehráva {artist}", "artist": "Aktuálne si prehráva {artist}",
"application": "Počúvať {{applicationName}}",
"title": "Aktuálne si prehráva {song title}" "title": "Aktuálne si prehráva {song title}"
} }
} }

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

@ -161,8 +161,7 @@
"default": "默认", "default": "默认",
"force-show": "强制显示", "force-show": "强制显示",
"hide": "隐藏", "hide": "隐藏",
"label": "点赞按钮", "label": "点赞按钮"
"swap": "交换“点赞”按钮顺序"
}, },
"remove-upgrade-button": "移除升级按钮", "remove-upgrade-button": "移除升级按钮",
"theme": { "theme": {
@ -413,17 +412,6 @@
"no-captions": "这首歌没有字幕" "no-captions": "这首歌没有字幕"
} }
}, },
"clock": {
"description": "添加时钟到导航栏",
"menu": {
"format": {
"24-hour-format": "24 小时格式",
"display-seconds": "显示秒数",
"label": "格式"
}
},
"name": "时钟"
},
"compact-sidebar": { "compact-sidebar": {
"description": "始终将侧边栏设为紧凑模式", "description": "始终将侧边栏设为紧凑模式",
"name": "紧凑式侧边栏" "name": "紧凑式侧边栏"

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,9 +1,15 @@
import { type DataConnection, Peer, type PeerError } from 'peerjs'; import {
type DataConnection,
Peer,
type PeerError,
PeerErrorType,
} from 'peerjs';
import delay from 'delay';
import type { Permission, Profile, VideoData } from './types'; import type { Permission, Profile, VideoData } from './types';
export type ConnectionEventMap = { export type ConnectionEventMap = {
CLEAR_QUEUE: null; CLEAR_QUEUE: {};
ADD_SONGS: { videoList: VideoData[]; index?: number }; ADD_SONGS: { videoList: VideoData[]; index?: number };
REMOVE_SONG: { index: number }; REMOVE_SONG: { index: number };
MOVE_SONG: { fromIndex: number; toIndex: number }; MOVE_SONG: { fromIndex: number; toIndex: number };
@ -98,14 +104,16 @@ export class Connection {
this.peer.disconnect(); this.peer.disconnect();
this.peer.destroy(); this.peer.destroy();
}); });
this.peer.on('error', (err) => { this.peer.on('error', async (err) => {
if (err.type === 'network') { if (err.type === PeerErrorType.Network) {
setTimeout(() => { // retrying after 10 seconds
await delay(10000);
try { try {
this.peer.reconnect(); this.peer.reconnect();
} catch {}
}, 10000);
return; return;
} catch {
//ignored
}
} }
this.waitOpen.reject(err); this.waitOpen.reject(err);
@ -168,9 +176,7 @@ export class Connection {
after?: ConnectionEventUnion[], after?: ConnectionEventUnion[],
) { ) {
await Promise.all( await Promise.all(
this.getConnections().map( this.getConnections().map((conn) => conn.send({ type, payload, after })),
(conn) => conn.send({ type, payload, after }) ?? Promise.resolve(),
),
); );
} }

View File

@ -224,7 +224,7 @@ export default createPlugin<
} }
this.queue?.clear(); this.queue?.clear();
await this.connection?.broadcast('CLEAR_QUEUE', null); await this.connection?.broadcast('CLEAR_QUEUE', {});
break; break;
} }
case 'SET_INDEX': { case 'SET_INDEX': {
@ -413,7 +413,7 @@ export default createPlugin<
this.ignoreChange = true; this.ignoreChange = true;
switch (event.type) { switch (event.type) {
case 'CLEAR_QUEUE': { case 'CLEAR_QUEUE': {
await this.connection?.broadcast('CLEAR_QUEUE', null); await this.connection?.broadcast('CLEAR_QUEUE', {});
break; break;
} }
case 'SET_INDEX': { case 'SET_INDEX': {

View File

@ -316,7 +316,7 @@ export class Queue {
this.ignoreFlag = true; this.ignoreFlag = true;
this.broadcast({ this.broadcast({
type: 'CLEAR_QUEUE', type: 'CLEAR_QUEUE',
payload: null, payload: {},
}); });
return; return;
} }

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);
}
} }
/** /**