Compare commits

..

66 Commits

Author SHA1 Message Date
b14e3b08f7 fix(deps): update dependency zod to v4.3.6 2026-01-29 14:17:15 +00:00
8f4dc17774 fix(deps): update dependency happy-dom to v20.4.0 (#4287)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-29 18:12:24 +09:00
Tix
bc890943ef Fix: discord activity sanitizer (#4119)
Co-authored-by: Angelos Bouklis <me@arjix.dev>
2026-01-29 18:04:31 +09:00
12fcb92a8a plugin: clock widget (#4161) 2026-01-29 17:55:01 +09:00
728be6633a dep: update libuuid for rpm builds (#4139)
Co-authored-by: JellyBrick <shlee1503@naver.com>
2026-01-29 17:51:34 +09:00
782204cc63 Add devcontainer setup (#4143)
Co-authored-by: Angelos Bouklis <me@arjix.dev>
Co-authored-by: JellyBrick <shlee1503@naver.com>
2026-01-29 17:48:47 +09:00
f3d988746a feat: Add toggle to swap the order of like/dislike buttons (#4221) 2026-01-29 17:41:52 +09:00
e3be1e7777 Merge branch 'master' of https://github.com/pear-devs/pear-desktop 2026-01-29 17:36:28 +09:00
93c1f411d0 fix: macOS runner 2026-01-29 17:35:59 +09:00
0c43ee1310 Update Homebrew installation command (#4228) 2026-01-29 17:27:17 +09:00
55ad129f57 Merge branch 'master' of https://github.com/pear-devs/pear-desktop 2026-01-29 17:25:53 +09:00
442dd51d3d refactor(visualizer): Removed restart requirement and refactored impls (#4200) 2026-01-29 17:24:00 +09:00
2180a2a810 chore(deps): update dependency @playwright/test to v1.58.0 (#4284)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-29 17:21:41 +09:00
2594d96272 fix(deps): update dependency solid-js to v1.9.11 (#4282)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-29 17:21:34 +09:00
ff80b6bca8 chore(deps): update dependency electron to v38.8.0 (#4285)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-29 17:21:25 +09:00
3f09a826ac chore: update pnpm-lock.yaml 2026-01-29 17:20:56 +09:00
e6555f215d chore: Updated electron-vite to 5.0.0 (#4203)
Co-authored-by: JellyBrick <shlee1503@naver.com>
2026-01-29 17:20:31 +09:00
c3852d17fe fix(deps): update dependency virtua to v0.48.5 (#4283)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-29 17:05:30 +09:00
7e4d1ab681 fix(discord): Fixed memory leak by repeated RPC failures (#4197) 2026-01-29 17:05:19 +09:00
d2d6db192e fix(deps): update dependency custom-electron-prompt to v1.6.1 (#4259)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-29 16:56:43 +09:00
d9f3c97bec fix(deps): update dependency electron-updater to v6.7.3 (#4260)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-29 16:56:36 +09:00
9ace05c511 fix(deps): update dependency fast-equals to v5.4.0 (#4261)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-29 16:56:27 +09:00
6c957a5563 chore(deps): update actions/github-script action to v8 (#4263)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-29 16:56:21 +09:00
2d40bc77b3 chore(deps): update actions/setup-node action to v6 (#4264)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-29 16:56:13 +09:00
62080c277e chore(deps): update dependency @stylistic/eslint-plugin to v5.7.1 (#4274)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-29 16:55:55 +09:00
9687e85699 fix(deps): update dependency @floating-ui/dom to v1.7.5 (#4275)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-29 16:55:48 +09:00
868ad322cf fix(deps): update dependency @hono/node-ws to v1.3.0 (#4258)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-29 16:55:41 +09:00
666f59f4c8 fix(deps): update dependency @ghostery/adblocker-electron-preload to v2.13.4 (#4257)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-29 00:10:36 +09:00
9f05926452 fix(deps): update dependency hono to v4.11.7 [security] (#4273)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-29 00:10:22 +09:00
ed7ef30aaa feat(synced-lyrics): Improve LRC parsing (#4269) 2026-01-26 11:41:53 +02:00
5bbf7f964c chore(i18n): Translated using Weblate (Polish)
Currently translated at 100.0% (463 of 463 strings)

Translation: pear-devs/pear-desktop/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/pl/
2026-01-25 21:28:00 +01:00
428151ad6e chore(i18n): Translated using Weblate (Estonian)
Currently translated at 34.5% (160 of 463 strings)

Translation: pear-devs/pear-desktop/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/et/
2026-01-24 01:09:19 +01:00
1d72d1260c chore(deps): update dependency cross-env to v10.1.0 (#4187)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-22 06:17:35 +09:00
20836d9e62 chore(deps): update dependency ws to v8.19.0 (#4254)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-22 06:17:26 +09:00
fc092177e1 fix(deps): update dependency @ghostery/adblocker-electron to v2.13.4 (#4255)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-22 06:17:15 +09:00
b3fe19c136 fix(deps): update dependency @hono/node-server to v1.19.9 (#4249)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-22 06:13:23 +09:00
4231289bbb fix(deps): update dependency virtua to v0.48.3 (#4252)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-22 06:13:08 +09:00
746e0ba584 chore(deps): update dependency electron-builder-squirrel-windows to v26.4.0 (#4253)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-22 06:13:00 +09:00
803e2b3312 chore(deps): update dependency node-gyp to v11.5.0 (#4189)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-22 04:47:48 +09:00
320a166f59 chore(deps): update dependency @stylistic/eslint-plugin to v5.7.0 (#4185)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-22 04:47:38 +09:00
c6c8899af8 fix(deps): update dependency @hono/swagger-ui to v0.5.3 (#4250)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-22 03:34:50 +09:00
7a63fc45c7 fix(deps): update dependency node-html-parser to v7.0.2 (#4251)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-22 03:34:44 +09:00
21533ee461 chore(deps): update eslint monorepo to v9.39.2 (#4191)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-22 02:17:00 +09:00
9e3d3662ce chore(deps): update dependency vite to v7.3.1 (#4248)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-22 02:14:02 +09:00
9f56befc3c chore(deps): update dependency bufferutil to v4.1.0 (#4186)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-22 02:13:50 +09:00
7b7c4a4153 chore(deps): update dependency typescript-eslint to v8.53.1 (#4190)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-22 02:13:27 +09:00
c07f1ef584 chore(deps): update dependency eslint-plugin-prettier to v5.5.5 (#4247)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-22 02:13:02 +09:00
c506bf21a9 chore(deps): update dependency @babel/runtime to v7.28.6 (#4246)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-22 02:00:51 +09:00
4e697f250a chore(deps): update dependency @playwright/test to v1.57.0 (#4192)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-22 01:17:53 +09:00
0e80e09313 fix(deps): update dependency solid-js to v1.9.10 (#4184)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-22 01:17:44 +09:00
70bede3f07 fix(deps): update dependency hono to v4.11.4 [security] (#4239)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-22 01:17:36 +09:00
b37db09e0c chore(i18n): Translated using Weblate (Catalan)
Currently translated at 100.0% (463 of 463 strings)

Translation: pear-devs/pear-desktop/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/ca/
2026-01-19 13:01:57 +00:00
7a4def8acc Fix weblate link (#4204) 2026-01-16 17:24:34 +09:00
f4bbd53e1a chore(i18n): Translated using Weblate (Turkish)
Currently translated at 100.0% (463 of 463 strings)

Translation: pear-devs/pear-desktop/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/tr/
2026-01-13 22:01:47 +00:00
272ee7bdb1 chore(i18n): Translated using Weblate (Estonian)
Currently translated at 32.8% (152 of 463 strings)

Translation: pear-devs/pear-desktop/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/et/
2026-01-12 19:01:50 +00:00
cece515696 chore(i18n): Translated using Weblate (Dutch)
Currently translated at 100.0% (463 of 463 strings)

Translation: pear-devs/pear-desktop/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/nl/
2026-01-12 19:01:48 +00:00
82b7b24ef7 chore(i18n): Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (463 of 463 strings)

Translation: pear-devs/pear-desktop/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/pt_BR/
2026-01-06 00:01:50 +01:00
732bbc17f4 chore(i18n): Translated using Weblate (Italian)
Currently translated at 99.7% (462 of 463 strings)

Translation: pear-devs/pear-desktop/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/it/
2026-01-06 00:01:48 +01:00
29da42d9ff chore(i18n): Translated using Weblate (Italian)
Currently translated at 99.7% (462 of 463 strings)

Translation: pear-devs/pear-desktop/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/it/
2026-01-06 00:01:47 +01:00
269d0cd638 chore(i18n): Translated using Weblate (Russian)
Currently translated at 100.0% (463 of 463 strings)

Translation: pear-devs/pear-desktop/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/ru/
2026-01-04 05:01:48 +00:00
69e15165e3 chore(i18n): Translated using Weblate (Georgian)
Currently translated at 32.3% (150 of 463 strings)

Translation: pear-devs/pear-desktop/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/ka/
2026-01-03 00:02:04 +01:00
Fin
7663a12ee4 chore(i18n): Translated using Weblate (German)
Currently translated at 100.0% (463 of 463 strings)

Translation: pear-devs/pear-desktop/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/de/
2026-01-03 00:02:03 +01:00
dcda0b3561 chore(i18n): Translated using Weblate (Korean)
Currently translated at 100.0% (463 of 463 strings)

Translation: pear-devs/pear-desktop/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/ko/
2026-01-03 00:02:02 +01:00
5f82fd9e5a chore: Update README with Warp details 2026-01-02 14:59:50 +09:00
af11fa31d3 chore(i18n): Translated using Weblate (Lithuanian)
Currently translated at 78.4% (363 of 463 strings)

Translation: pear-devs/pear-desktop/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/lt/
2026-01-01 12:02:01 +01:00
a049d618e5 chore(i18n): Translated using Weblate (Vietnamese)
Currently translated at 98.7% (457 of 463 strings)

Translation: pear-devs/pear-desktop/i18n
Translate-URL: https://hosted.weblate.org/projects/youtube-music/i18n/vi/
2026-01-01 12:02:01 +01:00
40 changed files with 2141 additions and 1967 deletions

View File

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

View File

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

View File

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

1
.gitignore vendored
View File

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

15
.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,15 @@
{
// 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,3 +1,17 @@
<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">
# :pear: Pear Desktop
@ -55,7 +69,7 @@
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/4h3zBxo" alt="translation status 2" />
</a>
@ -72,10 +86,10 @@ this [wiki page](https://wiki.archlinux.org/index.php/Arch_User_Repository#Insta
### macOS
You can install the app using Homebrew (see the [cask definition](https://github.com/pear-devs/pear-desktop-homebrew)):
You can install the app using Homebrew (see the [cask definition](https://github.com/pear-devs/homebrew-pear)):
```bash
brew install pear-devs/pear-desktop
brew install pear-devs/pear/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:
@ -130,6 +144,10 @@ pnpm install --frozen-lockfile
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
Using plugins, you can:
@ -265,6 +283,16 @@ export default createPlugin({
Builds the app for macOS, Linux, and Windows,
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
```bash

View File

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

View File

@ -1,7 +1,7 @@
import { dirname, join, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { defineConfig, defineViteConfig } from 'electron-vite';
import { defineConfig } from 'electron-vite';
import builtinModules from 'builtin-modules';
import Inspect from 'vite-plugin-inspect';
@ -21,168 +21,141 @@ const resolveAlias = {
'@assets': resolve(__dirname, './assets'),
};
export default defineConfig({
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,
},
};
export default defineConfig(({ mode }) => {
const isDev = mode === 'development';
if (mode === 'development') {
commonConfig.build!.sourcemap = 'inline';
commonConfig.plugins?.push(
Inspect({
build: true,
outputDir: join(__dirname, '.vite-inspect/backend'),
}),
);
return commonConfig;
}
const mainConfig: 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',
},
minify: !isDev,
cssMinify: !isDev,
sourcemap: isDev ? 'inline' : undefined,
},
resolve: {
alias: resolveAlias,
},
};
return {
...commonConfig,
build: {
...commonConfig.build,
minify: true,
cssMinify: true,
const preloadConfig: UserConfig = {
experimental: {
enableNativePlugin: true,
},
plugins: [
pluginLoader('preload'),
viteResolve({
'virtual:i18n': i18nImporter(),
'virtual:plugins': pluginVirtualModuleGenerator('preload'),
}),
],
build: {
lib: {
entry: 'src/preload.ts',
formats: ['cjs'],
},
};
}),
preload: defineViteConfig(({ mode }) => {
const commonConfig: UserConfig = {
experimental: {
enableNativePlugin: true,
outDir: 'dist/preload',
commonjsOptions: {
ignoreDynamicRequires: true,
},
plugins: [
pluginLoader('preload'),
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',
},
rolldownOptions: {
external: ['electron', 'custom-electron-prompt', ...builtinModules],
input: './src/preload.ts',
},
resolve: {
alias: resolveAlias,
},
};
minify: !isDev,
cssMinify: !isDev,
sourcemap: isDev ? 'inline' : undefined,
},
resolve: {
alias: resolveAlias,
},
};
if (mode === 'development') {
commonConfig.build!.sourcemap = 'inline';
commonConfig.plugins?.push(
Inspect({
build: true,
outputDir: join(__dirname, '.vite-inspect/preload'),
}),
);
return commonConfig;
}
const rendererConfig: UserConfig = {
experimental: {
enableNativePlugin: !isDev, // Disable native plugin in development mode to avoid issues with HMR (bug in rolldown-vite)
},
plugins: [
pluginLoader('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',
},
minify: !isDev,
cssMinify: !isDev,
sourcemap: isDev ? 'inline' : undefined,
},
resolve: {
alias: resolveAlias,
},
server: {
cors: {
origin: 'https://music.\u0079\u006f\u0075\u0074\u0075\u0062\u0065.com',
},
},
};
return {
...commonConfig,
build: {
...commonConfig.build,
minify: true,
cssMinify: true,
},
};
}),
renderer: defineViteConfig(({ mode }) => {
const commonConfig: UserConfig = {
experimental: {
enableNativePlugin: mode !== 'development', // Disable native plugin in development mode to avoid issues with HMR (bug in rolldown-vite)
},
plugins: [
pluginLoader('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',
},
},
};
if (isDev) {
mainConfig.plugins?.push(
Inspect({
build: true,
outputDir: join(__dirname, '.vite-inspect/backend'),
}),
);
preloadConfig.plugins?.push(
Inspect({
build: true,
outputDir: join(__dirname, '.vite-inspect/preload'),
}),
);
rendererConfig.plugins?.push(
Inspect({
build: true,
outputDir: join(__dirname, '.vite-inspect/renderer'),
}),
);
}
if (mode === 'development') {
commonConfig.build!.sourcemap = 'inline';
commonConfig.plugins?.push(
Inspect({
build: true,
outputDir: join(__dirname, '.vite-inspect/renderer'),
}),
);
return commonConfig;
}
return {
...commonConfig,
build: {
...commonConfig.build,
minify: true,
cssMinify: true,
},
};
}),
return {
main: mainConfig,
preload: preloadConfig,
renderer: rendererConfig,
};
});

View File

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

2724
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -321,6 +321,22 @@
"hostname": {
"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": {
"label": "Port"
}
@ -462,8 +478,8 @@
"set-status-display-type": {
"label": "Text d'estat",
"submenu": {
"artist": "Escoltant {artist}",
"application": "Escoltant {{applicationName}}",
"artist": "Escoltant {artist}",
"title": "Escoltant {song title}"
}
}

View File

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

View File

@ -154,6 +154,7 @@
"default": "Default",
"force-show": "Force show",
"hide": "Hide",
"swap": "Swap like buttons order",
"label": "Like buttons"
},
"custom-window-title": {
@ -412,6 +413,17 @@
"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": {
"description": "Always set the sidebar in compact mode",
"name": "Compact Sidebar"

View File

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

View File

@ -237,7 +237,8 @@
"submenu": {
"percent": "{{ratio}}%"
}
}
},
"enable-seekbar": "Abilita tematizzazione della seekbar"
},
"name": "Tema abbinato a colore album"
},
@ -320,6 +321,22 @@
"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": {
"label": "Porta"
}
@ -461,9 +478,9 @@
"set-status-display-type": {
"label": "Testo dello status",
"submenu": {
"application": "Ascoltando {{applicationName}}",
"artist": "Stai ascoltando {artist}",
"title": "Stai ascoltando {song title}",
"application": "Ascoltando {{applicationName}}"
"title": "Stai ascoltando {song title}"
}
}
},
@ -866,7 +883,7 @@
},
"name": "Testi sincronizzati",
"refetch-btn": {
"fetching": "Sto recuperando...",
"fetching": "Caricamento...",
"normal": "Recupera i testi"
},
"warnings": {

View File

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

View File

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

View File

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

View File

@ -237,7 +237,8 @@
"submenu": {
"percent": "{{ratio}}%"
}
}
},
"enable-seekbar": "Schakel thema's voor de schuifbalk in"
},
"name": "Albumkleurthema"
},
@ -320,6 +321,22 @@
"hostname": {
"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": {
"label": "Poort"
}

View File

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

View File

@ -321,6 +321,22 @@
"hostname": {
"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": {
"label": "Porta"
}
@ -462,8 +478,8 @@
"set-status-display-type": {
"label": "Texto de status",
"submenu": {
"artist": "Ouvindo {artist}",
"application": "Ouvindo {{applicationName}}",
"artist": "Ouvindo {artist}",
"title": "Ouvindo {song title}"
}
}

View File

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

View File

@ -321,6 +321,22 @@
"hostname": {
"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": {
"label": "Port"
}
@ -462,8 +478,8 @@
"set-status-display-type": {
"label": "Durum metni",
"submenu": {
"artist": "{artist} Dinleniyor",
"application": "{{applicationName}} Dinleniyor",
"artist": "{artist} Dinleniyor",
"title": "{song title} Dinleniyor"
}
}

View File

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

View File

@ -285,6 +285,19 @@ export const mainMenuTemplate = async (
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,
);
},
},
],
},
{

109
src/plugins/clock/index.tsx Normal file
View File

@ -0,0 +1,109 @@
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

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

View File

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

View File

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

View File

@ -19,6 +19,27 @@ export const truncateString = (str: string, length: number): string => {
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.
* @param config - The plugin configuration.

View File

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

View File

@ -18,7 +18,6 @@ export type VisualizerPluginConfig = {
type: 'butterchurn' | 'vudio' | 'wave';
butterchurn: {
preset: string;
renderingFrequencyInMs: number;
blendTimeInSeconds: number;
};
vudio: {
@ -57,17 +56,23 @@ export type VisualizerPluginConfig = {
};
};
type RenderProps = {
visualizerInstance: Visualizer | null;
audioContext: AudioContext | null;
audioSource: MediaElementAudioSourceNode | null;
observer: ResizeObserver | null;
};
export default createPlugin({
name: () => t('plugins.visualizer.name'),
description: () => t('plugins.visualizer.description'),
restartNeeded: true,
restartNeeded: false,
config: {
enabled: false,
type: 'butterchurn',
// Config per visualizer
butterchurn: {
preset: 'martin [shadow harlequins shape code] - fata morgana',
renderingFrequencyInMs: 500,
blendTimeInSeconds: 2.7,
},
vudio: {
@ -148,81 +153,91 @@ export default createPlugin({
},
renderer: {
async onPlayerApiReady(_, { getConfig }) {
const config = await getConfig();
props: {
visualizerInstance: null,
audioContext: null,
audioSource: null,
observer: null,
} as RenderProps,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let visualizerType: { new (...args: any[]): Visualizer<unknown> } = vudio;
createVisualizer(
this: { props: RenderProps },
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') {
visualizerType = wave;
} else if (config.type === '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(
'peard:audio-can-play',
(e) => {
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 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();
async (e) => {
this.props.audioContext = e.detail.audioContext;
this.props.audioSource = e.detail.audioSource;
this.createVisualizer(await getConfig());
},
{ passive: true },
);

View File

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

View File

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

View File

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

View File

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

View File

@ -406,6 +406,18 @@ async function onApiLoaded() {
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);
}
}
/**