Compare commits

..

1 Commits

Author SHA1 Message Date
c15719add5 chore(deps): update dependency node-gyp to v11.5.0 2026-01-08 17:53:24 +00:00
32 changed files with 2059 additions and 2085 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

@ -69,7 +69,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 +86,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 +144,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 +279,6 @@ export default createPlugin({
Builds the app for macOS, Linux, and Windows, Builds the app for macOS, Linux, and Windows,
using [electron-builder](https://github.com/electron-userland/electron-builder). using [electron-builder](https://github.com/electron-userland/electron-builder).
### Building in devcontainer
1. Clone the repo;
2. Open the folder in VS Code;
3. Reopen in container when prompted;
4. Run `pnpm build` as above (choosing the desired target);
5. Collect the built files from the `dist` folder.
Since devcontainer uses a mount for the workspace, the built files will be available on the host system as well.
## Production Preview ## Production Preview
```bash ```bash

View File

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

View File

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

View File

@ -45,12 +45,12 @@
}, },
"pnpm": { "pnpm": {
"overrides": { "overrides": {
"vite": "npm:rolldown-vite@7.3.1", "vite": "npm:rolldown-vite@7.3.0",
"node-gyp": "11.5.0", "node-gyp": "11.5.0",
"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,44 @@
}, },
"devDependencies": { "devDependencies": {
"@electron-toolkit/tsconfig": "1.0.1", "@electron-toolkit/tsconfig": "1.0.1",
"@eslint/js": "9.39.2", "@eslint/js": "9.35.0",
"@malept/flatpak-bundler": "0.4.0", "@malept/flatpak-bundler": "0.4.0",
"@playwright/test": "1.58.0", "@playwright/test": "1.55.0",
"@stylistic/eslint-plugin": "5.7.1", "@stylistic/eslint-plugin": "5.3.1",
"@total-typescript/ts-reset": "0.6.1", "@total-typescript/ts-reset": "0.6.1",
"@types/electron-localshortcut": "3.1.3", "@types/electron-localshortcut": "3.1.3",
"@types/howler": "2.2.12", "@types/howler": "2.2.12",
"@types/html-to-text": "9.0.4", "@types/html-to-text": "9.0.4",
"@types/semver": "7.7.1", "@types/semver": "7.7.1",
"@types/trusted-types": "2.0.7", "@types/trusted-types": "2.0.7",
"bufferutil": "4.1.0", "bufferutil": "4.0.9",
"builtin-modules": "5.0.0", "builtin-modules": "5.0.0",
"cross-env": "10.1.0", "cross-env": "10.0.0",
"del-cli": "6.0.0", "del-cli": "6.0.0",
"discord-api-types": "0.38.37", "discord-api-types": "0.38.37",
"electron": "38.8.0", "electron": "38.7.2",
"electron-builder": "26.4.0", "electron-builder": "26.4.0",
"electron-builder-squirrel-windows": "26.4.0", "electron-builder-squirrel-windows": "26.0.12",
"electron-devtools-installer": "4.0.0", "electron-devtools-installer": "4.0.0",
"electron-vite": "5.0.0", "electron-vite": "4.0.1",
"eslint": "9.39.2", "eslint": "9.35.0",
"eslint-config-prettier": "10.1.8", "eslint-config-prettier": "10.1.8",
"eslint-import-resolver-exports": "1.0.0-beta.5", "eslint-import-resolver-exports": "1.0.0-beta.5",
"eslint-import-resolver-typescript": "4.4.4", "eslint-import-resolver-typescript": "4.4.4",
"eslint-plugin-import": "2.32.0", "eslint-plugin-import": "2.32.0",
"eslint-plugin-prettier": "5.5.5", "eslint-plugin-prettier": "5.5.4",
"eslint-plugin-solid": "0.14.5", "eslint-plugin-solid": "0.14.5",
"glob": "11.1.0", "glob": "11.1.0",
"node-gyp": "11.5.0", "node-gyp": "11.5.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,

2899
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -321,22 +321,6 @@
"hostname": { "hostname": {
"label": "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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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