Compare commits

..

1 Commits

Author SHA1 Message Date
9b87ad26b1 chore(deps): update playwright monorepo to v1.57.0 2025-12-20 18:04:02 +00:00
65 changed files with 2110 additions and 2802 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

@ -1,35 +0,0 @@
<svg width="1024" height="1024" viewBox="0 0 1024 1024" fill="none" xmlns="http://www.w3.org/2000/svg">
<g filter="url(#filter0_d_18_2)">
<circle cx="512" cy="512" r="410" fill="url(#paint0_linear_18_2)"/>
<circle cx="512" cy="512" r="402" stroke="url(#paint1_radial_18_2)" stroke-opacity="0.5" stroke-width="16"/>
</g>
<path d="M675 505.072C680.333 508.152 680.333 515.849 675 518.928L436.5 656.626C431.167 659.705 424.5 655.857 424.5 649.698V374.302C424.5 368.24 430.96 364.415 436.249 367.234L436.5 367.374L675 505.072Z" fill="url(#paint2_linear_18_2)" stroke="url(#paint3_linear_18_2)" stroke-width="8" stroke-linejoin="round"/>
<defs>
<filter id="filter0_d_18_2" x="78" y="90" width="868" height="868" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="12"/>
<feGaussianBlur stdDeviation="12"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0.866667 0 0 0 0 0.141176 0 0 0 0 0.462745 0 0 0 0.4 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_18_2"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_18_2" result="shape"/>
</filter>
<linearGradient id="paint0_linear_18_2" x1="102" y1="102" x2="922" y2="922" gradientUnits="userSpaceOnUse">
<stop stop-color="#FF632F"/>
<stop offset="1" stop-color="#DC148C"/>
</linearGradient>
<radialGradient id="paint1_radial_18_2" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(512 512) rotate(45) scale(579.828)">
<stop offset="0.68" stop-color="white" stop-opacity="0"/>
<stop offset="0.72" stop-color="white"/>
</radialGradient>
<linearGradient id="paint2_linear_18_2" x1="512" y1="329" x2="512" y2="695" gradientUnits="userSpaceOnUse">
<stop stop-color="white"/>
<stop offset="1" stop-color="white" stop-opacity="0.4"/>
</linearGradient>
<linearGradient id="paint3_linear_18_2" x1="512" y1="329" x2="512" y2="695" gradientUnits="userSpaceOnUse">
<stop stop-color="white" stop-opacity="0"/>
<stop offset="1" stop-color="white" stop-opacity="0.5"/>
</linearGradient>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 2.2 KiB

View File

@ -1,67 +0,0 @@
{
"fill" : {
"linear-gradient" : [
"display-p3:1.00000,1.00000,1.00000,1.00000",
"srgb:0.84314,0.84314,0.84314,1.00000"
],
"orientation" : {
"start" : {
"x" : 0.5,
"y" : 0
},
"stop" : {
"x" : 0.5,
"y" : 0.7
}
}
},
"groups" : [
{
"blur-material" : null,
"hidden" : false,
"layers" : [
{
"blend-mode-specializations" : [
{
"appearance" : "dark",
"value" : "normal"
}
],
"image-name" : "SVG Image.svg",
"name" : "transparent-icon",
"opacity-specializations" : [
{
"value" : 1
},
{
"appearance" : "dark",
"value" : 1
}
]
}
],
"name" : "group",
"opacity-specializations" : [
{
"appearance" : "dark",
"value" : 0.8
}
],
"shadow" : {
"kind" : "layer-color",
"opacity" : 0.5
},
"specular" : true,
"translucency" : {
"enabled" : false,
"value" : 0.5
}
}
],
"supported-platforms" : {
"circles" : [
"watchOS"
],
"squares" : "shared"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 790 KiB

After

Width:  |  Height:  |  Size: 172 KiB

View File

@ -1,40 +1,10 @@
<svg width="1024" height="1024" viewBox="0 0 1024 1024" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="180" height="180" viewBox="0 0 200 200" fill="none" xmlns="http://www.w3.org/2000/svg" id="icon-play">
<g filter="url(#filter0_d_18_2)">
<circle cx="512" cy="512" r="410" fill="url(#paint0_linear_18_2)" />
<circle cx="512" cy="512" r="402" stroke="url(#paint1_radial_18_2)" stroke-opacity="0.5" stroke-width="16" />
</g>
<path
d="M675 505.072C680.333 508.152 680.333 515.849 675 518.928L436.5 656.626C431.167 659.705 424.5 655.857 424.5 649.698V374.302C424.5 368.24 430.96 364.415 436.249 367.234L436.5 367.374L675 505.072Z"
fill="url(#paint2_linear_18_2)" stroke="url(#paint3_linear_18_2)" stroke-width="8" stroke-linejoin="round" />
<defs> <defs>
<filter id="filter0_d_18_2" x="78" y="90" width="868" height="868" filterUnits="userSpaceOnUse" <linearGradient id="grad-play" x1="10%" y1="10%" x2="90%" y2="90%">
color-interpolation-filters="sRGB"> <stop offset="0%" style="stop-color:#FF512F; stop-opacity:1"/>
<feFlood flood-opacity="0" result="BackgroundImageFix" /> <stop offset="100%" style="stop-color:#DD2476; stop-opacity:1"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
result="hardAlpha" />
<feOffset dy="12" />
<feGaussianBlur stdDeviation="12" />
<feComposite in2="hardAlpha" operator="out" />
<feColorMatrix type="matrix" values="0 0 0 0 0.866667 0 0 0 0 0.141176 0 0 0 0 0.462745 0 0 0 0.4 0" />
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_18_2" />
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_18_2" result="shape" />
</filter>
<linearGradient id="paint0_linear_18_2" x1="102" y1="102" x2="922" y2="922" gradientUnits="userSpaceOnUse">
<stop stop-color="#FF632F" />
<stop offset="1" stop-color="#DC148C" />
</linearGradient>
<radialGradient id="paint1_radial_18_2" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse"
gradientTransform="translate(512 512) rotate(45) scale(579.828)">
<stop offset="0.68" stop-color="white" stop-opacity="0" />
<stop offset="0.72" stop-color="white" />
</radialGradient>
<linearGradient id="paint2_linear_18_2" x1="512" y1="329" x2="512" y2="695" gradientUnits="userSpaceOnUse">
<stop stop-color="white" />
<stop offset="1" stop-color="white" stop-opacity="0.4" />
</linearGradient>
<linearGradient id="paint3_linear_18_2" x1="512" y1="329" x2="512" y2="695" gradientUnits="userSpaceOnUse">
<stop stop-color="white" stop-opacity="0" />
<stop offset="1" stop-color="white" stop-opacity="0.5" />
</linearGradient> </linearGradient>
</defs> </defs>
<circle cx="100" cy="100" r="90" fill="url(#grad-play)"/>
<path d="M85 70 L130 100 L85 130 Z" fill="white" stroke="white" stroke-width="4" stroke-linejoin="round"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 544 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 6.7 KiB

View File

@ -20,7 +20,7 @@ mac:
arch: arch:
- x64 - x64
- arm64 - arm64
icon: assets/generated/icons/mac/icon.icon icon: assets/generated/icons/mac/icon.icns
compression: maximum compression: maximum
win: win:
icon: assets/generated/icons/win/icon.ico icon: assets/generated/icons/win/icon.ico
@ -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,141 +21,168 @@ 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 = {
experimental: {
enableNativePlugin: true,
},
plugins: [
pluginLoader('backend'),
viteResolve({
'virtual:i18n': i18nImporter(),
'virtual:plugins': pluginVirtualModuleGenerator('main'),
}),
],
publicDir: 'assets',
define: {
'__dirname': 'import.meta.dirname',
'__filename': 'import.meta.filename',
},
build: {
lib: {
entry: 'src/index.ts',
formats: ['es'],
},
outDir: 'dist/main',
rolldownOptions: {
external: ['electron', 'custom-electron-prompt', ...builtinModules],
input: './src/index.ts',
},
},
resolve: {
alias: resolveAlias,
},
};
const mainConfig: UserConfig = { if (mode === 'development') {
experimental: { commonConfig.build!.sourcemap = 'inline';
enableNativePlugin: true, commonConfig.plugins?.push(
}, Inspect({
plugins: [ build: true,
pluginLoader('backend'), outputDir: join(__dirname, '.vite-inspect/backend'),
viteResolve({ }),
'virtual:i18n': i18nImporter(), );
'virtual:plugins': pluginVirtualModuleGenerator('main'), return commonConfig;
}), }
],
publicDir: 'assets',
define: {
__dirname: 'import.meta.dirname',
__filename: 'import.meta.filename',
},
build: {
lib: {
entry: 'src/index.ts',
formats: ['es'],
},
outDir: 'dist/main',
rolldownOptions: {
external: ['electron', 'custom-electron-prompt', ...builtinModules],
input: './src/index.ts',
},
minify: !isDev,
cssMinify: !isDev,
sourcemap: isDev ? 'inline' : undefined,
},
resolve: {
alias: resolveAlias,
},
};
const preloadConfig: UserConfig = { return {
experimental: { ...commonConfig,
enableNativePlugin: true, build: {
}, ...commonConfig.build,
plugins: [ minify: true,
pluginLoader('preload'), cssMinify: true,
viteResolve({
'virtual:i18n': i18nImporter(),
'virtual:plugins': pluginVirtualModuleGenerator('preload'),
}),
],
build: {
lib: {
entry: 'src/preload.ts',
formats: ['cjs'],
}, },
outDir: 'dist/preload', };
commonjsOptions: { }),
ignoreDynamicRequires: true, preload: defineViteConfig(({ mode }) => {
const commonConfig: UserConfig = {
experimental: {
enableNativePlugin: true,
}, },
rolldownOptions: { plugins: [
external: ['electron', 'custom-electron-prompt', ...builtinModules], pluginLoader('preload'),
input: './src/preload.ts', viteResolve({
'virtual:i18n': i18nImporter(),
'virtual:plugins': pluginVirtualModuleGenerator('preload'),
}),
],
build: {
lib: {
entry: 'src/preload.ts',
formats: ['cjs'],
},
outDir: 'dist/preload',
commonjsOptions: {
ignoreDynamicRequires: true,
},
rolldownOptions: {
external: ['electron', 'custom-electron-prompt', ...builtinModules],
input: './src/preload.ts',
},
}, },
minify: !isDev, resolve: {
cssMinify: !isDev, alias: resolveAlias,
sourcemap: isDev ? 'inline' : undefined, },
}, };
resolve: {
alias: resolveAlias,
},
};
const rendererConfig: UserConfig = { if (mode === 'development') {
experimental: { commonConfig.build!.sourcemap = 'inline';
enableNativePlugin: !isDev, // Disable native plugin in development mode to avoid issues with HMR (bug in rolldown-vite) commonConfig.plugins?.push(
}, Inspect({
plugins: [ build: true,
pluginLoader('renderer'), outputDir: join(__dirname, '.vite-inspect/preload'),
viteResolve({ }),
'virtual:i18n': i18nImporter(), );
'virtual:plugins': pluginVirtualModuleGenerator('renderer'), return commonConfig;
}), }
withFilter(solidPlugin(), {
load: { id: [/\.(tsx|jsx)$/, '/@solid-refresh'] },
}),
],
root: './src/',
build: {
lib: {
entry: 'src/index.html',
formats: ['iife'],
name: 'renderer',
},
outDir: 'dist/renderer',
rolldownOptions: {
external: ['electron', ...builtinModules],
input: './src/index.html',
},
minify: !isDev,
cssMinify: !isDev,
sourcemap: isDev ? 'inline' : undefined,
},
resolve: {
alias: resolveAlias,
},
server: {
cors: {
origin: 'https://music.\u0079\u006f\u0075\u0074\u0075\u0062\u0065.com',
},
},
};
if (isDev) { return {
mainConfig.plugins?.push( ...commonConfig,
Inspect({ build: {
build: true, ...commonConfig.build,
outputDir: join(__dirname, '.vite-inspect/backend'), minify: true,
}), cssMinify: true,
); },
preloadConfig.plugins?.push( };
Inspect({ }),
build: true, renderer: defineViteConfig(({ mode }) => {
outputDir: join(__dirname, '.vite-inspect/preload'), const commonConfig: UserConfig = {
}), experimental: {
); enableNativePlugin: mode !== 'development', // Disable native plugin in development mode to avoid issues with HMR (bug in rolldown-vite)
rendererConfig.plugins?.push( },
Inspect({ plugins: [
build: true, pluginLoader('renderer'),
outputDir: join(__dirname, '.vite-inspect/renderer'), viteResolve({
}), 'virtual:i18n': i18nImporter(),
); 'virtual:plugins': pluginVirtualModuleGenerator('renderer'),
} }),
withFilter(solidPlugin(), {
load: { id: [/\.(tsx|jsx)$/, '/@solid-refresh'] },
}),
],
root: './src/',
build: {
lib: {
entry: 'src/index.html',
formats: ['iife'],
name: 'renderer',
},
outDir: 'dist/renderer',
rolldownOptions: {
external: ['electron', ...builtinModules],
input: './src/index.html',
},
},
resolve: {
alias: resolveAlias,
},
server: {
cors: {
origin:
'https://music.\u0079\u006f\u0075\u0074\u0075\u0062\u0065.com',
},
},
};
return { if (mode === 'development') {
main: mainConfig, commonConfig.build!.sourcemap = 'inline';
preload: preloadConfig, commonConfig.plugins?.push(
renderer: rendererConfig, Inspect({
}; build: true,
outputDir: join(__dirname, '.vite-inspect/renderer'),
}),
);
return commonConfig;
}
return {
...commonConfig,
build: {
...commonConfig.build,
minify: true,
cssMinify: true,
},
};
}),
}); });

View File

@ -18,89 +18,51 @@ export default tsEslint.config(
{ {
plugins: { plugins: {
stylistic, stylistic,
importPlugin, importPlugin
}, },
languageOptions: { languageOptions: {
parser: tsEslint.parser, parser: tsEslint.parser,
parserOptions: { parserOptions: {
project: ['tsconfig.json', 'tsconfig.test.json'], project: true,
sourceType: 'module', sourceType: 'module',
ecmaVersion: 'latest', ecmaVersion: 'latest'
}, }
}, },
rules: { rules: {
'stylistic/arrow-parens': ['error', 'always'], 'stylistic/arrow-parens': ['error', 'always'],
'stylistic/object-curly-spacing': ['error', 'always'], 'stylistic/object-curly-spacing': ['error', 'always'],
'stylistic/jsx-pascal-case': 'error', 'stylistic/jsx-pascal-case': 'error',
'stylistic/jsx-curly-spacing': [ 'stylistic/jsx-curly-spacing': ['error', { when: 'never', children: true }],
'error',
{ when: 'never', children: true },
],
'stylistic/jsx-sort-props': 'error', 'stylistic/jsx-sort-props': 'error',
'prettier/prettier': [ 'prettier/prettier': ['error', { singleQuote: true, semi: true, tabWidth: 2, trailingComma: 'all', quoteProps: 'preserve' }],
'error',
{
singleQuote: true,
semi: true,
tabWidth: 2,
trailingComma: 'all',
quoteProps: 'preserve',
},
],
'@typescript-eslint/no-floating-promises': 'off', '@typescript-eslint/no-floating-promises': 'off',
'@typescript-eslint/no-misused-promises': [ '@typescript-eslint/no-misused-promises': ['off', { checksVoidReturn: false }],
'off', '@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }],
{ checksVoidReturn: false },
],
'@typescript-eslint/no-unused-vars': [
'warn',
{ argsIgnorePattern: '^_' },
],
'@typescript-eslint/no-non-null-assertion': 'off', '@typescript-eslint/no-non-null-assertion': 'off',
'@typescript-eslint/consistent-type-imports': [ '@typescript-eslint/consistent-type-imports': ['error', {
'error', fixStyle: 'inline-type-imports',
{ prefer: 'type-imports',
fixStyle: 'inline-type-imports', disallowTypeAnnotations: false,
prefer: 'type-imports', }],
disallowTypeAnnotations: false,
},
],
'importPlugin/first': 'error', 'importPlugin/first': 'error',
'importPlugin/newline-after-import': 'off', 'importPlugin/newline-after-import': 'off',
'importPlugin/no-default-export': 'off', 'importPlugin/no-default-export': 'off',
'importPlugin/no-duplicates': 'error', 'importPlugin/no-duplicates': 'error',
'importPlugin/no-unresolved': [ 'importPlugin/no-unresolved': ['error', { ignore: ['^virtual:', '\\?inline$', '\\?raw$', '\\?asset&asarUnpack'] }],
'error', 'importPlugin/order': ['error', {
{ 'groups': ['builtin', 'external', ['internal', 'index', 'sibling'], 'parent', 'type'],
ignore: ['^virtual:', '\\?inline$', '\\?raw$', '\\?asset&asarUnpack'], 'newlines-between': 'always-and-inside-groups',
}, 'alphabetize': { order: 'ignore', caseInsensitive: false }
], }],
'importPlugin/order': [
'error',
{
'groups': [
'builtin',
'external',
['internal', 'index', 'sibling'],
'parent',
'type',
],
'newlines-between': 'always-and-inside-groups',
'alphabetize': { order: 'ignore', caseInsensitive: false },
},
],
'importPlugin/prefer-default-export': 'off', 'importPlugin/prefer-default-export': 'off',
'camelcase': ['error', { properties: 'never' }], 'camelcase': ['error', { properties: 'never' }],
'class-methods-use-this': 'off', 'class-methods-use-this': 'off',
'stylistic/lines-around-comment': [ 'stylistic/lines-around-comment': ['error', {
'error', beforeBlockComment: false,
{ afterBlockComment: false,
beforeBlockComment: false, beforeLineComment: false,
afterBlockComment: false, afterLineComment: false,
beforeLineComment: false, }],
afterLineComment: false,
},
],
'stylistic/max-len': 'off', 'stylistic/max-len': 'off',
'stylistic/no-mixed-operators': 'warn', // prettier does not support no-mixed-operators 'stylistic/no-mixed-operators': 'warn', // prettier does not support no-mixed-operators
'stylistic/no-multi-spaces': ['error', { ignoreEOLComments: true }], 'stylistic/no-multi-spaces': ['error', { ignoreEOLComments: true }],
@ -108,20 +70,16 @@ export default tsEslint.config(
'no-void': 'error', 'no-void': 'error',
'no-empty': 'off', 'no-empty': 'off',
'prefer-promise-reject-errors': 'off', 'prefer-promise-reject-errors': 'off',
'stylistic/quotes': [ 'stylistic/quotes': ['error', 'single', {
'error', avoidEscape: true,
'single', allowTemplateLiterals: false,
{ }],
avoidEscape: true,
allowTemplateLiterals: 'never',
},
],
'stylistic/quote-props': ['error', 'consistent'], 'stylistic/quote-props': ['error', 'consistent'],
'stylistic/semi': ['error', 'always'], 'stylistic/semi': ['error', 'always'],
}, },
settings: { settings: {
'import/parsers': { 'import/parsers': {
'@typescript-eslint/parser': ['.ts'], '@typescript-eslint/parser': ['.ts']
}, },
'import/resolver': { 'import/resolver': {
typescript: {}, typescript: {},

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.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",
@ -139,44 +139,45 @@
}, },
"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.57.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.0.12",
"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",
"playwright": "1.57.0",
"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,

2865
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

@ -184,31 +184,9 @@
} }
}, },
"plugins": { "plugins": {
"enabled": "Omogućeno", "enabled": "Omogući",
"label": "Dodaci", "label": "Dodaci",
"new": "Novo" "new": "Novo"
},
"view": {
"label": "Pogled",
"submenu": {
"force-reload": "Silom Ponovo Učitaj",
"reload": "Ponovo Učitaj",
"reset-zoom": "Stvarna Veličina",
"toggle-fullscreen": "Uključi/Isključi Prikaz Cijelog Ekrana",
"zoom-in": "Uvećaj",
"zoom-out": "Umanji"
}
}
},
"tray": {
"next": "Slijedeće",
"play-pause": "Plej/Pauza",
"previous": "Prethodno",
"quit": "Izlaz",
"restart": "Ponovo Pokreni Aplikaciju",
"show": "Pokaži prozor",
"tooltip": {
"default": "{{applicationName}}"
} }
} }
}, },
@ -219,53 +197,6 @@
"label": "Kvalitet" "label": "Kvalitet"
} }
} }
},
"discord": {
"menu": {
"set-status-display-type": {
"submenu": {
"application": "Slušate {{applicationName}}",
"artist": "Slušate {muzičar}",
"title": "Slušate {naziv pesme}"
}
}
},
"name": "Diskord Rich Presence",
"prompt": {
"set-inactivity-timeout": {
"label": "Unesi ograničenje neaktivnosti u sekundama:",
"title": "Postavi ograničenje neaktivnosti"
}
}
},
"downloader": {
"backend": {
"dialog": {
"error": {
"buttons": {
"ok": "OK"
},
"message": "Ufff! Izvinite, preuzimanje nije uspelo…",
"title": "Greška u preuzimanju!"
},
"start-download-playlist": {
"buttons": {
"ok": "OK"
},
"detail": "({{Playlist Size}} pjesme)",
"message": "Preuzimanje Plejliste {{playlist Title}}",
"title": "Preuzimanje počelo"
}
},
"feedback": {
"conversion-progress": "Pretvaranje: {{percent}}%",
"converting": "Pretvaranje…",
"done": "Gotovo: {{file Path}}",
"download-info": "Preuzimanje {{artist}} - {{title}} [{{videoId}}",
"download-progress": "Preuzimanje: {{percent}}%",
"downloading": "Preuzimanje…"
}
}
} }
} }
} }

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

@ -288,7 +288,7 @@
"name": "Ambiente-Modus" "name": "Ambiente-Modus"
}, },
"amuse": { "amuse": {
"description": "Fügt {{applicationName}} Unterstützung für das Amuse \"Spielt gerade\"-Widget von 6K Labs hinzu", "description": "Fügt Unterstützung für das Amuse \"Spielt gerade\"-Widget von 6K Labs hinzu",
"name": "Amuse", "name": "Amuse",
"response": { "response": {
"query": "Amuse API-Server läuft. /query für Liedinformationen." "query": "Amuse API-Server läuft. /query für Liedinformationen."
@ -321,22 +321,6 @@
"hostname": { "hostname": {
"label": "Hostname" "label": "Hostname"
}, },
"https": {
"label": "HTTPS & Zertifikate",
"submenu": {
"cert": {
"dialogTitle": "HTTPS Zertifikat Datei auswählen",
"label": "Zertifikate Datei (.crt/.pem)"
},
"enable-https": {
"label": "HTTPS aktivieren"
},
"key": {
"dialogTitle": "HTTPS privaten Schlüssel Datei auswählen",
"label": "Privater Schlüssel Datei (.key/.pem)"
}
}
},
"port": { "port": {
"label": "Port" "label": "Port"
} }
@ -478,8 +462,8 @@
"set-status-display-type": { "set-status-display-type": {
"label": "Status Text", "label": "Status Text",
"submenu": { "submenu": {
"application": "Hört {{applicationName}}",
"artist": "Hört {artist} zu", "artist": "Hört {artist} zu",
"application": "Hört {{applicationName}}",
"title": "Du hörst {song title}" "title": "Du hörst {song title}"
} }
} }
@ -883,12 +867,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

@ -321,22 +321,6 @@
"hostname": { "hostname": {
"label": "Nombre del host" "label": "Nombre del host"
}, },
"https": {
"label": "HTTPS & Certificados",
"submenu": {
"cert": {
"dialogTitle": "Selecciona el archivo de certificado HTTPS",
"label": "Archivo de certificado (.crt/.pem)"
},
"enable-https": {
"label": "Habilitar HTTPS"
},
"key": {
"dialogTitle": "Selecciona el archivo de clave privada HTTPS",
"label": "Archivo de clave privada (.key/.pem)"
}
}
},
"port": { "port": {
"label": "Puerto" "label": "Puerto"
} }
@ -478,8 +462,8 @@
"set-status-display-type": { "set-status-display-type": {
"label": "Texto de estado", "label": "Texto de estado",
"submenu": { "submenu": {
"application": "Escuchando {{applicationName}}",
"artist": "Escuchando a {artist}", "artist": "Escuchando a {artist}",
"application": "Escuchando {{applicationName}}",
"title": "Escuchando {song title}" "title": "Escuchando {song title}"
} }
} }

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

@ -320,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": "پورت"
} }
@ -477,8 +461,8 @@
"set-status-display-type": { "set-status-display-type": {
"label": "متن وضعىت", "label": "متن وضعىت",
"submenu": { "submenu": {
"application": "به پىر دسکتاپ گوش مىکند",
"artist": "به {artist} گوش مىکند", "artist": "به {artist} گوش مىکند",
"application": "به پىر دسکتاپ گوش مىکند",
"title": "به {song title} گوش مىکند" "title": "به {song title} گوش مىکند"
} }
} }

View File

@ -209,7 +209,7 @@
"show": "Afficher la fenêtre", "show": "Afficher la fenêtre",
"tooltip": { "tooltip": {
"default": "{{applicationName}}", "default": "{{applicationName}}",
"with-song-info": "{{applicationName}} : {{artist}} - {{title}}" "with-song-info": "{{applicationName}}: {{artist}} - {{title}}"
} }
} }
}, },
@ -321,22 +321,6 @@
"hostname": { "hostname": {
"label": "Nom de l'hôte" "label": "Nom de l'hôte"
}, },
"https": {
"label": "HTTPS & Certificats",
"submenu": {
"cert": {
"dialogTitle": "Sélectionner le fichier de certificat HTTPS",
"label": "Fichier de certificat (.crt/.pem)"
},
"enable-https": {
"label": "Activer HTTPS"
},
"key": {
"dialogTitle": "Sélectionner le fichier de clé privée HTTPS",
"label": "Fichier de clé privée (.key/.pem)"
}
}
},
"port": { "port": {
"label": "Port" "label": "Port"
} }
@ -478,8 +462,8 @@
"set-status-display-type": { "set-status-display-type": {
"label": "Texte d'état", "label": "Texte d'état",
"submenu": { "submenu": {
"application": "Écoute {{applicationName}}",
"artist": "Écoute {artiste}", "artist": "Écoute {artiste}",
"application": "Écoute {{applicationName}}",
"title": "Écoute {titre de la chanson}" "title": "Écoute {titre de la chanson}"
} }
} }
@ -499,8 +483,8 @@
"buttons": { "buttons": {
"ok": "Ok" "ok": "Ok"
}, },
"message": "Argh! Désolé, le téléchargement a échoué…", "message": "Argh ! Désolé, le téléchargement a échoué…",
"title": "Erreur de téléchargement!" "title": "Erreur de téléchargement !"
}, },
"start-download-playlist": { "start-download-playlist": {
"buttons": { "buttons": {
@ -508,30 +492,30 @@
}, },
"detail": "({{playlistSize}} chansons)", "detail": "({{playlistSize}} chansons)",
"message": "Téléchargement de la playlist {{playlistTitle}}", "message": "Téléchargement de la playlist {{playlistTitle}}",
"title": "Téléchargement commencé" "title": "Téléchargement a commencé"
} }
}, },
"feedback": { "feedback": {
"conversion-progress": "Conversion : {{percent}}%", "conversion-progress": "Conversion : {{percent}} %",
"converting": "Conversion…", "converting": "Conversion…",
"done": "Terminé : {{filePath}}", "done": "Terminé : {{filePath}}",
"download-info": "Téléchargement {{artist}} - {{title}} [{{videoId}}", "download-info": "Téléchargement {{artist}} - {{title}} [{{videoId}}",
"download-progress": "Téléchargé : {{percent}}%", "download-progress": "Téléchargé: {{percent}}%",
"downloading": "Télécharge…", "downloading": "Télécharge…",
"downloading-counter": "Télécharge {{current}}/{{total}}…", "downloading-counter": "Télécharge {{current}}/{{total}}…",
"downloading-playlist": "Téléchargement de la playlist \"{{playlistTitle}}\" - {{playlistSize}} chansons ({{playlistId}})", "downloading-playlist": "Téléchargement de la playlist \"{{playlistTitle}}\"  {{playlistSize}} chansons ({{playlistId}})",
"error-while-downloading": "Erreur lors du téléchargement de \"{{author}} - {{title}}\" : {{error}}", "error-while-downloading": "Erreur lors du téléchargement de \"{{author}} - {{title}}\" : {{error}}",
"folder-already-exists": "Le dossier {{playlistFolder}} existe déjà", "folder-already-exists": "Le dossier {{playlistFolder}} existe déjà",
"getting-playlist-info": "Obtention des données de la playlist…", "getting-playlist-info": "Obtention d'informations sur la liste de lecture…",
"loading": "Chargement…", "loading": "Chargement…",
"playlist-has-only-one-song": "La playlist ne contient qu'un seul élément, téléchargement du morceau seul", "playlist-has-only-one-song": "La liste de lecture ne contient qu'un seul élément, téléchargement du morceau seul",
"playlist-id-not-found": "Aucun ID de playlist trouvé", "playlist-id-not-found": "Aucun ID de liste de lecture trouvé",
"playlist-is-empty": "La playlist est vide", "playlist-is-empty": "La liste de lecture est vide",
"playlist-is-mix-or-private": "Erreur lors de l'obtention des informations sur la playlist : assurez-vous qu'il ne s'agit pas d'une playlist privée ou \"Mixée pour vous\"\n\n{{error}}", "playlist-is-mix-or-private": "Erreur lors de l'obtention des informations sur la liste de lecture: assurez-vous qu'il ne s'agit pas d'une liste privée ou \"Mixée pour vous\"\n\n{{error}}",
"preparing-file": "Préparation des fichier…", "preparing-file": "Péparer des fichier…",
"saving": "Sauvegarde…", "saving": "Sauvegarde…",
"trying-to-get-playlist-id": "Obtention de l'ID de la playlist : {{playlistId}}", "trying-to-get-playlist-id": "Obtention de l'ID de la liste de lecture: {{playlistId}}",
"video-id-not-found": "Vidéo introuvable", "video-id-not-found": "Vidéo non trouvée",
"writing-id3": "Écriture des balises ID3…" "writing-id3": "Écriture des balises ID3…"
} }
}, },
@ -541,19 +525,19 @@
"download-finish-settings": { "download-finish-settings": {
"label": "Télécharger une fois terminé", "label": "Télécharger une fois terminé",
"prompt": { "prompt": {
"last-percent": "Après x pourcents", "last-percent": "Après x pour cent",
"last-seconds": "Dernières x secondes", "last-seconds": "Dernières x secondes",
"title": "Configurer quand télécharger" "title": "Configurer quand télécharger"
}, },
"submenu": { "submenu": {
"advanced": "Avancé", "advanced": "Avancé",
"enabled": "Activé", "enabled": "Activé",
"mode": "Unité de temps", "mode": "Mode de temps",
"percent": "Pourcent", "percent": "Pourcent",
"seconds": "Secondes" "seconds": "Secondes"
} }
}, },
"download-playlist": "Télécharger la playlist", "download-playlist": "Télécharger la liste de lecture",
"presets": "Préconfigurations", "presets": "Préconfigurations",
"skip-existing": "Passer les fichiers existants" "skip-existing": "Passer les fichiers existants"
}, },
@ -590,7 +574,7 @@
}, },
"lumiastream": { "lumiastream": {
"description": "Ajoute la prise en charge de Lumia Stream", "description": "Ajoute la prise en charge de Lumia Stream",
"name": "Lumia Stream [Beta]" "name": "Lumia Stream [Bêta]"
}, },
"lyrics-genius": { "lyrics-genius": {
"description": "Ajoute la prise en charge des paroles pour la plupart des chansons", "description": "Ajoute la prise en charge des paroles pour la plupart des chansons",
@ -621,9 +605,9 @@
"host": "Hôte du Music Together", "host": "Hôte du Music Together",
"join": "Rejoindre le Music Together", "join": "Rejoindre le Music Together",
"permission": { "permission": {
"all": "Autorisez les invités à contrôler la playlist et le lecteur", "all": "Autorisez les invités à contrôler la musique et le player",
"host-only": "Seulement l'hôte peut contrôler la playlist et le lecteur", "host-only": "Seulement l'hôte peut contrôler les playlists et le lecteur",
"playlist": "Autoriser les invités à contrôler la playlist" "playlist": "Autoriser les invités à contrôler les playlists"
}, },
"set-permission": "Changer les permissions de contrôle", "set-permission": "Changer les permissions de contrôle",
"status": { "status": {
@ -641,7 +625,7 @@
"id-copied": "Identifiant de l'hôte copié dans le presse papier", "id-copied": "Identifiant de l'hôte copié dans le presse papier",
"id-copy-failed": "Echec de la copie de l'identifiant de l'hôte dans le presse papier", "id-copy-failed": "Echec de la copie de l'identifiant de l'hôte dans le presse papier",
"join-failed": "Echec en rejoignant le Music Together", "join-failed": "Echec en rejoignant le Music Together",
"joined": "Music Together rejoint", "joined": "Rejoint le Music Together",
"permission-changed": "Permission du Music Together changé à \"{{permission}}\"", "permission-changed": "Permission du Music Together changé à \"{{permission}}\"",
"remove-song-failed": "Echec du retrait de la piste", "remove-song-failed": "Echec du retrait de la piste",
"user-connected": "{{name}} à rejoint le Music Together", "user-connected": "{{name}} à rejoint le Music Together",
@ -673,7 +657,7 @@
"submenu": { "submenu": {
"hide-button-text": "Masquer le texte du bouton", "hide-button-text": "Masquer le texte du bouton",
"refresh-on-play-pause": "Actualiser lors de la lecture/pause", "refresh-on-play-pause": "Actualiser lors de la lecture/pause",
"tray-controls": "Ouvrir/Fermer au clic sur licône de la barre des tâches" "tray-controls": "Ouvrir/Fermer sur le plateau, cliquez"
} }
}, },
"priority": "Priorité des notifications", "priority": "Priorité des notifications",
@ -687,30 +671,30 @@
"name": "Amélioration des performances [Beta]" "name": "Amélioration des performances [Beta]"
}, },
"picture-in-picture": { "picture-in-picture": {
"description": "Permet de basculer lapplication en mode picture-in-picture", "description": "Permet de basculer lapplication en mode image dans image",
"menu": { "menu": {
"always-on-top": "Toujours au dessus", "always-on-top": "Toujours en haut",
"hotkey": { "hotkey": {
"label": "Raccourci clavier", "label": "Raccourci clavier",
"prompt": { "prompt": {
"keybind-options": { "keybind-options": {
"hotkey": "Raccourci clavier" "hotkey": "Raccourci clavier"
}, },
"label": "Choisissez un raccourci clavier pour activer le mode picture-in-picture", "label": "Choisissez un raccourci clavier pour activer le mode Image dans l'image",
"title": "Touche de raccourci picture-in-picture" "title": "Touche de raccourci Image dans l'image"
} }
}, },
"save-window-position": "Enregistrer la position de la fenêtre", "save-window-position": "Enregistrer la position de la fenêtre",
"save-window-size": "Enregistrer la taille de la fenêtre", "save-window-size": "Enregistrer la taille de la fenêtre",
"use-native-pip": "Utiliser le mode PiP natif du navigateur" "use-native-pip": "Utiliser le mode image dans image natif du navigateur"
}, },
"name": "Picture-in-picture", "name": "Image dans l'image",
"templates": { "templates": {
"button": "Picture-in-picture" "button": "Image dans l'image"
} }
}, },
"playback-speed": { "playback-speed": {
"description": "Écoutez vite, écoutez lentement! Ajoute un curseur qui contrôle la vitesse de la chanson", "description": "Écoutez vite, écoutez lentement ! Ajoute un curseur qui contrôle la vitesse de la chanson",
"name": "Vitesse de lecture", "name": "Vitesse de lecture",
"templates": { "templates": {
"button": "Vitesse" "button": "Vitesse"
@ -743,14 +727,14 @@
"backend": { "backend": {
"dialog": { "dialog": {
"quality-changer": { "quality-changer": {
"detail": "Qualité actuelle : {{quality}}", "detail": "Qualité actuelle: {{quality}}",
"message": "Choisissez la qualité vidéo :", "message": "Choisissez la qualité vidéo :",
"title": "Choisissez la qualité vidéo" "title": "Choisissez la qualité vidéo"
} }
} }
}, },
"description": "Permet de changer la qualité vidéo avec un bouton sur l'overlay vidéo", "description": "Permet de changer la qualité vidéo avec un bouton sur la vidéo",
"name": "Sélecteur de qualité vidéo", "name": "Changeur de qualité vidéo",
"renderer": { "renderer": {
"quality-settings-button": { "quality-settings-button": {
"label": "Ouvrir le sélecteur de qualité du lecteur" "label": "Ouvrir le sélecteur de qualité du lecteur"
@ -762,7 +746,7 @@
"dialog": { "dialog": {
"lastfm": { "lastfm": {
"auth-failed": { "auth-failed": {
"message": "Erreur lors de l'authetification avec Last.fm\nCacher la popup jusqu'au prochain redémarrage.", "message": "Erreur lors de l'authetification avec Last.fm\nCachez la popup jusqu'au prochain redémarrage.",
"title": "Authentification échouée" "title": "Authentification échouée"
} }
} }
@ -778,7 +762,7 @@
"scrobble-alternative-title": "Utiliser des titres alternatifs", "scrobble-alternative-title": "Utiliser des titres alternatifs",
"scrobble-other-media": "Scrobbler d'autres médias" "scrobble-other-media": "Scrobbler d'autres médias"
}, },
"name": "Scrobbler", "name": "Scrobble",
"prompt": { "prompt": {
"lastfm": { "lastfm": {
"api-key": "Clé API de Last.fm", "api-key": "Clé API de Last.fm",
@ -786,7 +770,7 @@
}, },
"listenbrainz": { "listenbrainz": {
"token": { "token": {
"label": "Entrez votre token utilisateur ListenBrainz :", "label": "Entrez votre token utilisateur ListenBrainz:",
"title": "Token ListenBrainz" "title": "Token ListenBrainz"
} }
} }
@ -812,8 +796,8 @@
} }
}, },
"skip-disliked-songs": { "skip-disliked-songs": {
"description": "Passe les titres \"Je n'aime pas\"", "description": "Passer les musiques que je n'aime pas",
"name": "Passer les titres \"Je n'aime pas\"" "name": "Passer les chansons « Je n'aime pas »"
}, },
"skip-silences": { "skip-silences": {
"description": "Ignorer automatiquement les sections de silence dans les chansons", "description": "Ignorer automatiquement les sections de silence dans les chansons",
@ -827,12 +811,12 @@
"description": "Ajoute des paroles synchronisées aux chansons, grâce à LRClib par exemple.", "description": "Ajoute des paroles synchronisées aux chansons, grâce à LRClib par exemple.",
"errors": { "errors": {
"fetch": "⚠️\tUne erreur s'est produite en allant chercher les paroles.\n\tMerci de réessayer plus tard.", "fetch": "⚠️\tUne erreur s'est produite en allant chercher les paroles.\n\tMerci de réessayer plus tard.",
"not-found": "⚠️ Aucune paroles trouvées pour ce titre." "not-found": "⚠️ Aucune paroles trouvées pour cette musique."
}, },
"menu": { "menu": {
"default-text-string": { "default-text-string": {
"label": "Caractère par défaut entre les paroles", "label": "Caractère par défaut entre les paroles",
"tooltip": "Choisi le caractère par défaut à utiliser pour les blancs entre les paroles" "tooltip": "Choisi le caractère par défaut à utiliser pour l'espace entre les paroles"
}, },
"line-effect": { "line-effect": {
"label": "Effet de ligne", "label": "Effet de ligne",
@ -843,7 +827,7 @@
}, },
"focus": { "focus": {
"label": "Focus", "label": "Focus",
"tooltip": "Rend blanche seulement la ligne actuelle" "tooltip": "Rend seulement la ligne actuelle blanche"
}, },
"offset": { "offset": {
"label": "Décalage", "label": "Décalage",
@ -866,7 +850,7 @@
"label": "Aucun", "label": "Aucun",
"tooltip": "Aucun fournisseur privilégié" "tooltip": "Aucun fournisseur privilégié"
}, },
"tooltip": "Choisissez le fournisseur à utiliser par défaut" "tooltip": "Choisissez le fournisseur par défaut à utiliser"
}, },
"romanization": { "romanization": {
"label": "Romaniser les paroles", "label": "Romaniser les paroles",
@ -878,7 +862,7 @@
}, },
"show-time-codes": { "show-time-codes": {
"label": "Afficher les timecodes", "label": "Afficher les timecodes",
"tooltip": "Affiche le timecode à côté de chaque paroles" "tooltip": "Affiche à côté de chaque paroles son timecode"
} }
}, },
"name": "Paroles Synchronisées", "name": "Paroles Synchronisées",
@ -889,7 +873,7 @@
"warnings": { "warnings": {
"duration-mismatch": "⚠️ - Les paroles peuvent ne pas être synchronisées à cause d'une différence de durée.", "duration-mismatch": "⚠️ - Les paroles peuvent ne pas être synchronisées à cause d'une différence de durée.",
"inexact": "⚠️ - Les paroles de cette chanson peuvent ne pas être exactes", "inexact": "⚠️ - Les paroles de cette chanson peuvent ne pas être exactes",
"instrumental": "⚠️ - C'est un titre instrumental" "instrumental": "⚠️ - Cette musique n'a pas de paroles"
} }
}, },
"taskbar-mediacontrol": { "taskbar-mediacontrol": {
@ -926,7 +910,7 @@
"name": "Tuna OBS" "name": "Tuna OBS"
}, },
"unobtrusive-player": { "unobtrusive-player": {
"description": "Empêche le lecteur de s'afficher quand un titre est en cours de lecture", "description": "Empêche le lecteur de s'afficher quand une chanson est en cours de lecture",
"name": "Lecteur Non-Intrusif" "name": "Lecteur Non-Intrusif"
}, },
"video-toggle": { "video-toggle": {
@ -950,7 +934,7 @@
} }
} }
}, },
"name": "Bouton de bascule vidéo", "name": "Basculer la vidéo",
"templates": { "templates": {
"button-song": "Musique", "button-song": "Musique",
"button-video": "Vidéo" "button-video": "Vidéo"

View File

@ -461,8 +461,8 @@
"set-status-display-type": { "set-status-display-type": {
"label": "स्टेटस टेक्स्ट", "label": "स्टेटस टेक्स्ट",
"submenu": { "submenu": {
"application": "{{applicationName}} सुन रहे है",
"artist": "{artist} को सुन रहे है", "artist": "{artist} को सुन रहे है",
"application": "{{applicationName}} सुन रहे है",
"title": "{song title} सुन रहे है" "title": "{song title} सुन रहे है"
} }
} }

View File

@ -237,8 +237,7 @@
"submenu": { "submenu": {
"percent": "{{ratio}}%" "percent": "{{ratio}}%"
} }
}, }
"enable-seekbar": "Omogući postavljanje teme \"seekbar\"-a"
}, },
"name": "Boja teme albuma" "name": "Boja teme albuma"
}, },
@ -321,22 +320,6 @@
"hostname": { "hostname": {
"label": "Naziv hosta" "label": "Naziv hosta"
}, },
"https": {
"label": "HTTPS i Sertifikati",
"submenu": {
"cert": {
"dialogTitle": "Biraj HTTPS sertifikat datoteku",
"label": "Sertifikat datoteka (.crt/.pem)"
},
"enable-https": {
"label": "Omogući HTTPS"
},
"key": {
"dialogTitle": "Biraj privatni ključ datoteku za HTTPS",
"label": "Privatni ključ datoteka (.key/.pem)"
}
}
},
"port": { "port": {
"label": "Port" "label": "Port"
} }
@ -478,8 +461,8 @@
"set-status-display-type": { "set-status-display-type": {
"label": "Tekst statusa", "label": "Tekst statusa",
"submenu": { "submenu": {
"application": "Slušate {{applicationName}}",
"artist": "Slušate {glazbenika}", "artist": "Slušate {glazbenika}",
"application": "Slušate {{applicationName}}",
"title": "Slušate {naslov pjesme}" "title": "Slušate {naslov pjesme}"
} }
} }

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

@ -5,10 +5,10 @@
"execute-failed": "Mislukt om plugin {{pluginName}}::{{contextName}} uit te voeren", "execute-failed": "Mislukt om plugin {{pluginName}}::{{contextName}} uit te voeren",
"executed-at-ms": "Plugin {{pluginName}}::{{contextName}} uitgevoerd in {{ms}}ms", "executed-at-ms": "Plugin {{pluginName}}::{{contextName}} uitgevoerd in {{ms}}ms",
"initialize-failed": "Initialisatie van plugin \"{{pluginName}}\" mislukt", "initialize-failed": "Initialisatie van plugin \"{{pluginName}}\" mislukt",
"load-all": "Alle plugins aan het laden", "load-all": "Alle plugins laden",
"load-failed": "Mislukt om plugin \"{{pluginName}}\" te laden", "load-failed": "Mislukt om plugin \"{{pluginName}}\" te laden",
"loaded": "Plugin \"{{pluginName}}\" geladen", "loaded": "Plugin \"{{pluginName}}\" geladen",
"unload-failed": "Mislukt om plugin \"{{pluginName}}\" te ontladen", "unload-failed": "Mislukt om plugin \"{{pluginName}}\" te lossen",
"unloaded": "Plugin \"{{pluginName}}\" gelost" "unloaded": "Plugin \"{{pluginName}}\" gelost"
} }
} }
@ -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"
} }
@ -478,9 +461,9 @@
"set-status-display-type": { "set-status-display-type": {
"label": "Status tekst", "label": "Status tekst",
"submenu": { "submenu": {
"application": "Naar {{applicationName}} aan het luisteren",
"artist": "Naar {artist} aan het luisteren", "artist": "Naar {artist} aan het luisteren",
"title": "Naar {song title} aan het luisteren" "title": "Naar {song title} aan het luisteren",
"application": "Naar {{applicationName}} aan het luisteren"
} }
} }
}, },

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}}"
} }
} }
}, },
@ -237,8 +237,7 @@
"submenu": { "submenu": {
"percent": "{{ratio}}%" "percent": "{{ratio}}%"
} }
}, }
"enable-seekbar": "Zezwól stylowanie paska wyszukiwań"
}, },
"name": "Motyw kolorów albumu" "name": "Motyw kolorów albumu"
}, },
@ -295,7 +294,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": {
@ -321,22 +320,6 @@
"hostname": { "hostname": {
"label": "Nazwa hosta (IP)" "label": "Nazwa hosta (IP)"
}, },
"https": {
"label": "HTTPS i Certyfikaty",
"submenu": {
"cert": {
"dialogTitle": "Wybierz plik certyfikatu HTTPS",
"label": "Certyfikat (.crt/.pem)"
},
"enable-https": {
"label": "Zezwól HTTPS"
},
"key": {
"dialogTitle": "Wybierz plik prywatnego klucza HTTPS",
"label": "Klucz prywatny (.key/.pem)"
}
}
},
"port": { "port": {
"label": "Port" "label": "Port"
} }
@ -478,8 +461,8 @@
"set-status-display-type": { "set-status-display-type": {
"label": "Opis statusu", "label": "Opis statusu",
"submenu": { "submenu": {
"application": "Słucha {{applicationName}}",
"artist": "Słucha {artist}", "artist": "Słucha {artist}",
"application": "Słucha {{applicationName}}",
"title": "Słucha {song title}" "title": "Słucha {song title}"
} }
} }

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

@ -321,22 +321,6 @@
"hostname": { "hostname": {
"label": "Nome do anfitrião" "label": "Nome do anfitrião"
}, },
"https": {
"label": "HTTPS e Certificados",
"submenu": {
"cert": {
"dialogTitle": "Selecionar arquivo do certificado HTTPS",
"label": "Arquivo do certificado (.crt/.pem)"
},
"enable-https": {
"label": "Ativar HTTPS"
},
"key": {
"dialogTitle": "Selecionar arquivo da chave privada HTTPS",
"label": "Arquivo da chave privada (.key/.pen)"
}
}
},
"port": { "port": {
"label": "Porta" "label": "Porta"
} }
@ -478,8 +462,8 @@
"set-status-display-type": { "set-status-display-type": {
"label": "Texto de estado", "label": "Texto de estado",
"submenu": { "submenu": {
"application": "A reproduzir {{applicationName}}",
"artist": "A ouvir {artist}", "artist": "A ouvir {artist}",
"application": "A reproduzir {{applicationName}}",
"title": "A ouvir {song title}" "title": "A ouvir {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": "Порт"
} }
@ -478,8 +461,8 @@
"set-status-display-type": { "set-status-display-type": {
"label": "Текст статуса", "label": "Текст статуса",
"submenu": { "submenu": {
"application": "Слушает {{applicationName}}",
"artist": "Слушает {исполнитель}", "artist": "Слушает {исполнитель}",
"application": "Слушает {{applicationName}}",
"title": "Слушает {название трека}" "title": "Слушает {название трека}"
} }
} }

View File

@ -237,8 +237,7 @@
"submenu": { "submenu": {
"percent": "{{ratio}}%" "percent": "{{ratio}}%"
} }
}, }
"enable-seekbar": "Omogući postavljanje teme \"seekbar\"-a"
}, },
"name": "Paleta boja albuma" "name": "Paleta boja albuma"
}, },
@ -321,22 +320,6 @@
"hostname": { "hostname": {
"label": "Ime host-a" "label": "Ime host-a"
}, },
"https": {
"label": "HTTPS i Sertifikati",
"submenu": {
"cert": {
"dialogTitle": "Izaberi HTTPS datoteku sertifikata",
"label": "Datoteka sertifikata (.crt/.pem)"
},
"enable-https": {
"label": "Omogući HTTPS"
},
"key": {
"dialogTitle": "Izaberi HTTPS datoteku privatnog ključa",
"label": "Datoteka privatnog ključa (.key/.pem)"
}
}
},
"port": { "port": {
"label": "Port" "label": "Port"
} }
@ -478,9 +461,9 @@
"set-status-display-type": { "set-status-display-type": {
"label": "Tekst statusa", "label": "Tekst statusa",
"submenu": { "submenu": {
"application": "Slušanje {{applicationName}}",
"artist": "Slušanje {artist}", "artist": "Slušanje {artist}",
"title": "Slušanje {song title}" "title": "Slušanje {song title}",
"application": "Slušanje {{applicationName}}"
} }
} }
}, },

View File

@ -321,22 +321,6 @@
"hostname": { "hostname": {
"label": "Värdnamn" "label": "Värdnamn"
}, },
"https": {
"label": "HTTPS och certifikat",
"submenu": {
"cert": {
"dialogTitle": "Välj HTTPS-certifikatfil",
"label": "Certifikatfil (.crt/.pem)"
},
"enable-https": {
"label": "Aktivera HTTPS"
},
"key": {
"dialogTitle": "Välj privat nyckelfil (.key/.pem)",
"label": "Privat nyckelfil (.key/.pem)"
}
}
},
"port": { "port": {
"label": "Port" "label": "Port"
} }
@ -478,8 +462,8 @@
"set-status-display-type": { "set-status-display-type": {
"label": "Statusmeddelande", "label": "Statusmeddelande",
"submenu": { "submenu": {
"application": "Lyssnar på {{applicationName}}",
"artist": "Lyssnar på {artist}", "artist": "Lyssnar på {artist}",
"application": "Lyssnar på {{applicationName}}",
"title": "Lyssnar på {song title}" "title": "Lyssnar på {song title}"
} }
} }

View File

@ -461,8 +461,8 @@
"set-status-display-type": { "set-status-display-type": {
"label": "நிலை உரை", "label": "நிலை உரை",
"submenu": { "submenu": {
"application": "வலையொளி இசையில் கேட்கிறது",
"artist": "{கலைஞர்} பாடலைக் கேட்கிறேன்", "artist": "{கலைஞர்} பாடலைக் கேட்கிறேன்",
"application": "வலையொளி இசையில் கேட்கிறது",
"title": "பாடலைக் கேட்கிறேன்{பாடல் தலைப்பு}" "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

@ -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": "端口号"
} }
@ -478,8 +462,8 @@
"set-status-display-type": { "set-status-display-type": {
"label": "状态文本", "label": "状态文本",
"submenu": { "submenu": {
"application": "在听 {{applicationName}}",
"artist": "在听 {artist}", "artist": "在听 {artist}",
"application": "在听 {{applicationName}}",
"title": "在听 {song title}" "title": "在听 {song title}"
} }
} }

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,11 +250,12 @@ 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);
try { if (this.rpc.isConnected) {
this.rpc.removeAllListeners(); try {
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 timeInMs =
const { minutes, seconds, milliseconds, word } = groups!; parseInt(minutes) * 60 * 1000 +
const timeInMs = parseInt(seconds) * 1000 +
parseInt(minutes) * 60 * 1000 + parseInt(milliseconds);
parseInt(seconds) * 1000 +
parseInt(milliseconds);
return { timeInMs, word }; const currentLine: LRCLine = {
}); time: `${minutes}:${seconds}:${milliseconds}`,
timeInMs,
text: text.trim(),
duration: Infinity,
};
if (words.length) { if (previousLine) {
text = words.map(({ word }) => word).join(' '); previousLine.duration = timeInMs - previousLine.timeInMs;
} }
for (const { time, timeInMs } of timestamps) { previousLine = currentLine;
lrc.lines.push({ lrc.lines.push(currentLine);
time,
timeInMs,
text,
words,
duration: Infinity,
});
}
} }
lrc.lines.sort(({ timeInMs: timeA }, { timeInMs: timeB }) => timeA - timeB); for (const line of lrc.lines) {
for (let i = 0; i < lrc.lines.length; i++) { line.timeInMs += offset;
const current = lrc.lines[i];
const next = lrc.lines[i + 1];
current.timeInMs += offset;
if (next) {
current.duration = next.timeInMs - current.timeInMs;
}
} }
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,91 +148,81 @@ 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.enabled) return;
const video = document.querySelector<
HTMLVideoElement & { captureStream(): MediaStream }
>('video');
if (!video) {
return;
}
const visualizerContainer =
document.querySelector<HTMLElement>('#player');
if (!visualizerContainer) {
return;
}
let canvas = document.querySelector<HTMLCanvasElement>('#visualizer');
if (!canvas) {
canvas = document.createElement('canvas');
canvas.id = 'visualizer';
visualizerContainer?.prepend(canvas);
}
const gainNode = this.props.audioContext.createGain();
gainNode.gain.value = 1.25;
this.props.audioSource.connect(gainNode);
let visualizerType: {
new (...args: ConstructorParameters<typeof vudio>): Visualizer;
} = vudio;
if (config.type === 'wave') { if (config.type === 'wave') {
visualizerType = wave; visualizerType = wave;
} else if (config.type === 'butterchurn') { } else if (config.type === 'butterchurn') {
visualizerType = butterchurn; visualizerType = butterchurn;
} }
this.props.visualizerInstance = new visualizerType(
this.props.audioContext,
this.props.audioSource,
canvas,
gainNode,
video.captureStream(),
config,
);
const resizeVisualizer = () => {
if (canvas && visualizerContainer) {
const { 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();
this.props.observer = new ResizeObserver(resizeVisualizer);
this.props.observer.observe(visualizerContainer);
},
onConfigChange(newConfig) {
this.createVisualizer(newConfig);
},
onPlayerApiReady(_, { getConfig }) {
document.addEventListener( document.addEventListener(
'peard:audio-can-play', 'peard:audio-can-play',
async (e) => { (e) => {
this.props.audioContext = e.detail.audioContext; const video = document.querySelector<
this.props.audioSource = e.detail.audioSource; HTMLVideoElement & { captureStream(): MediaStream }
this.createVisualizer(await getConfig()); >('video');
if (!video) {
return;
}
const visualizerContainer =
document.querySelector<HTMLElement>('#player');
if (!visualizerContainer) {
return;
}
let canvas = document.querySelector<HTMLCanvasElement>('#visualizer');
if (!canvas) {
canvas = document.createElement('canvas');
canvas.id = 'visualizer';
visualizerContainer?.prepend(canvas);
}
const resizeCanvas = () => {
if (canvas) {
canvas.width = visualizerContainer.clientWidth;
canvas.height = visualizerContainer.clientHeight;
}
};
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,
gainNode,
video.captureStream(),
config,
);
const resizeVisualizer = (width: number, height: number) => {
resizeCanvas();
visualizer.resize(width, height);
};
resizeVisualizer(canvas.width, canvas.height);
const visualizerContainerObserver = new ResizeObserver((entries) => {
for (const entry of entries) {
resizeVisualizer(
entry.contentRect.width,
entry.contentRect.height,
);
}
});
visualizerContainerObserver.observe(visualizerContainer);
visualizer.render();
}, },
{ 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);
}
} }
/** /**

View File

@ -1,10 +1,8 @@
import { Menu, nativeImage, screen, Tray } from 'electron'; import { Menu, nativeImage, screen, Tray } from 'electron';
import is from 'electron-is'; import is from 'electron-is';
import TrayIcon from '@assets/tray.png?asset&asarUnpack'; import defaultTrayIconAsset from '@assets/tray.png?asset&asarUnpack';
import PausedTrayIcon from '@assets/tray-paused.png?asset&asarUnpack'; import pausedTrayIconAsset from '@assets/tray-paused.png?asset&asarUnpack';
import TrayIconWhite from '@assets/tray-white.png?asset&asarUnpack';
import PausedTrayIconWhite from '@assets/tray-paused-white.png?asset&asarUnpack';
import * as config from './config'; import * as config from './config';
@ -54,15 +52,14 @@ export const setUpTray = (app: Electron.App, win: Electron.BrowserWindow) => {
const pixelRatio = is.windows() const pixelRatio = is.windows()
? screen.getPrimaryDisplay().scaleFactor || 1 ? screen.getPrimaryDisplay().scaleFactor || 1
: 1; : 1;
const defaultTrayIcon = nativeImage const defaultTrayIcon = nativeImage
.createFromPath(is.macOS() ? TrayIconWhite : TrayIcon) .createFromPath(defaultTrayIconAsset)
.resize({ .resize({
width: 16 * pixelRatio, width: 16 * pixelRatio,
height: 16 * pixelRatio, height: 16 * pixelRatio,
}); });
const pausedTrayIcon = nativeImage const pausedTrayIcon = nativeImage
.createFromPath(is.macOS() ? PausedTrayIconWhite : PausedTrayIcon) .createFromPath(pausedTrayIconAsset)
.resize({ .resize({
width: 16 * pixelRatio, width: 16 * pixelRatio,
height: 16 * pixelRatio, height: 16 * pixelRatio,

View File

@ -1,7 +1,7 @@
import path from 'node:path'; import path from 'node:path';
import process from 'node:process'; import process from 'node:process';
import { _electron as electron } from 'playwright';
import { test, expect, _electron as electron } from '@playwright/test'; import { test, expect } from '@playwright/test';
process.env.NODE_ENV = 'test'; process.env.NODE_ENV = 'test';
@ -32,11 +32,7 @@ test('Pear Desktop App - With default settings, app is launched and visible', as
// expect(title.replaceAll(/\s/g, ' ')).toEqual('Pear Desktop'); // expect(title.replaceAll(/\s/g, ' ')).toEqual('Pear Desktop');
const url = window.url(); const url = window.url();
expect( expect(url.startsWith('https://music.\u0079\u006f\u0075\u0074\u0075\u0062\u0065.com')).toBe(true);
url.startsWith(
'https://music.\u0079\u006f\u0075\u0074\u0075\u0062\u0065.com',
),
).toBe(true);
await app.close(); await app.close();
}); });

View File

@ -25,8 +25,7 @@
"exclude": ["./dist"], "exclude": ["./dist"],
"include": [ "include": [
"electron.vite.config.mts", "electron.vite.config.mts",
"playwright.config.ts",
"./src/**/*", "./src/**/*",
"*.config.*js" "*.config.*js",
] ]
} }

View File

@ -1,8 +0,0 @@
{
"extends": "./tsconfig.json",
"exclude": ["./dist"],
"include": [
"playwright.config.ts",
"./tests/**/*"
]
}