Compare commits

..

1 Commits

116 changed files with 2974 additions and 7492 deletions

View File

@ -20,7 +20,7 @@ jobs:
TAG_NAME: ${{ inputs.tag_name || github.event.release.tag_name }}
run: echo "WINGET_TAG_NAME=$(echo ${TAG_NAME#v})" >> $GITHUB_ENV
- name: Submit package to Windows Package Manager Community Repository
uses: vedantmgoyal2009/winget-releaser@main
uses: vedantmgoyal2009/winget-releaser@v2
with:
identifier: th-ch.YouTubeMusic
installers-regex: '^YouTube-Music-Web-Setup-[\d\.]+\.exe$'

View File

@ -21,7 +21,7 @@
</a>
</div>
Read this in other languages: [🇰🇷](./docs/readme/README-ko.md), [🇫🇷](./docs/readme/README-fr.md), [🇮🇸](./docs/readme/README-is.md), [🇨🇱 🇪🇸](./docs/readme/README-es.md), [🇷🇺](./docs/readme/README-ru.md), [🇭🇺](./docs/readme/README-hu.md)
Read this in other languages: [🇰🇷](./docs/readme/README-ko.md), [🇮🇸](./docs/readme/README-is.md), [🇨🇱 🇪🇸](./docs/readme/README-es.md), [🇷🇺](./docs/readme/README-ru.md), [🇭🇺](./docs/readme/README-hu.md)
**Electron wrapper around YouTube Music featuring:**

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@ -2,39 +2,8 @@
All notable changes to this project will be documented in this file. Dates are displayed in UTC.
#### [v3.7.5](https://github.com/th-ch/youtube-music/compare/v3.7.4...v3.7.5)
- chore(deps): update dependency builtin-modules to v5 [`#3038`](https://github.com/th-ch/youtube-music/pull/3038)
- chore(deps): update dependency typescript-eslint to v8.25.0 [`#2953`](https://github.com/th-ch/youtube-music/pull/2953)
- fix(deps): update dependency happy-dom to v17.1.8 [`#3001`](https://github.com/th-ch/youtube-music/pull/3001)
- chore(deps): update dependency eslint-config-prettier to v10.0.2 [`#3035`](https://github.com/th-ch/youtube-music/pull/3035)
- chore(deps): update dependency @electron/universal to v2.0.2 [`#3034`](https://github.com/th-ch/youtube-music/pull/3034)
- chore(deps): update dependency @stylistic/eslint-plugin-js to v4 [`#2950`](https://github.com/th-ch/youtube-music/pull/2950)
- chore(deps): update dependency electron-builder-squirrel-windows to v26.0.9 [`#2930`](https://github.com/th-ch/youtube-music/pull/2930)
- fix(no-google-login): Remove Library icon removal code [`#3010`](https://github.com/th-ch/youtube-music/pull/3010)
- fix: Updated tray pause icon for consistency [`#3025`](https://github.com/th-ch/youtube-music/pull/3025)
- chore(deps): update dependency eslint-import-resolver-typescript to v3.8.3 [`#2992`](https://github.com/th-ch/youtube-music/pull/2992)
- fix(deps): update dependency hono to v4.7.2 [`#2999`](https://github.com/th-ch/youtube-music/pull/2999)
- fix: Filter for only `MusicResponsiveListItem` in playlist items [`#3022`](https://github.com/th-ch/youtube-music/pull/3022)
- fix(deps): update dependency youtubei.js to v13.1.0 [`#3015`](https://github.com/th-ch/youtube-music/pull/3015)
- fix: Match engines.pnpm with the pnpm version used to create the lock files [`#2995`](https://github.com/th-ch/youtube-music/pull/2995)
- chore(deps): update dependency electron-builder to v26.0.9 [`457e1bb`](https://github.com/th-ch/youtube-music/commit/457e1bb48e2bcc742680d22b7d34ffdbe7f33eae)
- chore(deps): update deps [`c8bb1f3`](https://github.com/th-ch/youtube-music/commit/c8bb1f386d7053d755c38ca2cac8708af7af1fb9)
- chore(i18n): Translated using Weblate (Thai) [`c9b7901`](https://github.com/th-ch/youtube-music/commit/c9b790168130d0cfc9b2ff23cdcb56ab082a4b66)
#### [v3.7.4](https://github.com/th-ch/youtube-music/compare/v3.7.3...v3.7.4)
> 18 February 2025
- chore(deps): update dependency rollup to v4.34.8 [`#2982`](https://github.com/th-ch/youtube-music/pull/2982)
- fix(plugin-loader): update context filtering logic for backend mode [`#2990`](https://github.com/th-ch/youtube-music/pull/2990)
- Update changelog for v3.7.3 [`86c77d1`](https://github.com/th-ch/youtube-music/commit/86c77d141f2bec62a4a578fff96d51ed388c05a5)
- HOTFIX: Bump version to 3.7.4 [`0d462ac`](https://github.com/th-ch/youtube-music/commit/0d462ac3a26caee23854014cbf5e4b91e2d385e2)
#### [v3.7.3](https://github.com/th-ch/youtube-music/compare/v3.7.2...v3.7.3)
> 17 February 2025
- fix(downloader): use the upgrade button to check for premium status [`#2987`](https://github.com/th-ch/youtube-music/pull/2987)
- chore(deps): update dependency electron-vite to v3 [`#2986`](https://github.com/th-ch/youtube-music/pull/2986)
- chore(deps): update dependency @babel/runtime to v7.26.9 [`#2980`](https://github.com/th-ch/youtube-music/pull/2980)

View File

@ -21,8 +21,6 @@
</a>
</div>
Lee esto en otros idiomas: [🏴 Inglés](../../README.md), [🇰🇷 Coreano](./README-ko.md), [🇫🇷 Francés](./README-fr.md), [🇮🇸 Islandés](./README-is.md), [🇪🇸 Español](./README-es.md), [🇷🇺 Ruso](./README-ru.md)
**Electron wrapper de YouTube Music con las siguientes características:**
- Apariencia y sensación nativa, tiene como objetivo mantener la interfaz original

View File

@ -21,7 +21,6 @@
</a>
</div>
Lisez ceci dans d'autres langues: [🏴 Anglais](../../README.md), [🇰🇷 Coréen](./README-ko.md), [🇫🇷 Français](./README-fr.md), [🇮🇸 Islandais](./README-is.md), [🇪🇸 Espagnol](./README-es.md), [🇷🇺 Russe](./README-ru.md)
**Enveloppe Electron autour de YouTube Music offrant :**

View File

@ -21,7 +21,7 @@
</a>
</div>
Olvasd el más nyelveken: [🏴 Angol](../../README.md), [🇰🇷 Korea](./README-ko.md), [🇫🇷 Francia](./README-fr.md), [🇮🇸 Izland](./README-is.md), [🇪🇸 Spanyol](./README-es.md), [🇷🇺 Orosz](./README-ru.md)
Olvasd el más nyelveken: [🏴 Angol](./blob/master/README.md), [🇰🇷 Korea](./docs/readme/README-ko.md), [🇮🇸 Izland](./docs/readme/README-is.md), [🇪🇸 Spanyol](./docs/readme/README-es.md), [🇷🇺 Orosz](./docs/readme/README-ru.md)
**Electron keretrendszerre épülő alkalmazás a YouTube Music számára, amely a következőket kínálja:**

View File

@ -21,8 +21,6 @@
</a>
</div>
Lestu þetta á öðrum tungumálum: [🏴 Ensku](../../README.md), [🇰🇷 Kóreska](./README-ko.md), [🇫🇷 Franska](./README-fr.md), [🇮🇸 Íslenskur](./README-is.md), [🇪🇸 Spænska](./README-es.md), [🇷🇺 Rússneska](./README-ru.md)
**Electron umbúðir utan um YouTube Tónlist sem inniheldur:**
- Innfæddur útlit og tilfinning, miðar að því að halda upprunalegu viðmótinu

View File

@ -20,8 +20,6 @@
</a>
</div>
다른 언어로 읽어보세요: [🏴 영어](../../README.md), [🇰🇷 한국인](./README-ko.md), [🇫🇷 프랑스 국민](./README-fr.md), [🇮🇸 아이슬란드어](./README-is.md), [🇪🇸 스페인 사람](./README-es.md), [🇷🇺 러시아인](./README-ru.md)
**유튜브 뮤직의 Electron 래퍼; 기능:**
- 원래의 인터페이스를 유지하는 것을 목표로 하는 네이티브 디자인 및 느낌

View File

@ -21,7 +21,7 @@
</a>
</div>
Прочтите это на других языках: [🏴 Английский](../../README.md), [🇰🇷 корейский](./README-ko.md), [🇫🇷 Французский](./README-fr.md), [🇮🇸 исландский](./README-is.md), [🇪🇸 испанский](./README-es.md), [🇷🇺 Русский](./README-ru.md)
**Клиент для YouTube Music основанный на Electron с поддержкой:**

View File

@ -1,4 +1,4 @@
import { dirname, join, resolve } from 'node:path';
import { resolve, dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { UserConfig } from 'vite';

View File

@ -2,7 +2,7 @@
"name": "youtube-music",
"desktopName": "com.github.th_ch.youtube_music",
"productName": "YouTube Music",
"version": "3.8.0",
"version": "3.7.3",
"description": "YouTube Music Desktop App - including custom plugins",
"main": "./dist/main/index.js",
"license": "MIT",
@ -218,21 +218,21 @@
},
"engines": {
"node": ">=18",
"pnpm": ">=10"
"pnpm": ">=8"
},
"pnpm": {
"overrides": {
"vite": "6.2.3",
"vite": "6.1.0",
"node-gyp": "11.1.0",
"xml2js": "0.6.2",
"node-fetch": "3.3.2",
"@electron/universal": "2.0.2",
"@babel/runtime": "7.27.0"
"@electron/universal": "2.0.1",
"@babel/runtime": "7.26.9"
},
"patchedDependencies": {
"vudio@2.1.1": "patches/vudio@2.1.1.patch",
"@malept/flatpak-bundler@0.4.0": "patches/@malept__flatpak-bundler@0.4.0.patch",
"kuromoji@0.1.2": "patches/kuromoji@0.1.2.patch"
"app-builder-lib@26.0.6": "patches/app-builder-lib@26.0.6.patch",
"@malept/flatpak-bundler": "patches/@malept__flatpak-bundler.patch"
},
"neverBuiltDependencies": []
},
@ -245,105 +245,97 @@
"@foobar404/wave": "2.0.5",
"@ghostery/adblocker-electron": "2.5.0",
"@ghostery/adblocker-electron-preload": "2.5.0",
"@hono/node-server": "1.14.0",
"@hono/swagger-ui": "0.5.1",
"@hono/zod-openapi": "0.19.2",
"@hono/node-server": "1.13.8",
"@hono/swagger-ui": "0.5.0",
"@hono/zod-openapi": "0.18.4",
"@hono/zod-validator": "0.4.3",
"@jellybrick/dbus-next": "0.10.3",
"@jellybrick/electron-better-web-request": "1.0.4",
"@jellybrick/mpris-service": "2.1.5",
"@jimp/plugin-color": "1.6.0",
"@skyra/jaro-winkler": "1.1.1",
"@xhayper/discord-rpc": "1.2.1",
"@xhayper/discord-rpc": "1.2.0",
"async-mutex": "0.5.0",
"bgutils-js": "3.2.0",
"bgutils-js": "3.1.3",
"butterchurn": "3.0.0-beta.4",
"butterchurn-presets": "3.0.0-beta.4",
"color": "5.0.0",
"conf": "13.1.0",
"custom-electron-prompt": "1.5.8",
"deepmerge-ts": "7.1.5",
"deepmerge-ts": "7.1.4",
"electron-debug": "4.1.0",
"electron-is": "3.0.0",
"electron-localshortcut": "3.2.1",
"electron-store": "10.0.1",
"electron-unhandled": "4.0.1",
"electron-updater": "6.3.9",
"es-hangul": "2.3.2",
"fast-average-color": "9.5.0",
"fast-average-color": "9.4.0",
"fast-equals": "5.2.2",
"filenamify": "6.0.0",
"hanja": "1.1.4",
"happy-dom": "17.4.4",
"hono": "4.7.5",
"happy-dom": "17.1.0",
"hono": "4.7.1",
"howler": "2.2.4",
"html-to-text": "9.0.5",
"i18next": "24.2.3",
"i18next": "24.2.2",
"jimp": "1.6.0",
"keyboardevent-from-electron-accelerator": "2.0.0",
"keyboardevents-areequal": "0.2.2",
"kuromoji": "0.1.2",
"kuroshiro": "1.2.0",
"kuroshiro-analyzer-kuromoji": "1.1.0",
"lazy-var": "2.2.2",
"node-html-parser": "7.0.1",
"node-id3": "0.2.8",
"node-id3": "0.2.7",
"peerjs": "1.5.4",
"pinyin": "4.0.0-alpha.2",
"segmentit": "2.0.3",
"semver": "7.7.1",
"serve": "14.2.4",
"simple-youtube-age-restriction-bypass": "github:organization/Simple-YouTube-Age-Restriction-Bypass#v2.5.9",
"solid-floating-ui": "0.3.1",
"solid-js": "1.9.5",
"solid-js": "1.9.4",
"solid-styled-components": "0.28.5",
"solid-transition-group": "0.3.0",
"ts-morph": "25.0.1",
"vudio": "2.1.1",
"x11": "2.3.0",
"youtubei.js": "13.3.0",
"youtubei.js": "13.0.0",
"zod": "3.24.2"
},
"devDependencies": {
"@eslint/js": "9.23.0",
"@malept/flatpak-bundler": "0.4.0",
"@playwright/test": "1.51.1",
"@stylistic/eslint-plugin-js": "4.2.0",
"@eslint/js": "9.20.0",
"@playwright/test": "1.50.1",
"@stylistic/eslint-plugin-js": "3.1.0",
"@total-typescript/ts-reset": "0.6.1",
"@types/electron-localshortcut": "3.1.3",
"@types/eslint__js": "8.42.3",
"@types/howler": "2.2.12",
"@types/html-to-text": "9.0.4",
"@types/semver": "7.5.8",
"@types/trusted-types": "2.0.7",
"bufferutil": "4.0.9",
"builtin-modules": "5.0.0",
"builtin-modules": "4.0.0",
"cross-env": "7.0.3",
"del-cli": "6.0.0",
"discord-api-types": "0.37.119",
"electron": "35.1.0",
"electron-builder": "26.0.12",
"electron-builder-squirrel-windows": "26.0.12",
"electron": "34.2.0",
"electron-builder": "26.0.6",
"electron-builder-squirrel-windows": "26.0.6",
"electron-devtools-installer": "4.0.0",
"electron-vite": "3.1.0",
"esbuild": "0.25.1",
"eslint": "9.23.0",
"eslint-config-prettier": "10.1.1",
"electron-vite": "3.0.0",
"esbuild": "0.25.0",
"eslint": "9.20.1",
"eslint-config-prettier": "10.0.1",
"eslint-import-resolver-exports": "1.0.0-beta.5",
"eslint-import-resolver-typescript": "4.2.4",
"eslint-import-resolver-typescript": "3.8.0",
"eslint-plugin-import": "2.31.0",
"eslint-plugin-prettier": "5.2.5",
"eslint-plugin-prettier": "5.2.3",
"glob": "11.0.1",
"node-gyp": "11.1.0",
"playwright": "1.51.1",
"rollup": "4.37.0",
"typescript": "5.8.2",
"typescript-eslint": "8.28.0",
"playwright": "1.50.1",
"rollup": "4.34.7",
"typescript": "5.7.3",
"typescript-eslint": "8.24.0",
"utf-8-validate": "6.0.5",
"vite": "6.2.3",
"vite-plugin-inspect": "11.0.0",
"vite": "6.1.0",
"vite-plugin-inspect": "10.2.1",
"vite-plugin-resolve": "2.5.2",
"vite-plugin-solid": "2.11.6",
"ws": "8.18.1"
"vite-plugin-solid": "2.11.1",
"ws": "8.18.0"
},
"auto-changelog": {
"hideCredit": true,

View File

@ -0,0 +1,21 @@
diff --git a/out/targets/snap.js b/out/targets/snap.js
index f72c36355d27cd2d69fc5fdf2d8bb2451db0287f..baae112fe25ebb49ab8e25aaa48efd6bc43b598f 100644
--- a/out/targets/snap.js
+++ b/out/targets/snap.js
@@ -212,14 +212,14 @@ class SnapTarget extends core_1.Target {
args.push("--template-url", `electron4:${snapArch}`);
}
await (0, builder_util_1.executeAppBuilder)(args);
- const publishConfig = findSnapPublishConfig(this.packager.config);
+
await packager.info.callArtifactBuildCompleted({
file: artifactPath,
safeArtifactName: packager.computeSafeArtifactName(artifactName, "snap", arch, false),
target: this,
arch,
packager,
- publishConfig: publishConfig == null ? { provider: "snapStore" } : publishConfig,
+ publishConfig: options.publish == null ? { provider: "snapStore" } : null,
});
}
isElectronVersionGreaterOrEqualThan(version) {

View File

@ -0,0 +1,161 @@
diff --git a/lib/importDeclaration.js b/lib/importDeclaration.js
index afb4de779034cfea080825a5f4320661c48bee32..f10b0a11a39577fbd42569e6b0e768255c1ef276 100644
--- a/lib/importDeclaration.js
+++ b/lib/importDeclaration.js
@@ -1,5 +1,5 @@
-"use strict";Object.defineProperty(exports, "__esModule", { value: true });exports["default"] = importDeclaration;function importDeclaration(context) {
- var ancestors = context.getAncestors();
+"use strict";Object.defineProperty(exports, "__esModule", { value: true });exports["default"] = importDeclaration;function importDeclaration(context, node) {
+ var ancestors = context.getSourceCode().getAncestors(node);
return ancestors[ancestors.length - 1];
}
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIi4uL3NyYy9pbXBvcnREZWNsYXJhdGlvbi5qcyJdLCJuYW1lcyI6WyJpbXBvcnREZWNsYXJhdGlvbiIsImNvbnRleHQiLCJhbmNlc3RvcnMiLCJnZXRBbmNlc3RvcnMiLCJsZW5ndGgiXSwibWFwcGluZ3MiOiJnR0FBd0JBLGlCLENBQVQsU0FBU0EsaUJBQVQsQ0FBMkJDLE9BQTNCLEVBQW9DO0FBQ2pELE1BQU1DLFlBQVlELFFBQVFFLFlBQVIsRUFBbEI7QUFDQSxTQUFPRCxVQUFVQSxVQUFVRSxNQUFWLEdBQW1CLENBQTdCLENBQVA7QUFDRCIsImZpbGUiOiJpbXBvcnREZWNsYXJhdGlvbi5qcyIsInNvdXJjZXNDb250ZW50IjpbImV4cG9ydCBkZWZhdWx0IGZ1bmN0aW9uIGltcG9ydERlY2xhcmF0aW9uKGNvbnRleHQpIHtcbiAgY29uc3QgYW5jZXN0b3JzID0gY29udGV4dC5nZXRBbmNlc3RvcnMoKTtcbiAgcmV0dXJuIGFuY2VzdG9yc1thbmNlc3RvcnMubGVuZ3RoIC0gMV07XG59XG4iXX0=
\ No newline at end of file
diff --git a/lib/rules/first.js b/lib/rules/first.js
index a77168660cf32c8c3e96f3ff4b8240a36d7de3a6..c0e00d75f9989916057fef3999eeee8d21820292 100644
--- a/lib/rules/first.js
+++ b/lib/rules/first.js
@@ -66,7 +66,7 @@ module.exports = {
}
}
if (nonImportCount > 0) {var _iteratorNormalCompletion = true;var _didIteratorError = false;var _iteratorError = undefined;try {
- for (var _iterator = context.getDeclaredVariables(node)[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) {var variable = _step.value;
+ for (var _iterator = sourceCode.getDeclaredVariables(node)[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) {var variable = _step.value;
if (!shouldSort) {break;}
var references = variable.references;
if (references.length) {var _iteratorNormalCompletion2 = true;var _didIteratorError2 = false;var _iteratorError2 = undefined;try {
diff --git a/lib/rules/namespace.js b/lib/rules/namespace.js
index 574d89a60d15c7e0e712956ea6a3ad2d0eac7f08..82e7cb3cff4246592d762cce86323f2b72de92e4 100644
--- a/lib/rules/namespace.js
+++ b/lib/rules/namespace.js
@@ -86,7 +86,7 @@ module.exports = {
// same as above, but does not add names to local map
ExportNamespaceSpecifier: function () {function ExportNamespaceSpecifier(namespace) {
- var declaration = (0, _importDeclaration2['default'])(context);
+ var declaration = (0, _importDeclaration2['default'])(context, namespace);
var imports = _ExportMap2['default'].get(declaration.source.value, context);
if (imports == null) {return null;}
diff --git a/lib/rules/newline-after-import.js b/lib/rules/newline-after-import.js
index 6cc15686464a17803a0b976c35b99627cdbfabee..520eec6d9a375527ab72c459960fe4416c046c17 100644
--- a/lib/rules/newline-after-import.js
+++ b/lib/rules/newline-after-import.js
@@ -194,7 +194,7 @@ module.exports = {
}return CallExpression;}(),
'Program:exit': function () {function ProgramExit() {
log('exit processing for', context.getPhysicalFilename ? context.getPhysicalFilename() : context.getFilename());
- var scopeBody = getScopeBody(context.getScope());
+ var scopeBody = getScopeBody(context.getSourceCode().getScope(node));
log('got scope:', scopeBody);
requireCalls.forEach(function (node, index) {
diff --git a/lib/rules/no-amd.js b/lib/rules/no-amd.js
index 7ac108bf812ca4f78bfa6fe5ae8b9cf38e2ff497..346c3105dc70f72c4d76fcc6b96b946d1d4ec6d5 100644
--- a/lib/rules/no-amd.js
+++ b/lib/rules/no-amd.js
@@ -23,7 +23,7 @@ module.exports = {
create: function () {function create(context) {
return {
CallExpression: function () {function CallExpression(node) {
- if (context.getScope().type !== 'module') {return;}
+ if (context.getSourceCode().getScope(node).type !== 'module') {return;}
if (node.callee.type !== 'Identifier') {return;}
if (node.callee.name !== 'require' && node.callee.name !== 'define') {return;}
diff --git a/lib/rules/no-commonjs.js b/lib/rules/no-commonjs.js
index befeff0026d61d3ac1e6bbcea29f5c471dc1d353..e91c5ed34e968d5867e884ea800e166cda345aef 100644
--- a/lib/rules/no-commonjs.js
+++ b/lib/rules/no-commonjs.js
@@ -107,7 +107,7 @@ module.exports = {
// exports.
if (node.object.name === 'exports') {
- var isInScope = context.getScope().
+ var isInScope = context.getSourceCode().getScope(node).
variables.
some(function (variable) {return variable.name === 'exports';});
if (!isInScope) {
@@ -117,7 +117,7 @@ module.exports = {
}return MemberExpression;}(),
CallExpression: function () {function CallExpression(call) {
- if (!validateScope(context.getScope())) {return;}
+ if (!validateScope(context.getSourceCode().getScope(call))) {return;}
if (call.callee.type !== 'Identifier') {return;}
if (call.callee.name !== 'require') {return;}
diff --git a/lib/rules/no-mutable-exports.js b/lib/rules/no-mutable-exports.js
index 40bd1b4cfa95d41732bb13bba0ed1969a91cc7ff..8a25abfbfadb299204b36a6cbf283259bcc2e790 100644
--- a/lib/rules/no-mutable-exports.js
+++ b/lib/rules/no-mutable-exports.js
@@ -32,7 +32,7 @@ module.exports = {
}
function handleExportDefault(node) {
- var scope = context.getScope();
+ var scope = context.getSourceCode().getScope(node);
if (node.declaration.name) {
checkDeclarationsInScope(scope, node.declaration.name);
@@ -40,7 +40,7 @@ module.exports = {
}
function handleExportNamed(node) {
- var scope = context.getScope();
+ var scope = context.getSourceCode().getScope(node);
if (node.declaration) {
checkDeclaration(node.declaration);
diff --git a/lib/rules/no-named-as-default-member.js b/lib/rules/no-named-as-default-member.js
index 0c15051e027ad7d1d45f1b51c20be1c000b0af01..5b3d6ba415511b7f9f83a52e1acfebe5a1045a7b 100644
--- a/lib/rules/no-named-as-default-member.js
+++ b/lib/rules/no-named-as-default-member.js
@@ -35,7 +35,7 @@ module.exports = {
return {
ImportDefaultSpecifier: function () {function ImportDefaultSpecifier(node) {
- var declaration = (0, _importDeclaration2['default'])(context);
+ var declaration = (0, _importDeclaration2['default'])(context, node);
var exportMap = _ExportMap2['default'].get(declaration.source.value, context);
if (exportMap == null) {return;}
diff --git a/lib/rules/no-named-as-default.js b/lib/rules/no-named-as-default.js
index 63378a33a1c7da004c57a524cec1a1cddf23e210..c81b1f93b11628676158b79f1c4015911943cc7d 100644
--- a/lib/rules/no-named-as-default.js
+++ b/lib/rules/no-named-as-default.js
@@ -18,7 +18,7 @@ module.exports = {
// #566: default is a valid specifier
if (defaultSpecifier[nameKey].name === 'default') {return;}
- var declaration = (0, _importDeclaration2['default'])(context);
+ var declaration = (0, _importDeclaration2['default'])(context, defaultSpecifier);
var imports = _ExportMap2['default'].get(declaration.source.value, context);
if (imports == null) {return;}
diff --git a/lib/rules/no-namespace.js b/lib/rules/no-namespace.js
index 2b0c783adea788101b779b17f977bbcb582cfd3f..a7f7b202ac7c4a342febef2a993586c4cc84fc7a 100644
--- a/lib/rules/no-namespace.js
+++ b/lib/rules/no-namespace.js
@@ -43,7 +43,7 @@ var _docsUrl = require('../docsUrl');var _docsUrl2 = _interopRequireDefault(_doc
return;
}
- var scopeVariables = context.getScope().variables;
+ var scopeVariables = context.getSourceCode().getScope(node).variables;
var namespaceVariable = scopeVariables.find(function (variable) {return variable.defs[0].node === node;});
var namespaceReferences = namespaceVariable.references;
var namespaceIdentifiers = namespaceReferences.map(function (reference) {return reference.identifier;});
diff --git a/package.json b/package.json
index 5c0af48543483a21791fa23a4a583071d3551772..5deeac3d0accc3878ef0fc93dfb52a8ca7c46e84 100644
--- a/package.json
+++ b/package.json
@@ -72,7 +72,7 @@
"chai": "^4.3.10",
"cross-env": "^4.0.0",
"escope": "^3.6.0",
- "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8",
+ "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9",
"eslint-doc-generator": "^1.6.1",
"eslint-import-resolver-node": "file:./resolvers/node",
"eslint-import-resolver-typescript": "^1.0.2 || ^1.1.1",

View File

@ -1,580 +0,0 @@
diff --git a/build/kuromoji.js b/build/kuromoji.js
index f0f4ae9183ff8965fda64a2042f29936f76506d1..8912a754d184742d2768854c7bba83d66f9ff95f 100644
--- a/build/kuromoji.js
+++ b/build/kuromoji.js
@@ -1,5 +1,5 @@
-(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.kuromoji = f()}})(function(){var define,module,exports;return (function(){function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s}return e})()({1:[function(require,module,exports){
-(function (process,global){
+(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.kuromoji = f()}})(function(){var define,module,exports;return (function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i<t.length;i++)o(t[i]);return o}return r})()({1:[function(require,module,exports){
+(function (process,global,setImmediate){(function (){
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
typeof define === 'function' && define.amd ? define(['exports'], factory) :
@@ -666,10 +666,13 @@ var reIsUint = /^(?:0|[1-9]\d*)$/;
* @returns {boolean} Returns `true` if `value` is a valid index, else `false`.
*/
function isIndex(value, length) {
+ var type = typeof value;
length = length == null ? MAX_SAFE_INTEGER$1 : length;
+
return !!length &&
- (typeof value == 'number' || reIsUint.test(value)) &&
- (value > -1 && value % 1 == 0 && value < length);
+ (type == 'number' ||
+ (type != 'symbol' && reIsUint.test(value))) &&
+ (value > -1 && value % 1 == 0 && value < length);
}
/** `Object#toString` result references. */
@@ -755,6 +758,14 @@ var freeProcess = moduleExports$1 && freeGlobal.process;
/** Used to access faster Node.js helpers. */
var nodeUtil = (function() {
try {
+ // Use `util.types` for Node.js 10+.
+ var types = freeModule$1 && freeModule$1.require && freeModule$1.require('util').types;
+
+ if (types) {
+ return types;
+ }
+
+ // Legacy `process.binding('util')` for Node.js < 10.
return freeProcess && freeProcess.binding && freeProcess.binding('util');
} catch (e) {}
}());
@@ -939,6 +950,9 @@ function createObjectIterator(obj) {
var len = okeys.length;
return function next() {
var key = okeys[++i];
+ if (key === '__proto__') {
+ return next();
+ }
return i < len ? {value: obj[key], key: key} : null;
};
}
@@ -970,6 +984,7 @@ function _eachOfLimit(limit) {
var nextElem = iterator(obj);
var done = false;
var running = 0;
+ var looping = false;
function iterateeCallback(err, value) {
running -= 1;
@@ -981,12 +996,13 @@ function _eachOfLimit(limit) {
done = true;
return callback(null);
}
- else {
+ else if (!looping) {
replenish();
}
}
function replenish () {
+ looping = true;
while (running < limit && !done) {
var elem = nextElem();
if (elem === null) {
@@ -999,6 +1015,7 @@ function _eachOfLimit(limit) {
running += 1;
iteratee(elem.value, elem.key, onlyOnce(iterateeCallback));
}
+ looping = false;
}
replenish();
@@ -3819,7 +3836,7 @@ function memoize(fn, hasher) {
/**
* Calls `callback` on a later loop around the event loop. In Node.js this just
- * calls `process.nextTicl`. In the browser it will use `setImmediate` if
+ * calls `process.nextTick`. In the browser it will use `setImmediate` if
* available, otherwise `setTimeout(callback, 0)`, which means other higher
* priority events may precede the execution of `callback`.
*
@@ -5596,8 +5613,8 @@ Object.defineProperty(exports, '__esModule', { value: true });
})));
-}).call(this,require('_process'),typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
-},{"_process":4}],2:[function(require,module,exports){
+}).call(this)}).call(this,require('_process'),typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {},require("timers").setImmediate)
+},{"_process":3,"timers":4}],2:[function(require,module,exports){
// Copyright (c) 2014 Takuya Asano All Rights Reserved.
(function () {
@@ -6391,234 +6408,6 @@ Object.defineProperty(exports, '__esModule', { value: true });
})();
},{}],3:[function(require,module,exports){
-(function (process){
-// Copyright Joyent, Inc. and other Node contributors.
-//
-// Permission is hereby granted, free of charge, to any person obtaining a
-// copy of this software and associated documentation files (the
-// "Software"), to deal in the Software without restriction, including
-// without limitation the rights to use, copy, modify, merge, publish,
-// distribute, sublicense, and/or sell copies of the Software, and to permit
-// persons to whom the Software is furnished to do so, subject to the
-// following conditions:
-//
-// The above copyright notice and this permission notice shall be included
-// in all copies or substantial portions of the Software.
-//
-// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
-// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
-// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
-// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
-// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
-// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
-// USE OR OTHER DEALINGS IN THE SOFTWARE.
-
-// resolves . and .. elements in a path array with directory names there
-// must be no slashes, empty elements, or device names (c:\) in the array
-// (so also no leading and trailing slashes - it does not distinguish
-// relative and absolute paths)
-function normalizeArray(parts, allowAboveRoot) {
- // if the path tries to go above the root, `up` ends up > 0
- var up = 0;
- for (var i = parts.length - 1; i >= 0; i--) {
- var last = parts[i];
- if (last === '.') {
- parts.splice(i, 1);
- } else if (last === '..') {
- parts.splice(i, 1);
- up++;
- } else if (up) {
- parts.splice(i, 1);
- up--;
- }
- }
-
- // if the path is allowed to go above the root, restore leading ..s
- if (allowAboveRoot) {
- for (; up--; up) {
- parts.unshift('..');
- }
- }
-
- return parts;
-}
-
-// Split a filename into [root, dir, basename, ext], unix version
-// 'root' is just a slash, or nothing.
-var splitPathRe =
- /^(\/?|)([\s\S]*?)((?:\.{1,2}|[^\/]+?|)(\.[^.\/]*|))(?:[\/]*)$/;
-var splitPath = function(filename) {
- return splitPathRe.exec(filename).slice(1);
-};
-
-// path.resolve([from ...], to)
-// posix version
-exports.resolve = function() {
- var resolvedPath = '',
- resolvedAbsolute = false;
-
- for (var i = arguments.length - 1; i >= -1 && !resolvedAbsolute; i--) {
- var path = (i >= 0) ? arguments[i] : process.cwd();
-
- // Skip empty and invalid entries
- if (typeof path !== 'string') {
- throw new TypeError('Arguments to path.resolve must be strings');
- } else if (!path) {
- continue;
- }
-
- resolvedPath = path + '/' + resolvedPath;
- resolvedAbsolute = path.charAt(0) === '/';
- }
-
- // At this point the path should be resolved to a full absolute path, but
- // handle relative paths to be safe (might happen when process.cwd() fails)
-
- // Normalize the path
- resolvedPath = normalizeArray(filter(resolvedPath.split('/'), function(p) {
- return !!p;
- }), !resolvedAbsolute).join('/');
-
- return ((resolvedAbsolute ? '/' : '') + resolvedPath) || '.';
-};
-
-// path.normalize(path)
-// posix version
-exports.normalize = function(path) {
- var isAbsolute = exports.isAbsolute(path),
- trailingSlash = substr(path, -1) === '/';
-
- // Normalize the path
- path = normalizeArray(filter(path.split('/'), function(p) {
- return !!p;
- }), !isAbsolute).join('/');
-
- if (!path && !isAbsolute) {
- path = '.';
- }
- if (path && trailingSlash) {
- path += '/';
- }
-
- return (isAbsolute ? '/' : '') + path;
-};
-
-// posix version
-exports.isAbsolute = function(path) {
- return path.charAt(0) === '/';
-};
-
-// posix version
-exports.join = function() {
- var paths = Array.prototype.slice.call(arguments, 0);
- return exports.normalize(filter(paths, function(p, index) {
- if (typeof p !== 'string') {
- throw new TypeError('Arguments to path.join must be strings');
- }
- return p;
- }).join('/'));
-};
-
-
-// path.relative(from, to)
-// posix version
-exports.relative = function(from, to) {
- from = exports.resolve(from).substr(1);
- to = exports.resolve(to).substr(1);
-
- function trim(arr) {
- var start = 0;
- for (; start < arr.length; start++) {
- if (arr[start] !== '') break;
- }
-
- var end = arr.length - 1;
- for (; end >= 0; end--) {
- if (arr[end] !== '') break;
- }
-
- if (start > end) return [];
- return arr.slice(start, end - start + 1);
- }
-
- var fromParts = trim(from.split('/'));
- var toParts = trim(to.split('/'));
-
- var length = Math.min(fromParts.length, toParts.length);
- var samePartsLength = length;
- for (var i = 0; i < length; i++) {
- if (fromParts[i] !== toParts[i]) {
- samePartsLength = i;
- break;
- }
- }
-
- var outputParts = [];
- for (var i = samePartsLength; i < fromParts.length; i++) {
- outputParts.push('..');
- }
-
- outputParts = outputParts.concat(toParts.slice(samePartsLength));
-
- return outputParts.join('/');
-};
-
-exports.sep = '/';
-exports.delimiter = ':';
-
-exports.dirname = function(path) {
- var result = splitPath(path),
- root = result[0],
- dir = result[1];
-
- if (!root && !dir) {
- // No dirname whatsoever
- return '.';
- }
-
- if (dir) {
- // It has a dirname, strip trailing slash
- dir = dir.substr(0, dir.length - 1);
- }
-
- return root + dir;
-};
-
-
-exports.basename = function(path, ext) {
- var f = splitPath(path)[2];
- // TODO: make this comparison case-insensitive on windows?
- if (ext && f.substr(-1 * ext.length) === ext) {
- f = f.substr(0, f.length - ext.length);
- }
- return f;
-};
-
-
-exports.extname = function(path) {
- return splitPath(path)[3];
-};
-
-function filter (xs, f) {
- if (xs.filter) return xs.filter(f);
- var res = [];
- for (var i = 0; i < xs.length; i++) {
- if (f(xs[i], i, xs)) res.push(xs[i]);
- }
- return res;
-}
-
-// String.prototype.substr - negative index don't work in IE8
-var substr = 'ab'.substr(-1) === 'b'
- ? function (str, start, len) { return str.substr(start, len) }
- : function (str, start, len) {
- if (start < 0) start = str.length + start;
- return str.substr(start, len);
- }
-;
-
-}).call(this,require('_process'))
-},{"_process":4}],4:[function(require,module,exports){
// shim for using process in browser
var process = module.exports = {};
@@ -6804,7 +6593,86 @@ process.chdir = function (dir) {
};
process.umask = function() { return 0; };
-},{}],5:[function(require,module,exports){
+},{}],4:[function(require,module,exports){
+(function (setImmediate,clearImmediate){(function (){
+var nextTick = require('process/browser.js').nextTick;
+var apply = Function.prototype.apply;
+var slice = Array.prototype.slice;
+var immediateIds = {};
+var nextImmediateId = 0;
+
+// DOM APIs, for completeness
+
+exports.setTimeout = function() {
+ return new Timeout(apply.call(setTimeout, window, arguments), clearTimeout);
+};
+exports.setInterval = function() {
+ return new Timeout(apply.call(setInterval, window, arguments), clearInterval);
+};
+exports.clearTimeout =
+exports.clearInterval = function(timeout) { timeout.close(); };
+
+function Timeout(id, clearFn) {
+ this._id = id;
+ this._clearFn = clearFn;
+}
+Timeout.prototype.unref = Timeout.prototype.ref = function() {};
+Timeout.prototype.close = function() {
+ this._clearFn.call(window, this._id);
+};
+
+// Does not start the time, just sets up the members needed.
+exports.enroll = function(item, msecs) {
+ clearTimeout(item._idleTimeoutId);
+ item._idleTimeout = msecs;
+};
+
+exports.unenroll = function(item) {
+ clearTimeout(item._idleTimeoutId);
+ item._idleTimeout = -1;
+};
+
+exports._unrefActive = exports.active = function(item) {
+ clearTimeout(item._idleTimeoutId);
+
+ var msecs = item._idleTimeout;
+ if (msecs >= 0) {
+ item._idleTimeoutId = setTimeout(function onTimeout() {
+ if (item._onTimeout)
+ item._onTimeout();
+ }, msecs);
+ }
+};
+
+// That's not how node.js implements it but the exposed api is the same.
+exports.setImmediate = typeof setImmediate === "function" ? setImmediate : function(fn) {
+ var id = nextImmediateId++;
+ var args = arguments.length < 2 ? false : slice.call(arguments, 1);
+
+ immediateIds[id] = true;
+
+ nextTick(function onNextTick() {
+ if (immediateIds[id]) {
+ // fn.call() is faster so we optimize for the common use-case
+ // @see http://jsperf.com/call-apply-segu
+ if (args) {
+ fn.apply(null, args);
+ } else {
+ fn.call(null);
+ }
+ // Prevent ids from leaking
+ exports.clearImmediate(id);
+ }
+ });
+
+ return id;
+};
+
+exports.clearImmediate = typeof clearImmediate === "function" ? clearImmediate : function(id) {
+ delete immediateIds[id];
+};
+}).call(this)}).call(this,require("timers").setImmediate,require("timers").clearImmediate)
+},{"process/browser.js":3,"timers":4}],5:[function(require,module,exports){
/** @license zlib.js 2012 - imaya [ https://github.com/imaya/zlib.js ] The MIT License */(function() {'use strict';function n(e){throw e;}var p=void 0,aa=this;function t(e,b){var d=e.split("."),c=aa;!(d[0]in c)&&c.execScript&&c.execScript("var "+d[0]);for(var a;d.length&&(a=d.shift());)!d.length&&b!==p?c[a]=b:c=c[a]?c[a]:c[a]={}};var x="undefined"!==typeof Uint8Array&&"undefined"!==typeof Uint16Array&&"undefined"!==typeof Uint32Array&&"undefined"!==typeof DataView;new (x?Uint8Array:Array)(256);var y;for(y=0;256>y;++y)for(var A=y,ba=7,A=A>>>1;A;A>>>=1)--ba;function B(e,b,d){var c,a="number"===typeof b?b:b=0,f="number"===typeof d?d:e.length;c=-1;for(a=f&7;a--;++b)c=c>>>8^C[(c^e[b])&255];for(a=f>>3;a--;b+=8)c=c>>>8^C[(c^e[b])&255],c=c>>>8^C[(c^e[b+1])&255],c=c>>>8^C[(c^e[b+2])&255],c=c>>>8^C[(c^e[b+3])&255],c=c>>>8^C[(c^e[b+4])&255],c=c>>>8^C[(c^e[b+5])&255],c=c>>>8^C[(c^e[b+6])&255],c=c>>>8^C[(c^e[b+7])&255];return(c^4294967295)>>>0}
var D=[0,1996959894,3993919788,2567524794,124634137,1886057615,3915621685,2657392035,249268274,2044508324,3772115230,2547177864,162941995,2125561021,3887607047,2428444049,498536548,1789927666,4089016648,2227061214,450548861,1843258603,4107580753,2211677639,325883990,1684777152,4251122042,2321926636,335633487,1661365465,4195302755,2366115317,997073096,1281953886,3579855332,2724688242,1006888145,1258607687,3524101629,2768942443,901097722,1119000684,3686517206,2898065728,853044451,1172266101,3705015759,
2882616665,651767980,1373503546,3369554304,3218104598,565507253,1454621731,3485111705,3099436303,671266974,1594198024,3322730930,2970347812,795835527,1483230225,3244367275,3060149565,1994146192,31158534,2563907772,4023717930,1907459465,112637215,2680153253,3904427059,2013776290,251722036,2517215374,3775830040,2137656763,141376813,2439277719,3865271297,1802195444,476864866,2238001368,4066508878,1812370925,453092731,2181625025,4111451223,1706088902,314042704,2344532202,4240017532,1658658271,366619977,
@@ -6984,7 +6852,7 @@ module.exports = Tokenizer;
"use strict";
var Tokenizer = require("./Tokenizer");
-var DictionaryLoader = require("./loader/NodeDictionaryLoader");
+var BrowserDictionaryLoader = require("./loader/BrowserDictionaryLoader");
/**
* TokenizerBuilder create Tokenizer instance.
@@ -7005,7 +6873,7 @@ function TokenizerBuilder(option) {
* @param {TokenizerBuilder~onLoad} callback Callback function
*/
TokenizerBuilder.prototype.build = function (callback) {
- var loader = new DictionaryLoader(this.dic_path);
+ var loader = new BrowserDictionaryLoader(this.dic_path);
loader.load(function (err, dic) {
callback(err, new Tokenizer(dic));
});
@@ -7020,7 +6888,7 @@ TokenizerBuilder.prototype.build = function (callback) {
module.exports = TokenizerBuilder;
-},{"./Tokenizer":6,"./loader/NodeDictionaryLoader":19}],8:[function(require,module,exports){
+},{"./Tokenizer":6,"./loader/BrowserDictionaryLoader":19}],8:[function(require,module,exports){
/*
* Copyright 2014 Takuya Asano
* Copyright 2010-2014 Atilika Inc. and contributors
@@ -8163,7 +8031,6 @@ module.exports = BrowserDictionaryLoader;
"use strict";
-var path = require("path");
var async = require("async");
var DynamicDictionaries = require("../dict/DynamicDictionaries");
@@ -8194,7 +8061,7 @@ DictionaryLoader.prototype.load = function (load_callback) {
// Trie
function (callback) {
async.map([ "base.dat.gz", "check.dat.gz" ], function (filename, _callback) {
- loadArrayBuffer(path.join(dic_path, filename), function (err, buffer) {
+ loadArrayBuffer(dic_path + filename, function (err, buffer) {
if(err) {
return _callback(err);
}
@@ -8214,7 +8081,7 @@ DictionaryLoader.prototype.load = function (load_callback) {
// Token info dictionaries
function (callback) {
async.map([ "tid.dat.gz", "tid_pos.dat.gz", "tid_map.dat.gz" ], function (filename, _callback) {
- loadArrayBuffer(path.join(dic_path, filename), function (err, buffer) {
+ loadArrayBuffer(dic_path + filename, function (err, buffer) {
if(err) {
return _callback(err);
}
@@ -8234,7 +8101,7 @@ DictionaryLoader.prototype.load = function (load_callback) {
},
// Connection cost matrix
function (callback) {
- loadArrayBuffer(path.join(dic_path, "cc.dat.gz"), function (err, buffer) {
+ loadArrayBuffer(dic_path + "cc.dat.gz", function (err, buffer) {
if(err) {
return callback(err);
}
@@ -8246,7 +8113,7 @@ DictionaryLoader.prototype.load = function (load_callback) {
// Unknown dictionaries
function (callback) {
async.map([ "unk.dat.gz", "unk_pos.dat.gz", "unk_map.dat.gz", "unk_char.dat.gz", "unk_compat.dat.gz", "unk_invoke.dat.gz" ], function (filename, _callback) {
- loadArrayBuffer(path.join(dic_path, filename), function (err, buffer) {
+ loadArrayBuffer(dic_path + filename, function (err, buffer) {
if(err) {
return _callback(err);
}
@@ -8282,7 +8149,7 @@ DictionaryLoader.prototype.load = function (load_callback) {
module.exports = DictionaryLoader;
-},{"../dict/DynamicDictionaries":11,"async":1,"path":3}],21:[function(require,module,exports){
+},{"../dict/DynamicDictionaries":11,"async":1}],21:[function(require,module,exports){
/*
* Copyright 2014 Takuya Asano
* Copyright 2010-2014 Atilika Inc. and contributors
diff --git a/src/TokenizerBuilder.js b/src/TokenizerBuilder.js
index 9ef5c6a2efc63e8b12735a8a9f1cb08d6c52c20c..98881e9fd731047c3fca848a71ede7e381e74f51 100644
--- a/src/TokenizerBuilder.js
+++ b/src/TokenizerBuilder.js
@@ -18,7 +18,7 @@
"use strict";
var Tokenizer = require("./Tokenizer");
-var DictionaryLoader = require("./loader/NodeDictionaryLoader");
+var BrowserDictionaryLoader = require("./loader/BrowserDictionaryLoader");
/**
* TokenizerBuilder create Tokenizer instance.
@@ -39,7 +39,7 @@ function TokenizerBuilder(option) {
* @param {TokenizerBuilder~onLoad} callback Callback function
*/
TokenizerBuilder.prototype.build = function (callback) {
- var loader = new DictionaryLoader(this.dic_path);
+ var loader = new BrowserDictionaryLoader(this.dic_path);
loader.load(function (err, dic) {
callback(err, new Tokenizer(dic));
});
diff --git a/src/loader/DictionaryLoader.js b/src/loader/DictionaryLoader.js
index 5f88c0b7f9a786dd8c072a7b84ae86a6f31412cb..3d6f8a67e16d251b3e4ba4dbbbc947679c364382 100644
--- a/src/loader/DictionaryLoader.js
+++ b/src/loader/DictionaryLoader.js
@@ -17,7 +17,6 @@
"use strict";
-var path = require("path");
var async = require("async");
var DynamicDictionaries = require("../dict/DynamicDictionaries");
@@ -48,7 +47,7 @@ DictionaryLoader.prototype.load = function (load_callback) {
// Trie
function (callback) {
async.map([ "base.dat.gz", "check.dat.gz" ], function (filename, _callback) {
- loadArrayBuffer(path.join(dic_path, filename), function (err, buffer) {
+ loadArrayBuffer(dic_path + filename, function (err, buffer) {
if(err) {
return _callback(err);
}
@@ -68,7 +67,7 @@ DictionaryLoader.prototype.load = function (load_callback) {
// Token info dictionaries
function (callback) {
async.map([ "tid.dat.gz", "tid_pos.dat.gz", "tid_map.dat.gz" ], function (filename, _callback) {
- loadArrayBuffer(path.join(dic_path, filename), function (err, buffer) {
+ loadArrayBuffer(dic_path + filename, function (err, buffer) {
if(err) {
return _callback(err);
}
@@ -88,7 +87,7 @@ DictionaryLoader.prototype.load = function (load_callback) {
},
// Connection cost matrix
function (callback) {
- loadArrayBuffer(path.join(dic_path, "cc.dat.gz"), function (err, buffer) {
+ loadArrayBuffer(dic_path + "cc.dat.gz", function (err, buffer) {
if(err) {
return callback(err);
}
@@ -100,7 +99,7 @@ DictionaryLoader.prototype.load = function (load_callback) {
// Unknown dictionaries
function (callback) {
async.map([ "unk.dat.gz", "unk_pos.dat.gz", "unk_map.dat.gz", "unk_char.dat.gz", "unk_compat.dat.gz", "unk_invoke.dat.gz" ], function (filename, _callback) {
- loadArrayBuffer(path.join(dic_path, filename), function (err, buffer) {
+ loadArrayBuffer(dic_path + filename, function (err, buffer) {
if(err) {
return _callback(err);
}
diff --git a/src/loader/NodeDictionaryLoader.js b/src/loader/NodeDictionaryLoader.js
deleted file mode 100644
index 26eb79249121efe39bd5ae77c17e1caa197fb4ce..0000000000000000000000000000000000000000

2446
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -202,7 +202,7 @@
"show": "عرض النافدة",
"tooltip": {
"default": "يوتيوب اغاني",
"with-song-info": "أغاني يوتيوب: {{artist}} - {{title}}"
"with-song-info": "يوتيوب أغاني: {{الفنان}}-{{العنوان}}"
}
}
},
@ -219,7 +219,7 @@
"name": "حاجب الإعلانات"
},
"album-actions": {
"description": "يضيف أزرار \"إلغاء عدم الاعجاب\" و\"عدم الاعجاب\" و\"الإعجاب\" و\"إلغاء الإعجاب\" لتطبيق ذلك على جميع الأغاني في قائمة تشغيل أو ألبوم",
"description": "يضيف أزرار \"إلغاء عدم الإعجاب\"، \"عدم الإعجاب\"، \"الإعجاب\"، و\"إلغاء الإعجاب\" لتطبيقها على جميع الأغاني في قائمة التشغيل أو الألبوم",
"name": "إجراءات الألبوم"
},
"album-color-theme": {
@ -279,13 +279,6 @@
},
"name": "الوضع المحيطي"
},
"amuse": {
"description": "تكامل دعم YouTube Music مع ويدجت Amuse لعرض الأغنية قيد التشغيل، من إنتاج 6K Labs",
"name": "تلسيه",
"response": {
"query": "خادم Amuse API قيد التشغيل. استخدم GET /query للحصول على معلومات الأغنية."
}
},
"api-server": {
"description": "يضيف خادم للتحكم في المشغل",
"dialog": {
@ -293,544 +286,41 @@
"buttons": {
"allow": "سماح",
"deny": "رفض"
},
"message": "السماح لـ {{ID}} ({{origin}}) بالوصول إلى واجهة برمجة التطبيقات (API)؟",
"title": "طلب السماح بالوصول إلى واجهة برمجة التطبيقات(API)"
}
}
},
"menu": {
"auth-strategy": {
"label": "نهج التفويض",
"submenu": {
"auth-at-first": {
"label": "التفويض المبدئي عند الطلب الأول"
},
"none": {
"label": "بدون تفويض"
}
}
},
"hostname": {
"label": "اسم المضيف"
},
"port": {
"label": "المنفذ"
}
},
"name": "خادم API [تجريبي]",
"prompt": {
"hostname": {
"label": "أدخل اسم المضيف (مثل 0.0.0.0) لخادم API:",
"title": "اسم الخادم"
},
"port": {
"label": "أدخل المنفذ لخادم API:",
"title": "منفذ"
}
}
},
"audio-compressor": {
"description": "تطبيق الضغط على الصوت (يخفض مستوى صوت الأجزاء الأعلى من الإشارة ويرفع مستوى صوت الأجزاء الأكثر نعومة)",
"name": "ضاغط الصوت"
},
"blur-nav-bar": {
"description": "يجعل شريط التنقل شفاف و ضبابي",
"name": "تغبيش شريط التنقل"
"description": "يجعل شريط التنقل شفاف و ضبابي"
},
"bypass-age-restrictions": {
"description": "تجاوز تَحَقّق اليوتيوب من السن",
"name": "تجاوز التحقق من السن"
},
"captions-selector": {
"description": "محدد ترجمات المقاطع الصوتية لYoutube Music",
"menu": {
"autoload": "اختار اخر ترجمة مستخدمة تلقائيا",
"disable-captions": "لا توجد ترجمات بشكل افتراضي"
},
"name": "محدد الترجمة",
"prompt": {
"selector": {
"label": "لغة الترجمة الحالية: {{language}}",
"none": "لا شيء",
"title": "اختار لغة الترجمة"
}
},
"templates": {
"title": "فتح محدد الترجمة"
}
},
"compact-sidebar": {
"description": "قم دائمًا بتعيين الشريط الجانبي في الوضع الملموم",
"name": "شريط جانبي ملموم"
},
"crossfade": {
"description": "التداخل بين الأغاني",
"menu": {
"advanced": "متقدم"
},
"name": "التداخل بين الأغاني [تجريبي]",
"prompt": {
"options": {
"multi-input": {
"fade-in-duration": "مدة التداخل (بأجزاء الثانية)",
"fade-out-duration": "مدة التلاشي (جزء ثانية)",
"fade-scaling": {
"label": "توسيع التداخل",
"linear": "خطي",
"logarithmic": "لوغاريتمي"
},
"seconds-before-end": "التلاشي قبل النهاية بـ N ثوانٍ"
},
"title": "خيارات التداخل"
}
}
},
"disable-autoplay": {
"description": "يجعل الأغنية تبدأ في وضع \"الإيقاف المؤقت\"",
"menu": {
"apply-once": "ينطبق فقط عند بدء التشغيل"
},
"name": "تعطيل التشغيل التلقائي"
},
"discord": {
"backend": {
"already-connected": "تمت محاولة الاتصال بالاتصال النشط",
"connected": "متصل بDiscord",
"disconnected": "انقطع الاتصال بDiscord"
},
"description": "أظهر لأصدقائك ما تستمع إليه من خلال Rich Presence",
"menu": {
"auto-reconnect": "إعادة اتصال تلقائي",
"clear-activity": "مسح النشاط",
"clear-activity-after-timeout": "مسح النشاط بعد انتهاء المهلة",
"connected": "متصل",
"disconnected": "قطع الاتصال",
"hide-duration-left": "إخفاء المدة المتبقية",
"hide-github-button": "إخفاء زر رابط GitHub",
"play-on-youtube-music": "شغل في YouTube Music",
"set-inactivity-timeout": "ضبط مهلة عدم النشاط"
},
"name": "حالة ديسكورد",
"prompt": {
"set-inactivity-timeout": {
"label": "أدخل مهلة عدم النشاط بالثواني:",
"title": "ضبط مهلة عدم النشاط"
}
}
},
"downloader": {
"backend": {
"dialog": {
"error": {
"buttons": {
"ok": "حسنا"
},
"message": "نعتذر، فشل التحميل…",
"title": "خطأ في التحميل!"
},
"start-download-playlist": {
"buttons": {
"ok": "حسنا"
},
"detail": "({{playlistSize}} أغنية)",
"message": "تحميل القائمة {{playlistTitle}}",
"title": "بدأ التحميل"
}
},
"feedback": {
"conversion-progress": "التحويل: {{percent}}%",
"converting": "جارٍ التحويل…",
"done": "تم: {{filePath}}",
"download-info": "تحميل {{artist}} - {{title}} {{videoId}}",
"download-progress": "تحميل: {{percent}}%",
"downloading": "تحميل…",
"downloading-counter": "تنزيل {{current}}/{{total}}…",
"downloading-playlist": "يتم تحميل القائمة \"{{playlistTitle}}\" - {{playlistSize}} أغاني ({{playlistId}})",
"error-while-downloading": "خطأ في تحميل \"{{author}} - {{title}}\": {{error}}",
"folder-already-exists": "الملف {{playlistFolder}} موجود بالفعل",
"getting-playlist-info": "الحصول على معلومات القائمة…",
"loading": "جار التحميل…",
"playlist-has-only-one-song": "تحتوي قائمة التشغيل على عنصر واحد فقط، يتم تحميله الأن",
"playlist-id-not-found": "لم يتم العثور على معرف قائمة التشغيل",
"playlist-is-empty": "قائمة التشغيل فارغة",
"playlist-is-mix-or-private": "حدث خطأ أثناء الحصول على معلومات قائمة التشغيل: تأكد من أنها ليست قائمة تشغيل خاصة أو قائمة تشغيل \"مختلطة لك\"\n\n{{error}}",
"preparing-file": "يتم تجهيز الملف…",
"saving": "يتم الحفظ…",
"trying-to-get-playlist-id": "محاولة الحصول على معرف قائمة التشغيل: {{playlistId}}",
"video-id-not-found": "لم يتم ايجاد الفيديو",
"writing-id3": "كتابة علامات ID3…"
}
},
"description": "يقوم بتنزيل ملفات MP3/مصدر الصوت مباشرة من الواجهة",
"menu": {
"choose-download-folder": "اختر مكان التحميل",
"download-finish-settings": {
"label": "تحميل عند الانتهاء",
"prompt": {
"last-percent": "بعد ( عدد مجهول ) بالمئة",
"last-seconds": "آخر (x) ثانية",
"title": "تكوين وقت التحميل"
},
"submenu": {
"advanced": "متقدم",
"enabled": "مفعل",
"mode": "وضع الوقت",
"percent": "النسبة",
"seconds": "ثواني"
}
},
"download-playlist": "تحميل قائمة التشغيل",
"presets": "الإعدادات المسبقة",
"skip-existing": "تخطي الملفات الموجودة"
},
"name": "أداة التنزيل",
"renderer": {
"can-not-update-progress": "لا يمكن تحديث التقدم"
},
"templates": {
"button": "تحميل"
}
},
"equalizer": {
"description": "يضيف معادل صوتي للمشغل",
"menu": {
"presets": {
"label": "إعدادات مسبقة",
"list": {
"bass-booster": "مزود البيس"
}
}
},
"name": "معادل صوتي"
},
"exponential-volume": {
"description": "يجعل شريط تمرير مستوى الصوت أسيًا بحيث يسهل تحديد مستويات الصوت الأقل.",
"name": "الصوت الأسي"
},
"in-app-menu": {
"description": "يعطي أشرطة القوائم مظهرًا أنيقًا و داكنًا أو بلون الألبوم",
"menu": {
"hide-dom-window-controls": "إخفاء عناصر التحكم في نافذة DOM"
},
"name": "قائمة داخل التطبيق"
},
"lumiastream": {
"description": "يضيف دعم Lumia Stream",
"name": "Lumia Stream [بيتا]"
},
"lyrics-genius": {
"description": "يضيف دعم الكلمات لمعظم الأغاني",
"menu": {
"romanized-lyrics": "كلمات مكتوبة بحروف رومانية"
},
"name": "كلمات الأغاني من Genius",
"renderer": {
"fetched-lyrics": "تم جلب الكلمات من Genius"
}
},
"music-together": {
"description": "مشاركة قائمة تشغيل مع الآخرين. عندما يقوم المضيف بتشغيل أغنية، سيسمع الجميع نفس الأغنية",
"dialog": {
"enter-host": "أدخل معرف المضيف"
},
"internal": {
"save": "حفظ",
"track-source": "مصدر الاغنية",
"unknown-user": "مستخدم مجهول"
},
"menu": {
"click-to-copy-id": "نسخ معرف المستضيف",
"close": "إغلاق \"الموسيقى معًا\"",
"connected-users": "المستخدمون المتصلون",
"disconnect": "قطع اتصال من \"الموسيقى معًا\"",
"empty-user": "لا يوجد مستعملون متصلون",
"host": "مضيف \"الموسيقى معًا\"",
"join": "الانضمام إلى \"الموسيقى معا\"",
"permission": {
"all": "السماح للضيوف بالتحكم في قائمة التشغيل والمشغل",
"host-only": "فقط المضيف يستطيع التحكم بالقائمة و المشغل",
"playlist": "السماح للضيوف بالتحكم بقائمة التشغيل"
},
"set-permission": "تغيير إذن التحكم",
"status": {
"disconnected": "قطع الاتصال",
"guest": "متصل كضيف",
"host": "متصل كمضيف"
}
},
"name": "الموسيقى معا [بيتا]",
"toast": {
"add-song-failed": "فشل في إضافة أغنية",
"closed": "تم إغلاق \"الموسيقى معا\"",
"disconnected": "تم قطع اتصال \"الموسيقى معًا\"",
"host-failed": "فشل في استضافة \"الموسيقى معا\"",
"id-copied": "تم نسخ معرف المضيف",
"id-copy-failed": "لم يتم نسخ معرف المضيف",
"join-failed": "فشل الانضمام إلى \"الموسيقى معا\"",
"joined": "تم الانضمام إلى \"الموسيقى معا\"",
"permission-changed": "تم تغيير إذن \"الموسيقى معًا\" إلى \"{{permission}}\"",
"remove-song-failed": "فشل في إزالة الأغنية",
"user-connected": "{{name}} انضم إلى \"الموسيقى معًا\"",
"user-disconnected": "{{name}} غادر \"الموسيقى معًا\""
}
},
"navigation": {
"description": "أسهم التنقل \"التالي/السابق\" مدمجة مباشرة في الواجهة، كما في متصفحك",
"name": "التنقل"
},
"no-google-login": {
"description": "إزالة أزرار وروابط تسجيل الدخول بجوجل من الواجهة",
"name": "لا يوجد تسجيل دخول بجوجل"
},
"notifications": {
"description": "عرض إشعار عندما تبدأ الأغنية بالتشغيل (الإشعارات التفاعلية متوفرة على ويندوز)",
"menu": {
"interactive": "إشعارات تفاعلية",
"interactive-settings": {
"label": "إعدادات تفاعلية",
"submenu": {
"hide-button-text": "إخفاء زر النص",
"refresh-on-play-pause": "تحديث عند التشغيل/الإيقاف المؤقت",
"tray-controls": "فتح/إغلاق عند النقر على علامة الشريط"
}
},
"priority": "أولوية الإشعار",
"toast-style": "تنسيق التوست",
"unpause-notification": "إظهار إشعار عند استئناف التشغيل"
},
"name": "الإشعارات"
},
"picture-in-picture": {
"description": "يسمح بتحويل التطبيق إلى وضع الصورة داخل الصورة",
"menu": {
"always-on-top": "دائمًا في الأعلى",
"hotkey": {
"label": "مفتاح اختصار",
"prompt": {
"keybind-options": {
"hotkey": "مفتاح اختصار"
},
"label": "اختر مفتاح اختصار لتبديل وضع الصورة داخل الصورة",
"title": "مفتاح اختصار الصورة داخل الصورة"
}
},
"save-window-position": "حفظ موقع النافذة",
"save-window-size": "حفظ حجم النافذة",
"use-native-pip": "استخدام وضع الصورة داخل الصورة الأصلي للمتصفح"
},
"name": "الصورة داخل الصورة",
"templates": {
"button": "وضع الصورة داخل الصورة"
}
},
"playback-speed": {
"description": "استمع بسرعة، استمع ببطء! يضيف شريط تمرير يتحكم في سرعة الأغنية",
"name": "سرعة التشغيل",
"templates": {
"button": "السرعة"
}
},
"precise-volume": {
"description": "التحكم في مستوى الصوت بدقة باستخدام عجلة الفأرة/مفاتيح الاختصار، مع واجهة مستخدم مخصصة وقابلة للتخصيص وخطوات صوتية قابلة للتعديل",
"menu": {
"arrows-shortcuts": "عناصر التحكم بأسهم المفاتيح",
"custom-volume-steps": "تعيين خطوات صوتية خاصة",
"global-shortcuts": "مفاتيح اختصار عام"
},
"name": "مستوى صوت دقيق",
"prompt": {
"global-shortcuts": {
"keybind-options": {
"decrease": "تقليل مستوى الصوت",
"increase": "زيادة مستوى الصوت"
},
"label": "اختر اختصارات لوحة المفاتيح للتحكم بمستوى الصوت:",
"title": "اختصارات لوحة المفاتيح للتحكم بمستوى الصوت"
},
"volume-steps": {
"label": "اختر خطوات زيادة/تقليل مستوى الصوت",
"title": "خطوات زيادة الصوت"
}
}
},
"quality-changer": {
"backend": {
"dialog": {
"quality-changer": {
"detail": "الجودة الحالية: {{quality}}",
"message": "اختر جودة الفيديو:",
"title": "اختر جودة الفيديو"
}
}
},
"description": "يسمح بتغيير جودة الفيديو باستخدام زر على صورة الفيديو",
"name": "مغير جودة الفيديو"
},
"scrobbler": {
"description": "إضافة دعم Scrobbling (مثل Last.fm، ListenBrainz)",
"dialog": {
"lastfm": {
"auth-failed": {
"message": "فشل المصادقة مع Last.fm\nإخفاء النافذة المنبثقة حتى إعادة التشغيل التالية.",
"title": "فشلت المصادقة"
}
"video-id-not-found": "لم يتم ايجاد الفيديو"
}
},
"menu": {
"lastfm": {
"api-settings": "إعدادات Last.fm API"
},
"listenbrainz": {
"token": "أدخل رمز مستخدم ListenBrainz"
},
"scrobble-other-media": "Scrobble الوسائط الأخرى"
},
"name": "أداة تتبع الاستماع",
"prompt": {
"lastfm": {
"api-key": "مفتاح Last.fm API",
"api-secret": "الرمز السري لـ Last.fm API"
},
"listenbrainz": {
"token": {
"label": "أدخل رمز مستخدم ListenBrainz الخاص بك:",
"title": "رمز ListenBrainz"
}
}
"choose-download-folder": "اختر مكان التحميل"
}
},
"shortcuts": {
"description": "يسمح بضبط اختصارات لوحة المفاتيح العالمية للتحكم في التشغيل (تشغيل/إيقاف مؤقت/التالي/السابق) وإيقاف تشغيل OSD الوسائط عن طريق تجاوز مفاتيح الوسائط، وتشغيل Ctrl/CMD + F للبحث، وتفعيل دعم Linux MPRIS لمفاتيح الوسائط، واختصارات مخصصة للمستخدمين المتقدمين",
"menu": {
"override-media-keys": "تجاوز مفاتيح الوسائط",
"set-keybinds": "تعيين عناصر التحكم بالأغاني"
},
"name": "الاختصارات (& MPRIS)",
"prompt": {
"keybind": {
"keybind-options": {
"next": "التالي",
"play-pause": "تشغيل/ إيقاف",
"previous": "السابق"
},
"label": "اختر اختصارات لوحة المفاتيح للتحكم في الأغاني:",
"title": "اختصارات لوحة المفاتيح العالمية"
}
}
},
"skip-disliked-songs": {
"description": "تخطي الأغاني غير المرغوب فيها",
"name": "تخطي الأغاني الغير مرغوب فيها"
},
"skip-silences": {
"description": "تخطي أقسام الصمت تلقائيًا في الأغاني",
"name": "تخطي فترات الصمت"
},
"sponsorblock": {
"description": "تخطي تلقائيًا الأجزاء غير الموسيقية مثل المقدمة/الختام أو أجزاء مقاطع الفيديو الموسيقية حيث لا يتم تشغيل الأغنية",
"name": "SponsorBlock"
},
"synced-lyrics": {
"description": "يوفر كلمات الأغاني المتزامنة باستخدام مزودين مثل LRClib.",
"errors": {
"fetch": "⚠️ حدث خطأ أثناء جلب كلمات الأغنية.\nيرجى المحاولة مرة أخرى لاحقًا.",
"not-found": "⚠️ لم يتم العثور على كلمات لهذه الأغنية."
},
"menu": {
"default-text-string": {
"label": "المسافة الافتراضي بين كلمات الأغاني",
"tooltip": "اختر الحرف الافتراضي لاستخدامه في الفجوة بين كلمات الأغنية"
},
"line-effect": {
"label": "تأثير الخط",
"submenu": {
"fancy": {
"label": "فاخر",
"tooltip": "استخدم تأثيرات كبيرة تشبه التطبيقات على السطر الحالي"
},
"focus": {
"label": "تركيز",
"tooltip": "اجعل السطر الحالي فقط باللون الأبيض"
},
"offset": {
"label": "مزاح",
"tooltip": "مزاح الى يمين السطر الحالي"
},
"scale": {
"label": "تحجيم",
"tooltip": "تكبير السطر الحالي"
}
},
"tooltip": "اختر التأثير لتطبيقه على السطر الحالي"
},
"precise-timing": {
"label": "اجعل كلمات الأغنية متزامنة بشكل مثالي",
"tooltip": "احسب بدقة الملي ثانية عرض السطر التالي (قد يكون له تأثير طفيف على الأداء)"
},
"show-lyrics-even-if-inexact": {
"label": "أظهر كلمات الأغنية حتى لو كانت غير دقيقة",
"tooltip": "إذا لم يتم العثور على الأغنية، سوف يتم البحث مرة أخرى باستخدام استعلام بحث مختلف.\nقد لا تكون النتيجة من المحاولة الثانية دقيقة."
},
"show-time-codes": {
"label": "أظهر الرموز الزمنية",
"tooltip": "أظهر الرموز الزمنية بجانب كلمات الأغنية"
}
},
"name": "كلمات متزامنة",
"refetch-btn": {
"fetching": "جارٍ الجلب...",
"normal": "إعادة جلب كلمات الأغنية"
},
"warnings": {
"duration-mismatch": "⚠️ - قد تكون الكلمات غير متزامنة بسبب عدم تطابق المدة.",
"inexact": "⚠️ - قد لا تكون كلمات هذه الأغنية دقيقة",
"instrumental": "⚠️ - هذه أغنية آلية (بدون كلمات)"
}
},
"taskbar-mediacontrol": {
"description": "التحكم في المشغل من شريط المهام ويندوز",
"name": "التحكم بالوسائط من شريط المهام"
},
"touchbar": {
"description": "يضيف أداة TouchBar لمستخدمي macOS",
"name": "شريط اللمس (TouchBar)"
},
"tuna-obs": {
"description": "التكامل مع الإضافة\" Tuna\" الخاصة بـ OBS",
"name": "إضافة Tuna OBS"
},
"video-toggle": {
"description": "يضيف زرًا للتبديل بين وضع الفيديو/الأغنية. يمكن أيضًا اختياريًا إزالة علامة الفيديو بالكامل",
"menu": {
"align": {
"label": "المحاذاة",
"submenu": {
"left": "يسار",
"middle": "المنتصف",
"right": "يمين"
}
},
"force-hide": "إزالة علامة تبويب الفيديو",
"mode": {
"label": "وضع",
"submenu": {
"custom": "تبديل مخصص",
"disabled": "غير مفعل",
"native": "تبديل طبيعي"
}
}
},
"name": "تفعيل الفيديو",
"templates": {
"button": "أغنية"
}
},
"visualizer": {
"description": "يضيف معاينًا بصريًا للمشغل",
"menu": {
"visualizer-type": "نوع المعاينة المصرية"
},
"name": "معاين بصري"
}
}
}

View File

@ -158,14 +158,6 @@
},
"remove-upgrade-button": "Премахване на \"Ъпгрейд\" бутона",
"theme": {
"dialog": {
"button": {
"cancel": "Откажи",
"remove": "Премахни"
},
"remove-theme": "Сигурни ли сте, че искате да премахнете персонализираната тема?",
"remove-theme-message": "Това ще премахне персонализираната тема"
},
"label": "Тема",
"submenu": {
"import-css-file": "Импортиране на потребителски CSS файл",
@ -180,26 +172,7 @@
"enabled": "Активирани",
"label": "Плъгини",
"new": "НОВО"
},
"view": {
"label": "Изглед",
"submenu": {
"force-reload": "Презареди принудително",
"reload": "Презареди",
"reset-zoom": "Истински размер",
"toggle-fullscreen": "Превключи на пълен екран",
"zoom-in": "Увеличи",
"zoom-out": "Намали"
}
}
},
"tray": {
"next": "Следващ",
"play-pause": "Пусни/Паузирай",
"previous": "Предишен",
"quit": "Изход",
"restart": "Рестартирай приложението",
"show": "Покажи прозорец"
}
}
}

View File

@ -279,9 +279,6 @@
},
"name": "Ambientní režim"
},
"amuse": {
"description": "Přídá YouTube Music podporu pro Amuse právě těď hraje widget od 6k Labs"
},
"api-server": {
"description": "Vlož API server abys mohl ovládat přehrávač",
"dialog": {

View File

@ -1,315 +0,0 @@
{
"common": {
"console": {
"plugins": {
"execute-failed": "Fejl ved udføring af plugin {{pluginName}}::{{contextName}}",
"executed-at-ms": "Plugin {{pluginName}}::{{contextName}} udført på {{ms}}ms",
"initialize-failed": "Fejl ved igangsætning af plugin \"{{pluginName}}\"",
"load-all": "Indlæser alle plugins",
"load-failed": "Fejl ved indlæsning af plugin \"{{pluginName}}\"",
"loaded": "Plugin \"{{pluginName}}\" indlæst",
"unload-failed": "Fejl ved unload af plugin \"{{pluginName}}\""
}
}
},
"language": {
"code": "dk",
"local-name": "Dansk",
"name": "Danish"
},
"main": {
"console": {
"did-finish-load": {
"dev-tools": "Indlæsning færdig. DevTools åbnet"
},
"i18n": {
"loaded": "i18n indlæst"
},
"second-instance": {
"receive-command": "Modtog kommando over protokol: \"{{command}}\""
},
"theme": {
"css-file-not-found": "CSS fil \"{{cssFile}}\" eksisterer ikke, ignorere"
},
"unresponsive": {
"details": "Uresponsiv fejl!\n{{error}}"
},
"when-ready": {
"clearing-cache-after-20s": "Rydder op i appens cache"
},
"window": {
"tried-to-render-offscreen": "Windows forsøgte at indlæse uden for skærmen, windowSize={{windowSize}}, displaySize={{displaySize}}, position={{position}}"
}
},
"dialog": {
"hide-menu-enabled": {
"detail": "Menuen er gemt, brug 'Alt' knappen for at vise den igen (eller 'Escape' hvis In-App menuen bruges)",
"message": "Skjul menuen er aktiveret",
"title": "Skjult menu aktiveret"
},
"need-to-restart": {
"buttons": {
"later": "Senere",
"restart-now": "Genstart nu"
},
"detail": "\"{{pluginName}}\" plugin kræver en genstart for at have en effekt",
"message": "\"{{pluginName}}\" skal genstarte",
"title": "Genstart krævet"
},
"unresponsive": {
"buttons": {
"quit": "Afslut",
"relaunch": "Genåben",
"wait": "Vent"
},
"detail": "Vi undskylder for ubelejligheden! Vælg næste handling:",
"message": "Appen svarer ikke",
"title": "Vindue svarer ikke"
},
"update-available": {
"buttons": {
"disable": "Slå opdateringer fra",
"download": "Hent",
"ok": "OK"
},
"detail": "En ny version er tilgængelig og kan downloades her: {{downloadLink}}",
"message": "En ny version er tilgængelig",
"title": "Opdatering tilgængelig"
}
},
"menu": {
"about": "Om",
"navigation": {
"label": "Navigering",
"submenu": {
"copy-current-url": "Kopier nuværende URL",
"go-back": "Tilbage",
"go-forward": "Frem",
"quit": "Afslut",
"restart": "Genstart Appen"
}
},
"options": {
"label": "Indstillinger",
"submenu": {
"advanced-options": {
"label": "Avancerede indstillinger",
"submenu": {
"auto-reset-app-cache": "Nulstil app cache når appen starter",
"disable-hardware-acceleration": "Deaktiver hardware acceleration",
"edit-config-json": "Rediger config.json",
"restart-on-config-changes": "Genstart ved config ændringer",
"set-proxy": {
"label": "Indstil proxy",
"prompt": {
"label": "Skriv proxy adresse: (Efterlad tom for at deaktivere)",
"placeholder": "Eksempel: SOCKS5://127.0.0.1:9999",
"title": "Sæt proxy"
}
},
"toggle-dev-tools": "Skift DevTools"
}
},
"always-on-top": "Altid øverst",
"auto-update": "Automatisk opdatering",
"hide-menu": {
"dialog": {
"message": "Menuen vil være lukket næste gang appen starter. Brug [Alt] for at vise den (Eller backtick [`] hvis in-app-menu bruges)",
"title": "Gemt menu aktiveret"
},
"label": "Skjul menu"
},
"language": {
"dialog": {
"message": "Sproget vil blive ændret efter genstart",
"title": "Sprog ændret"
},
"label": "Sprog",
"submenu": {
"to-help-translate": "Vil du hjælpe med at oversætte? Klik her"
}
},
"resume-on-start": "Genoptag sidste sang når appen starter",
"start-at-login": "Start ved login",
"starting-page": {
"label": "Startside",
"unset": "Ikke valgt"
},
"tray": {
"submenu": {
"disabled": "Deaktiveret",
"enabled-and-show-app": "Aktiver og vis app",
"play-pause-on-click": "Start/Stop ved klik"
}
},
"visual-tweaks": {
"submenu": {
"like-buttons": {
"default": "Standard",
"hide": "Skjul",
"label": "Like knapper"
},
"remove-upgrade-button": "Fjern opgrader knappen",
"theme": {
"dialog": {
"button": {
"remove": "Fjern"
}
},
"label": "Tema",
"submenu": {
"no-theme": "Intet tema"
}
}
}
}
}
},
"plugins": {
"enabled": "Aktiveret",
"label": "Plugins",
"new": "NY"
},
"view": {
"label": "Vis",
"submenu": {
"reload": "Genindlæs",
"zoom-in": "Zoom ind",
"zoom-out": "Zoom ud"
}
}
},
"tray": {
"next": "Næste",
"play-pause": "Afspil",
"previous": "Sidste",
"quit": "Luk",
"restart": "Genstart app",
"show": "Vis vindue",
"tooltip": {
"default": "YouTube Music",
"with-song-info": "YouTube Music: {{artist}} - {{title}}"
}
}
},
"plugins": {
"ad-speedup": {
"description": "Hvis en reklame afspilles, slår den lyden fra og sætter hastigheden til 16x",
"name": "Spol igennem reklamen"
},
"adblocker": {
"description": "Bloker alle reklamer og sporing fra starten af",
"menu": {
"blocker": "Bloker"
},
"name": "Bloker reklamer"
},
"album-color-theme": {
"menu": {
"color-mix-ratio": {
"submenu": {
"percent": "{{ratio}}%"
}
}
},
"name": "Albummets farve tema"
},
"ambient-mode": {
"menu": {
"blur-amount": {
"label": "Sløringsmængde",
"submenu": {
"pixels": "{{blurAmount}} pixel"
}
},
"buffer": {
"label": "Buffer",
"submenu": {
"buffer": "{{buffer}}"
}
},
"opacity": {
"label": "Gennemsigtighed",
"submenu": {
"percent": "{{opacity}}%"
}
},
"quality": {
"label": "Kvalitet",
"submenu": {
"pixels": "{{quality}} pixel"
}
},
"size": {
"label": "Størrelse",
"submenu": {
"percent": "{{size}}%"
}
},
"use-fullscreen": {
"label": "Bruger fuldskærm"
}
}
},
"api-server": {
"dialog": {
"request": {
"buttons": {
"allow": "Tillad",
"deny": "Afvis"
},
"message": "Tillad at {{ID}} ({{origin}}) får adgang til API'en?"
}
},
"menu": {
"auth-strategy": {
"label": "Godkendelsesstrategi"
},
"hostname": {
"label": "Hostname"
},
"port": {
"label": "Port"
}
},
"name": "API Server [Beta]",
"prompt": {
"hostname": {
"label": "Skriv API serverens hostname (f. eks. 0.0.0.0):",
"title": "Hostname"
},
"port": {
"label": "Skriv API serverens port:",
"title": "Port"
}
}
},
"audio-compressor": {
"name": "Lyd kompressor"
},
"blur-nav-bar": {
"description": "Gør navigationsbaren gennemsigtig og sløret",
"name": "Slør navigationsbar"
},
"captions-selector": {
"menu": {
"disable-captions": "Ingen undertekster som standard"
},
"name": "Vælg undertekster",
"prompt": {
"selector": {
"label": "Nuværende sprog på undertekster: {{language}}",
"none": "Ingen",
"title": "Vælg underteksternes sprog"
}
}
},
"crossfade": {
"description": "Fade imellem sange",
"menu": {
"advanced": "Avanceret"
},
"name": "Fade [Beta]"
}
}
}

View File

@ -683,7 +683,6 @@
"listenbrainz": {
"token": "ListenBrainz-Benutzer-Token eintragen"
},
"scrobble-alternative-title": "Nutze alternative Titel",
"scrobble-other-media": "Andere Medien scrobbeln"
},
"name": "Scrobbler",

View File

@ -671,8 +671,7 @@
"listenbrainz": {
"token": "Enter ListenBrainz user token"
},
"scrobble-other-media": "Scrobble other media",
"scrobble-alternative-title": "Use alternative titles"
"scrobble-other-media": "Scrobble other media"
},
"name": "Scrobbler",
"prompt": {
@ -756,10 +755,6 @@
"label": "Make the lyrics perfectly synced",
"tooltip": "Calculate to the milisecond the display of the next line (can have a small impact on performance)"
},
"romanization": {
"label": "Romanize lyrics",
"tooltip": "If the lyrics are in a different language, try to display a latin version."
},
"show-lyrics-even-if-inexact": {
"label": "Show lyrics even if inexact",
"tooltip": "If the song is not found, the plugin tries again with a different search query.\nThe result from the second attempt may not be exact."
@ -792,10 +787,6 @@
"description": "Integration with OBS's plugin Tuna",
"name": "Tuna OBS"
},
"unobtrusive-player": {
"description": "Prevents the player from popping up when playing a song",
"name": "Unobtrusive Player"
},
"video-toggle": {
"description": "Adds a button to switch between Video/Song mode. can also optionally remove the whole video tab",
"menu": {

View File

@ -4,7 +4,7 @@
"plugins": {
"execute-failed": "Error al ejecutar el plugin {{pluginName}}::{{contextName}}",
"executed-at-ms": "Plugin {{pluginName}}: {{contextName}} ejecutado en {{ms}}ms",
"initialize-failed": "Error al inicializar el plugin \"{{pluginName}}\"",
"initialize-failed": "Error al inicializar plugin \"{{pluginName}}\"",
"load-all": "Cargando todos los plugins",
"load-failed": "Error al cargar el plugin \"{{pluginName}}\"",
"loaded": "Plugin \"{{pluginName}}\" cargado",
@ -21,7 +21,7 @@
"main": {
"console": {
"did-finish-load": {
"dev-tools": "Carga finalizada. DevTools abierto"
"dev-tools": "Carga finalizada. DevTools abiertos"
},
"i18n": {
"loaded": "i18n cargado"
@ -36,7 +36,7 @@
"details": "¡Error sin repuesta!\n{{error}}"
},
"when-ready": {
"clearing-cache-after-20s": "Borrando caché de la aplicación"
"clearing-cache-after-20s": "Borrar caché de la aplicación"
},
"window": {
"tried-to-render-offscreen": "La ventana intentó mostrarse fuera de la pantalla, windowSize={{windowSize}}, displaySize={{displaySize}}, posicion={{position}}"
@ -45,21 +45,21 @@
"dialog": {
"hide-menu-enabled": {
"detail": "El menú está oculto, utiliza \"Alt\" para mostrarlo (o \"Escape\" si utilizas el menú integrado en la aplicación)",
"message": "Menú Oculto está habilitado",
"title": "Menú oculto habilitado"
"message": "Menu oculto esta deshabilitado",
"title": "Menú oculto activado"
},
"need-to-restart": {
"buttons": {
"later": "Más tarde",
"restart-now": "Reiniciar ahora"
},
"detail": "El plugin \"{{pluginName}}\" requiere reiniciar para tomar efecto",
"detail": "\"{{pluginName}}\" se requiere reiniciar para que el plugin tome efecto",
"message": "\"{{pluginName}}\" necesita reiniciar",
"title": "Se requiere reinicio"
},
"unresponsive": {
"buttons": {
"quit": "Salir",
"quit": "Dejar",
"relaunch": "Volver a abrir",
"wait": "Espera"
},
@ -74,7 +74,7 @@
"ok": "OK"
},
"detail": "Una nueva versión está disponible y puede descargarse en {{downloadLink}}",
"message": "Hay una nueva versión disponible",
"message": "Ya está disponible una nueva versión",
"title": "Actualización disponible"
}
},
@ -84,7 +84,7 @@
"label": "Navegación",
"submenu": {
"copy-current-url": "Copiar la URL actual",
"go-back": "Atrás",
"go-back": "Regresar",
"go-forward": "Adelante",
"quit": "Salir",
"restart": "Reiniciar la aplicación"
@ -99,10 +99,10 @@
"auto-reset-app-cache": "Restablecer la caché de la aplicación al iniciarla",
"disable-hardware-acceleration": "Desactivar la aceleración por hardware",
"edit-config-json": "Editar config.json",
"override-user-agent": "Sobrescribir User-Agent",
"restart-on-config-changes": "Reiniciar al modificar la configuración",
"override-user-agent": "sobrescribir User-Agent",
"restart-on-config-changes": "Reinicie al cambiar la configuración",
"set-proxy": {
"label": "Establecer proxy",
"label": "Definir proxy",
"prompt": {
"label": "Introduzca la dirección del proxy: (déjela vacía para desactivarla)",
"placeholder": "Ejemplo: SOCKS5://127.0.0.1:9999",
@ -112,28 +112,28 @@
"toggle-dev-tools": "Activar DevTools"
}
},
"always-on-top": "Siempre al frente",
"always-on-top": "Siempre arriba",
"auto-update": "Actualización automática",
"hide-menu": {
"dialog": {
"message": "El menú se ocultará la próxima vez que inicies la aplicación, usa [Alt] para mostrarlo (o pulsa [`] si usas el menú dentro de la aplicación)",
"title": "Menú oculto habilitado"
"message": "El menú se ocultará la próxima vez que lo inicies, usa [Alt] para mostrarlo (o pulsa [`] si usas el menú dentro de la aplicación)",
"title": "Ocultar menú habilitado"
},
"label": "Ocultar menú"
},
"language": {
"dialog": {
"message": "El idioma se cambiará después de reiniciar",
"title": "Se cambió el idioma"
"title": "Se cambio el idioma"
},
"label": "Idioma",
"submenu": {
"to-help-translate": "¿Quieres ayudar a traducir? Haz clic aquí"
}
},
"resume-on-start": "Reanudar la última canción reproducida al iniciar la aplicación",
"single-instance-lock": "Limitar a una única instancia",
"start-at-login": "Iniciar al iniciar sesión",
"resume-on-start": "Reanudar la última canción al iniciar la aplicación",
"single-instance-lock": "Bloquear en una instancia unica",
"start-at-login": "Comenzar al iniciar sesión",
"starting-page": {
"label": "Página de inicio",
"unset": "Sin configurar"
@ -142,8 +142,8 @@
"label": "Bandeja",
"submenu": {
"disabled": "Desactivado",
"enabled-and-hide-app": "Habilitado y ocultar la aplicación",
"enabled-and-show-app": "Habilitado y mostrar aplicación",
"enabled-and-hide-app": "Activar y ocultar la aplicación",
"enabled-and-show-app": "Activado y mostrar aplicación",
"play-pause-on-click": "Reproducir/Pausar al hacer clic"
}
},
@ -156,7 +156,7 @@
"hide": "Ocultar",
"label": "Botones de \"Me Gusta\""
},
"remove-upgrade-button": "Eliminar el botón de Actualización",
"remove-upgrade-button": "Remover el botón de Actualización",
"theme": {
"dialog": {
"button": {
@ -212,14 +212,14 @@
"name": "Aumento de la velocidad de anuncios"
},
"adblocker": {
"description": "Bloquear todos los anuncios y el rastreo por defecto",
"description": "Bloquear todos los anuncios y el rastreo",
"menu": {
"blocker": "Bloqueador"
},
"name": "Bloqueador de anuncios"
},
"album-actions": {
"description": "Añade los botones \"Quitar no me gusta\", \"No me gusta\", \"Me gusta\" y \"Quitar me gusta\" para aplicarlos a todas las canciones de una lista de reproducción o un álbum",
"description": "Añade los botones \"No me gusta\", \"No me gusta\", \"Me gusta\" y \"No me gusta\" para aplicarlos a todas las canciones de una lista de reproducción o un álbum",
"name": "Acciones en el álbum"
},
"album-color-theme": {
@ -232,15 +232,15 @@
}
}
},
"name": "Tema de color del álbum"
"name": "Color del álbum"
},
"ambient-mode": {
"description": "Aplica un efecto de iluminación mediante la proyección de colores suaves extraídos del vídeo sobre el fondo de pantalla",
"description": "Aplica un efecto de iluminación mediante la proyección de colores suaves del vídeo en el fondo de la pantalla",
"menu": {
"blur-amount": {
"label": "Cantidad de desenfoque",
"submenu": {
"pixels": "{{blurAmount}} píxeles"
"pixels": "{{blurAmount}} pixeles"
}
},
"buffer": {
@ -250,7 +250,7 @@
}
},
"opacity": {
"label": "Opacidad",
"label": "Transparencia",
"submenu": {
"percent": "{{opacity}}%"
}
@ -280,7 +280,7 @@
"name": "Modo ambiente"
},
"amuse": {
"description": "Agrega soporte a YouTube Music para el widget \"reproduciendo\" de Amuse por 6K Labs",
"description": "Agrega soporte de YouTube Music para el widget Amuse de reproduciendo ahora de 6K Labs",
"name": "Amuse",
"response": {
"query": "El servidor API de Amuse se está ejecutando. Usa GET /query para obtener información de la canción."
@ -294,7 +294,7 @@
"allow": "Permitir",
"deny": "Denegar"
},
"message": "¿Permitir que {{ID}} ({{origin}}) acceda a la API?",
"message": "¿Permitir {{ID}} ({{origin}}) acceder a la API?",
"title": "Petición de autorización API"
}
},
@ -303,7 +303,7 @@
"label": "Estrategia de autorización",
"submenu": {
"auth-at-first": {
"label": "Autorizar a la primera solicitud"
"label": "Autorizar la primera solicitud"
},
"none": {
"label": "Sin autorización"
@ -334,12 +334,12 @@
"name": "Compresor de audio"
},
"blur-nav-bar": {
"description": "Hace que la barra de navegación se vea transparente y borrosa",
"description": "Hace que la barra de navegación sea transparente y borrosa",
"name": "Desenfocar barra de navegación"
},
"bypass-age-restrictions": {
"description": "Saltarse la verificación de edad de YouTube",
"name": "Saltarse las restricciones de edad"
"description": "Saltar la verificación de edad de YouTube",
"name": "Saltar las restricciones de edad"
},
"captions-selector": {
"description": "Selector de subtítulos para pistas de audio de YouTube Music",
@ -350,17 +350,17 @@
"name": "Selector de subtítulos",
"prompt": {
"selector": {
"label": "Idioma actual de los subtítulos: {{language}}",
"label": "Idioma actual: {{language}}",
"none": "Ninguno",
"title": "Seleccionar idioma de los subtítulos"
}
},
"templates": {
"title": "Abrir el selector de subtítulos"
"title": "Abra el selector de subtítulos"
}
},
"compact-sidebar": {
"description": "Establecer siempre la barra lateral en modo compacto",
"description": "Poner siempre la barra lateral en modo compacto",
"name": "Barra lateral compacta"
},
"crossfade": {
@ -372,16 +372,16 @@
"prompt": {
"options": {
"multi-input": {
"fade-in-duration": "Duración del fundido de entrada (ms)",
"fade-in-duration": "Duración del fundido (ms)",
"fade-out-duration": "Duración del fundido de salida (ms)",
"fade-scaling": {
"label": "Escala del fundido",
"label": "Escala de fundido",
"linear": "Lineal",
"logarithmic": "Logarítmico"
},
"seconds-before-end": "Activar Crossfade N segundos antes del final"
"seconds-before-end": "Crossfade N segundos antes del final"
},
"title": "Opciones de Crossfade"
"title": "Opciones de crossfade"
}
}
},
@ -410,7 +410,7 @@
"play-on-youtube-music": "Reproducir en YouTube Music",
"set-inactivity-timeout": "Establecer tiempo de inactividad"
},
"name": "Discord Rich Presence",
"name": "Estado de actividad de Discord",
"prompt": {
"set-inactivity-timeout": {
"label": "Introduzca el tiempo de inactividad en segundos:",
@ -433,7 +433,7 @@
"ok": "OK"
},
"detail": "({{playlistSize}} canciones)",
"message": "Descargando Playlist {{playlistTitle}}",
"message": "Descargar Playlist {{playlistTitle}}",
"title": "Descarga iniciada"
}
},
@ -445,45 +445,45 @@
"download-progress": "Descarga: {{percent}}%",
"downloading": "Descargando…",
"downloading-counter": "Descargando {{current}}/{{total}}…",
"downloading-playlist": "Descargando lista de reproducción \"{{playlistTitle}}\" - {{playlistSize}} canciones ({{playlistId}})",
"downloading-playlist": "Descargar lista de reproducción \"{{playlistTitle}}\" - {{playlistSize}} canciones ({{playlistId}})",
"error-while-downloading": "Error al descargar \"{{author}} - {{title}}\": {{error}}",
"folder-already-exists": "La carpeta {{playlistFolder}} ya existe",
"getting-playlist-info": "Obteniendo información de la lista de reproducción…",
"loading": "Cargando…",
"playlist-has-only-one-song": "La lista de reproducción sólo tiene un elemento, descargándolo directamente",
"playlist-id-not-found": "No se ha encontrado la ID de la lista de reproducción",
"playlist-has-only-one-song": "La lista de reproducción sólo tiene un elemento, descárgala directamente",
"playlist-id-not-found": "No se ha encontrado el ID de la lista de reproducción",
"playlist-is-empty": "La lista de reproducción está vacía",
"playlist-is-mix-or-private": "Error obteniendo la información de la lista de reproducción: asegúrese de que no es una lista privada o \"Mixed for you\"\n\n{{error}}",
"playlist-is-mix-or-private": "Error obteniendo información de la lista de reproducción: asegúrese de que no es una lista privada o \"Mixed for you\"\n\n{{error}}",
"preparing-file": "Preparando archivo…",
"saving": "Guardando…",
"trying-to-get-playlist-id": "Intentando obtener la ID de la lista de reproducción: {{playlistId}}",
"trying-to-get-playlist-id": "Intentando obtener el ID de la lista de reproducción: {{playlistId}}",
"video-id-not-found": "Video no encontrado",
"writing-id3": "Escribiendo las etiquetas ID3…"
}
},
"description": "Descarga audio MP3 / fuente directamente desde la interfaz",
"description": "Descarga MP3 / audio fuente directamente desde la interfaz",
"menu": {
"choose-download-folder": "Elija la carpeta de descarga",
"download-finish-settings": {
"label": "Descargar al finalizar",
"prompt": {
"last-percent": "Después de x porcentaje",
"last-percent": "Después del x por ciento",
"last-seconds": "Últimos x segundos",
"title": "Configurar cuándo descargar"
},
"submenu": {
"advanced": "Avanzado",
"enabled": "Habilitado",
"enabled": "Activado",
"mode": "Modo de tiempo",
"percent": "Porcentaje",
"seconds": "Segundos"
}
},
"download-playlist": "Descargar lista de reproducción",
"presets": "Ajustes preestablecidos",
"presets": "Preajustes",
"skip-existing": "Saltar archivos existentes"
},
"name": "Gestor de descargas",
"name": "Descargador",
"renderer": {
"can-not-update-progress": "No se puede actualizar el progreso"
},
@ -504,7 +504,7 @@
"name": "Ecualizador"
},
"exponential-volume": {
"description": "Hace que la barra de volumen sea exponencial para que sea más fácil seleccionar volúmenes más bajos.",
"description": "Hace que el control deslizante de volumen sea exponencial para que sea más fácil seleccionar volúmenes más bajos.",
"name": "Volumen exponencial"
},
"in-app-menu": {
@ -519,19 +519,19 @@
"name": "Lumia Stream [Beta]"
},
"lyrics-genius": {
"description": "Añade soporte para letras para la mayoría de las canciones",
"description": "Añade el soporte para las letras para la mayoría de las canciones",
"menu": {
"romanized-lyrics": "Letras Romanizadas"
},
"name": "Letras Genius",
"renderer": {
"fetched-lyrics": "Letras obtenidas de Genius"
"fetched-lyrics": "Letras recuperadas de Genius"
}
},
"music-together": {
"description": "Comparte una lista de reproducción con los demás. Cuando el anfitrión reproduzca una canción, todos los demás escucharán la misma",
"dialog": {
"enter-host": "Introduzca la ID del host"
"enter-host": "Introduzca el ID del host"
},
"internal": {
"save": "Guardar",
@ -539,7 +539,7 @@
"unknown-user": "Usuario desconocido"
},
"menu": {
"click-to-copy-id": "Copiar la ID del host",
"click-to-copy-id": "Copiar el ID del host",
"close": "Cerrar Music Together",
"connected-users": "Usuarios conectados",
"disconnect": "Desactivar Music Together",
@ -549,7 +549,7 @@
"permission": {
"all": "Permite a los invitados controlar la lista de reproducción y el reproductor",
"host-only": "Sólo el anfitrión puede controlar la lista de reproducción y el reproductor",
"playlist": "Permitir que los invitados controlen la lista de reproducción"
"playlist": "Permita que los invitados controlen la lista de reproducción"
},
"set-permission": "Permiso de control de cambios",
"status": {
@ -562,11 +562,11 @@
"toast": {
"add-song-failed": "No se puede añadir la canción",
"closed": "Music Together cerrado",
"disconnected": "Music Together desconectado",
"host-failed": "Fallo al hostear Music Together",
"id-copied": "ID del host copiada al portapapeles",
"id-copy-failed": "No se ha podido copiar la ID del host al portapapeles",
"join-failed": "Fallo al unirse a Music Together",
"disconnected": "Music Together desconectados",
"host-failed": "Fallo el host de Music Together",
"id-copied": "ID del host copiado en el portapapeles",
"id-copy-failed": "No se ha podido copiar el ID del host en el portapapeles",
"join-failed": "Fallo en la unión a Music Together",
"joined": "Unido a Music Together",
"permission-changed": "Permiso de Music Together cambiado a \"{{permission}}\"",
"remove-song-failed": "Error al eliminar la canción",
@ -601,7 +601,7 @@
"name": "Notificaciones"
},
"picture-in-picture": {
"description": "Permite cambiar la aplicación al modo picture-in-picture",
"description": "Permite cambiar la aplicación al modo de imagen en imagen",
"menu": {
"always-on-top": "Siempre encima",
"hotkey": {
@ -610,17 +610,17 @@
"keybind-options": {
"hotkey": "Tecla de acceso rápido"
},
"label": "Elige una tecla de acceso rápido para activar la función picture-in-picture",
"title": "Tecla de acceso directo a picture-in-picture"
"label": "Elige una tecla de acceso rápido para activar la función de imagen en imagen",
"title": "Tecla de acceso directo a imagen en imagen"
}
},
"save-window-position": "Guardar la posición de la ventana",
"save-window-size": "Guardar tamaño de la ventana",
"use-native-pip": "Utilizar PiP nativo del navegador"
"use-native-pip": "Utilizar \"Dos imágenes a la vez\" PiP nativo del navegador"
},
"name": "Picture-in-picture",
"name": "Imagen en imagen",
"templates": {
"button": "Picture-in-picture"
"button": "Imagen en imagen"
}
},
"playback-speed": {
@ -631,7 +631,7 @@
}
},
"precise-volume": {
"description": "Controla el volumen de manera precisa utilizando la rueda del ratón/teclas de acceso rápido, con una interfaz personalizada y niveles de volumen personalizables",
"description": "Controla el volumen de manera precisa utilizando la rueda del ratón/teclas de acceso rápido, con una interfaz personalizada y pasos de volumen personalizables",
"menu": {
"arrows-shortcuts": "Controles de teclas de flechas locales",
"custom-volume-steps": "Establecer niveles de volumen personalizados",
@ -648,7 +648,7 @@
"title": "Combinaciones de teclas para el volumen"
},
"volume-steps": {
"label": "Escoge los niveles de aumento o disminución del volumen",
"label": "Escoge los pasos de aumento o disminución del volumen",
"title": "Niveles de volumen"
}
}
@ -700,7 +700,7 @@
}
},
"shortcuts": {
"description": "Permite configurar teclas de acceso rápido globales para la reproducción (reproducir/pausa/siguiente/anterior) y desactivar el OSD multimedia anulando las teclas multimedia, activar Ctrl/CMD + F para buscar, activar la compatibilidad con MPRIS de Linux para las teclas multimedia y teclas de acceso rápido personalizadas para usuarios avanzados",
"description": "Permite configurar teclas de acceso rápido globales para la reproducción (reproducir/pausa/siguiente/anterior) y desactivar la OSD multimedia anulando las teclas multimedia, activar Ctrl/CMD + F para buscar, activar la compatibilidad con MPRIS de Linux para las teclas multimedia y teclas de acceso rápido personalizadas para usuarios avanzados",
"menu": {
"override-media-keys": "Anular teclas de medios",
"set-keybinds": "Configurar controles globales de canciones"
@ -733,8 +733,8 @@
"synced-lyrics": {
"description": "Proporciona letras de canciones sincronizadas, utilizando proveedores como LRClib.",
"errors": {
"fetch": "⚠️\tHa ocurrido un error al obtener la letra.\n\tPor favor, inténtalo de nuevo más tarde.",
"not-found": "⚠️ No se ha encontrado ninguna letra para esta canción."
"fetch": "⚠️ - Se ha producido un error al descargar la letra. Por favor, vuelve a intentarlo más tarde.",
"not-found": "⚠️ - No se ha encontrado ninguna letra para esta canción."
},
"menu": {
"default-text-string": {
@ -758,18 +758,18 @@
},
"scale": {
"label": "Escala",
"tooltip": "Escalar la línea actual"
"tooltip": "Escala de la línea actual"
}
},
"tooltip": "Elige el efecto que deseas aplicar a la línea actual"
},
"precise-timing": {
"label": "Haz que la letra esté perfectamente sincronizada",
"tooltip": "Calcular al milisegundo la visualización de la siguiente línea (puede tener un pequeño impacto en el rendimiento)"
"tooltip": "Calcular al milisegundo la visualización de la línea siguiente (puede tener un pequeño impacto en el rendimiento)"
},
"show-lyrics-even-if-inexact": {
"label": "Mostrar la letra aunque sea inexacta",
"tooltip": "Si no se encuentra la canción, el plugin vuelve a intentarlo con una búsqueda diferente.\nEl resultado del segundo intento puede no ser exacto."
"tooltip": "Si no se encuentra la canción, el complemento vuelve a intentarlo con una búsqueda diferente.\nEl resultado del segundo intento puede no ser exacto."
},
"show-time-codes": {
"label": "Visualización del código de tiempo",
@ -778,7 +778,7 @@
},
"name": "Letras sincronizadas",
"refetch-btn": {
"fetching": "Obteniendo...",
"fetching": "Recuperando...",
"normal": "Volver a buscar letras"
},
"warnings": {
@ -789,24 +789,24 @@
},
"taskbar-mediacontrol": {
"description": "Controla la reproducción desde la barra de tareas de Windows",
"name": "Control de medios desde la barra de tareas"
"name": "Control de medios de la barra de tareas"
},
"touchbar": {
"description": "Añade un widget TouchBar para los usuarios de macOS",
"name": "TouchBar"
},
"tuna-obs": {
"description": "Integración con el plugin Tuna de OBS",
"description": "Integración con el complemento Tuna de OBS",
"name": "Tuna OBS"
},
"video-toggle": {
"description": "Añade un botón para cambiar entre el modo Vídeo/Canción. También puede eliminar opcionalmente toda la pestaña de vídeo",
"description": "Añade un botón para cambiar entre el modo Vídeo/Canción. también puede eliminar opcionalmente toda la pestaña de vídeo",
"menu": {
"align": {
"label": "Alineación",
"submenu": {
"left": "Izquierda",
"middle": "Centro",
"middle": "Medio",
"right": "Derecha"
}
},

View File

@ -272,32 +272,7 @@
"use-fullscreen": {
"label": "Käytetään koko näytön tilaa"
}
},
"name": "Tunnelmallinen Tila"
},
"api-server": {
"description": "Lisää API-palvelimen hallitsemaan soitinta",
"dialog": {
"request": {
"buttons": {
"allow": "Hyväksy",
"deny": "Kiellä"
}
}
},
"menu": {
"auth-strategy": {
"submenu": {
"none": {
"label": "Ei valtuuksia"
}
}
},
"port": {
"label": "Portti"
}
},
"name": "API Serveri [Beta]"
}
},
"audio-compressor": {
"description": "Lisää äänen kompressointia (hiljentää voimakkaimpien äänien voimakkuutta ja tehostaa pehmeämpien äänien voimakkuutta)",

View File

@ -33,7 +33,7 @@
"css-file-not-found": "Le fichier de CSS \"{{cssFile}}\" n'existe pas, ignorer"
},
"unresponsive": {
"details": "Erreur : Aucune réponse!\n{{error}}"
"details": "Erreur: ne répond pas!\n{{error}}"
},
"when-ready": {
"clearing-cache-after-20s": "Effacement du cache de l'application"
@ -44,7 +44,7 @@
},
"dialog": {
"hide-menu-enabled": {
"detail": "Le menu est masqué, utilisez 'Alt' pour l'afficher (ou 'Échap' si vous utilisez le menu de l'application)",
"detail": "Le menu est masqué, utilisez « Alt » pour l'afficher (ou « Échap » si vous utilisez le menu de l'application)",
"message": "Le masquage du menu est activé",
"title": "Masquer le menu activé"
},
@ -279,13 +279,6 @@
},
"name": "Mode ambiant"
},
"amuse": {
"description": "Ajoute la prise en charge de YouTube Music pour le widget de lecture en cours Amuse par 6K Labs",
"name": "Amuse",
"response": {
"query": "Le serveur API Amuse est en cours d'exécution. Envoyez une requête GET /query pour obtenir des informations sur la chanson."
}
},
"api-server": {
"description": "Ajouter un serveur API pour contrôler le lecteur",
"dialog": {
@ -734,7 +727,7 @@
"description": "Ajoute des paroles synchronisées aux chansons, grâce à LRClib par exemple.",
"errors": {
"fetch": "⚠️\tUne erreur s'est produite en allant chercher les paroles.\n\tMerci de réessayer plus tard.",
"not-found": "⚠️ Aucune paroles trouvées pour cette musique."
"not-found": "⚠️ - Aucune paroles trouvées pour cette musique."
},
"menu": {
"default-text-string": {
@ -744,10 +737,6 @@
"line-effect": {
"label": "Effet de ligne",
"submenu": {
"fancy": {
"label": "Raffiné",
"tooltip": "Utilise de grands effets de type application sur la ligne actuelle"
},
"focus": {
"label": "Focus",
"tooltip": "Rend seulement la ligne actuelle blanche"

View File

@ -86,8 +86,7 @@
"copy-current-url": "העתק את כתובת ה-URL",
"go-back": "חזור אחורה",
"go-forward": "לך קדימה",
"quit": "יציאה",
"restart": "הפעל מחדש את היישום"
"quit": "יציאה"
}
},
"options": {
@ -97,96 +96,10 @@
"label": "אפשרויות מתקדמות",
"submenu": {
"auto-reset-app-cache": "אפס את מטמון האפליקציה כאשר האפליקציה מתחילה",
"disable-hardware-acceleration": "השבת האצת החומרה",
"edit-config-json": "ערוך את config.json",
"override-user-agent": "עוקף את סוכן המשתמש",
"restart-on-config-changes": "הפעל מחדש בשינויי תצורה",
"set-proxy": {
"label": "הגדר שרת proxy",
"prompt": {
"label": "הזן כתובת פרוקסי: (להשאיר ריק כדי להשבית)",
"placeholder": "דוגמה: SOCKS5://127.0.0.1:9999",
"title": "הגדר שרת proxy"
}
},
"toggle-dev-tools": "שנה את מצב כלי המפתחים"
}
},
"always-on-top": "תמיד מעל הכל",
"auto-update": "עדכון אוטומטי",
"hide-menu": {
"dialog": {
"message": "התפריט יוסתר בהפעלה הבאה, השתמש ב-[Alt] כדי להציג אותו (או סמן את [`] אם אתה משתמש בתפריט בתוך האפליקציה)",
"title": "הסתר תפריט מופעל"
},
"label": "הסתר את התפריט"
},
"language": {
"dialog": {
"message": "השפה תשתנה לאחר הפעלת היישום מחדש",
"title": "השפה שונתה"
},
"label": "שפה",
"submenu": {
"to-help-translate": "רוצים לעזור לתרגם? לחץ כאן"
}
},
"resume-on-start": "המשך את השיר האחרון עם הפעלת האפליקציה",
"single-instance-lock": "נעילת מופע יחיד",
"start-at-login": "התחל בכניסה",
"starting-page": {
"label": "דף פתיחה",
"unset": "בטל"
},
"tray": {
"label": "מגש",
"submenu": {
"disabled": "מושבת",
"enabled-and-hide-app": "מופעל והסתר אפליקציה",
"enabled-and-show-app": "מופעל והמציג את האפליקציה",
"play-pause-on-click": "הפעל/השהה בלחיצה"
}
},
"visual-tweaks": {
"label": "תיקונים חזותיים",
"submenu": {
"like-buttons": {
"default": "ברירת מחדל",
"force-show": "הפעל בכוח",
"hide": "הסתר",
"label": "כפתורי לייק"
},
"remove-upgrade-button": "הסר לחצן שדרוג",
"theme": {
"dialog": {
"button": {
"cancel": "ביטול",
"remove": "הסר"
},
"remove-theme": "האם אתה בטוח שברצונך להסיר את העיצוב המותאם אישית?",
"remove-theme-message": "פעולה זו תסיר את ערכת הנושא המותאמת אישית"
},
"label": "ערכת נושא",
"submenu": {
"import-css-file": "ייבא קובץ CSS מותאם אישית",
"no-theme": "ללא ערכת נושא"
}
}
"disable-hardware-acceleration": "השבת האצת החומרה"
}
}
}
},
"plugins": {
"enabled": "מופעל",
"label": "פלאגינים",
"new": "חדש"
},
"view": {
"label": "מראה",
"submenu": {
"force-reload": "התחל מחדש בכוח",
"reload": "טען מחדש"
}
}
}
}

View File

@ -269,19 +269,6 @@
},
"smoothness-transition": {
"label": "चिकनाई संक्रमण"
},
"use-fullscreen": {
"label": "पूर्णस्क्रीन का उपयोग"
}
}
},
"api-server": {
"dialog": {
"request": {
"buttons": {
"allow": "अनुमति दें",
"deny": "मना करना"
}
}
}
},

View File

@ -2,7 +2,7 @@
"common": {
"console": {
"plugins": {
"execute-failed": "Plugin {{pluginName}}::{{contextName}} dieksekusi di {{ms}}ms",
"execute-failed": "Gagal saat mengeksekusi plugin {{pluginName}}::{{contextName}}",
"executed-at-ms": "Plugin {{pluginName}}::{{contextName}} dieksekusi pada {{ms}}ms",
"initialize-failed": "Gagal dalam menginisialisasi plugin \"{{pluginName}}\"",
"load-all": "Memuat semua plugin",
@ -683,7 +683,6 @@
"listenbrainz": {
"token": "Masukkan token pengguna ListenBrainz"
},
"scrobble-alternative-title": "Gunakan judul alternatif",
"scrobble-other-media": "Scrobble media lain"
},
"name": "Scrobbler",

View File

@ -683,7 +683,6 @@
"listenbrainz": {
"token": "Inserire il token utente per ListenBrainz"
},
"scrobble-alternative-title": "Usa titoli alternativi",
"scrobble-other-media": "Scrobble altri media"
},
"name": "Scrobbler",
@ -745,9 +744,6 @@
"line-effect": {
"label": "Effetto linea",
"submenu": {
"fancy": {
"tooltip": "Usa effetti grandi, simili a quelli di un'app sulla riga attuale"
},
"focus": {
"label": "Focus",
"tooltip": "Rendi bianca solo la riga corrente"

View File

@ -1,16 +1,4 @@
{
"common": {
"console": {
"plugins": {
"execute-failed": "პლაგინის დაყენების შეცდომა {{pluginName}}::{{contextName}}",
"executed-at-ms": "პლაგინი {{pluginName}}::{{contextName}} გაეშვა {{ms}} მილიწამში",
"initialize-failed": "პლაგინის ინიციალიზაცია ვერ მოხდა\"{{pluginName}}\"",
"load-all": "იტვირთება ყველა პლაგინი",
"load-failed": "პლაგინის ჩატვირთვა ვერ მოხდა \"{{pluginName}}\"",
"loaded": "პლაგინი \"{{pluginName}}\" ჩაიტვირთა"
}
}
},
"language": {
"code": "ka",
"local-name": "ქართული",

View File

@ -541,7 +541,7 @@
"menu": {
"click-to-copy-id": "호스트 아이디 복사",
"close": "Music Together 닫기",
"connected-users": "연결된 사용자",
"connected-users": "연결된 사용자: {{count}}명",
"disconnect": "Music Together 연결 끊기",
"empty-user": "연결된 사용자 없음",
"host": "Music Together 호스트",
@ -683,7 +683,6 @@
"listenbrainz": {
"token": "ListenBrainz 유저 토큰 입력"
},
"scrobble-alternative-title": "대체 제목 사용하기",
"scrobble-other-media": "다른 미디어 스크로블하기"
},
"name": "스크로블러",
@ -768,10 +767,6 @@
"label": "가사를 최대한 정교하게 동기화",
"tooltip": "다음 줄의 표시를 밀리초 단위로 계산합니다 (성능에 약간의 영향을 미칠 수 있음)"
},
"romanization": {
"label": "가사 로마자 변환",
"tooltip": "가사가 영어가 아닌 언어로 되어있는 경우, 로마자 표기를 표시합니다."
},
"show-lyrics-even-if-inexact": {
"label": "가사가 정확하지 않더라도 표시",
"tooltip": "노래를 찾을 수 없는 경우, 플러그인이 다른 검색어로 다시 검색합니다.\n두번째 검색 결과는 정확하지 않을 수 있습니다."
@ -804,10 +799,6 @@
"description": "OBS의 확장인 Tuna와의 통합을 활성화합니다",
"name": "Tuna OBS"
},
"unobtrusive-player": {
"description": "노래 재생 중 플레이어가 팝업되는 것을 방지합니다",
"name": "플레이어 방해 금지"
},
"video-toggle": {
"description": "영상/노래 모드를 전환하는 버튼을 추가합니다. 선택적으로 전체 영상 탭을 제거할 수도 있습니다",
"menu": {

View File

@ -207,10 +207,6 @@
}
},
"plugins": {
"ad-speedup": {
"description": "यदि कुनै विज्ञापन चल्छ भने, यसले अडियो म्यूट गर्छ र प्लेब्याक गतिको गति १६x मा सेट गर्छ",
"name": "विज्ञापन तीव्रगति"
},
"adblocker": {
"description": "सबै विज्ञापन र ट्र्याकइंगहरू ब्लक गर्नुहोस्",
"menu": {
@ -279,56 +275,6 @@
},
"name": "परिवेश मोड"
},
"amuse": {
"description": "६के ल्याब्सद्वारा अम्यूस नाउ प्लेइङ विजेटका लागि युट्युब म्युजिक समर्थन थप्दछ",
"name": "अम्यूस",
"response": {
"query": "Amuse API सर्भर चलिरहेको छ। गीत जानकारी प्राप्त गर्न GET /query प्रयोग गर्नुहोस्।"
}
},
"api-server": {
"description": "प्लेयर नियन्त्रण गर्नका लागि API सर्भर थप्दछ",
"dialog": {
"request": {
"buttons": {
"allow": "अनुमति दिनुहोस्",
"deny": "अस्वीकार गर्नुहोस्"
},
"message": "{{ID}} ({{origin}}) लाई API पहुँच अनुमति दिनुहुन्छ?",
"title": "API अनुमतिको अनुरोध"
}
},
"menu": {
"auth-strategy": {
"label": "अनुमति रणनीति",
"submenu": {
"auth-at-first": {
"label": "पहिलो अनुरोधमै अनुमति दिनुहोस्"
},
"none": {
"label": "कुनै अनुमति आवश्यक छैन"
}
}
},
"hostname": {
"label": "होस्टनेम"
},
"port": {
"label": "पोर्ट"
}
},
"name": "API सर्भर [बिटा]",
"prompt": {
"hostname": {
"label": "API सर्भरका लागि होस्टनेम प्रविष्ट गर्नुहोस् (उदाहरण: 0.0.0.0):",
"title": "होस्टनेम"
},
"port": {
"label": "API सर्भरका लागि पोर्ट प्रविष्ट गर्नुहोस्:",
"title": "पोर्ट"
}
}
},
"audio-compressor": {
"description": "अडियोमा कम्प्रेसन लागू गर्नुहोस् (सङ्केतको सबैभन्दा चर्को भागहरूको भोल्युम कम गर्दछ र नरम भागहरूको भोल्युम बढाउँछ)",
"name": "अडियो कम्प्रेसर"
@ -464,21 +410,6 @@
"description": "इन्टरफेसबाट सिधै MP3/स्रोत अडियो डाउनलोड गर",
"menu": {
"choose-download-folder": "डाउनलोड फोल्डर चयन गर्नुहोस्",
"download-finish-settings": {
"label": "समाप्त भएपछि डाउनलोड गर्नुहोस्",
"prompt": {
"last-percent": "x प्रतिशतपछि",
"last-seconds": "अन्तिम x सेकेन्ड",
"title": "कहिले डाउनलोड गर्ने भनेर कन्फिगर गर्नुहोस्"
},
"submenu": {
"advanced": "उन्नत",
"enabled": "सक्रिय गरिएको",
"mode": "समय मोड",
"percent": "प्रतिशत",
"seconds": "सेकेन्डहरू"
}
},
"download-playlist": "डाउनलोड प्लेलिस्ट",
"presets": "प्रिसेटहरू",
"skip-existing": "विद्यमान फाइलहरू स्किप गर्नुहोस्"
@ -491,18 +422,6 @@
"button": "डाउनलोड"
}
},
"equalizer": {
"description": "प्लेयरमा इक्वलाइजर थप्दछ",
"menu": {
"presets": {
"label": "पूर्वसेटहरू",
"list": {
"bass-booster": "बास बूस्टर"
}
}
},
"name": "इक्वलाइजर"
},
"exponential-volume": {
"description": "भोल्युम स्लाइडरलाई घातीय बनाउँछ त्यसैले कम भोल्युमहरू चयन गर्न सजिलो हुन्छ।",
"name": "एक्सपोनेन्सियल भोल्युम"
@ -730,63 +649,6 @@
"description": "स्वचालित रूपमा गैर-सङ्गीत भागहरू जस्तै इन्ट्रो/आउट्रो वा सङ्गीत भिडियोका भागहरू छोड्नुहोस्",
"name": "स्पन्सरब्लक"
},
"synced-lyrics": {
"description": "LRClib जस्ता प्रदायकहरू प्रयोग गरेर गीतहरूका लागि समक्रमित गीतहरू प्रदान गर्छ।",
"errors": {
"fetch": "⚠️ गीतहरूको जानकारी ल्याउने क्रममा त्रुटि भयो।\n\tकृपया केही समयपछि फेरि प्रयास गर्नुहोस्।",
"not-found": "⚠️ यो गीतका लागि कुनै गीतशब्द फेला परेन।"
},
"menu": {
"default-text-string": {
"label": "पूर्वनिर्धारित अक्षर गीतशब्दहरू बीचमा",
"tooltip": "गीतशब्दहरू बीचको खाली ठाउँका लागि प्रयोग हुने पूर्वनिर्धारित अक्षर छनोट गर्नुहोस्"
},
"line-effect": {
"label": "रेखा प्रभाव",
"submenu": {
"fancy": {
"label": "रमणीय",
"tooltip": "हालको लाइनमा ठूलो, एप-जस्तै प्रभावहरू प्रयोग गर्नुहोस्"
},
"focus": {
"label": "केन्द्रित गर्नुहोस्",
"tooltip": "मात्रै हालको लाइन सेतो बनाउनुहोस्"
},
"offset": {
"label": "अफसेट",
"tooltip": "हालको लाइनलाई दायाँतर्फ अफसेट गर्नुहोस्"
},
"scale": {
"label": "स्केल",
"tooltip": "हालको लाइनको आकार परिवर्तन गर्नुहोस्"
}
},
"tooltip": "हालको लाइनमा लागू गर्ने प्रभाव चयन गर्नुहोस्"
},
"precise-timing": {
"label": "गीतशब्दहरू पूर्ण रूपमा समक्रमित बनाउनुहोस्",
"tooltip": "अर्को लाइनको प्रदर्शनलाई मिलिसेकेन्डमा गणना गर्नुहोस् (यसले प्रदर्शनमा हल्का प्रभाव पार्न सक्छ)"
},
"show-lyrics-even-if-inexact": {
"label": "गीतशब्दहरू अपर्याप्त भए पनि देखाउनुहोस्",
"tooltip": "यदि गीत फेला परेन भने, प्लगिनले फरक खोजी सोधपुछसँग पुन: प्रयास गर्छ।\nदोस्रो प्रयासको परिणाम ठ्याक्कै मिल्न नपनि सक्छ।"
},
"show-time-codes": {
"label": "समय कोडहरू देखाउनुहोस्",
"tooltip": "गीतशब्दहरूको छेउमा समय कोडहरू देखाउनुहोस्"
}
},
"name": "समक्रमित गीतशब्दहरू",
"refetch-btn": {
"fetching": "ल्याउँदैछ...",
"normal": "गीतशब्दहरू पुनः ल्याउनुहोस्"
},
"warnings": {
"duration-mismatch": "⚠️ - अवधि असमानताको कारण गीतशब्दहरू असमक्रमित हुन सक्छन्।",
"inexact": "⚠️ - यो गीतका लागि गीतशब्दहरू ठ्याक्कै मिल्दैनन्",
"instrumental": "⚠️ - यो एउटा वाद्य संगीत (इन्स्ट्रुमेन्टल) गीत हो"
}
},
"taskbar-mediacontrol": {
"description": "तपाईँको विन्डोज टास्कबारबाट प्लेब्याक नियन्त्रण गर्नुहोस्",
"name": "टास्कबार मेडिया कन्ट्रोल"

View File

@ -683,7 +683,6 @@
"listenbrainz": {
"token": "Podaj token użytkownika ListenBrainz"
},
"scrobble-alternative-title": "Użyj alternatywnych tytułów",
"scrobble-other-media": "Scrobbluj pozostałe multimedia"
},
"name": "Scrobblowanie",

View File

@ -2,14 +2,14 @@
"common": {
"console": {
"plugins": {
"execute-failed": "Não foi possível executar o plugin {{pluginName}}::{{contextName}}",
"executed-at-ms": "Plugin {{pluginName}}::{{contextName}} executado em {{ms}}ms",
"initialize-failed": "Não foi possível iniciar o plugin \"{{pluginName}}\"",
"load-all": "A carregar todos os plugins",
"load-failed": "Não foi possível ativar o plugin \"{{pluginName}}\"",
"loaded": "Plugin \"{{pluginName}}\" ativado",
"unload-failed": "Não foi possível desativar o plugin \"{{pluginName}}\"",
"unloaded": "Plugin \"{{pluginName}}\" desativado"
"execute-failed": "Falha ao executar o plugin {{pluginName}}::{{contextName}}",
"executed-at-ms": "Plugin {{pluginName}}::{{contextName}} executado com {{ms}}ms",
"initialize-failed": "Falha ao inicializar o plugin \"{{pluginName}}\"",
"load-all": "Carregando todos os plugins",
"load-failed": "Falha ao carregar o plugin \"{{pluginName}}\"",
"loaded": "Plugin \"{{pluginName}}\" carregado",
"unload-failed": "Falha ao descarregar o plugin \"{{pluginName}}\"",
"unloaded": "Plugin \"{{pluginName}}\" descarregado"
}
}
},
@ -21,7 +21,7 @@
"main": {
"console": {
"did-finish-load": {
"dev-tools": "Carregamento concluído. DevTools aberto"
"dev-tools": "Carregamento finalizado. DevTools aberto"
},
"i18n": {
"loaded": "i18n carregado"
@ -30,64 +30,64 @@
"receive-command": "Comando recebido através do protocolo: \"{{command}}\""
},
"theme": {
"css-file-not-found": "O ficheiro CSS \"{{cssFile}}\" não existe, a ignorar"
"css-file-not-found": "Arquivo CSS \"{{cssFile}}\" não existe, ignorando"
},
"unresponsive": {
"details": "Erro de falta de resposta!\n{{error}}"
"details": "Erro sem resposta!\n{{error}}"
},
"when-ready": {
"clearing-cache-after-20s": "A limpar a cache da aplicação"
"clearing-cache-after-20s": "Limpando o cache do aplicativo"
},
"window": {
"tried-to-render-offscreen": "Tentativa de desenho fora do ecrã na janela, windowSize={{windowSize}}, displaySize={{displaySize}}, position={{position}}"
"tried-to-render-offscreen": "Janela tentou desenhar fora do ecrã, windowSize={{windowSize}}, displaySize={{displaySize}}, position={{position}}"
}
},
"dialog": {
"hide-menu-enabled": {
"detail": "O menu está oculto, utilize \"Alt\" para o mostrar (ou \"Escape\" se estiver a utilizar o menu da aplicação)",
"message": "Ocultar Menu está ativado",
"title": "Ocultar Menu ativado"
"detail": "O menu está oculto, use a tecla 'Alt' para mostra-lo (ou 'Esc' se estiver usando o menu do aplicativo)",
"message": "Ocultar menu está ativado",
"title": "Ocultar menu ativado"
},
"need-to-restart": {
"buttons": {
"later": "Depois",
"restart-now": "Reiniciar agora"
},
"detail": "O plugin \"{{pluginName}}\" requer um reinício para ter efeito",
"message": "\"{{pluginName}}\" precisa de ser reiniciado",
"detail": "O plugin {{pluginName}} precisa ser reiniciado para ter efeito",
"message": "\"{{pluginName}}\" precisa ser reiniciado",
"title": "É necessário reiniciar"
},
"unresponsive": {
"buttons": {
"quit": "Sair",
"relaunch": "Reiniciar",
"wait": "Esperar"
"wait": "Espere"
},
"detail": "Lamentamos o incómodo! Por favor, escolha o que fazer:",
"message": "A aplicação não está a responder",
"title": "A janela não está a responder"
"detail": "Lamentamos o inconveniente! Por favor escolha o que fazer:",
"message": "A aplicação não está respondendo",
"title": "A janela não está respondendo"
},
"update-available": {
"buttons": {
"disable": "Desativar atualizações",
"download": "Transferir",
"disable": "Desabilitar atualizações",
"download": "Baixar",
"ok": "Ok"
},
"detail": "Está disponível uma nova versão que pode ser descarregada em {{downloadLink}}",
"message": "Está disponível uma nova versão",
"detail": "Uma nova versão está disponível e pode ser baixada em {{downloadLink}}",
"message": "Uma nova versão está disponível",
"title": "Atualização disponível"
}
},
"menu": {
"about": "Acerca de",
"about": "Sobre",
"navigation": {
"label": "Navegação",
"submenu": {
"copy-current-url": "Copiar URL atual",
"go-back": "Retroceder",
"go-back": "Voltar",
"go-forward": "Avançar",
"quit": "Sair",
"restart": "Reiniciar a aplicação"
"quit": "Saída",
"restart": "Reiniciar aplicativo"
}
},
"options": {
@ -96,15 +96,15 @@
"advanced-options": {
"label": "Opções avançadas",
"submenu": {
"auto-reset-app-cache": "Repor a cache quando a aplicação é iniciada",
"disable-hardware-acceleration": "Desativar a aceleração de hardware",
"auto-reset-app-cache": "Reiniciar cache do aplicativo quando o aplicativo abrir",
"disable-hardware-acceleration": "Desabilitar aceleração por hardware",
"edit-config-json": "Editar config.json",
"override-user-agent": "Substituir User-Agent",
"restart-on-config-changes": "Reiniciar após alterações de configuração",
"restart-on-config-changes": "Reinicie as alterações de configurações feitas",
"set-proxy": {
"label": "Definir proxy",
"prompt": {
"label": "Introduza o endereço do proxy: (deixe em branco para desativar)",
"label": "Inserir Endereço do Proxy: (deixe em branco para desativar)",
"placeholder": "Exemplo: SOCKS5://127.0.0.1:9999",
"title": "Definir proxy"
}
@ -113,62 +113,62 @@
}
},
"always-on-top": "Sempre no topo",
"auto-update": "Atualizações automáticas",
"auto-update": "Atualização automática",
"hide-menu": {
"dialog": {
"message": "O menu será ocultado da próxima vez que abrir a aplicação, utilize [Alt] para o mostrar (ou [`] se estiver a utilizar o menu interno da aplicação)",
"title": "Ocultar Menu ativado"
"message": "O menu será ocultado na próxima inicialização, use [Alt] para mostrá-lo (ou acento grave [`] se estiver usando o menu interno do aplicativo)",
"title": "Ocultar Menu Ativado"
},
"label": "Ocultar Menu"
},
"language": {
"dialog": {
"message": "O idioma será alterado após o reinício",
"title": "Idioma alterado"
"message": "Idioma será alterado após reiniciar",
"title": "Idioma Alterado"
},
"label": "Idioma",
"submenu": {
"to-help-translate": "Quer ajudar na tradução? Clique aqui"
}
},
"resume-on-start": "Retomar a última música quando a app é iniciada",
"single-instance-lock": "Limitar a uma única instância",
"start-at-login": "Iniciar com o sistema",
"resume-on-start": "Continuar última música ao iniciar o aplicativo",
"single-instance-lock": "Trava de instância única",
"start-at-login": "Iniciar no login",
"starting-page": {
"label": "Página inicial",
"unset": "Não definida"
"unset": "Indefinido"
},
"tray": {
"label": "Tabuleiro do sistema",
"label": "Bandeja",
"submenu": {
"disabled": "Desativado",
"enabled-and-hide-app": "Ativado e ocultar a aplicação",
"enabled-and-show-app": "Ativado e mostrar a aplicação",
"play-pause-on-click": "Reproduzir/Pausar ao clicar"
"disabled": "Desabilitado",
"enabled-and-hide-app": "Ativado e esconder aplicativo",
"enabled-and-show-app": "Ativado e mostrar aplicativo",
"play-pause-on-click": "Play/Pausa ao clicar"
}
},
"visual-tweaks": {
"label": "Ajustes visuais",
"label": "Tweaks Visuais",
"submenu": {
"like-buttons": {
"default": "Padrão",
"force-show": "Mostrar sempre",
"force-show": "Forçar mostrar",
"hide": "Esconder",
"label": "Botões do \"Gosto\""
"label": "Botões de curtida"
},
"remove-upgrade-button": "Remover o botão de upgrade",
"remove-upgrade-button": "Remover botão upgrade",
"theme": {
"dialog": {
"button": {
"cancel": "Cancelar",
"remove": "Remover"
},
"remove-theme": "Tem a certeza de que pretende remover o tema personalizado?",
"remove-theme-message": "Isto irá remover o tema personalizado"
"remove-theme": "Você tem certeza que quer remover o tema customizado?",
"remove-theme-message": "Isso removerá o tema customizado"
},
"label": "Tema",
"submenu": {
"import-css-file": "Importar ficheiro CSS personalizado",
"import-css-file": "Importar arquivo CSS personalizado",
"no-theme": "Sem tema"
}
}
@ -186,19 +186,19 @@
"submenu": {
"force-reload": "Forçar Recarregamento",
"reload": "Recarregar",
"reset-zoom": "Tamanho real",
"toggle-fullscreen": "Ativar ecrã inteiro",
"zoom-in": "Aumentar o zoom",
"zoom-out": "Diminuir o zoom"
"reset-zoom": "Tamanho Atual",
"toggle-fullscreen": "Ativar Tela Cheia",
"zoom-in": "Zoom Dentro",
"zoom-out": "Zoom Fora"
}
}
},
"tray": {
"next": "Próxima",
"play-pause": "Reproduzir/Pausar",
"next": "Próximo",
"play-pause": "Play/Pausa",
"previous": "Anterior",
"quit": "Sair",
"restart": "Reiniciar aplicação",
"restart": "Reiniciar aplicativo",
"show": "Mostrar janela",
"tooltip": {
"default": "YouTube Music",
@ -208,11 +208,11 @@
},
"plugins": {
"ad-speedup": {
"description": "Se um anúncio for reproduzido, silencia o áudio e define a velocidade de reprodução para 16x",
"description": "Se um anúncio for reproduzido, ele será silenciado o áudio e será definido a velocidade de reprodução para 16x",
"name": "Acelerar os anúncios"
},
"adblocker": {
"description": "Bloqueie todos os anúncios e monitorização automaticamente",
"description": "Bloquear todos os anúncios e rastreamento automaticamente",
"menu": {
"blocker": "Bloqueador"
},
@ -223,10 +223,10 @@
"name": "Ações no álbum"
},
"album-color-theme": {
"description": "Aplica um tema dinâmico e efeitos visuais baseados na paleta de cores do álbum",
"description": "Aplica um tema dinâmico e efeitos visuais com base na paleta de cores do álbum",
"menu": {
"color-mix-ratio": {
"label": "Proporção de mistura de cores",
"label": "Relação de mistura de cores",
"submenu": {
"percent": "{{ratio}}%"
}
@ -235,7 +235,7 @@
"name": "Tema de cores do álbum"
},
"ambient-mode": {
"description": "Aplica um efeito de iluminação, projetando cores suaves do vídeo no fundo do ecrã",
"description": "Aplica um efeito de iluminação lançando cores suaves do vídeo no fundo da tela.",
"menu": {
"blur-amount": {
"label": "Quantidade de desfoque",
@ -274,64 +274,45 @@
}
},
"use-fullscreen": {
"label": "Utilizar o ecrã inteiro"
"label": "Tela cheia"
}
},
"name": "Modo Ambiente"
},
"amuse": {
"description": "Adiciona suporte ao YouTube Music para o widget Amuse now playing da 6K Labs",
"name": "Amuse",
"response": {
"query": "O servidor da API Amuse está a ser executado. GET /query para obter informações sobre uma música."
}
},
"api-server": {
"description": "Adiciona um servidor API para controlar o leitor",
"dialog": {
"request": {
"buttons": {
"allow": "Permitir",
"deny": "Negar"
},
"message": "Permitir que {{ID}} ({{origin}}) aceda à API?",
"title": "Pedido de autorização da API"
}
}
},
"menu": {
"auth-strategy": {
"label": "Estratégia de autorização",
"label": "Estratégia de Autorização",
"submenu": {
"auth-at-first": {
"label": "Autorizar no primeiro pedido"
"label": "Autorizar ao primeiro pedido"
},
"none": {
"label": "Sem autorização"
}
}
},
"hostname": {
"label": "Nome do anfitrião"
},
"port": {
"label": "Porta"
}
},
"name": "Servidor API [Beta]",
"prompt": {
"hostname": {
"label": "Introduza o nome do anfitrião (como 0.0.0.0) para o servidor API:",
"title": "Nome do anfitrião"
},
"port": {
"label": "Introduza a porta para o servidor API:",
"title": "Porta"
}
}
},
"audio-compressor": {
"description": "Aplicar compressão ao áudio (diminui o volume das partes mais altas do sinal e aumenta o volume das partes mais suaves)",
"name": "Compressor de Áudio"
"name": "Compressor de áudio"
},
"blur-nav-bar": {
"description": "Torna a barra de navegação transparente e desfocada",
@ -342,21 +323,21 @@
"name": "Ignorar restrições de idade"
},
"captions-selector": {
"description": "Seletor de legendas para as faixas de áudio do YouTube Music",
"description": "Seletor de legenda para faixas de áudio do YouTube Music",
"menu": {
"autoload": "Selecionar automaticamente a última legenda utilizada",
"disable-captions": "Sem legendas por defeito"
"autoload": "Selecionar automaticamente a última legenda usada",
"disable-captions": "Sem legendas por padrão"
},
"name": "Seletor de legendas",
"prompt": {
"selector": {
"label": "Idioma atual das legendas: {{language}}",
"label": "Idioma da legenda atual: {{language}}",
"none": "Nenhuma",
"title": "Selecione o idioma das legendas"
"title": "Selecione o idioma da legenda"
}
},
"templates": {
"title": "Abrir o seletor de legendas"
"title": "Seletor de legendas aberto"
}
},
"compact-sidebar": {
@ -372,41 +353,41 @@
"prompt": {
"options": {
"multi-input": {
"fade-in-duration": "Duração da transição (fade-in) no início (ms)",
"fade-out-duration": "Duração da transição (fade-out) no final (ms)",
"fade-in-duration": "Duração da transição no início (ms)",
"fade-out-duration": "Duração da transição no final (ms)",
"fade-scaling": {
"label": "Escala da transição",
"linear": "Linear",
"logarithmic": "Logarítmica"
},
"seconds-before-end": "Realizar transição N segundos antes do fim"
"seconds-before-end": "Realizar transição N segundos antes do final"
},
"title": "Opções da transição"
"title": "Opções de transição"
}
}
},
"disable-autoplay": {
"description": "Faz com que a música comece no modo \"pausado\"",
"description": "Faz a música começar no modo \"pausado\"",
"menu": {
"apply-once": "Aplicar apenas no arranque"
"apply-once": "Aplicar apenas na inicialização"
},
"name": "Desativar reprodução automática"
},
"discord": {
"backend": {
"already-connected": "Tentativa de conexão com ligação já ativa",
"already-connected": "Tentativa de conexão com conexão já ativa",
"connected": "Conectado ao Discord",
"disconnected": "Desconectado do Discord"
},
"description": "Mostre aos seus amigos o que está a ouvir com a Rich Presence",
"description": "Mostre aos seus amigos o que você ouve com Rich Presence",
"menu": {
"auto-reconnect": "Reconectar automaticamente",
"auto-reconnect": "Reconexão automática",
"clear-activity": "Limpar atividade",
"clear-activity-after-timeout": "Limpar atividade após o tempo limite",
"connected": "Conectado",
"disconnected": "Desconectado",
"hide-duration-left": "Ocultar duração restante",
"hide-github-button": "Ocultar botão do GitHub",
"hide-github-button": "Ocultar botão de link do GitHub",
"play-on-youtube-music": "Reproduzir no YouTube Music",
"set-inactivity-timeout": "Definir tempo limite de inatividade"
},
@ -425,97 +406,85 @@
"buttons": {
"ok": "OK"
},
"message": "Ah! Desculpas, o download falhou…",
"title": "Erro ao transferir!"
"message": "Poxa! Desculpe, o download falhou…",
"title": "Erro no download!"
},
"start-download-playlist": {
"buttons": {
"ok": "OK"
},
"detail": "({{playlistSize}} músicas)",
"message": "A descarregar a lista de reprodução {{playlistTitle}}",
"message": "Baixando lista de reprodução {{playlistTitle}}",
"title": "Download iniciado"
}
},
"feedback": {
"conversion-progress": "Conversão: {{percent}}%",
"converting": "A converter…",
"done": "Concluído: {{filePath}}",
"download-info": "A descarregar {{artist}} - {{title}} {{videoId}}",
"download-progress": "A transferir: {{percent}}%",
"downloading": "A transferir…",
"downloading-counter": "A transferir {{current}}/{{total}}…",
"downloading-playlist": "A descarregar a lista de reprodução \"{{playlistTitle}}\" - {{playlistSize}} músicas ({{playlistId}})",
"error-while-downloading": "Erro ao descarregar \"{{author}} - {{title}}\": {{error}}",
"converting": "Convertendo…",
"done": "Finalizado: {{filePath}}",
"download-info": "Baixando {{artist}} - {{title}} {{videoId}}",
"download-progress": "Baixando: {{percent}}%",
"downloading": "Baixando…",
"downloading-counter": "Baixando {{current}}/{{total}}…",
"downloading-playlist": "Baixando lista de reprodução \"{{playlistTitle}}\" - {{playlistSize}} músicas ({{playlistId}})",
"error-while-downloading": "Erro ao baixar \"{{author}} - {{title}}\": {{error}}",
"folder-already-exists": "A pasta {{playlistFolder}} já existe",
"getting-playlist-info": "A obter informações da playlist…",
"loading": "A carregar…",
"playlist-has-only-one-song": "A lista de reprodução tem apenas um item, descarregando-o diretamente",
"playlist-id-not-found": "Não foi encontrado nenhum ID da lista de reprodução",
"getting-playlist-info": "Obtendo informações da lista de reprodução…",
"loading": "Carregando…",
"playlist-has-only-one-song": "A lista de reprodução possui apenas um item, baixando-o diretamente",
"playlist-id-not-found": "ID da lista de reprodução não encontrado",
"playlist-is-empty": "Lista de reprodução vazia",
"playlist-is-mix-or-private": "Erro ao obter informações da playlist: certifique-se de que ela não seja privada ou uma mix personalizada\n\n{{error}}",
"preparing-file": "A preparar o ficheiro…",
"saving": "A guardar…",
"trying-to-get-playlist-id": "A tentar obter o ID da playlist: {{playlistId}}",
"playlist-is-mix-or-private": "Erro ao obter informações da lista de reprodução: tenha certeza que ela não está privada ou não seja \"Mixtapes criadas para você\"\n\n{{error}}",
"preparing-file": "Preparando arquivos…",
"saving": "Salvando…",
"trying-to-get-playlist-id": "Tentando pegar ID da lista de reprodução: {{playlistId}}",
"video-id-not-found": "Vídeo não encontrado",
"writing-id3": "A guardar etiquetas ID3…"
"writing-id3": "Escrevendo tags ID3…"
}
},
"description": "Descarregue MP3 / fonte de áudio diretamente da interface",
"description": "Baixa MP3 / fonte de áudio diretamente da interface",
"menu": {
"choose-download-folder": "Escolha a pasta de download",
"download-finish-settings": {
"label": "Transferir ao terminar",
"label": "Baixar ao terminar",
"prompt": {
"last-percent": "Depois de x por cento",
"last-seconds": "Últimos x segundos",
"title": "Configurar quando descarregar"
"title": "Configurar quando baixar"
},
"submenu": {
"advanced": "Avançado",
"enabled": "Ativado",
"mode": "Modo de tempo",
"percent": "Percentagem",
"percent": "Porcentagem",
"seconds": "Segundos"
}
},
"download-playlist": "Descarregar playlist",
"presets": "Pré-configurações",
"skip-existing": "Ignorar ficheiros existentes"
"download-playlist": "Baixar lista de reprodução",
"presets": "Predefinições",
"skip-existing": "Ignorar arquivos existentes"
},
"name": "Downloader",
"renderer": {
"can-not-update-progress": "Não é possível atualizar o progresso"
},
"templates": {
"button": "Descarregar"
"button": "Baixar"
}
},
"equalizer": {
"description": "Adiciona um equalizador ao leitor",
"menu": {
"presets": {
"label": "Pré-configurações",
"list": {
"bass-booster": "Amplificador de graves"
}
}
},
"name": "Equalizador"
},
"exponential-volume": {
"description": "Torna o controlo do volume exponencial para que seja mais fácil selecionar volumes mais baixos.",
"description": "Torna o controle deslizante de volume exponencial, facilitando a seleção de volumes mais baixos.",
"name": "Volume Exponencial"
},
"in-app-menu": {
"description": "Dá às barras de menu uma aparência sofisticada, escura ou com a cor do álbum",
"menu": {
"hide-dom-window-controls": "Ocultar os controlos da janela DOM"
"hide-dom-window-controls": "Ocultar controles da janela DOM"
},
"name": "Menu da aplicação"
"name": "Menu no aplicativo"
},
"lumiastream": {
"description": "Adiciona suporte a Lumia Stream",
"description": "Adiciona suporte Lumia Stream",
"name": "Lumia Stream [Beta]"
},
"lyrics-genius": {
@ -525,61 +494,61 @@
},
"name": "Letras Genius",
"renderer": {
"fetched-lyrics": "Letras encontradas no Genius"
"fetched-lyrics": "Buscar letras no Genius"
}
},
"music-together": {
"description": "Partilhe uma playlist com outros. Quando o anfitrião tocar uma música, todos os outros ouvirão a mesma música",
"description": "Compartilha a playlist com outros. Quando o host tocar uma música, todos poderão ouvir a mesma canção",
"dialog": {
"enter-host": "Introduza o ID do anfitrião"
"enter-host": "Digite o ID do Host"
},
"internal": {
"save": "Guardar",
"track-source": "Origem da faixa",
"unknown-user": "Utilizador desconhecido"
"save": "Salvar",
"track-source": "Fonte da faixa",
"unknown-user": "Usuário Desconhecido"
},
"menu": {
"click-to-copy-id": "Copiar o ID do anfitrião",
"close": "Fechar o Music Together",
"connected-users": "Utilizadores conectados",
"disconnect": "Desconectar Music Together",
"empty-user": "Sem utilizadores conectados",
"host": "Anfitrião do Music Together",
"join": "Juntar-se ao Music Together",
"click-to-copy-id": "Copiar ID do Host",
"close": "Fechar o Música Juntos",
"connected-users": "Usuários Conectados",
"disconnect": "Desconectar do Música Juntos",
"empty-user": "Sem usuários conectados",
"host": "Host do Música Juntos",
"join": "Juntar ao Música Juntos",
"permission": {
"all": "Permitir que os convidados controlem a playlist e o leitor",
"host-only": "Apenas o anfitrião pode controlar a playlist e o leitor",
"playlist": "Permitir que os convidados controlem a playlist"
"all": "Permita que outros controlem a playlist e ao player",
"host-only": "Apenas o host pode controlar a playlist e ao player",
"playlist": "Permitir que outros controlem a playlist"
},
"set-permission": "Alterar permissões de controlo",
"set-permission": "Alterar permissões de controle",
"status": {
"disconnected": "Desconectado",
"guest": "Conectado como convidado",
"host": "Conectado como anfitrião"
"guest": "Conectado como Convidado",
"host": "Conectado como Host"
}
},
"name": "Music Together [Beta]",
"name": "Música Juntos [Beta]",
"toast": {
"add-song-failed": "Falha ao adicionar música",
"closed": "Music Together fechado",
"disconnected": "Music Together desconectado",
"add-song-failed": "Falha ao adicionar canção",
"closed": "Música Juntos encerrado",
"disconnected": "Música Juntos foi desconectado",
"host-failed": "Falha ao hospedar o Music Together",
"id-copied": "ID do anfitrião copiado para a Área de Transferência",
"id-copy-failed": "Falha ao copiar o ID do anfitrião para a Área de Transferência",
"join-failed": "Falha ao entrar no Music Together",
"joined": "Entrou no Music Together",
"id-copied": "ID de anfitrião copiado para a área de transferência",
"id-copy-failed": "Falha ao copiar o ID de anfitrião para a área de transferência",
"join-failed": "Falha ao entrar em Music Together",
"joined": "Entrou em Music Together",
"permission-changed": "A permissão do Music Together foi alterada para \"{{permission}}\"",
"remove-song-failed": "Falha ao remover música",
"user-connected": "{{name}} entrou no Music Together",
"user-connected": "{{name}} entrou em Music Together",
"user-disconnected": "{{name}} saiu do Music Together"
}
},
"navigation": {
"description": "Setas de navegação Avançar/Retroceder integradas diretamente na interface, como no seu navegador favorito",
"description": "Setas de navegação Próximo/Voltar integradas diretamente na interface, como no seu navegador favorito",
"name": "Navegação"
},
"no-google-login": {
"description": "Remove os botões de login do Google e links da interface",
"description": "Remove os botões e links de login do Google da interface",
"name": "Sem login do Google"
},
"notifications": {
@ -587,36 +556,36 @@
"menu": {
"interactive": "Notificações interativas",
"interactive-settings": {
"label": "Definições interativas",
"label": "Configurações interativas",
"submenu": {
"hide-button-text": "Ocultar o texto do botão",
"hide-button-text": "Ocultar texto do botão",
"refresh-on-play-pause": "Atualizar ao reproduzir/pausar",
"tray-controls": "Abrir/Fechar com um clique no tabuleiro do sistema"
"tray-controls": "Abrir/Fechar no clique da bandeja"
}
},
"priority": "Prioridade da notificação",
"toast-style": "Estilo da notificação",
"unpause-notification": "Mostrar notificação ao desativar a pausa"
"priority": "Prioridade de notificação",
"toast-style": "Estilo de alerta",
"unpause-notification": "Mostrar notificação ao despausar"
},
"name": "Notificações"
},
"picture-in-picture": {
"description": "Permite mudar a aplicação para o modo picture-in-picture",
"description": "Permite mudar o aplicativo para o modo picture-in-picture",
"menu": {
"always-on-top": "Sempre em cima",
"always-on-top": "Sempre no topo",
"hotkey": {
"label": "Tecla de atalho",
"prompt": {
"keybind-options": {
"hotkey": "Tecla de atalho"
},
"label": "Escolha uma tecla de atalho para ativar o picture-in-picture",
"label": "Escolha uma tecla de atalho para alternar o picture-in-picture",
"title": "Tecla de atalho picture-in-picture"
}
},
"save-window-position": "Guardar posição da janela",
"save-window-size": "Guardar tamanho da janela",
"use-native-pip": "Utilizar PiP nativo do navegador"
"save-window-position": "Salvar posição da janela",
"save-window-size": "Salvar tamanho da janela",
"use-native-pip": "Use PiP nativo do navegador"
},
"name": "Picture-in-picture",
"templates": {
@ -624,17 +593,17 @@
}
},
"playback-speed": {
"description": "Ouça rápido, ouça devagar! Adiciona um controlo deslizante que controla a velocidade da música",
"description": "Ouça rápido, ouça devagar! Adiciona um controle deslizante que controla a velocidade da música",
"name": "Velocidade de reprodução",
"templates": {
"button": "Velocidade"
}
},
"precise-volume": {
"description": "Controle o volume com precisão utilizando a roda do rato/teclas de atalho, com um HUD personalizado e incrementos de volume personalizáveis",
"description": "Controle o volume com precisão usando a roda do mouse/teclas de atalho, com um HUD personalizado e etapas de volume personalizáveis",
"menu": {
"arrows-shortcuts": "Controlo preciso com as teclas de seta esq./dir.",
"custom-volume-steps": "Definir incrementos de volume personalizados",
"arrows-shortcuts": "Controles locais das teclas de seta",
"custom-volume-steps": "Definir etapas de volume personalizadas",
"global-shortcuts": "Teclas de atalho globais"
},
"name": "Volume preciso",
@ -644,12 +613,12 @@
"decrease": "Diminuir o volume",
"increase": "Aumentar o volume"
},
"label": "Escolha atalhos de teclado para o volume global:",
"title": "Atalhos de teclado para o volume global"
"label": "Escolha atalhos de teclado de volume global:",
"title": "Atalhos de teclado de volume global"
},
"volume-steps": {
"label": "Escolha o tamanho dos incrementos/decrementos de volume",
"title": "Incrementos de volume"
"label": "Escolha as etapas de aumento/diminuição de volume",
"title": "Etapas de volume"
}
}
},
@ -663,94 +632,90 @@
}
}
},
"description": "Permite alterar a qualidade do vídeo com um botão sobreposto ao vídeo",
"name": "Alterador de qualidade de vídeo"
"description": "Permite alterar a qualidade do vídeo com um botão na sobreposição de vídeo",
"name": "Trocador de qualidade do vídeo"
},
"scrobbler": {
"description": "Adicionar suporte para scrobbling (last.fm, Listenbrainz, etc.)",
"description": "Adicionar suporte para scrobbling (Last.fm, ListenBrainz)",
"dialog": {
"lastfm": {
"auth-failed": {
"message": "Falha ao autenticar com a Last.fm\nO pop-up será ocultado até reiniciar a aplicação.",
"message": "Falha ao autenticar com a Last.fm\nOculte o pop-up até a próxima reinicialização.",
"title": "Falha na autenticação"
}
}
},
"menu": {
"lastfm": {
"api-settings": "Definições da API da Last.fm"
"api-settings": "Configurações de API Last.fm"
},
"listenbrainz": {
"token": "Introduza o token de utilizador do ListenBrainz"
"token": "Insira o token de utilizador ListenBrainz"
},
"scrobble-other-media": "Scrobble de outros conteúdos"
"scrobble-other-media": "Scrobble outros mídia"
},
"name": "Scrobbler",
"prompt": {
"lastfm": {
"api-key": "Chave da API da Last.fm",
"api-secret": "Segredo da API da Last.fm"
"api-key": "Chave de API Last.fm",
"api-secret": "Segredo da API Last.fm"
},
"listenbrainz": {
"token": {
"label": "Introduza o seu token de utilizador ListenBrainz:",
"label": "Insira seu token de usuário do ListenBrainz:",
"title": "Token ListenBrainz"
}
}
}
},
"shortcuts": {
"description": "Permite definir teclas de atalho globais para a reprodução (reproduzir/pausar/próximo/anterior) e desativar o OSD multimédia substituindo as teclas multimédia, ativar Ctrl/CMD + F para pesquisar, ativar o suporte Linux MPRIS para teclas multimédia e teclas de atalho personalizadas para utilizadores avançados",
"description": "Permite definir teclas de atalho globais para reprodução (reproduzir/pausar/próximo/anterior) e desligar o OSD de mídia substituindo as teclas de mídia, ativando Ctrl/CMD + F para pesquisar, ativando o suporte Linux MPRIS para teclas de mídia e teclas de atalho personalizadas para usuários avançados",
"menu": {
"override-media-keys": "Substituir as teclas de multimédia",
"set-keybinds": "Definir controlos globais para a música"
"override-media-keys": "Substituir teclas de mídia",
"set-keybinds": "Definir controles globais de música"
},
"name": "Atalhos (& MPRIS)",
"prompt": {
"keybind": {
"keybind-options": {
"next": "Próximo",
"play-pause": "Reproduzir / Pausar",
"play-pause": "Reproduzir/Pausar",
"previous": "Anterior"
},
"label": "Escolha teclas globais para o controlo da música:",
"label": "Escolha atalhos de teclado globais para controle de músicas:",
"title": "Atalhos globais do teclado"
}
}
},
"skip-disliked-songs": {
"description": "Salta as canções de que não gosta",
"name": "Saltar músicas que não gostei"
"description": "Pula músicas com 'não gostei'",
"name": "Pular músicas com 'não gostei'"
},
"skip-silences": {
"description": "Saltar automaticamente as partes silenciosas das canções",
"name": "Saltar silêncios"
"description": "Pular automaticamente seções de silêncio nas músicas",
"name": "Pular silêncios"
},
"sponsorblock": {
"description": "Salta automaticamente partes que não são música, como a intro/outro ou partes de vídeos de música em que a música não está a ser reproduzida",
"name": "SponsorBlock"
"description": "Ignora automaticamente partes não musicais, como introdução/final ou partes de videoclipes onde a música não está tocando",
"name": "SponsorBlock (bloqueador de patrocínios)"
},
"synced-lyrics": {
"description": "Fornece letras de músicas sincronizadas, utilizando fornecedores como o LRClib.",
"description": "Fornece letras sincronizadas de músicas, utilizando fornecedores como o LRClib.",
"errors": {
"fetch": "⚠️ Ocorreu um erro ao obter a letra da música.\n\tPor favor, tente novamente mais tarde.",
"not-found": "⚠️ Não foram encontradas letras para esta canção."
"fetch": "⚠️ - Ocorreu um erro ao obter as letras da música. Por favor, tenta novamente mais tarde.",
"not-found": "⚠️ - Não foram encontradas letras para esta música."
},
"menu": {
"default-text-string": {
"label": "Carácter predefinido entre letras",
"tooltip": "Escolha o carácter predefinido a utilizar para o intervalo entre letras"
"label": "Caractere padrão entre as letras",
"tooltip": "Escolha o caractere padrão para usar no espaço entre as letras"
},
"line-effect": {
"label": "Efeito da linha",
"label": "Efeito de linha",
"submenu": {
"fancy": {
"label": "Elegante",
"tooltip": "Utilizar grandes efeitos semelhantes aos da aplicação na linha atual"
},
"focus": {
"label": "Foco",
"tooltip": "Tornar branca apenas a linha atual"
"tooltip": "Deixe apenas a linha atual branca"
},
"offset": {
"label": "Deslocamento",
@ -764,11 +729,11 @@
"tooltip": "Escolha o efeito a ser aplicado à linha atual"
},
"precise-timing": {
"label": "Fazer com que as letras estejam perfeitamente sincronizadas",
"tooltip": "Calcular ao milissegundo a visualização da linha seguinte (pode ter um pequeno impacto no desempenho)"
"label": "Sincronize perfeitamente as letras",
"tooltip": "Calcule até o milissegundo a exibição da próxima linha (pode ter um pequeno impacto no desempenho)"
},
"show-lyrics-even-if-inexact": {
"label": "Mostrar as letras, mesmo que sejam imprecisas",
"label": "Mostrar letras mesmo que imprecisas",
"tooltip": "Se a música não for encontrada, o plugin tenta novamente com uma consulta de pesquisa diferente.\nO resultado da segunda tentativa pode não ser exato."
},
"show-time-codes": {
@ -778,29 +743,29 @@
},
"name": "Letras Sincronizadas",
"refetch-btn": {
"fetching": "A obter...",
"fetching": "Buscando...",
"normal": "Buscar as letras novamente"
},
"warnings": {
"duration-mismatch": "⚠️ - A letra da música pode estar dessincronizada devido a um erro de duração.",
"inexact": "⚠️ - A letra desta canção pode não ser exata",
"instrumental": "⚠️ - Esta é uma música instrumental"
"duration-mismatch": "⚠️ - As letras da música pode estar dessincronizada devido a um erro de duração.",
"inexact": "⚠️ - As letras desta música podem não ser exactas.",
"instrumental": "⚠️ - Esta é uma música instrumental."
}
},
"taskbar-mediacontrol": {
"description": "Controle a reprodução a partir da barra de tarefas do Windows",
"name": "Controlo multimédia da barra de tarefas"
"description": "Controle a reprodução na barra de tarefas do Windows",
"name": "Controle de mídia da barra de tarefas"
},
"touchbar": {
"description": "Adiciona um widget TouchBar para utilizadores do macOS",
"name": "TouchBar"
"description": "Adiciona um widget TouchBar para usuários do macOS",
"name": "Barra de toque"
},
"tuna-obs": {
"description": "Integração com o plugin Tuna do OBS",
"name": "Tuna OBS"
},
"video-toggle": {
"description": "Adiciona um botão para alternar entre o modo Vídeo/ Música. Opcionalmente, também pode remover completamente o separador do vídeo",
"description": "Adiciona um botão para alternar entre o modo Vídeo/Música. Também pode, opcionalmente, remover toda a guia de vídeo",
"menu": {
"align": {
"label": "Alinhamento",
@ -810,12 +775,12 @@
"right": "Direita"
}
},
"force-hide": "Forçar a remoção do separador de vídeo",
"force-hide": "Forçar remoção da guia de vídeo",
"mode": {
"label": "Modo",
"submenu": {
"custom": "Alternar personalizado",
"disabled": "Desativado",
"disabled": "Desabilitado",
"native": "Alternar nativo"
}
}
@ -826,11 +791,11 @@
}
},
"visualizer": {
"description": "Adiciona um visualizador ao leitor",
"description": "Adiciona um visualizador ao player",
"menu": {
"visualizer-type": "Tipo de visualizador"
},
"name": "Visualizador"
"name": "Visualizer"
}
}
}

View File

@ -683,7 +683,6 @@
"listenbrainz": {
"token": "Введите токен пользователя ListenBrainz"
},
"scrobble-alternative-title": "Использовать альтернативные названия",
"scrobble-other-media": "Скробблинг других медиа"
},
"name": "Скробблер",

View File

@ -33,7 +33,7 @@
"css-file-not-found": "සීඑස්එස් ගොනුව \"{{cssFile}}\" නොපවතී, නොසලකා හැරීම"
},
"unresponsive": {
"details": "ප්‍රතිචාර නොදක්වන දෝෂයක්\n{{error}}"
"details": "ප්‍රතිචාර නොදක්වන දෝෂයක් {{error}}"
},
"when-ready": {
"clearing-cache-after-20s": "යෙදුම් කෑශ් නිදහස් කරමින්"
@ -41,7 +41,7 @@
},
"dialog": {
"hide-menu-enabled": {
"detail": "මෙනුව සැගවී ඇත, නැවත පෙන්වීමට 'Alt' යතුර භාවිතා කරන්න. (හෝ In-App මෙනුවේ 'Escape')",
"detail": "මෙනුව සැගවී ඇත, 'Alt' යතුර නැවත පෙන්වීමට භාවිතා කරන්න. (හෝ In-App මෙනුවේ 'Escape')",
"message": "මෙනුව සැගවීම සාර්තකයි",
"title": "මෙනුව සැගවීම සක්‍රීයයි"
},

View File

@ -1,119 +1,9 @@
{
"common": {
"console": {
"plugins": {
"execute-failed": "Misslyckades med att köra plugin {{pluginName}}::{{contextName}}",
"executed-at-ms": "Plugin {{pluginName}}::{{contextName}} kördes på {{ms}} ms",
"initialize-failed": "Misslyckades med att initialisera pluginen \"{{pluginName}}\"",
"load-all": "Laddar alla pluginer",
"load-failed": "Misslyckades med att ladda pluginen \"{{pluginName}}\"",
"loaded": "Pluginen \"{{pluginName}}\" laddad",
"unload-failed": "Misslyckades med att avlasta pluginen \"{{pluginName}}\"",
"unloaded": "Pluginen \"{{pluginName}}\" avlastad"
}
}
},
"language": {
"code": "sv",
"local-name": "Svenska",
"name": "Swedish"
},
"main": {
"console": {
"did-finish-load": {
"dev-tools": "Laddning klar. DevTools öppnad"
},
"i18n": {
"loaded": "i18n laddad"
},
"second-instance": {
"receive-command": "Mottog kommando via protokoll: \"{{command}}\""
},
"theme": {
"css-file-not-found": "CSS-filen \"{{cssFile}}\" finns inte, ignorerar"
},
"unresponsive": {
"details": "Oresponsivt fel!\n{{error}}"
},
"when-ready": {
"clearing-cache-after-20s": "Rensar appens cache"
},
"window": {
"tried-to-render-offscreen": "Fönstret försökte rendera utanför skärmen, windowSize={{windowSize}}, displaySize={{displaySize}}, position={{position}}"
}
},
"dialog": {
"hide-menu-enabled": {
"detail": "Menyn är dold, använd 'Alt' för att visa den (eller 'Escape' om du använder inbyggd meny)",
"message": "Dölj Meny är aktiverat",
"title": "Dölj Meny aktiverad"
},
"need-to-restart": {
"buttons": {
"later": "Senare",
"restart-now": "Starta om nu"
},
"detail": "\"{{pluginName}}\" pluginen kräver en omstart för att träda i kraft",
"message": "\"{{pluginName}}\" behöver startas om",
"title": "Omstart krävs"
},
"unresponsive": {
"buttons": {
"quit": "Avsluta",
"relaunch": "Starta om",
"wait": "Vänta"
},
"detail": "Vi beklagar besväret! Välj vad du vill göra:",
"message": "Programmet svarar inte",
"title": "Fönstret svarar inte"
},
"update-available": {
"buttons": {
"disable": "Stäng av uppdateringar",
"download": "Ladda ned",
"ok": "OK"
},
"detail": "En ny version är tillgänglig och kan laddas ned på {{downloadLink}}",
"message": "En ny version är tillgänglig",
"title": "Uppdatering tillgänglig"
}
},
"menu": {
"about": "Om",
"navigation": {
"label": "Navigering",
"submenu": {
"copy-current-url": "Kopiera nuvarande länk",
"go-back": "Föregående",
"go-forward": "Nästa",
"quit": "Lämna",
"restart": "Starta om appen"
}
},
"options": {
"label": "Valmöjligheter",
"submenu": {
"advanced-options": {
"label": "Avancerade valmöjligheter",
"submenu": {
"auto-reset-app-cache": "Nollställ appcache när appen startar",
"disable-hardware-acceleration": "Stäng av hårdvaruacceleration",
"edit-config-json": "Redigera config.json",
"restart-on-config-changes": "Starta om vid konfigurationsändringar",
"set-proxy": {
"label": "Ställ in proxy",
"prompt": {
"title": "Ställ in proxy"
}
}
}
},
"always-on-top": "Alltid överst",
"auto-update": "Uppdatera automatiskt"
}
}
}
},
"plugins": {
"navigation": {
"name": "Navigering"

View File

@ -1,836 +0,0 @@
{
"common": {
"console": {
"plugins": {
"execute-failed": "சொருகி செயல்படுத்துவதில் தோல்வி {{pluginName}} :: {{contextName}}",
"executed-at-ms": "சொருகி {{pluginName}} :: {{contextName}} {{ms}} ms இல் செயல்படுத்தப்படுகிறது",
"initialize-failed": "சொருகி தொடங்குவதில் தோல்வி \"{{pluginName}}\"",
"load-all": "அனைத்து செருகுநிரல்களையும் ஏற்றுகிறது",
"load-failed": "சொருகி ஏற்றுவதில் தோல்வி \"{{pluginName}}\"",
"loaded": "சொருகி \"{{pluginName}}\" ஏற்றப்பட்டது",
"unload-failed": "சொருகி \"{{pluginName}}\"",
"unloaded": "சொருகி \"{{pluginName}}\" இறக்கப்பட்டது"
}
}
},
"language": {
"code": "என்",
"local-name": "ஆங்கிலம்",
"name": "ஆங்கிலம்"
},
"main": {
"console": {
"did-finish-load": {
"dev-tools": "ஏற்றுதல் முடிந்தது. டெவ்டூல்ச் திறக்கப்பட்டது"
},
"i18n": {
"loaded": "ப18ல் ஏற்றப்பட்டது"
},
"second-instance": {
"receive-command": "நெறிமுறை மீது கட்டளை பெறப்பட்டது: \"{{command}}\""
},
"theme": {
"css-file-not-found": "சிஎச்எச் கோப்பு \"{{cssFile}}\" புறக்கணிக்கிறது"
},
"unresponsive": {
"details": "பதிலளிக்காத பிழை!\n {{error}}"
},
"when-ready": {
"clearing-cache-after-20s": "பயன்பாட்டு தற்காலிக சேமிப்பை அழித்தல்"
},
"window": {
"tried-to-render-offscreen": "சாளரம் ஆஃப்ச்கிரீன், சாளரங்கள் = {{windowSize}}, டிச்ப்ளேச்ச் = {{displaySize}}, நிலை = {{position}}"
}
},
"dialog": {
"hide-menu-enabled": {
"detail": "பட்டியல் மறைக்கப்பட்டுள்ளது, அதைக் காட்ட 'ஆல்ட்' ஐப் பயன்படுத்தவும் (அல்லது பயன்பாட்டில் மெனுவைப் பயன்படுத்தினால் 'தப்பிக்க')",
"message": "மறை பட்டியல் இயக்கப்பட்டது",
"title": "பட்டியல் இயக்கப்பட்டது"
},
"need-to-restart": {
"buttons": {
"later": "பின்னர்",
"restart-now": "இப்போது மறுதொடக்கம் செய்யுங்கள்"
},
"detail": "\"{{pluginName}}\" சொருகி நடைமுறைக்கு மறுதொடக்கம் தேவை",
"message": "\"{{pluginName}}\" மறுதொடக்கம் செய்ய வேண்டும்",
"title": "மறுதொடக்கம் தேவை"
},
"unresponsive": {
"buttons": {
"quit": "வெளியேறு",
"relaunch": "மீண்டும் தொடங்கவும்",
"wait": "காத்திருங்கள்"
},
"detail": "சிரமத்திற்கு வருந்துகிறோம்! என்ன செய்ய வேண்டும் என்பதைத் தேர்வுசெய்க:",
"message": "பயன்பாடு பதிலளிக்கவில்லை",
"title": "சாளரம் பதிலளிக்கவில்லை"
},
"update-available": {
"buttons": {
"disable": "புதுப்பிப்புகளை முடக்கு",
"download": "பதிவிறக்கம்",
"ok": "சரி"
},
"detail": "ஒரு புதிய பதிப்பு கிடைக்கிறது மற்றும் {{downloadLink}} இல் பதிவிறக்கம் செய்யலாம்",
"message": "புதிய பதிப்பு கிடைக்கிறது",
"title": "புதுப்பிப்பு கிடைக்கிறது"
}
},
"menu": {
"about": "பற்றி",
"navigation": {
"label": "வானோடல்",
"submenu": {
"copy-current-url": "தற்போதைய முகவரி ஐ நகலெடுக்கவும்",
"go-back": "திரும்பிச் செல்லுங்கள்",
"go-forward": "முன்னோக்கிச் செல்லுங்கள்",
"quit": "வெளியேறு",
"restart": "பயன்பாட்டை மறுதொடக்கம் செய்யுங்கள்"
}
},
"options": {
"label": "விருப்பங்கள்",
"submenu": {
"advanced-options": {
"label": "மேம்பட்ட விருப்பங்கள்",
"submenu": {
"auto-reset-app-cache": "பயன்பாடு தொடங்கும் போது பயன்பாட்டு தற்காலிக சேமிப்பை மீட்டமைக்கவும்",
"disable-hardware-acceleration": "வன்பொருள் முடுக்கம் முடக்கு",
"edit-config-json": "Config.json ஐத் திருத்து",
"override-user-agent": "பயனர்-முகவர் மீறவும்",
"restart-on-config-changes": "கட்டமைப்பு மாற்றங்களை மறுதொடக்கம் செய்யுங்கள்",
"set-proxy": {
"label": "பதிலாள் அமைக்கவும்",
"prompt": {
"label": "பதிலாள் முகவரியை உள்ளிடவும்: (முடக்க காலியாக விடவும்)",
"placeholder": "எடுத்துக்காட்டு: SOCKS5: //127.0.0.1: 9999",
"title": "பதிலாள் அமைக்கவும்"
}
},
"toggle-dev-tools": "டெவ்டூல்சை மாற்றவும்"
}
},
"always-on-top": "எப்போதும் மேலே",
"auto-update": "ஆட்டோ புதுப்பிப்பு",
"hide-menu": {
"dialog": {
"message": "அடுத்த துவக்கத்தில் பட்டியல் மறைக்கப்படும், அதைக் காட்ட [Alt] ஐப் பயன்படுத்தவும் (அல்லது App-menu ஐப் பயன்படுத்தினால் [அல்லது பின்னணி [`])",
"title": "பட்டியல் இயக்கப்பட்டது"
},
"label": "மெனுவை மறைக்கவும்"
},
"language": {
"dialog": {
"message": "மறுதொடக்கம் செய்த பிறகு மொழி மாற்றப்படும்",
"title": "மொழி மாற்றப்பட்டது"
},
"label": "மொழி",
"submenu": {
"to-help-translate": "மொழிபெயர்க்க உதவ வேண்டுமா? இங்கே சொடுக்கு செய்க"
}
},
"resume-on-start": "பயன்பாடு தொடங்கும் போது கடைசி பாடலை மீண்டும் தொடங்குங்கள்",
"single-instance-lock": "ஒற்றை நிகழ்வு பூட்டு",
"start-at-login": "உள்நுழைவில் தொடங்கவும்",
"starting-page": {
"label": "தொடக்க பக்கம்",
"unset": "அமைக்கப்படாதது"
},
"tray": {
"label": "தட்டு",
"submenu": {
"disabled": "முடக்கப்பட்டது",
"enabled-and-hide-app": "இயக்கப்பட்ட மற்றும் பயன்பாட்டை மறைக்கவும்",
"enabled-and-show-app": "இயக்கப்பட்டது மற்றும் பயன்பாட்டைக் காட்டு",
"play-pause-on-click": "சொடுக்கு செய்யவும்/இடைநிறுத்தவும்"
}
},
"visual-tweaks": {
"label": "காட்சி மாற்றங்கள்",
"submenu": {
"like-buttons": {
"default": "இயல்புநிலை",
"force-show": "படை நிகழ்ச்சி",
"hide": "மறை",
"label": "பொத்தான்கள் போன்றவை"
},
"remove-upgrade-button": "மேம்படுத்தல் பொத்தானை அகற்று",
"theme": {
"dialog": {
"button": {
"cancel": "ரத்துசெய்",
"remove": "அகற்று"
},
"remove-theme": "தனிப்பயன் கருப்பொருளை அகற்ற விரும்புகிறீர்களா?",
"remove-theme-message": "இது தனிப்பயன் கருப்பொருளை அகற்றும்"
},
"label": "கருப்பொருள்",
"submenu": {
"import-css-file": "தனிப்பயன் சிஎச்எச் கோப்பை இறக்குமதி செய்க",
"no-theme": "கருப்பொருள் இல்லை"
}
}
}
}
}
},
"plugins": {
"enabled": "இயக்கப்பட்டது",
"label": "செருகுநிரல்கள்",
"new": "புதிய"
},
"view": {
"label": "பார்வை",
"submenu": {
"force-reload": "படை மறுஏற்றம்",
"reload": "ஏற்றவும்",
"reset-zoom": "உண்மையான அளவு",
"toggle-fullscreen": "முழுத் திரையை மாற்றவும்",
"zoom-in": "பெரிதாக்கு",
"zoom-out": "சிறிதாக்கு"
}
}
},
"tray": {
"next": "அடுத்தது",
"play-pause": "விளையாடு/இடைநிறுத்தம்",
"previous": "முந்தைய",
"quit": "வெளியேறு",
"restart": "பயன்பாட்டை மறுதொடக்கம் செய்யுங்கள்",
"show": "சாளரத்தைக் காட்டு",
"tooltip": {
"default": "யூடியூப் இசை",
"with-song-info": "YouTube இசை: {{artist}} - {{title}}"
}
}
},
"plugins": {
"ad-speedup": {
"description": "ஒரு விளம்பரம் விளையாடினால் அது ஆடியோவை முடக்குகிறது மற்றும் பின்னணி வேகத்தை 16x ஆக அமைக்கிறது",
"name": "விளம்பர விரைவு"
},
"adblocker": {
"description": "எல்லா விளம்பரங்களையும் தடுத்து பெட்டியிலிருந்து கண்காணிக்கவும்",
"menu": {
"blocker": "தடுப்பான்"
},
"name": "விளம்பர தடுப்பான்"
},
"album-actions": {
"description": "பிளேலிச்ட் அல்லது ஆல்பத்தில் உள்ள அனைத்து பாடல்களுக்கும் இதைப் பயன்படுத்த பொத்தான்கள் போன்றவற்றைச் சேர்க்கவும், விரும்பாதது, போன்றவை, மற்றும் பொத்தான்களைப் போலல்லாமல் சேர்க்கவும்",
"name": "ஆல்பம் செயல்கள்"
},
"album-color-theme": {
"description": "வண்ணத் தட்டு ஆல்பத்தின் அடிப்படையில் மாறும் கருப்பொருள் மற்றும் காட்சி விளைவுகளைப் பயன்படுத்துகிறது",
"menu": {
"color-mix-ratio": {
"label": "வண்ண கலவை விகிதம்",
"submenu": {
"percent": "{{ratio}}%"
}
}
},
"name": "ஆல்பம் வண்ண கருப்பொருள்"
},
"ambient-mode": {
"description": "வீடியோவிலிருந்து மென்மையான வண்ணங்களை உங்கள் திரையின் பின்னணியில் போடுவதன் மூலம் லைட்டிங் விளைவைப் பயன்படுத்துகிறது",
"menu": {
"blur-amount": {
"label": "மங்கலான தொகை",
"submenu": {
"pixels": "{{blurAmount}} படப்புள்ளிகள்"
}
},
"buffer": {
"label": "இடையக",
"submenu": {
"buffer": "{{buffer}}"
}
},
"opacity": {
"label": "ஒளிபுகாநிலை",
"submenu": {
"percent": "{{opacity}}%"
}
},
"quality": {
"label": "தகுதி",
"submenu": {
"pixels": "{{quality}} படப்புள்ளிகள்"
}
},
"size": {
"label": "அளவு",
"submenu": {
"percent": "{{size}}%"
}
},
"smoothness-transition": {
"label": "மென்மையான மாற்றம்",
"submenu": {
"during": "{{interpolationTime}} s இன் போது"
}
},
"use-fullscreen": {
"label": "முழுத்திரை பயன்படுத்துதல்"
}
},
"name": "சுற்றுப்புற முறை"
},
"amuse": {
"description": "6K ஆய்வகங்களால் இப்போது விட்செட்டில் விளையாடும் Amuse க்கு YouTube இசை ஆதரவை சேர்க்கிறது",
"name": "பொழுதுபோக்கு",
"response": {
"query": "பநிஇ சேவையகம் இயங்குகிறது. பாடல் தகவலைப் பெற /வினவல்."
}
},
"api-server": {
"description": "பிளேயரைக் கட்டுப்படுத்த பநிஇ சேவையகத்தைச் சேர்க்கிறது",
"dialog": {
"request": {
"buttons": {
"allow": "இசைவு",
"deny": "மறுக்கவும்"
},
"message": "பநிஇ ஐ அணுக {{ID}} ({{origin}}) அனுமதிக்கவா?",
"title": "பநிஇ அங்கீகார கோரிக்கை"
}
},
"menu": {
"auth-strategy": {
"label": "அங்கீகார உத்தி",
"submenu": {
"auth-at-first": {
"label": "முதல் கோரிக்கையின் பேரில் ஏற்பு"
},
"none": {
"label": "ஏற்பு இல்லை"
}
}
},
"hostname": {
"label": "புரவலன்பெயர்"
},
"port": {
"label": "துறைமுகம்"
}
},
"name": "பநிஇ சேவையகம் [பீட்டா]",
"prompt": {
"hostname": {
"label": "பநிஇ சேவையகத்திற்கு ஓச்ட்பெயரை (0.0.0.0 போன்றவை) உள்ளிடவும்:",
"title": "புரவலன்பெயர்"
},
"port": {
"label": "பநிஇ சேவையகத்திற்கான துறைமுகத்தை உள்ளிடவும்:",
"title": "துறைமுகம்"
}
}
},
"audio-compressor": {
"description": "ஆடியோவுக்கு சுருக்கத்தைப் பயன்படுத்துங்கள் (சமிக்ஞையின் உரத்த பகுதிகளின் அளவைக் குறைத்து, மென்மையான பகுதிகளின் அளவை உயர்த்துகிறது)",
"name": "ஆடியோ அமுக்கி"
},
"blur-nav-bar": {
"description": "வழிசெலுத்தல் பட்டியை வெளிப்படையானதாகவும் மங்கலாகவும் ஆக்குகிறது",
"name": "மங்கலான வழிசெலுத்தல் பட்டி"
},
"bypass-age-restrictions": {
"description": "YouTube அகவை சரிபார்ப்பு பைபாச்",
"name": "அகவை கட்டுப்பாடுகள் பைபாச்"
},
"captions-selector": {
"description": "YouTube இசை ஆடியோ டிராக்குகளுக்கான தலைப்பு தேர்வாளர்",
"menu": {
"autoload": "கடைசியாக பயன்படுத்தப்பட்ட தலைப்பை தானாகத் தேர்ந்தெடுக்கவும்",
"disable-captions": "முன்னிருப்பாக தலைப்புகள் இல்லை"
},
"name": "தலைப்புகள் தேர்வாளர்",
"prompt": {
"selector": {
"label": "தற்போதைய தலைப்பு மொழி: {{language}}",
"none": "எதுவுமில்லை",
"title": "தலைப்பு மொழியைத் தேர்ந்தெடுக்கவும்"
}
},
"templates": {
"title": "திறந்த தலைப்புகள் தேர்வாளர்"
}
},
"compact-sidebar": {
"description": "எப்போதும் பக்கப்பட்டியை சிறிய பயன்முறையில் அமைக்கவும்",
"name": "சிறிய பக்கப்பட்டி"
},
"crossfade": {
"description": "பாடல்களுக்கு இடையில் கிராச்ஃபேட்",
"menu": {
"advanced": "மேம்பட்ட"
},
"name": "கிராச்ஃபேட் [பீட்டா]",
"prompt": {
"options": {
"multi-input": {
"fade-in-duration": "காலகட்டத்தில் (எம்.எச்) மங்கிவிடும்",
"fade-out-duration": "காலத்தை மங்கச் செய்யுங்கள் (எம்.எச்)",
"fade-scaling": {
"label": "மங்கலான அளவிடுதல்",
"linear": "நேரியல்",
"logarithmic": "மடக்கை"
},
"seconds-before-end": "கிராச்ஃபேட் n வினாடிகள் முடிவுக்கு முன்"
},
"title": "கிராச்ஃபேட் விருப்பங்கள்"
}
}
},
"disable-autoplay": {
"description": "\"இடைநிறுத்தப்பட்ட\" பயன்முறையில் பாடல் தொடங்குகிறது",
"menu": {
"apply-once": "தொடக்கத்தில் மட்டுமே பொருந்தும்"
},
"name": "ஆட்டோபிளேவை முடக்கு"
},
"discord": {
"backend": {
"already-connected": "செயலில் இணைப்புடன் இணைக்க முயற்சித்தது",
"connected": "முரண்பாட்டுடன் இணைக்கப்பட்டுள்ளது",
"disconnected": "முரண்பாட்டிலிருந்து துண்டிக்கப்பட்டது"
},
"description": "பணக்கார இருப்பைக் கொண்டு நீங்கள் கேட்பதை உங்கள் நண்பர்களுக்குக் காட்டுங்கள்",
"menu": {
"auto-reconnect": "ஆட்டோ மீண்டும் இணைக்கவும்",
"clear-activity": "தெளிவான செயல்பாடு",
"clear-activity-after-timeout": "காலக்கெடுவுக்குப் பிறகு தெளிவான செயல்பாடு",
"connected": "இணைக்கப்பட்டுள்ளது",
"disconnected": "துண்டிக்கப்பட்டது",
"hide-duration-left": "காலம் மீதமுள்ளதை மறைக்கவும்",
"hide-github-button": "அறிவிலிமையம் இணைப்பு பொத்தானை மறைக்கவும்",
"play-on-youtube-music": "யூடியூப் இசையில் விளையாடுங்கள்",
"set-inactivity-timeout": "செயலற்ற நேரம் முடிந்தது"
},
"name": "முரண்பாடு பணக்கார இருப்பு",
"prompt": {
"set-inactivity-timeout": {
"label": "நொடிகளில் செயலற்ற நேரத்தை உள்ளிடவும்:",
"title": "செயலற்ற நேரம் முடிந்தது"
}
}
},
"downloader": {
"backend": {
"dialog": {
"error": {
"buttons": {
"ok": "சரி"
},
"message": "ஆர்க்! மன்னிப்பு, பதிவிறக்கம் தோல்வியுற்றது…",
"title": "பதிவிறக்கத்தில் பிழை!"
},
"start-download-playlist": {
"buttons": {
"ok": "சரி"
},
"detail": "({{playlistSize}} பாடல்கள்)",
"message": "பிளேலிச்ட்டைப் பதிவிறக்குகிறது {{playlistTitle}}",
"title": "பதிவிறக்கம் தொடங்கியது"
}
},
"feedback": {
"conversion-progress": "மாற்றம்: {{percent}}%",
"converting": "மாற்றுகிறது…",
"done": "முடிந்தது: {{filePath}}",
"download-info": "பதிவிறக்கம் {{artist}} - {{title}} [{{videoId}}}",
"download-progress": "பதிவிறக்கம்: {{percent}}%",
"downloading": "பதிவிறக்கம்…",
"downloading-counter": "பதிவிறக்கம் {{current}}/{{{total}}…",
"downloading-playlist": "பிளேலிச்ட்டைப் பதிவிறக்குதல் \"{{playlistTitle}}\" - {{playlistSize}} பாடல்கள் ({{playlistId}})",
"error-while-downloading": "பிழை \"{{author}} - {{title}}\": {{error}}",
"folder-already-exists": "{{playlistFolder}}} ஏற்கனவே உள்ளது",
"getting-playlist-info": "பிளேலிச்ட் தகவலைப் பெறுதல்…",
"loading": "ஏற்றுகிறது…",
"playlist-has-only-one-song": "பிளேலிச்ட்டில் ஒரே ஒரு உருப்படி மட்டுமே உள்ளது, அதை நேரடியாக பதிவிறக்குகிறது",
"playlist-id-not-found": "பிளேலிச்ட் ஐடி எதுவும் கிடைக்கவில்லை",
"playlist-is-empty": "பிளேலிச்ட் காலியாக உள்ளது",
"playlist-is-mix-or-private": "பிளேலிச்ட் தகவலைப் பெறுவதில் பிழை: இது ஒரு தனிப்பட்ட அல்லது \"உங்களுக்காக கலப்பு\" பிளேலிச்ட் அல்ல என்பதை உறுதிப்படுத்திக் கொள்ளுங்கள்\n\n {{error}}",
"preparing-file": "கோப்பைத் தயாரித்தல்…",
"saving": "சேமிப்பு…",
"trying-to-get-playlist-id": "பிளேலிச்ட் ஐடியைப் பெற முயற்சிக்கிறது: {{playlistId}}}",
"video-id-not-found": "வீடியோ கிடைக்கவில்லை",
"writing-id3": "ஐடி 3 குறிச்சொற்களை எழுதுதல்…"
}
},
"description": "எம்பி 3 / மூல ஆடியோவை இடைமுகத்திலிருந்து நேரடியாக பதிவிறக்குகிறது",
"menu": {
"choose-download-folder": "பதிவிறக்க கோப்புறையைத் தேர்வுசெய்க",
"download-finish-settings": {
"label": "முடிவில் பதிவிறக்கவும்",
"prompt": {
"last-percent": "ஃச் சதவீதத்திற்குப் பிறகு",
"last-seconds": "கடைசி ஃச் விநாடிகள்",
"title": "எப்போது பதிவிறக்கம் செய்ய வேண்டும் என்பதை உள்ளமைக்கவும்"
},
"submenu": {
"advanced": "மேம்பட்ட",
"enabled": "இயக்கப்பட்டது",
"mode": "நேர முறை",
"percent": "விழுக்காடு",
"seconds": "நொடிகள்"
}
},
"download-playlist": "பிளேலிச்ட்டைப் பதிவிறக்கவும்",
"presets": "முன்னமைவுகள்",
"skip-existing": "இருக்கும் கோப்புகளைத் தவிர்க்கவும்"
},
"name": "பதிவிறக்குபவர்",
"renderer": {
"can-not-update-progress": "முன்னேற்றத்தை புதுப்பிக்க முடியாது"
},
"templates": {
"button": "பதிவிறக்கம்"
}
},
"equalizer": {
"description": "பிளேயருக்கு ஒரு சமநிலையைச் சேர்க்கிறது",
"menu": {
"presets": {
"label": "முன்னமைவுகள்",
"list": {
"bass-booster": "பாச் பூச்டர்"
}
}
},
"name": "சமநிலைப்படுத்தி"
},
"exponential-volume": {
"description": "தொகுதி ச்லைடர் அதிவேகமானது, எனவே குறைந்த தொகுதிகளைத் தேர்ந்தெடுப்பது எளிது.",
"name": "அதிவேக தொகுதி"
},
"in-app-menu": {
"description": "மெனு-பட்டியை ஒரு ஆடம்பரமான, இருண்ட அல்லது ஆல்பம்-வண்ண தோற்றத்தைக் கொடுங்கள்",
"menu": {
"hide-dom-window-controls": "டோம் சாளர கட்டுப்பாடுகளை மறைக்கவும்"
},
"name": "பயன்பாட்டில் பட்டியல்"
},
"lumiastream": {
"description": "லூமியா ச்ட்ரீம் ஆதரவை சேர்க்கிறது",
"name": "லூமியா ச்ட்ரீம் [பீட்டா]"
},
"lyrics-genius": {
"description": "பெரும்பாலான பாடல்களுக்கு பாடல் ஆதரவை சேர்க்கிறது",
"menu": {
"romanized-lyrics": "ரோமானிய பாடல்"
},
"name": "பாடல் சீனியச்",
"renderer": {
"fetched-lyrics": "சீனியசுக்கு பாடல் வரிகள்"
}
},
"music-together": {
"description": "ஒரு பிளேலிச்ட்டை மற்றவர்களுடன் பகிர்ந்து கொள்ளுங்கள். புரவலன் ஒரு பாடலை விளையாடும்போது, மற்றவர்கள் அனைவரும் ஒரே பாடலைக் கேட்பார்கள்",
"dialog": {
"enter-host": "புரவலன் ஐடியை உள்ளிடவும்"
},
"internal": {
"save": "சேமி",
"track-source": "ட்ராக் சோர்ச்",
"unknown-user": "தெரியாத பயனர்"
},
"menu": {
"click-to-copy-id": "புரவலன் ஐடியை நகலெடுக்கவும்",
"close": "ஒன்றாக இசையை மூடு",
"connected-users": "இணைக்கப்பட்ட பயனர்கள்",
"disconnect": "ஒன்றாக இசையை துண்டிக்கவும்",
"empty-user": "இணைக்கப்பட்ட பயனர்கள் இல்லை",
"host": "இசை ஒன்றாக புரவலன்",
"join": "ஒன்றாக இசையில் சேரவும்",
"permission": {
"all": "பிளேலிச்ட் மற்றும் பிளேயரைக் கட்டுப்படுத்த விருந்தினர்களை அனுமதிக்கவும்",
"host-only": "ஓச்டால் மட்டுமே பிளேலிச்ட் மற்றும் பிளேயரை கட்டுப்படுத்த முடியும்",
"playlist": "பிளேலிச்ட்டைக் கட்டுப்படுத்த விருந்தினர்களை அனுமதிக்கவும்"
},
"set-permission": "கட்டுப்பாட்டு அனுமதியை மாற்றவும்",
"status": {
"disconnected": "துண்டிக்கப்பட்டது",
"guest": "விருந்தினராக இணைக்கப்பட்டுள்ளது",
"host": "ஓச்டாக இணைக்கப்பட்டுள்ளது"
}
},
"name": "இசை ஒன்றாக [பீட்டா]",
"toast": {
"add-song-failed": "பாடல் சேர்க்கத் தவறிவிட்டது",
"closed": "இசை ஒன்றாக மூடப்பட்டது",
"disconnected": "இசை ஒன்றாக துண்டிக்கப்பட்டது",
"host-failed": "ஒன்றாக இசையை புரவலன் செய்யத் தவறிவிட்டது",
"id-copied": "இடைநிலைப்பலகைக்கு புரவலன் ஐடி நகலெடுக்கப்பட்டது",
"id-copy-failed": "புரவலன் ஐடியை இடைநிலைப்பலகைக்கு நகலெடுப்பதில் தோல்வி",
"join-failed": "ஒன்றாக இசையில் சேரத் தவறிவிட்டது",
"joined": "ஒன்றாக இசையில் சேர்ந்தார்",
"permission-changed": "இசை ஒன்றாக இசைவு \"{{permission}}\" என மாற்றப்பட்டது",
"remove-song-failed": "பாடலை அகற்றுவதில் தோல்வி",
"user-connected": "{{name}} ஒன்றாக இசையில் சேர்ந்தார்",
"user-disconnected": "{{name}}} இடது இசையை ஒன்றாக"
}
},
"navigation": {
"description": "உங்களுக்கு பிடித்த உலாவியைப் போலவே இடைமுகத்தில் நேரடியாக ஒருங்கிணைக்கப்பட்ட அடுத்த/பின் வழிசெலுத்தல் அம்புகள்",
"name": "வானோடல்"
},
"no-google-login": {
"description": "இடைமுகத்திலிருந்து Google உள்நுழைவு பொத்தான்கள் மற்றும் இணைப்புகளை அகற்று",
"name": "கூகிள் உள்நுழைவு இல்லை"
},
"notifications": {
"description": "ஒரு பாடல் இயக்கத் தொடங்கும் போது அறிவிப்பைக் காண்பி (விண்டோசில் ஊடாடும் அறிவிப்புகள் கிடைக்கின்றன)",
"menu": {
"interactive": "ஊடாடும் அறிவிப்புகள்",
"interactive-settings": {
"label": "ஊடாடும் அமைப்புகள்",
"submenu": {
"hide-button-text": "பொத்தான் உரையை மறைக்கவும்",
"refresh-on-play-pause": "விளையாட்டு/இடைநிறுத்தத்தில் புதுப்பிக்கவும்",
"tray-controls": "தட்டு சொடுக்கு செய்யவும்/மூடு"
}
},
"priority": "அறிவிப்பு முன்னுரிமை",
"toast-style": "சிற்றுண்டி நடை",
"unpause-notification": "பொருத்தமற்ற அறிவிப்பைக் காட்டு"
},
"name": "அறிவிப்புகள்"
},
"picture-in-picture": {
"description": "பயன்பாட்டை பட-பட பயன்முறைக்கு மாற்ற அனுமதிக்கிறது",
"menu": {
"always-on-top": "எப்போதும் மேலே",
"hotkey": {
"label": "ஆட்ச்கி",
"prompt": {
"keybind-options": {
"hotkey": "ஆட்ச்கி"
},
"label": "படம்-இன்-படத்தை மாற்ற ஆட்கியைத் தேர்வுசெய்க",
"title": "படம்-இன்-பட ஆட்ச்கி"
}
},
"save-window-position": "சாளர நிலையை சேமி",
"save-window-size": "சாளர அளவை சேமி",
"use-native-pip": "உலாவி சொந்த பிப் பயன்படுத்தவும்"
},
"name": "படம்-படம்",
"templates": {
"button": "படம்-படம்"
}
},
"playback-speed": {
"description": "வேகமாக கேளுங்கள், மெதுவாக கேளுங்கள்! பாடல் வேகத்தைக் கட்டுப்படுத்தும் ச்லைடரைச் சேர்க்கிறது",
"name": "பின்னணி விரைவு",
"templates": {
"button": "வேகம்"
}
},
"precise-volume": {
"description": "தனிப்பயன் HUD மற்றும் தனிப்பயனாக்கக்கூடிய தொகுதி படிகளுடன், மவுச்வீல்/ஆட்கீசைப் பயன்படுத்தி துல்லியமாக அளவைக் கட்டுப்படுத்தவும்",
"menu": {
"arrows-shortcuts": "உள்ளக அம்பு-கீச் கட்டுப்பாடுகள்",
"custom-volume-steps": "தனிப்பயன் தொகுதி படிகளை அமைக்கவும்",
"global-shortcuts": "உலகளாவிய ஆட்கீச்"
},
"name": "துல்லியமான தொகுதி",
"prompt": {
"global-shortcuts": {
"keybind-options": {
"decrease": "அளவைக் குறைக்கவும்",
"increase": "அளவை அதிகரிக்கவும்"
},
"label": "உலகளாவிய தொகுதி விசைப்பலகைகளைத் தேர்வுசெய்க:",
"title": "உலகளாவிய தொகுதி கீபிண்ட்ச்"
},
"volume-steps": {
"label": "தொகுதி அதிகரிப்பு/குறைத்தல் படிகளைத் தேர்வுசெய்க",
"title": "தொகுதி படிகள்"
}
}
},
"quality-changer": {
"backend": {
"dialog": {
"quality-changer": {
"detail": "தற்போதைய தரம்: {{quality}}",
"message": "வீடியோ தரத்தைத் தேர்வுசெய்க:",
"title": "வீடியோ தரத்தைத் தேர்வுசெய்க"
}
}
},
"description": "வீடியோ மேலடுக்கில் ஒரு பொத்தானைக் கொண்டு வீடியோ தரத்தை மாற்ற அனுமதிக்கிறது",
"name": "வீடியோ தர மாற்றி"
},
"scrobbler": {
"description": "ச்க்ரோப்ளிங் ஆதரவைச் சேர்க்கவும் (முதலியன.",
"dialog": {
"lastfm": {
"auth-failed": {
"message": "Last.fm உடன் அங்கீகரிக்கத் தவறிவிட்டது\n அடுத்த மறுதொடக்கம் வரை பாப்அப்பை மறைக்கவும்.",
"title": "ஏற்பு தோல்வியடைந்தது"
}
}
},
"menu": {
"lastfm": {
"api-settings": "Last.fm பநிஇ அமைப்புகள்"
},
"listenbrainz": {
"token": "WeskBrainz பயனர் கிள்ளாக்கை உள்ளிடவும்"
},
"scrobble-other-media": "மற்ற ஊடகங்களை வெல்லுங்கள்"
},
"name": "ச்க்ரோபிளர்",
"prompt": {
"lastfm": {
"api-key": "Last.fm பநிஇ key",
"api-secret": "Last.fm பநிஇ மறைபொருள்"
},
"listenbrainz": {
"token": {
"label": "உங்கள் கயிட் பிரைன்ச் பயனர் கிள்ளாக்கை உள்ளிடவும்:",
"title": "கேளுங்கள் பிரெய்ன்ச் கிள்ளாக்கு"
}
}
}
},
"shortcuts": {
"description": "பிளேபேக்கிற்கான உலகளாவிய ஆட்கீசை அமைக்க அனுமதிக்கிறது (நாடகம்/இடைநிறுத்தம்/அடுத்த/முந்தைய) மீடியா விசைகளை மீறுவதன் மூலம் மீடியா ஓ.எச்.டி.",
"menu": {
"override-media-keys": "மீடியா விசைகளை மேலெழுதவும்",
"set-keybinds": "உலகளாவிய பாடல் கட்டுப்பாடுகளை அமைக்கவும்"
},
"name": "குறுக்குவழிகள் (& MPRIS)",
"prompt": {
"keybind": {
"keybind-options": {
"next": "அடுத்தது",
"play-pause": "விளையாடு / இடைநிறுத்தம்",
"previous": "முந்தைய"
},
"label": "பாடல்கள் கட்டுப்பாட்டுக்கு உலகளாவிய விசைப்பல்களைத் தேர்வுசெய்க:",
"title": "உலகளாவிய கீபிண்ட்ச்"
}
}
},
"skip-disliked-songs": {
"description": "ச்கிப்ச் விரும்பாத பாடல்கள்",
"name": "விரும்பாத பாடல்களைத் தவிர்க்கவும்"
},
"skip-silences": {
"description": "பாடல்களில் அமைதியான பகுதிகளை தானாகத் தவிர்க்கவும்",
"name": "ம n னங்களைத் தவிர்க்கவும்"
},
"sponsorblock": {
"description": "அறிமுகம்/அவுட்ரோ போன்ற இசை அல்லாத பகுதிகள் அல்லது பாடல் இசைக்காத இசை வீடியோக்களின் பகுதிகளை தானாகவே தவிர்க்கிறது",
"name": "ஒப்புரவாளர் தொகுதி"
},
"synced-lyrics": {
"description": "எல்.ஆர்.சி.எல்.ஐ.பி போன்ற வழங்குநர்களைப் பயன்படுத்தி பாடல்களுக்கு ஒத்திசைக்கப்பட்ட பாடல்களை வழங்குகிறது.",
"errors": {
"fetch": "The பாடல் வரிகளைப் பெறும்போது பிழை ஏற்பட்டது.\n தயவுசெய்து பின்னர் மீண்டும் முயற்சிக்கவும்.",
"not-found": "Ling இந்த பாடலுக்கு வரிகள் எதுவும் கிடைக்கவில்லை."
},
"menu": {
"default-text-string": {
"label": "பாடல் வரிகளுக்கு இடையில் இயல்புநிலை எழுத்து",
"tooltip": "பாடல் வரிகளுக்கு இடையிலான இடைவெளியைப் பயன்படுத்த இயல்புநிலை எழுத்தைத் தேர்வுசெய்க"
},
"line-effect": {
"label": "வரி விளைவு",
"submenu": {
"fancy": {
"label": "ஆடம்பரமான",
"tooltip": "தற்போதைய வரியில் பெரிய, பயன்பாடு போன்ற விளைவுகளைப் பயன்படுத்தவும்"
},
"focus": {
"label": "குவி",
"tooltip": "தற்போதைய வரியை மட்டுமே வெள்ளை செய்யுங்கள்"
},
"offset": {
"label": "ஈடுசெய்யும்",
"tooltip": "வலதுபுறத்தில் தற்போதைய வரியை ஈடுசெய்யவும்"
},
"scale": {
"label": "அளவு",
"tooltip": "தற்போதைய வரியை அளவிடவும்"
}
},
"tooltip": "தற்போதைய வரிக்கு பொருந்தக்கூடிய விளைவைத் தேர்வுசெய்க"
},
"precise-timing": {
"label": "பாடல் வரிகளை சரியாக ஒத்திசைக்கவும்",
"tooltip": "அடுத்த வரியின் காட்சியைக் கணக்கிடுங்கள் (செயல்திறனில் ஒரு சிறிய தாக்கத்தை ஏற்படுத்தும்)"
},
"show-lyrics-even-if-inexact": {
"label": "துல்லியமாக இருந்தாலும் பாடல்களைக் காட்டு",
"tooltip": "பாடல் காணப்படாவிட்டால், சொருகி மீண்டும் வேறு தேடல் வினவலுடன் முயற்சிக்கிறது.\n இரண்டாவது முயற்சியின் முடிவு துல்லியமாக இருக்காது."
},
"show-time-codes": {
"label": "நேரக் குறியீடுகளைக் காட்டு",
"tooltip": "பாடல் வரிகளுக்கு அடுத்த நேர குறியீடுகளைக் காட்டு"
}
},
"name": "ஒத்திசைக்கப்பட்ட பாடல்",
"refetch-btn": {
"fetching": "பெறுதல் ...",
"normal": "ரீஃபட்ச் பாடல்"
},
"warnings": {
"duration-mismatch": "⚠.எம் - கால பொருத்தமின்மை காரணமாக பாடல் வரிகள் ஒத்திசைக்கப்படாமல் இருக்கலாம்.",
"inexact": "- - இந்த பாடலுக்கான வரிகள் துல்லியமாக இருக்காது",
"instrumental": "- இது ஒரு கருவி பாடல்"
}
},
"taskbar-mediacontrol": {
"description": "உங்கள் சாளரங்கள் பணிப்பட்டியிலிருந்து பிளேபேக்கைக் கட்டுப்படுத்தவும்",
"name": "பணிப்பட்டு மீடியா கட்டுப்பாடு"
},
"touchbar": {
"description": "MACOS பயனர்களுக்கான டச்ச்பார் விட்செட்டை சேர்க்கிறது",
"name": "டக்பார்"
},
"tuna-obs": {
"description": "OBS இன் சொருகி டுனாவுடன் ஒருங்கிணைப்பு",
"name": "டுனா குறிப்பு"
},
"video-toggle": {
"description": "வீடியோ/பாடல் பயன்முறைக்கு இடையில் மாற ஒரு பொத்தானைச் சேர்க்கிறது. முழு வீடியோ தாவலையும் விருப்பமாக அகற்றலாம்",
"menu": {
"align": {
"label": "இருப்புவழி",
"submenu": {
"left": "இடது",
"middle": "நடுத்தர",
"right": "வலது"
}
},
"force-hide": "வீடியோ தாவலை அகற்றவும்",
"mode": {
"label": "பயன்முறை",
"submenu": {
"custom": "தனிப்பயன் மாற்று",
"disabled": "முடக்கப்பட்டது",
"native": "சொந்த மாற்று"
}
}
},
"name": "வீடியோ மாற்று",
"templates": {
"button": "பாடல்"
}
},
"visualizer": {
"description": "பிளேயருக்கு ஒரு காட்சிப்படுத்தியைச் சேர்க்கிறது",
"menu": {
"visualizer-type": "விசுவலைசர் வகை"
},
"name": "காட்சிப்படுத்தல்"
}
}
}

View File

@ -39,29 +39,29 @@
"clearing-cache-after-20s": "กำลังล้างแคชแอป"
},
"window": {
"tried-to-render-offscreen": "หน้าต่างพยายามแสดงผลเกินขนาดหน้าจอ ขนาดหน้าต่าง={{windowSize}}, ขนาดหน้าจอ={{displaySize}}, ตำแหน่ง={{position}}"
"tried-to-render-offscreen": "หน้าต่างพยายามแสดงผลเกินขนาดหน้าจอ windowSize={{windowSize}}, displaySize={{displaySize}}, position={{position}}"
}
},
"dialog": {
"hide-menu-enabled": {
"detail": "เมนูถูกซ่อนไว้ กด'Alt' เพื่อแสดงเมนู (หรือ 'Escape' หากกำลังใช้เมนูบนแอป)",
"message": "การซ่อนเมนูเปิดใช้งานอยู่",
"detail": "เมนูถูกซ่อนไว้ กด'Alt' เพื่อแสดงเมนู (หรือ 'Escape' หากอยู่ในเมนูแอป)",
"message": "การซ่อนเมนูถูกเปิดใช้งาน",
"title": "เปิดใช้งานการซ่อนเมนู"
},
"need-to-restart": {
"buttons": {
"later": "รีสตาร์ตภายหลัง",
"restart-now": "รีสตาร์ตอนนี้"
"later": "ภายหลัง",
"restart-now": "รีสตาร์ตอนนี้"
},
"detail": "ปลั๊กอิน \"{{pluginName}}\" ต้องการการรีสตาร์เพื่อจะทำงานได้",
"message": "\"{{pluginName}}\" ต้องการรีสตาร์",
"title": "แนะนำให้รีสตาร์"
"detail": "\"{{pluginName}}\" ปลั๊กอินต้องการการรีสตาร์เพื่อแสดงผล",
"message": "\"{{pluginName}}\" ต้องการรีสตาร์",
"title": "แนะนำให้รีสตาร์"
},
"unresponsive": {
"buttons": {
"quit": "ออก",
"relaunch": "ปิดแล้วเปิดใหม่",
"wait": "รอให้ตอบสนอง"
"relaunch": "เปิดใหม่",
"wait": "รอซักครู่"
},
"detail": "ขออภัยในความไม่สะดวก! โปรดเลือกสิ่งที่ต้องการจะทำ:",
"message": "แอปพลิเคชันไม่ตอบสนอง",
@ -73,7 +73,7 @@
"download": "ดาวน์โหลด",
"ok": "ตกลง"
},
"detail": "มีเวอร์ชันใหม่ให้ใช้งานได้สามารถดาวน์โหลดได้ที่ {{downloadLink}}",
"detail": "มีเวอร์ชันใหม่ให้ดาวน์โหลดแล้วที่ {{downloadLink}}",
"message": "มีเวอร์ชันใหม่ให้ใช้งานแล้ว",
"title": "อัปเดตพร้อมใช้งาน"
}
@ -87,7 +87,7 @@
"go-back": "ก่อนหน้า",
"go-forward": "ถัดไป",
"quit": "ออก",
"restart": "รีสตาร์แอป"
"restart": "รีสตาร์แอป"
}
},
"options": {
@ -96,23 +96,23 @@
"advanced-options": {
"label": "ตัวเลือกขั้นสูง",
"submenu": {
"auto-reset-app-cache": "รีเซตแคชของแอปเมื่อเริ่มแอป",
"auto-reset-app-cache": "รีเซตแอปแคชเมื่อเริ่มแอป",
"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": {
"label": "ใส่ที่อยู่ของพร็อกซี่: (เว้นว่างเพื่อปิดใช้งาน)",
"label": "ใส่ที่อยู่พร็อกซี่: (ปล่อยให้ว่างเพื่อปิดใช้งาน)",
"placeholder": "ตัวอย่าง: SOCKS5://127.0.0.1:9999",
"title": "ตั้งค่าพร็อกซี่"
}
},
"toggle-dev-tools": "เปิด/ปิดเครื่องมือสำหรับนักพัฒนา"
"toggle-dev-tools": "เปิด-ปิด DevTools"
}
},
"always-on-top": "อยู่ด้านบนหน้าต่างอื่นตลอดเวลา",
"always-on-top": "อยู่ด้านบนตลอดเวลา",
"auto-update": "อัปเดตอัตโนมัติ",
"hide-menu": {
"dialog": {
@ -123,17 +123,17 @@
},
"language": {
"dialog": {
"message": "ภาษาจะเปลี่ยนหลังจากทำการรีสตาร์",
"message": "ภาษาจะเปลี่ยนหลังจากทำการรีสตาร์",
"title": "ภาษาถูกเปลี่ยนแล้ว"
},
"label": "ภาษา",
"submenu": {
"to-help-translate": "ต้องการช่วยแปลภาษา? คลิกที่นี่"
"to-help-translate": "ต้องการช่วยแปล? คลิกที่นี่"
}
},
"resume-on-start": "เล่นเพลงที่เล่นล่าสุดต่อ เมื่อเปิดแอป",
"single-instance-lock": "ล็อกไม่ให้เปิดซำ้ซ้อน",
"start-at-login": "เริ่มต้นเมื่อเข้าสู่ระบบ",
"resume-on-start": "เล่นเพลงล่าสุดต่อ เมื่อแอปเริ่มต้น",
"single-instance-lock": "ล็อกการทำงานเพียงรายการเดียว",
"start-at-login": "เริ่มต้นที่การเข้าสู่ระบบ",
"starting-page": {
"label": "หน้าเริ่มต้น",
"unset": "ยกเลิกการตั้งค่า"
@ -148,7 +148,7 @@
}
},
"visual-tweaks": {
"label": "ปรับแต่งหน้าตาแอป",
"label": "การปรับแต่งหน้าตาแอป",
"submenu": {
"like-buttons": {
"default": "ค่าเริ่มต้น",
@ -169,7 +169,7 @@
"label": "ธีม",
"submenu": {
"import-css-file": "นำเข้าไฟล์ CSS ที่กำหนดเอง",
"no-theme": "ไม่ใช้ธีม"
"no-theme": "ไม่มีธีม"
}
}
}
@ -187,28 +187,28 @@
"force-reload": "บังคับโหลดใหม่",
"reload": "โหลดใหม่",
"reset-zoom": "ขนาดจริง",
"toggle-fullscreen": "เปิด/ปิดโหมดเต็มหน้าจอ",
"toggle-fullscreen": "สลับเต็มหน้าจอ",
"zoom-in": "ซูมเข้า",
"zoom-out": "ซูมออก"
}
}
},
"tray": {
"next": "่อไป",
"play-pause": "เล่น/หยุดชั่วคราว",
"next": "่อไป",
"play-pause": "เล่น/พัก",
"previous": "ก่อนหน้า",
"quit": "ออก",
"restart": "รีสตาร์แอป",
"restart": "รีสตาร์แอป",
"show": "แสดงหน้าต่าง",
"tooltip": {
"default": "YouTube Music",
"with-song-info": "YouTube Music: {{artist}} - {{title}}"
"default": "ยูทุปมิวสิค",
"with-song-info": "ยูทูปมิวสิค: {{artist}} - {{title}}"
}
}
},
"plugins": {
"ad-speedup": {
"description": "หากมีโฆษณาเล่นขึ้น เสียงจะถูกปิดและตั้งความเร็วในการเล่นเป็น 16 เท่า",
"description": "หากมีการเล่นโฆษณา เสียงจะถูกปิดและตั้งค่าความเร็วในการเล่นเป็น 16x",
"name": "เพิ่มความเร็วโฆษณา"
},
"adblocker": {
@ -219,7 +219,7 @@
"name": "ตัวบล็อกโฆษณา"
},
"album-actions": {
"description": "เพิ่ม ยกเลิกไม่ชอบ, ไม่ชอบ, ถูกใจ, และ ยกเลิกถูกใจ ที่มีผลกับเพลงทั้งหมดในรายการเพลงหรืออัลบั้ม",
"description": "เพิ่ม ยกเลิกไม่ชอบ, ไม่ชอบ, 'ถูกใจ', และ 'ยกเลิกถูกใจ' เพื่อให้สามารถใช้งานกับเพลงทั้งหมดในรายการเพลงหรืออัลบั้ม",
"name": "การกระทำที่เกี่ยวกับอัลบั้ม"
},
"album-color-theme": {
@ -228,14 +228,14 @@
"color-mix-ratio": {
"label": "สัดส่วนการผสมสี",
"submenu": {
"percent": "{{ratio}}%"
"percent": "{{ratio}}เปอร์เซ็น"
}
}
},
"name": "ธีมสีของอัลบั้ม"
},
"ambient-mode": {
"description": "เอฟเฟกต์แสงที่ดึงสีอ่อนๆจากวีดิโอมาบนพื้นหลังแอป",
"description": "เอฟเฟกต์แสงจะใช้สีอ่อนๆจากวีดิโอมาเป็พื้นหลังสกรีน",
"menu": {
"blur-amount": {
"label": "ระดับความเบลอ",
@ -268,9 +268,9 @@
}
},
"smoothness-transition": {
"label": "ความเรียบการเปลี่ยน",
"label": "การเปลี่ยนแบบสมูท",
"submenu": {
"during": "ในระยะเวลา {{interpolationTime}} วินาที"
"during": "ระหว่าง {{interpolationTime}} วินาที"
}
},
"use-fullscreen": {
@ -279,13 +279,6 @@
},
"name": "โหมดสภาพแวดล้อม"
},
"amuse": {
"description": "เพิ่มการรองรับ Youtube Music สำหรับ Widget Amuse Now Playing ของ 6K Labs",
"name": "Amuse",
"response": {
"query": "API Server ของ Amuse ทำงานอยู่ ส่ง GET ไปที่ /query เพื่อขอข้อมูลเกี่ยวกับเพลง"
}
},
"api-server": {
"description": "เพิ่มเซิร์ฟเวอร์ API เพื่อควบคุมการเล่น",
"dialog": {
@ -294,38 +287,7 @@
"allow": "อนุญาต",
"deny": "ปฏิเสธ"
},
"message": "อนุญาตให้ {{ID}} ({{origin}}) เข้าถึง API หรือไม่?",
"title": "คำขอลงชื่อเข้าใช้งาน API"
}
},
"menu": {
"auth-strategy": {
"label": "วิธีการลงชื่อเข้าใช้งาน",
"submenu": {
"auth-at-first": {
"label": "ลงชื่อเข้าใช้งานในคำขอแรก"
},
"none": {
"label": "ไม่ต้องลงชื่อเข้าใช้งาน"
}
}
},
"hostname": {
"label": "ชื่อโฮสต์ (Hostname)"
},
"port": {
"label": "พอร์ต (Port) ของ Server"
}
},
"name": "เซิร์ฟเวอร์ API [เบต้า]",
"prompt": {
"hostname": {
"label": "ใส่ชื่อโฮสต์ (Hostname) เช่น 0.0.0.0 สำหรับเซิร์ฟเวอร์ API:",
"title": "ชื่อโฮสต์ (Hostname)"
},
"port": {
"label": "ใส่พอร์ต (Port) ในการเข้าถึงเซิร์ฟเวอร์ API:",
"title": "พอร์ต (Port) ของ Server"
"message": "อนุญาตให้ {{ID}} ({{origin}}) เข้าถึง API หรือไม่?"
}
}
},
@ -338,11 +300,11 @@
"name": "เบลอแถบนำทาง"
},
"bypass-age-restrictions": {
"description": "ข้ามการตรวจสอบอายุของ YouTube",
"description": "ข้ามการตรวจสอบอายุของยูทูป",
"name": "ข้ามข้อจำกัดอายุ"
},
"captions-selector": {
"description": "ตัวเลือกคำบรรยายสำหรับเพลงใน YouTube Music",
"description": "ตัวเลือกคำบรรยายสำหรับเพลงในYoutube Music",
"menu": {
"autoload": "เลือกคำบรรยายที่ใช้ครั้งล่าสุดโดยอัตโนมัติ",
"disable-captions": "ไม่มีคำบรรยายเป็นค่าเริ่มต้น"
@ -386,19 +348,19 @@
}
},
"disable-autoplay": {
"description": "เริ่มเพลงในโหมดหยุดชั่วคราว",
"description": "เริ่มเพลงในโหมดหยุด",
"menu": {
"apply-once": "ใช้เฉพาะเมื่อเริ่มต้นแอป"
"apply-once": "ใช้เฉพาะเมื่อเริ่มต้น"
},
"name": "ปิดใช้งานการเล่นอัตโนมัติ"
},
"discord": {
"backend": {
"already-connected": "กำลังพยายามเชื่อมต่อด้วยการเชื่อมต่อที่มีอยู่",
"already-connected": "พยายามเชื่อมต่อกับการเชื่อมต่อที่ทำงานอยู่",
"connected": "เชื่อมต่อกับดิสคอร์ดแล้ว",
"disconnected": "ไม่มีการเชื่อมต่อกับดิสคอร์ดอยู่"
"disconnected": "ตัดการเชื่อมต่อออกจากดิสคอร์ด"
},
"description": "แสดงให้เพื่อนเห็นว่าคุณกำลังฟังอะไรด้วย Rich Presence บนดิสคอร์ด",
"description": "แสดงให้เพื่อนเห็นว่าคุณกำลังฟังอะไรด้วย Rich Presence",
"menu": {
"auto-reconnect": "เชื่อมต่อใหม่โดยอัตโนมัติ",
"clear-activity": "ล้างกิจกรรม",
@ -407,10 +369,10 @@
"disconnected": "ตัดการเชื่อมต่อ",
"hide-duration-left": "ซ่อนระยะเวลาที่เหลือ",
"hide-github-button": "ซ่อนปุ่มลิงก์ GitHub",
"play-on-youtube-music": "เล่นบน YouTube Music",
"play-on-youtube-music": "เล่นบนยูทูปมิวสุค",
"set-inactivity-timeout": "ตั้งระยะเวลาไม่มีกิจกรรม"
},
"name": "แสดงกิจกรรมบนดิสคอร์ด",
"name": "Discord Rich Presence",
"prompt": {
"set-inactivity-timeout": {
"label": "ป้อนระยะเวลาไม่มีกิจกรรมเป็นวินาที:",
@ -464,21 +426,6 @@
"description": "ดาวน์โหลด MP3 / เสียงต้นฉบับโดยตรงจากอินเทอร์เฟซ",
"menu": {
"choose-download-folder": "เลือกโฟลเดอร์ดาวน์โหลด",
"download-finish-settings": {
"label": "ดาวน์โหลดเมื่อเล่นเสร็จ",
"prompt": {
"last-percent": "หลังจาก x เปอร์เซ็นต์",
"last-seconds": "x วินาทีล่าสุด",
"title": "ตั่งค่าเวลาที่จะดาวน์โหลด"
},
"submenu": {
"advanced": "การตั้งค่าขั้นสูง",
"enabled": "เปิดใช้งาน",
"mode": "โหมดเวลา",
"percent": "โหมดเปอร์เซ็นต์",
"seconds": "โหมดวินาที"
}
},
"download-playlist": "ดาวน์โหลดเพลย์ลิสต์",
"presets": "พรีเซ็ต",
"skip-existing": "ข้ามไฟล์ที่มีอยู่แล้ว"
@ -491,24 +438,12 @@
"button": "ดาวน์โหลด"
}
},
"equalizer": {
"description": "เพิ่มอีควอไลเซอร์ให้ที่เล่นเพลง",
"menu": {
"presets": {
"label": "การตั้งค่าล่วงหน้า",
"list": {
"bass-booster": "เพิ่มเบส"
}
}
},
"name": "อีควอไลเซอร์"
},
"exponential-volume": {
"description": "ทำให้ตัวเลือกความดังมีลักษณะเอ็กซ์โปเนนเชียล เพื่อให้ง่ายต่อการเลือกระดับความดังที่ต่ำลง",
"name": "ระดับเสียงแบบเอ็กซโปเนนเชียล"
},
"in-app-menu": {
"description": "ให้เแถบเมนูดูทันสมัย มืดหรือเป็นสีของอัลบั้มอย่างน่าสนใจ",
"description": "ให้เมนูบาร์ดูทันสมัย มืดหรือเป็นสีของอัลบั้มอย่างน่าสนใจ",
"menu": {
"hide-dom-window-controls": "ซ่อนตัวควบคุมหน้าต่าง DOM"
},
@ -595,7 +530,6 @@
}
},
"priority": "ลำดับความสำคัญของการแจ้งเตือน",
"toast-style": "แบบไม่ถาวร",
"unpause-notification": "แสดงการแจ้งเตือนเมื่อหยุดพัก"
},
"name": "การแจ้งเตือน"
@ -615,222 +549,8 @@
}
},
"save-window-position": "บันทึกตำแหน่งหน้าต่าง",
"save-window-size": "บันทึกขนาดหน้าต่าง",
"use-native-pip": "ใช้โหมดภาพซ้อนภาพของเบราเซอร์"
},
"name": "โหมดภาพซ้อนภาพ",
"templates": {
"button": "เปิดโหมดภาพซ้อนภาพ"
"save-window-size": "บันทึกขนาดหน้าต่าง"
}
},
"playback-speed": {
"description": "ฟังเพลงได้ทั้งช้าและเร็ว เพิ่มที่เลื่อนปรับความเร็วของเพลง",
"name": "ความเร็วเพลง",
"templates": {
"button": "ความเร็ว"
}
},
"precise-volume": {
"description": "ควบคุมระดับเสียงได้อย่างแม่นยำด้วยที่เลื่อนเมาส์หรือปุ่มลัด พร้อมด้วยหน้าจอแสดงข้อมูลและขั้นระดับเสียงที่ปรับแต่งได้",
"menu": {
"arrows-shortcuts": "ควบคุมด้วยปุ่มลูกศรเมื่ออยู่ในแอป",
"custom-volume-steps": "ตั้งขั้นการปรับระดับเสียง",
"global-shortcuts": "ตั้งปุ่มลัดที่ใช้ได้ทั้งระบบ"
},
"name": "ระดับเสียงแม่นยำ",
"prompt": {
"global-shortcuts": {
"keybind-options": {
"decrease": "ปุ่มลดระดับเสียง",
"increase": "ปุ่มเพิ่มระดับเสียง"
},
"label": "ตั้งค่าปุ่มลัดระดับเสียงที่ใช้ได้ทั้งระบบ:",
"title": "ปุ่มลัดระดับเสียงที่ใช้ได้ทั้งระบบ"
},
"volume-steps": {
"label": "ตั้งขั้นการเพิ่ม/ลดระดับเสียง",
"title": "ขั้นการปรับระดับเสียง"
}
}
},
"quality-changer": {
"backend": {
"dialog": {
"quality-changer": {
"detail": "คุณภาพปัจจุบัน: {{quality}}",
"message": "เลือกคุณภาพวิดีโอ:",
"title": "เลือกคุณภาพวิดีโอ"
}
}
},
"description": "อนุญาตให้เปลี่ยนคุณภาพของวิดีโอด้วยปุ่มที่แสดงเหนือวิดีโอ",
"name": "ที่เปลี่ยนคุณภาพวิดีโอ"
},
"scrobbler": {
"description": "รองรับการบันทึกการเล่นเพลง (เช่น last.fm, Listenbrainz)",
"dialog": {
"lastfm": {
"auth-failed": {
"message": "เกิดปัญหาระหว่างเข้าใช้งาน Last.fm\nซ่อนข้อความนี้จนการรีสตาร์ตครั้งถัดไป",
"title": "เกิดปัญหาในการเข้าใช้งาน"
}
}
},
"menu": {
"lastfm": {
"api-settings": "การตั้งค่า API Last.fm"
},
"listenbrainz": {
"token": "ใส่ user token ของ ListenBrainz"
},
"scrobble-other-media": "บันทึกการเล่นสื่ออื่นๆ"
},
"name": "บันทึกการเล่น (Scrobbler)",
"prompt": {
"lastfm": {
"api-key": "API Key ของ Last.fm",
"api-secret": "API secret ของ Last.fm"
},
"listenbrainz": {
"token": {
"label": "ใส่ user token ListenBrainz ของคุณ:",
"title": "token ListenBrainz"
}
}
}
},
"shortcuts": {
"description": "อนุญาตให้ตั้งปุ่มลัดทั่วระบบสำหรับการเล่น (เล่น/หยุดชั่วคราว/ถัดไป/ก่อนหน้า) และปิดการแสดงผลสื่อโดยทับปุ่มควบคุมสื่อ เปิด Ctrl/CMD + F เพื่อค้นหา เปิดการใช้งานการควบคุมเพลงผ่าน MPRIS สำหรับ Linux และปุ่มลัดที่กำหนดเองได้สำหรับผู้ใช้ขั้นสูง",
"menu": {
"override-media-keys": "เปลี่ยนการทำงานของปุ่มควบคุมสื่อ",
"set-keybinds": "ตั้งค่าปุ่มลัดควบคุมเพลง"
},
"name": "ปุ่มลัด (และ MPRIS)",
"prompt": {
"keybind": {
"keybind-options": {
"next": "เพลงถัดไป",
"play-pause": "เล่น/หยุดชั่วคราว",
"previous": "เพลงก่อนหน้า"
},
"label": "ตั้งค่าปุ่มลัดทั่วระบบสำหรับควบคุมเพลง:",
"title": "ปุ่มลัดทั่วระบบ"
}
}
},
"skip-disliked-songs": {
"description": "ข้ามเพลงที่ไม่ถูกใจ",
"name": "ข้ามเพลงที่ไม่ถูกใจ"
},
"skip-silences": {
"description": "ข้ามช่วงที่เงียบในเพลงโดยอัตโนมัติ",
"name": "ข้ามช่วงเงียบ"
},
"sponsorblock": {
"description": "ข้ามช่วงที่ไม่ใช่เพลงเช่น intro/outro หรือ ช่วงที่ไม่มีเพลงเล่นใน mv โดยอัตโนมัติ",
"name": "SponsorBlock"
},
"synced-lyrics": {
"description": "ให้เนื้อเพลงที่ตรงกับเวลาของเพลง ผ่านผู้ให้บริการเช่น LRClib",
"errors": {
"fetch": "⚠️\tเกิดปัญหาในการดึงเนื้อเพลง\n\tกรุณาลองใหม่ภายหลัง",
"not-found": "⚠️ ไม่เจอเนื้อเพลงสำหรับเพลงนี้"
},
"menu": {
"default-text-string": {
"label": "อักขระคั่นระหว่างเนื้อเพลง",
"tooltip": "เลือกอักขระที่คั่นในช่วงที่อยู่ระหว่างเนื้อเพลง"
},
"line-effect": {
"label": "เอฟเฟกต์บรรทัด",
"submenu": {
"fancy": {
"label": "อลังการ",
"tooltip": "ใช้เอฟเฟกต์ใหญ่คล้ายบนแอพ บนบรรทัดปัจจุบัน"
},
"focus": {
"label": "เด่น",
"tooltip": "ทำให้แค่บรรทัดปัจจุบันสีขาว"
},
"offset": {
"label": "เลื่อน",
"tooltip": "เลื่อนบรรทัดปัจจุบันไปทางขวา"
},
"scale": {
"label": "ขยายขนาด",
"tooltip": "ขยายบรรทัดปัจจุบันให้ใหญ่ขึ้น"
}
},
"tooltip": "เลือกเอฟเฟกต์ที่จะใช้กับบรรทัดปัจจุบัน"
},
"precise-timing": {
"label": "ให้เนื้อเพลงตรงกับเพลงเป๊ะๆ",
"tooltip": "คำนวณมิลิวินาทีในการแสดงบรรทัดถัดไป (มีผลเล็กน้อยกับประสิทธิภาพการทำงาน)"
},
"show-lyrics-even-if-inexact": {
"label": "แสดงเนื้อเพลงแม้ไม่ตรงเป๊ะ",
"tooltip": "ถ้าหาเนื้อเพลงไม่เจอจะลองหาด้วยคำค้นหาที่ต่างกัน\nอาจได้เนื้อเพลงไม่ตรง"
},
"show-time-codes": {
"label": "แสดงตำแหน่งเวลา",
"tooltip": "แสดงตำแหน่งเวลาข้างๆเนื้อเพลง"
}
},
"name": "เนื้อเพลงตรงกับเพลง",
"refetch-btn": {
"fetching": "กำลังดึงข้อมูล...",
"normal": "ดึงเนื้อเพลงใหม่"
},
"warnings": {
"duration-mismatch": "⚠️ - เนื้อเพลงอาจไม่ตรงกับเวลาเนื่องจากความยาวไม่ตรงกัน",
"inexact": "⚠️ - เนื้อเพลงอาจไม่ตรง",
"instrumental": "⚠️ - เพลงนี้เป็นเพลงบรรเลง"
}
},
"taskbar-mediacontrol": {
"description": "ควบคุมการเล่นผ่าน taskbar ของ Windows",
"name": "ควบคุมสื่อผ่าน Taskbar"
},
"touchbar": {
"description": "เพิ่ม Widget บน TouchBar สำหรับผู้ใช้ macOS",
"name": "TouchBar"
},
"tuna-obs": {
"description": "ใช้งานร่วมกันกับปลั้กอิน Tuna บน OBS",
"name": "Tuna OBS"
},
"video-toggle": {
"description": "เพิ่มปุ่มสลับระหว่างโหมดเพลงกับโหมดวิดีโอ หรือลบแถบวิดีโอออกทั้งแถบ",
"menu": {
"align": {
"label": "ตำแหน่งปุ่ม",
"submenu": {
"left": "ซ้าย",
"middle": "กลาง",
"right": "ขวา"
}
},
"force-hide": "บังคับลบแถบวิดีโอ",
"mode": {
"label": "โหมด",
"submenu": {
"custom": "ปุ่มกำหนดเอง",
"disabled": "ปิด",
"native": "ปุ่มเริ่มต้น"
}
}
},
"name": "ปุ่มวิดีโอ",
"templates": {
"button": "เพลง"
}
},
"visualizer": {
"description": "เพิ่มวิชวลไลเซอร์ให้ที่เล่นเพลง",
"menu": {
"visualizer-type": "ประเภทวิชวลไลเซอร์"
},
"name": "วิชวลไลเซอร์"
}
}
}

View File

@ -199,7 +199,11 @@
"previous": "上一首",
"quit": "退出",
"restart": "重启应用",
"show": "显示窗口"
"show": "显示窗口",
"tooltip": {
"default": "YouTube Music",
"with-song-info": "YouTube Music: {{artist}} - {{title}}"
}
}
},
"plugins": {
@ -277,6 +281,7 @@
},
"amuse": {
"description": "为 6K Labs 的 Amuse 正在播放小部件添加 YouTube Music 支持",
"name": "Amuse",
"response": {
"query": "Amuse API服务器已在运行。使用 /query 以获取歌曲信息。"
}
@ -729,7 +734,7 @@
"description": "透过 LRClib 等服务提供滚动歌词显示。",
"errors": {
"fetch": "⚠️ - 获取歌词时发生错误。请稍后再试。",
"not-found": "⚠️ 未找到此歌曲的歌词。"
"not-found": "⚠️ - 未找到此歌曲的歌词。"
},
"menu": {
"default-text-string": {

View File

@ -683,7 +683,6 @@
"listenbrainz": {
"token": "輸入 ListenBrainz 使用者憑證"
},
"scrobble-alternative-title": "使用另類歌曲標題",
"scrobble-other-media": "紀錄其他媒體文件"
},
"name": "Scrobbler",

View File

@ -825,7 +825,7 @@ app.whenReady().then(async () => {
// Optimized for Mac OS X
if (is.macOS() && !config.get('options.appVisible')) {
app.dock?.hide();
app.dock.hide();
}
let forceQuit = false;

View File

@ -720,7 +720,7 @@ export const register = (
app.openapi(routes.addSongToQueue, (ctx) => {
const { videoId, insertPosition } = ctx.req.valid('json');
controller.addSongToQueue(videoId, insertPosition);
controller.addSongToQueue(videoId, { queueInsertPosition: insertPosition });
ctx.status(204);
return ctx.body(null);

View File

@ -38,12 +38,6 @@ const info: Info = {
*/
const refreshCallbacks: (() => void)[] = [];
const truncateString = (str: string, length: number): string => {
if (str.length > length)
return `${str.substring(0, length - 3)}...`;
return str;
}
const resetInfo = () => {
info.ready = false;
clearTimeout(clearActivity);
@ -190,8 +184,8 @@ export const backend = createBackend<
const activityInfo: SetActivity = {
type: ActivityType.Listening,
details: truncateString(songInfo.title, 128),
state: truncateString(songInfo.artist, 128),
details: songInfo.title,
state: songInfo.artist,
largeImageKey: songInfo.imageSrc ?? '',
largeImageText: songInfo.album ?? '',
buttons,

View File

@ -639,12 +639,7 @@ export async function downloadPlaylist(givenUrl?: string | URL) {
try {
playlist = await yt.music.getPlaylist(playlistId);
if (playlist?.items) {
const filteredItems = playlist.items.filter(
(item): item is YTNodes.MusicResponsiveListItem =>
item instanceof YTNodes.MusicResponsiveListItem,
);
items.push(...filteredItems);
items.push(...playlist.items.as(YTNodes.MusicResponsiveListItem));
}
} catch (error: unknown) {
sendError(
@ -679,13 +674,9 @@ export async function downloadPlaylist(givenUrl?: string | URL) {
while (playlist.has_continuation) {
playlist = await playlist.getContinuation();
const filteredItems = playlist.items.filter(
(item): item is YTNodes.MusicResponsiveListItem =>
item instanceof YTNodes.MusicResponsiveListItem,
);
items.push(...filteredItems);
if (playlist?.items) {
items.push(...playlist.items.as(YTNodes.MusicResponsiveListItem));
}
}
if (items.length === 1) {

View File

@ -0,0 +1,117 @@
import { setQueue } from '../store/queue';
import { ConnectionEventUnion, MusicTogetherConfig, VideoData } from '../types';
import { setStatus } from '../store/status';
import { IPC } from '../constants';
import { Connection } from '../connection';
import type { AppElement } from '@/types/queue';
import type { RendererContext } from '@/types/contexts';
type BuildListenerOptions = {
ipc: RendererContext<MusicTogetherConfig>['ipc'];
app: AppElement;
};
export const Guest = {
buildListener: (_: Connection, { ipc, app }: BuildListenerOptions) => {
const listener = async (event: ConnectionEventUnion) => {
switch (event.type) {
case 'ADD_SONGS': {
await ipc.invoke(
IPC.addSongToQueue,
event.payload.videoList.map((v) => v.videoId),
{
index: event.payload.index,
},
);
setQueue('queue', (queue) => {
const result: VideoData[] = [...queue];
if (event.payload.index) {
result.splice(event.payload.index, 0, ...event.payload.videoList);
} else {
result.push(...event.payload.videoList);
}
return result;
});
break;
}
case 'REMOVE_SONG': {
await ipc.invoke(IPC.removeSongFromQueue, event.payload.index);
setQueue('queue', (queue) => {
const result: VideoData[] = [...queue];
result.splice(event.payload.index, 1);
return result;
});
break;
}
case 'MOVE_SONG': {
await ipc.invoke(
IPC.moveSongInQueue,
event.payload.fromIndex,
event.payload.toIndex,
);
setQueue('queue', (queue) => {
const result: VideoData[] = [...queue];
const [removed] = result.splice(event.payload.fromIndex, 1);
result.splice(event.payload.toIndex, 0, removed);
return result;
});
break;
}
case 'IDENTIFY': {
console.warn('Music Together [Guest]: Not allowed Event', event);
break;
}
case 'SYNC_USER': {
setStatus('users', event.payload?.users ?? []);
break;
}
case 'PERMISSION': {
const permission = event.payload;
if (!permission) break;
setStatus('permission', permission);
break;
}
case 'SYNC_QUEUE': {
await ipc.invoke(IPC.clearQueue);
await ipc.invoke(
IPC.addSongToQueue,
event.payload?.videoList.map((v) => v.videoId),
{
queueInsertPosition: 'INSERT_AT_END',
},
);
setQueue('queue', event.payload?.videoList ?? []);
break;
}
case 'SYNC_PROGRESS': {
if (typeof event.payload?.progress === 'number') {
app.playerApi?.seekTo(event.payload.progress);
}
if (app.playerApi?.getPlayerState() !== event.payload?.state) {
if (event.payload?.state === 2) app.playerApi?.pauseVideo();
if (event.payload?.state === 1) app.playerApi?.playVideo();
}
if (typeof event.payload?.index === 'number') {
await ipc.invoke(IPC.setQueueIndex, event.payload.index);
}
break;
}
default: {
console.warn('Music Together [Host]: Unknown Event', event);
break;
}
}
};
return listener;
},
};

View File

@ -0,0 +1,140 @@
import { DataConnection } from 'peerjs';
import { RendererContext } from '@/types/contexts';
import { queue } from '@/plugins/music-together/store/queue';
import { AppElement } from '@/types/queue';
import { ConnectionEventUnion, MusicTogetherConfig } from '../types';
import { setStatus, status } from '../store/status';
import { IPC } from '../constants';
import { Connection } from '../connection';
type BuildListenerOptions = {
ipc: RendererContext<MusicTogetherConfig>['ipc'];
app: AppElement;
};
export const Host = {
buildListener: (conn: Connection, { ipc, app }: BuildListenerOptions) => {
const listener = async (
event: ConnectionEventUnion,
dataConnection?: DataConnection,
) => {
switch (event.type) {
case 'ADD_SONGS': {
if (dataConnection && status.permission === 'host-only') return;
await ipc.invoke(
IPC.addSongToQueue,
event.payload.videoList.map((v) => v.videoId),
{
index: event.payload.index,
},
);
console.log('ADD_SONGS', event);
await conn?.broadcast(event.type, event.payload);
break;
}
case 'REMOVE_SONG': {
if (dataConnection && status.permission === 'host-only') return;
await ipc.invoke(IPC.removeSongFromQueue, event.payload.index);
await conn?.broadcast(event.type, event.payload);
break;
}
case 'MOVE_SONG': {
if (dataConnection && status.permission === 'host-only') {
// await conn.broadcast('SYNC_QUEUE', {
// videoList: queue?.videoList ?? [],
// });
break;
}
await ipc.invoke(
IPC.moveSongInQueue,
event.payload.fromIndex,
event.payload.toIndex,
);
await conn?.broadcast(event.type, event.payload);
break;
}
case 'IDENTIFY': {
const newUser = event.payload?.user;
if (!newUser) return;
// api?.toastService?.show(
// t('plugins.music-together.toast.user-connected', {
// name: event.payload.profile.name,
// }),
// );
setStatus('users', (users) => [...users, newUser]);
await conn?.broadcast('SYNC_USER', {
users: status.users,
});
break;
}
case 'SYNC_USER': {
await conn?.broadcast('SYNC_USER', {
users: status.users,
});
break;
}
case 'PERMISSION': {
await conn?.broadcast('PERMISSION', status.permission);
break;
}
case 'SYNC_QUEUE': {
await conn?.broadcast('SYNC_QUEUE', {
videoList: queue.queue,
});
break;
}
case 'SYNC_PROGRESS': {
let permissionLevel = 0;
if (status.permission === 'all') permissionLevel = 2;
if (status.permission === 'playlist') permissionLevel = 1;
if (status.permission === 'host-only') permissionLevel = 0;
if (!conn) permissionLevel = 3;
if (permissionLevel >= 2) {
if (typeof event.payload?.progress === 'number') {
const currentTime = app.playerApi?.getCurrentTime() ?? 0;
const offset = Math.abs(event.payload.progress - currentTime);
if (offset > 3)
app.playerApi?.seekTo(event.payload.progress + offset);
}
if (app.playerApi?.getPlayerState() !== event.payload?.state) {
if (event.payload?.state === 2) app.playerApi?.pauseVideo();
if (event.payload?.state === 1) app.playerApi?.playVideo();
}
}
if (permissionLevel >= 1) {
if (typeof event.payload?.index === 'number') {
await ipc.invoke(IPC.setQueueIndex, event.payload.index);
}
}
await conn?.broadcast('SYNC_PROGRESS', event.payload);
break;
}
default: {
console.warn('Music Together [Host]: Unknown Event', event);
break;
}
}
if (event.after) {
const now = event.after.shift();
if (now) {
now.after = event.after;
await listener(now, dataConnection);
}
}
};
return listener;
},
};

View File

View File

@ -0,0 +1,53 @@
import prompt from 'custom-electron-prompt';
import { MusicTogetherConfig } from './types';
import promptOptions from '@/providers/prompt-options';
import getSongControls from '@/providers/song-controls';
import { IPC } from './constants';
import type { BackendContext } from '@/types/contexts';
export const onMainLoad = ({
ipc,
window,
}: BackendContext<MusicTogetherConfig>) => {
const controller = getSongControls(window);
ipc.handle(IPC.prompt, async (title: string, label: string) =>
prompt({
title,
label,
type: 'input',
...promptOptions(),
}),
);
ipc.handle(IPC.play, () => controller.play());
ipc.handle(IPC.pause, () => controller.pause());
ipc.handle(IPC.previous, () => controller.previous());
ipc.handle(IPC.next, () => controller.next());
ipc.handle(IPC.seekTo, (seconds: number) => controller.seekTo(seconds));
ipc.handle(
IPC.addSongToQueue,
(
ids: string | string[],
options: {
queueInsertPosition?: 'INSERT_AT_END' | 'INSERT_AFTER_CURRENT_VIDEO';
index?: number;
},
) => controller.addSongToQueue(ids, options),
);
ipc.handle(IPC.removeSongFromQueue, (index: number) =>
controller.removeSongFromQueue(index),
);
ipc.handle(IPC.moveSongInQueue, (fromIndex: number, toIndex: number) =>
controller.moveSongInQueue(fromIndex, toIndex),
);
ipc.handle(IPC.clearQueue, () => controller.clearQueue());
ipc.handle(IPC.setQueueIndex, (index: number) =>
controller.setQueueIndex(index),
);
};

View File

@ -1,26 +1,6 @@
import { DataConnection, Peer } from 'peerjs';
import type { Permission, Profile, VideoData } from './types';
export type ConnectionEventMap = {
ADD_SONGS: { videoList: VideoData[]; index?: number };
REMOVE_SONG: { index: number };
MOVE_SONG: { fromIndex: number; toIndex: number };
IDENTIFY: { profile: Profile } | undefined;
SYNC_PROFILE: { profiles: Record<string, Profile> } | undefined;
SYNC_QUEUE: { videoList: VideoData[] } | undefined;
SYNC_PROGRESS:
| { progress?: number; state?: number; index?: number }
| undefined;
PERMISSION: Permission | undefined;
};
export type ConnectionEventUnion = {
[Event in keyof ConnectionEventMap]: {
type: Event;
payload: ConnectionEventMap[Event];
after?: ConnectionEventUnion[];
};
}[keyof ConnectionEventMap];
import { ConnectedState, ConnectionEventMap, ConnectionEventUnion } from './types';
type PromiseUtil<T> = {
promise: Promise<T>;
@ -32,10 +12,10 @@ export type ConnectionListener = (
event: ConnectionEventUnion,
conn: DataConnection,
) => void;
export type ConnectionMode = 'host' | 'guest' | 'disconnected';
export class Connection {
private peer: Peer;
private _mode: ConnectionMode = 'disconnected';
private _state: ConnectedState = 'disconnected';
private connections: Record<string, DataConnection> = {};
private waitOpen: PromiseUtil<string> = {} as PromiseUtil<string>;
@ -51,15 +31,15 @@ export class Connection {
});
this.peer.on('open', (id) => {
this._mode = 'host';
this._state = 'connecting';
this.waitOpen.resolve(id);
});
this.peer.on('connection', (conn) => {
this._mode = 'host';
this._state = 'host';
this.registerConnection(conn);
});
this.peer.on('error', (err) => {
this._mode = 'disconnected';
this._state = 'disconnected';
this.waitOpen.reject(err);
this.connectionListeners.forEach((listener) => listener());
@ -73,16 +53,16 @@ export class Connection {
}
async connect(id: string) {
this._mode = 'guest';
this._state = 'guest';
const conn = this.peer.connect(id);
await this.registerConnection(conn);
return conn;
}
disconnect() {
if (this._mode === 'disconnected') throw new Error('Already disconnected');
if (this._state === 'disconnected') throw new Error('Already disconnected');
this._mode = 'disconnected';
this._state = 'disconnected';
this.connections = {};
this.peer.destroy();
}
@ -92,8 +72,8 @@ export class Connection {
return this.peer.id;
}
public get mode() {
return this._mode;
public get state() {
return this._state;
}
public getConnections() {
@ -121,7 +101,7 @@ export class Connection {
private async registerConnection(conn: DataConnection) {
return new Promise<DataConnection>((resolve, reject) => {
this.peer.once('error', (err) => {
this._mode = 'disconnected';
this._state = 'disconnected';
reject(err);
this.connectionListeners.forEach((listener) => listener());
@ -133,6 +113,12 @@ export class Connection {
this.connectionListeners.forEach((listener) => listener(conn));
conn.on('data', (data) => {
if (typeof data === 'string') {
try {
data = JSON.parse(data);
} catch {}
}
if (
!data ||
typeof data !== 'object' ||
@ -140,7 +126,7 @@ export class Connection {
!('payload' in data) ||
!data.type
) {
console.warn('Music Together: Invalid data', data);
console.warn('Music Together: Invalid data', data, typeof data);
return;
}

View File

@ -0,0 +1,13 @@
export const IPC = {
prompt: 'music-together:prompt',
play: 'music-together:play',
pause: 'music-together:pause',
previous: 'music-together:previous',
next: 'music-together:next',
seekTo: 'music-together:seekTo',
addSongToQueue: 'music-together:addSongToQueue',
removeSongFromQueue: 'music-together:removeSongFromQueue',
moveSongInQueue: 'music-together:moveSongInQueue',
clearQueue: 'music-together:clearQueue',
setQueueIndex: 'music-together:setQueueIndex',
};

View File

@ -0,0 +1,31 @@
import { createContext, JSX, splitProps, useContext } from 'solid-js';
import { MusicTogetherConfig } from '../types';
import { RendererContext } from '@/types/contexts';
export type RendererContextContextType = {
context: RendererContext<MusicTogetherConfig>;
};
export const RendererContextContext =
createContext<RendererContextContextType>();
export type RendererContextProviderProps = RendererContextContextType & {
children: JSX.Element;
};
export const RendererContextProvider = (
props: RendererContextProviderProps,
) => {
const [local, left] = splitProps(props, ['children']);
return (
<RendererContextContext.Provider value={left}>
{local.children}
</RendererContextContext.Provider>
);
};
export const useRendererContext = () => {
const context = useContext(RendererContextContext);
if (!context) throw Error('RendererContextProvider not found');
return context.context;
};

View File

@ -0,0 +1,22 @@
import { createContext, JSX, useContext } from 'solid-js';
import { ToastService } from '@/types/queue';
export type ToastContextType = {
service: ToastService;
};
export const ToastContext = createContext<ToastContextType>();
export type ToastProviderProps = ToastContextType & {
children: JSX.Element;
};
export const ToastProvider = (props: ToastProviderProps) => (
<ToastContext.Provider value={props}>{props.children}</ToastContext.Provider>
);
export const useToast = () => {
const context = useContext(ToastContext);
return (message: string) => {
context?.service.show(message);
};
};

View File

@ -1,148 +0,0 @@
import { ElementFromHtml } from '@/plugins/utils/renderer';
import itemHTML from './templates/item.html?raw';
import popupHTML from './templates/popup.html?raw';
type Placement =
| 'top'
| 'bottom'
| 'right'
| 'left'
| 'center'
| 'middle'
| 'center-middle'
| 'top-left'
| 'top-right'
| 'bottom-left'
| 'bottom-right';
type PopupItem =
| (ItemRendererProps & { type: 'item' })
| { type: 'divider' }
| { type: 'custom'; element: HTMLElement };
type PopupProps = {
data: PopupItem[];
anchorAt?: Placement;
popupAt?: Placement;
};
export const Popup = (props: PopupProps) => {
const popup = ElementFromHtml(popupHTML);
const container = popup.querySelector<HTMLElement>(
'.music-together-popup-container',
)!;
const items = props.data
.map((props) => {
if (props.type === 'item')
return {
type: 'item' as const,
...ItemRenderer(props),
};
if (props.type === 'divider')
return {
type: 'divider' as const,
element: ElementFromHtml(
'<div class="music-together-divider horizontal"></div>',
),
};
if (props.type === 'custom')
return {
type: 'custom' as const,
element: props.element,
};
return null;
})
.filter(Boolean);
container.append(...items.map(({ element }) => element));
popup.style.setProperty('opacity', '0');
popup.style.setProperty('pointer-events', 'none');
document.body.append(popup);
return {
element: popup,
container,
items,
show(x: number, y: number, anchor?: HTMLElement) {
let left = x;
let top = y;
if (anchor) {
if (props.anchorAt?.includes('right')) left += anchor.clientWidth;
if (props.anchorAt?.includes('bottom')) top += anchor.clientHeight;
if (props.anchorAt?.includes('center')) left += anchor.clientWidth / 2;
if (props.anchorAt?.includes('middle')) top += anchor.clientHeight / 2;
}
if (props.popupAt?.includes('right')) left -= popup.clientWidth;
if (props.popupAt?.includes('bottom')) top -= popup.clientHeight;
if (props.popupAt?.includes('center')) left -= popup.clientWidth / 2;
if (props.popupAt?.includes('middle')) top -= popup.clientHeight / 2;
popup.style.setProperty('left', `${left}px`);
popup.style.setProperty('top', `${top}px`);
popup.style.setProperty('opacity', '1');
popup.style.setProperty('pointer-events', 'unset');
setTimeout(() => {
const onClose = (event: MouseEvent) => {
const isPopupClick = event
.composedPath()
.some((element) => element === popup);
if (!isPopupClick) {
this.dismiss();
document.removeEventListener('click', onClose);
}
};
document.addEventListener('click', onClose);
}, 16);
},
showAtAnchor(anchor: HTMLElement) {
const { x, y } = anchor.getBoundingClientRect();
this.show(x, y, anchor);
},
isShowing() {
return popup.style.getPropertyValue('opacity') === '1';
},
dismiss() {
popup.style.setProperty('opacity', '0');
popup.style.setProperty('pointer-events', 'none');
},
};
};
type ItemRendererProps = {
id?: string;
icon?: Element;
text: string;
onClick?: () => void;
};
export const ItemRenderer = (props: ItemRendererProps) => {
const element = ElementFromHtml(itemHTML);
const iconContainer = element.querySelector<HTMLElement>('div.icon')!;
const textContainer = element.querySelector<HTMLElement>('div.text')!;
if (props.icon) iconContainer.appendChild(props.icon);
textContainer.append(props.text);
if (props.onClick) {
element.addEventListener('click', () => {
props.onClick?.();
});
}
if (props.id) element.id = props.id;
return {
element,
setIcon(icon: Element) {
iconContainer.replaceChildren(icon);
},
setText(text: string) {
textContainer.replaceChildren(text);
},
id: props.id,
};
};

View File

@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24">
<path d="M480-640 280-440l56 56 104-103v407h80v-407l104 103 56-56-200-200ZM146-260q-32-49-49-105T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 59-17 115t-49 105l-58-58q22-37 33-78t11-84q0-134-93-227t-227-93q-134 0-227 93t-93 227q0 43 11 84t33 78l-58 58Z"/>
</svg>

Before

Width:  |  Height:  |  Size: 408 B

View File

@ -1,4 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24">
<path
d="M280-400q-33 0-56.5-23.5T200-480q0-33 23.5-56.5T280-560q33 0 56.5 23.5T360-480q0 33-23.5 56.5T280-400Zm0 160q-100 0-170-70T40-480q0-100 70-170t170-70q67 0 121.5 33t86.5 87h352l120 120-180 180-80-60-80 60-85-60h-47q-32 54-86.5 87T280-240Zm0-80q56 0 98.5-34t56.5-86h125l58 41 82-61 71 55 75-75-40-40H435q-14-52-56.5-86T280-640q-66 0-113 47t-47 113q0 66 47 113t113 47Z"/>
</svg>

Before

Width:  |  Height:  |  Size: 480 B

View File

@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24">
<path d="M560-160q-66 0-113-47t-47-113q0-66 47-113t113-47q23 0 42.5 5.5T640-458v-342h240v120H720v360q0 66-47 113t-113 47ZM80-320q0-99 38-186.5T221-659q65-65 152.5-103T560-800v80q-82 0-155 31.5t-127.5 86q-54.5 54.5-86 127T160-320H80Zm160 0q0-66 25.5-124.5t69-102Q378-590 436-615t124-25v80q-100 0-170 70t-70 170h-80Z"/>
</svg>

Before

Width:  |  Height:  |  Size: 416 B

View File

@ -1,4 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24">
<path
d="M792-56 686-160H260q-92 0-156-64T40-380q0-77 47.5-137T210-594q3-8 6-15.5t6-16.5L56-792l56-56 736 736-56 56ZM260-240h346L284-562q-2 11-3 21t-1 21h-20q-58 0-99 41t-41 99q0 58 41 99t99 41Zm185-161Zm419 191-58-56q17-14 25.5-32.5T840-340q0-42-29-71t-71-29h-60v-80q0-83-58.5-141.5T480-720q-27 0-52 6.5T380-693l-58-58q35-24 74.5-36.5T480-800q117 0 198.5 81.5T760-520q69 8 114.5 59.5T920-340q0 39-15 72.5T864-210ZM593-479Z"/>
</svg>

Before

Width:  |  Height:  |  Size: 529 B

View File

@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24">
<path d="M440-120v-240h80v80h320v80H520v80h-80Zm-320-80v-80h240v80H120Zm160-160v-80H120v-80h160v-80h80v240h-80Zm160-80v-80h400v80H440Zm160-160v-240h80v80h160v80H680v80h-80Zm-480-80v-80h400v80H120Z"/>
</svg>

Before

Width:  |  Height:  |  Size: 298 B

View File

@ -1,84 +1,10 @@
import prompt from 'custom-electron-prompt';
import { DataConnection } from 'peerjs';
import { t } from '@/i18n';
import { createPlugin } from '@/utils';
import promptOptions from '@/providers/prompt-options';
import {
getDefaultProfile,
type Permission,
type Profile,
type VideoData,
} from './types';
import { Queue } from './queue';
import { Connection, type ConnectionEventUnion } from './connection';
import { createHostPopup } from './ui/host';
import { createGuestPopup } from './ui/guest';
import { createSettingPopup } from './ui/setting';
import { onMainLoad } from './backend';
import { onRendererLoad } from './src';
import settingHTML from './templates/setting.html?raw';
import style from './style.css?inline';
import type { YoutubePlayer } from '@/types/youtube-player';
import type { RendererContext } from '@/types/contexts';
import type { VideoDataChanged } from '@/types/video-data-changed';
import type { AppElement } from '@/types/queue';
type RawAccountData = {
accountName: {
runs: { text: string }[];
};
accountPhoto: {
thumbnails: { url: string; width: number; height: number }[];
};
settingsEndpoint: unknown;
manageAccountTitle: unknown;
trackingParams: string;
channelHandle: {
runs: { text: string }[];
};
};
export default createPlugin<
unknown,
unknown,
{
connection?: Connection;
ipc?: RendererContext<never>['ipc'];
api: AppElement | null;
queue?: Queue;
playerApi?: YoutubePlayer;
showPrompt: (title: string, label: string) => Promise<string>;
popups: {
host: ReturnType<typeof createHostPopup>;
guest: ReturnType<typeof createGuestPopup>;
setting: ReturnType<typeof createSettingPopup>;
};
elements: {
setting: HTMLElement;
icon: SVGElement;
spinner: HTMLElement;
};
stateInterval?: number;
updateNext: boolean;
ignoreChange: boolean;
rollbackInjector?: () => void;
me?: Omit<Profile, 'id'>;
profiles: Record<string, Profile>;
permission: Permission;
videoChangeListener: (event: CustomEvent<VideoDataChanged>) => void;
videoStateChangeListener: () => void;
onHost: () => Promise<boolean>;
onJoin: () => Promise<boolean>;
onStop: () => void;
putProfile: (id: string, profile?: Profile) => void;
showSpinner: () => void;
hideSpinner: () => void;
initMyProfile: () => void;
}
>({
export default createPlugin({
name: () => t('plugins.music-together.name'),
description: () => t('plugins.music-together.description'),
restartNeeded: false,
@ -86,757 +12,9 @@ export default createPlugin<
config: {
enabled: false,
},
stylesheets: [style],
backend({ ipc }) {
ipc.handle('music-together:prompt', async (title: string, label: string) =>
prompt({
title,
label,
type: 'input',
...promptOptions(),
}),
);
},
stylesheets: [],
backend: onMainLoad,
renderer: {
updateNext: false,
ignoreChange: false,
permission: 'playlist',
popups: {} as {
host: ReturnType<typeof createHostPopup>;
guest: ReturnType<typeof createGuestPopup>;
setting: ReturnType<typeof createSettingPopup>;
},
elements: {} as {
setting: HTMLElement;
icon: SVGElement;
spinner: HTMLElement;
},
profiles: {},
showPrompt: () => Promise.resolve(''),
api: null,
/* events */
videoChangeListener(event: CustomEvent<VideoDataChanged>) {
if (event.detail.name === 'dataloaded' || this.updateNext) {
if (this.connection?.mode === 'host') {
const videoList: VideoData[] =
this.queue?.flatItems.map(
(it) =>
({
videoId: it!.videoId,
ownerId: this.connection!.id,
}) satisfies VideoData,
) ?? [];
this.queue?.setVideoList(videoList, false);
this.queue?.syncQueueOwner();
this.connection.broadcast('SYNC_QUEUE', {
videoList,
});
this.updateNext = event.detail.name === 'dataloaded';
}
}
},
videoStateChangeListener() {
if (this.connection?.mode !== 'guest') return;
if (this.ignoreChange) return;
if (this.permission !== 'all') return;
const state = this.playerApi?.getPlayerState();
if (state !== 1 && state !== 2) return;
this.connection.broadcast('SYNC_PROGRESS', {
// progress: this.playerApi?.getCurrentTime(),
state: this.playerApi?.getPlayerState(),
// index: this.queue?.selectedIndex ?? 0,
});
},
/* connection */
async onHost() {
this.connection = new Connection();
const wait = await this.connection.waitForReady().catch(() => null);
if (!wait) return false;
if (!this.me) this.me = getDefaultProfile(this.connection.id);
const rawItems =
this.queue?.flatItems?.map(
(it) =>
({
videoId: it!.videoId,
ownerId: this.connection!.id,
}) satisfies VideoData,
) ?? [];
this.queue?.setOwner({
id: this.connection.id,
...this.me,
});
this.queue?.setVideoList(rawItems, false);
this.queue?.syncQueueOwner();
this.queue?.initQueue();
this.queue?.injection();
this.profiles = {};
this.connection.onConnections((connection) => {
if (!connection) {
this.api?.toastService?.show(
t('plugins.music-together.toast.disconnected'),
);
this.onStop();
return;
}
if (!connection.open) {
this.api?.toastService?.show(
t('plugins.music-together.toast.user-disconnected', {
name: this.profiles[connection.peer]?.name,
}),
);
this.putProfile(connection.peer, undefined);
}
});
this.putProfile(this.connection.id, {
id: this.connection.id,
...this.me,
});
const listener = async (
event: ConnectionEventUnion,
conn?: DataConnection,
) => {
this.ignoreChange = true;
switch (event.type) {
case 'ADD_SONGS': {
if (conn && this.permission === 'host-only') return;
await this.queue?.addVideos(
event.payload.videoList,
event.payload.index,
);
await this.connection?.broadcast('ADD_SONGS', event.payload);
break;
}
case 'REMOVE_SONG': {
if (conn && this.permission === 'host-only') return;
this.queue?.removeVideo(event.payload.index);
await this.connection?.broadcast('REMOVE_SONG', event.payload);
break;
}
case 'MOVE_SONG': {
if (conn && this.permission === 'host-only') {
await this.connection?.broadcast('SYNC_QUEUE', {
videoList: this.queue?.videoList ?? [],
});
break;
}
this.queue?.moveItem(
event.payload.fromIndex,
event.payload.toIndex,
);
await this.connection?.broadcast('MOVE_SONG', event.payload);
break;
}
case 'IDENTIFY': {
if (!event.payload || !conn) {
console.warn(
'Music Together [Host]: Received "IDENTIFY" event without payload or connection',
);
break;
}
this.api?.toastService?.show(
t('plugins.music-together.toast.user-connected', {
name: event.payload.profile.name,
}),
);
this.putProfile(conn.peer, event.payload.profile);
break;
}
case 'SYNC_PROFILE': {
await this.connection?.broadcast('SYNC_PROFILE', {
profiles: this.profiles,
});
break;
}
case 'PERMISSION': {
await this.connection?.broadcast('PERMISSION', this.permission);
this.popups.guest.setPermission(this.permission);
this.popups.host.setPermission(this.permission);
this.popups.setting.setPermission(this.permission);
break;
}
case 'SYNC_QUEUE': {
await this.connection?.broadcast('SYNC_QUEUE', {
videoList: this.queue?.videoList ?? [],
});
break;
}
case 'SYNC_PROGRESS': {
let permissionLevel = 0;
if (this.permission === 'all') permissionLevel = 2;
if (this.permission === 'playlist') permissionLevel = 1;
if (this.permission === 'host-only') permissionLevel = 0;
if (!conn) permissionLevel = 3;
if (permissionLevel >= 2) {
if (typeof event.payload?.progress === 'number') {
const currentTime = this.playerApi?.getCurrentTime() ?? 0;
if (Math.abs(event.payload.progress - currentTime) > 3)
this.playerApi?.seekTo(event.payload.progress);
}
if (this.playerApi?.getPlayerState() !== event.payload?.state) {
if (event.payload?.state === 2) this.playerApi?.pauseVideo();
if (event.payload?.state === 1) this.playerApi?.playVideo();
}
}
if (permissionLevel >= 1) {
if (typeof event.payload?.index === 'number') {
const nowIndex = this.queue?.selectedIndex ?? 0;
if (nowIndex !== event.payload.index) {
this.queue?.setIndex(event.payload.index);
}
}
}
break;
}
default: {
console.warn('Music Together [Host]: Unknown Event', event);
break;
}
}
if (event.after) {
const now = event.after.shift();
if (now) {
now.after = event.after;
await listener(now, conn);
}
}
};
this.connection.on(listener);
this.queue?.on(listener);
setTimeout(() => {
this.ignoreChange = false;
}, 16); // wait 1 frame
return true;
},
async onJoin() {
this.connection = new Connection();
const wait = await this.connection.waitForReady().catch(() => null);
if (!wait) return false;
this.profiles = {};
const id = await this.showPrompt(
t('plugins.music-together.name'),
t('plugins.music-together.dialog.enter-host'),
);
if (typeof id !== 'string') return false;
const connection = await this.connection.connect(id).catch(() => false);
if (!connection) return false;
this.connection.onConnections((connection) => {
if (!connection?.open) {
this.api?.toastService?.show(
t('plugins.music-together.toast.disconnected'),
);
this.onStop();
}
});
let resolveIgnore: number | null = null;
const listener = async (event: ConnectionEventUnion) => {
this.ignoreChange = true;
switch (event.type) {
case 'ADD_SONGS': {
await this.queue?.addVideos(
event.payload.videoList,
event.payload.index,
);
break;
}
case 'REMOVE_SONG': {
this.queue?.removeVideo(event.payload.index);
break;
}
case 'MOVE_SONG': {
this.queue?.moveItem(
event.payload.fromIndex,
event.payload.toIndex,
);
break;
}
case 'IDENTIFY': {
console.warn(
'Music Together [Guest]: Received "IDENTIFY" event from guest',
);
break;
}
case 'SYNC_QUEUE': {
if (Array.isArray(event.payload?.videoList)) {
await this.queue?.setVideoList(event.payload.videoList);
}
break;
}
case 'SYNC_PROFILE': {
if (!event.payload) {
console.warn(
'Music Together [Guest]: Received "SYNC_PROFILE" event without payload',
);
break;
}
Object.entries(event.payload.profiles).forEach(([id, profile]) => {
this.putProfile(id, profile);
});
break;
}
case 'SYNC_PROGRESS': {
if (typeof event.payload?.progress === 'number') {
const currentTime = this.playerApi?.getCurrentTime() ?? 0;
if (Math.abs(event.payload.progress - currentTime) > 3)
this.playerApi?.seekTo(event.payload.progress);
}
if (this.playerApi?.getPlayerState() !== event.payload?.state) {
if (event.payload?.state === 2) this.playerApi?.pauseVideo();
if (event.payload?.state === 1) this.playerApi?.playVideo();
}
if (typeof event.payload?.index === 'number') {
const nowIndex = this.queue?.selectedIndex ?? 0;
if (nowIndex !== event.payload.index) {
this.queue?.setIndex(event.payload.index);
}
}
break;
}
case 'PERMISSION': {
if (!event.payload) {
console.warn(
'Music Together [Guest]: Received "PERMISSION" event without payload',
);
break;
}
this.permission = event.payload;
this.popups.guest.setPermission(this.permission);
this.popups.host.setPermission(this.permission);
this.popups.setting.setPermission(this.permission);
const permissionLabel = t(
`plugins.music-together.menu.permission.${this.permission}`,
);
this.api?.toastService?.show(
t('plugins.music-together.toast.permission-changed', {
permission: permissionLabel,
}),
);
break;
}
default: {
console.warn('Music Together [Guest]: Unknown Event', event);
break;
}
}
if (typeof resolveIgnore === 'number') clearTimeout(resolveIgnore);
resolveIgnore = window.setTimeout(() => {
this.ignoreChange = false;
}, 16); // wait 1 frame
};
this.connection.on(listener);
this.queue?.on(async (event: ConnectionEventUnion) => {
this.ignoreChange = true;
switch (event.type) {
case 'ADD_SONGS': {
await this.connection?.broadcast('ADD_SONGS', event.payload);
await this.connection?.broadcast('SYNC_QUEUE', undefined);
break;
}
case 'REMOVE_SONG': {
await this.connection?.broadcast('REMOVE_SONG', event.payload);
break;
}
case 'MOVE_SONG': {
await this.connection?.broadcast('MOVE_SONG', event.payload);
await this.connection?.broadcast('SYNC_QUEUE', undefined);
break;
}
case 'SYNC_PROGRESS': {
if (this.permission === 'host-only')
await this.connection?.broadcast('SYNC_QUEUE', undefined);
else
await this.connection?.broadcast('SYNC_PROGRESS', event.payload);
break;
}
}
if (typeof resolveIgnore === 'number') clearTimeout(resolveIgnore);
resolveIgnore = window.setTimeout(() => {
this.ignoreChange = false;
}, 16); // wait 1 frame
});
if (!this.me) this.me = getDefaultProfile(this.connection.id);
this.queue?.injection();
this.queue?.setOwner({
id: this.connection.id,
...this.me,
});
const progress = Array.from(
document.querySelectorAll<
HTMLElement & {
_update: (...args: unknown[]) => void;
}
>('tp-yt-paper-progress'),
);
const rollbackList = progress.map((progress) => {
const original = progress._update;
progress._update = (...args) => {
const now = args[0];
if (this.permission === 'all' && typeof now === 'number') {
const currentTime = this.playerApi?.getCurrentTime() ?? 0;
if (Math.abs(now - currentTime) > 3)
this.connection?.broadcast('SYNC_PROGRESS', {
progress: now,
state: this.playerApi?.getPlayerState(),
});
}
original.call(progress, ...args);
};
return () => {
progress._update = original;
};
});
this.rollbackInjector = () => {
rollbackList.forEach((rollback) => rollback());
};
this.connection.broadcast('IDENTIFY', {
profile: {
id: this.connection.id,
handleId: this.me.handleId,
name: this.me.name,
thumbnail: this.me.thumbnail,
},
});
this.connection.broadcast('SYNC_PROFILE', undefined);
this.connection.broadcast('PERMISSION', undefined);
this.queue?.clear();
this.queue?.syncQueueOwner();
this.queue?.initQueue();
this.connection.broadcast('SYNC_QUEUE', undefined);
return true;
},
onStop() {
this.connection?.disconnect();
this.queue?.rollbackInjection();
this.queue?.removeQueueOwner();
if (this.rollbackInjector) {
this.rollbackInjector();
this.rollbackInjector = undefined;
}
this.profiles = {};
this.popups.host.setUsers(Object.values(this.profiles));
this.popups.guest.setUsers(Object.values(this.profiles));
this.popups.host.dismiss();
this.popups.guest.dismiss();
this.popups.setting.dismiss();
},
/* methods */
putProfile(id: string, profile?: Profile) {
if (profile === undefined) {
delete this.profiles[id];
} else {
this.profiles[id] = profile;
}
this.popups.host.setUsers(Object.values(this.profiles));
this.popups.guest.setUsers(Object.values(this.profiles));
},
showSpinner() {
this.elements.icon.style.setProperty('display', 'none');
this.elements.spinner.removeAttribute('hidden');
this.elements.spinner.setAttribute('active', '');
},
hideSpinner() {
this.elements.icon.style.removeProperty('display');
this.elements.spinner.removeAttribute('active');
this.elements.spinner.setAttribute('hidden', '');
},
initMyProfile() {
const accountButton = document.querySelector<
HTMLElement & {
onButtonTap: () => void;
}
>('ytmusic-settings-button');
accountButton?.onButtonTap();
setTimeout(() => {
accountButton?.onButtonTap();
const renderer = document.querySelector<
HTMLElement & { data: unknown }
>('ytd-active-account-header-renderer');
if (!accountButton || !renderer) {
console.warn('Music Together: Cannot find account');
this.me = getDefaultProfile(this.connection?.id ?? '');
return;
}
const accountData = renderer.data as RawAccountData;
this.me = {
handleId:
accountData.channelHandle.runs[0].text ??
accountData.accountName.runs[0].text,
name: accountData.accountName.runs[0].text,
thumbnail: accountData.accountPhoto.thumbnails[0].url,
};
if (this.me.thumbnail) {
this.popups.host.setProfile(this.me.thumbnail);
this.popups.guest.setProfile(this.me.thumbnail);
this.popups.setting.setProfile(this.me.thumbnail);
}
}, 0);
},
/* hooks */
start({ ipc }) {
this.ipc = ipc;
this.showPrompt = async (title: string, label: string) =>
ipc.invoke('music-together:prompt', title, label) as Promise<string>;
this.api = document.querySelector<AppElement>('ytmusic-app');
/* setup */
document
.querySelector('#right-content > ytmusic-settings-button')
?.insertAdjacentHTML('beforebegin', settingHTML);
const setting = document.querySelector<HTMLElement>(
'#music-together-setting-button',
);
const icon = document.querySelector<SVGElement>(
'#music-together-setting-button > svg',
);
const spinner = document.querySelector<HTMLElement>(
'#music-together-setting-button > tp-yt-paper-spinner-lite',
);
if (!setting || !icon || !spinner) {
console.warn('Music Together: Cannot inject html');
console.log(setting, icon, spinner);
return;
}
this.elements = {
setting,
icon,
spinner,
};
this.stateInterval = window.setInterval(() => {
if (this.connection?.mode !== 'host') return;
const index = this.queue?.selectedIndex ?? 0;
this.connection.broadcast('SYNC_PROGRESS', {
progress: this.playerApi?.getCurrentTime(),
state: this.playerApi?.getPlayerState(),
index,
});
}, 1000);
/* UI */
const hostPopup = createHostPopup({
onItemClick: (id) => {
if (id === 'music-together-close') {
this.onStop();
this.api?.toastService?.show(
t('plugins.music-together.toast.closed'),
);
hostPopup.dismiss();
}
if (id === 'music-together-copy-id') {
navigator.clipboard
.writeText(this.connection?.id ?? '')
.then(() => {
this.api?.toastService?.show(
t('plugins.music-together.toast.id-copied'),
);
hostPopup.dismiss();
})
.catch(() => {
this.api?.toastService?.show(
t('plugins.music-together.toast.id-copy-failed'),
);
hostPopup.dismiss();
});
}
if (id === 'music-together-permission') {
if (this.permission === 'all') this.permission = 'host-only';
else if (this.permission === 'playlist') this.permission = 'all';
else if (this.permission === 'host-only')
this.permission = 'playlist';
this.connection?.broadcast('PERMISSION', this.permission);
hostPopup.setPermission(this.permission);
guestPopup.setPermission(this.permission);
settingPopup.setPermission(this.permission);
const permissionLabel = t(
`plugins.music-together.menu.permission.${this.permission}`,
);
this.api?.toastService?.show(
t('plugins.music-together.toast.permission-changed', {
permission: permissionLabel,
}),
);
const item = hostPopup.items.find((it) => it?.element.id === id);
if (item?.type === 'item') {
item.setText(t('plugins.music-together.menu.set-permission'));
}
}
},
});
const guestPopup = createGuestPopup({
onItemClick: (id) => {
if (id === 'music-together-disconnect') {
this.onStop();
this.api?.toastService?.show(
t('plugins.music-together.toast.disconnected'),
);
guestPopup.dismiss();
}
},
});
const settingPopup = createSettingPopup({
onItemClick: async (id) => {
if (id === 'music-together-host') {
settingPopup.dismiss();
this.showSpinner();
const result = await this.onHost();
this.hideSpinner();
if (result) {
navigator.clipboard
.writeText(this.connection?.id ?? '')
.then(() => {
this.api?.toastService?.show(
t('plugins.music-together.toast.id-copied'),
);
hostPopup.showAtAnchor(setting);
})
.catch(() => {
this.api?.toastService?.show(
t('plugins.music-together.toast.id-copy-failed'),
);
hostPopup.showAtAnchor(setting);
});
} else {
this.api?.toastService?.show(
t('plugins.music-together.toast.host-failed'),
);
}
}
if (id === 'music-together-join') {
settingPopup.dismiss();
this.showSpinner();
const result = await this.onJoin();
this.hideSpinner();
if (result) {
this.api?.toastService?.show(
t('plugins.music-together.toast.joined'),
);
guestPopup.showAtAnchor(setting);
} else {
this.api?.toastService?.show(
t('plugins.music-together.toast.join-failed'),
);
}
}
},
});
this.popups = {
host: hostPopup,
guest: guestPopup,
setting: settingPopup,
};
setting.addEventListener('click', () => {
let popup = settingPopup;
if (this.connection?.mode === 'host') popup = hostPopup;
if (this.connection?.mode === 'guest') popup = guestPopup;
if (popup.isShowing()) popup.dismiss();
else popup.showAtAnchor(setting);
});
/* account data getter */
this.initMyProfile();
},
onPlayerApiReady(playerApi) {
this.queue = new Queue({
owner: {
id: this.connection?.id ?? '',
...this.me!,
},
getProfile: (id) => this.profiles[id],
});
this.playerApi = playerApi;
this.playerApi.addEventListener(
'onStateChange',
this.videoStateChangeListener,
);
document.addEventListener('videodatachange', this.videoChangeListener);
},
stop() {
const dividers = Array.from(
document.querySelectorAll('.music-together-divider'),
);
dividers.forEach((divider) => divider.remove());
this.elements.setting?.remove();
this.onStop();
if (typeof this.stateInterval === 'number')
clearInterval(this.stateInterval);
if (this.playerApi)
this.playerApi.removeEventListener(
'onStateChange',
this.videoStateChangeListener,
);
if (this.videoChangeListener)
document.removeEventListener(
'videodatachange',
this.videoChangeListener,
);
},
start: onRendererLoad,
},
});

View File

@ -1,48 +0,0 @@
import { SHA1Hash } from './sha1hash';
export const extractToken = (cookie = document.cookie) =>
cookie.match(/SAPISID=([^;]+);/)?.[1] ??
cookie.match(/__Secure-3PAPISID=([^;]+);/)?.[1];
export const getHash = async (
papisid: string,
millis = Date.now(),
origin: string = 'https://music.youtube.com',
) => (await SHA1Hash(`${millis} ${papisid} ${origin}`)).toLowerCase();
export const getAuthorizationHeader = async (
papisid: string,
millis = Date.now(),
origin: string = 'https://music.youtube.com',
) => {
return `SAPISIDHASH ${millis}_${await getHash(papisid, millis, origin)}`;
};
export const getClient = () => {
return {
hl: navigator.language.split('-')[0] ?? 'en',
gl: navigator.language.split('-')[1] ?? 'US',
deviceMake: '',
deviceModel: '',
userAgent: navigator.userAgent,
clientName: 'WEB_REMIX',
clientVersion: '1.20231208.05.02',
osName: '',
osVersion: '',
platform: 'DESKTOP',
timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
locationInfo: {
locationPermissionAuthorizationStatus:
'LOCATION_PERMISSION_AUTHORIZATION_STATUS_UNSUPPORTED',
},
musicAppInfo: {
pwaInstallabilityStatus: 'PWA_INSTALLABILITY_STATUS_UNKNOWN',
webDisplayMode: 'WEB_DISPLAY_MODE_BROWSER',
storeDigitalGoodsApiSupportStatus: {
playStoreDigitalGoodsApiSupportStatus:
'DIGITAL_GOODS_API_SUPPORT_STATUS_UNSUPPORTED',
},
},
utcOffsetMinutes: -1 * new Date().getTimezoneOffset(),
};
};

View File

@ -1 +0,0 @@
export * from './queue';

View File

@ -1,544 +0,0 @@
import { getMusicQueueRenderer } from './song';
import { mapQueueItem } from './utils';
import { t } from '@/i18n';
import type { ConnectionEventUnion } from '@/plugins/music-together/connection';
import type { Profile, VideoData } from '../types';
import type { QueueItem } from '@/types/datahost-get-state';
import type { QueueElement, Store } from '@/types/queue';
const getHeaderPayload = (() => {
let payload: {
items?: QueueItem[] | undefined;
title: {
runs: {
text: string;
}[];
};
subtitle: {
runs: {
text: string;
}[];
};
buttons: {
chipCloudChipRenderer: {
style: {
styleType: string;
};
text: {
runs: {
text: string;
}[];
};
navigationEndpoint: {
saveQueueToPlaylistCommand: unknown;
};
icon: {
iconType: string;
};
accessibilityData: {
accessibilityData: {
label: string;
};
};
isSelected: boolean;
uniqueId: string;
};
}[];
} | null = null;
return () => {
if (!payload) {
payload = {
title: {
runs: [
{
text: t('plugins.music-together.internal.track-source'),
},
],
},
subtitle: {
runs: [
{
text: t('plugins.music-together.name'),
},
],
},
buttons: [
{
chipCloudChipRenderer: {
style: {
styleType: 'STYLE_TRANSPARENT',
},
text: {
runs: [
{
text: t('plugins.music-together.internal.save'),
},
],
},
navigationEndpoint: {
saveQueueToPlaylistCommand: {},
},
icon: {
iconType: 'ADD_TO_PLAYLIST',
},
accessibilityData: {
accessibilityData: {
label: t('plugins.music-together.internal.save'),
},
},
isSelected: false,
uniqueId: t('plugins.music-together.internal.save'),
},
},
],
};
}
return payload;
};
})();
export type QueueOptions = {
videoList?: VideoData[];
owner?: Profile;
queue?: QueueElement;
getProfile: (id: string) => Profile | undefined;
};
export type QueueEventListener = (event: ConnectionEventUnion) => void;
export class Queue {
private readonly queue: QueueElement;
private originalDispatch?: (obj: {
type: string;
payload?: { items?: QueueItem[] | undefined };
}) => void;
private internalDispatch = false;
private ignoreFlag = false;
private listeners: QueueEventListener[] = [];
private owner: Profile | null;
private readonly getProfile: (id: string) => Profile | undefined;
constructor(options: QueueOptions) {
this.getProfile = options.getProfile;
this.queue =
options.queue ?? document.querySelector<QueueElement>('#queue')!;
this.owner = options.owner ?? null;
this._videoList = options.videoList ?? [];
}
private _videoList: VideoData[] = [];
/* utils */
get videoList() {
return this._videoList;
}
get selectedIndex() {
return (
mapQueueItem(
(it) => it?.selected,
this.queue.queue.store.store.getState().queue.items,
).findIndex(Boolean) ?? 0
);
}
get rawItems() {
return this.queue?.queue.store.store.getState().queue.items;
}
get flatItems() {
return mapQueueItem((it) => it, this.rawItems);
}
setOwner(owner: Profile) {
this.owner = owner;
}
/* public */
async setVideoList(videoList: VideoData[], sync = true) {
this._videoList = videoList;
if (sync) await this.syncVideo();
}
async addVideos(videos: VideoData[], index?: number) {
const response = await getMusicQueueRenderer(
videos.map((it) => it.videoId),
);
if (!response) return false;
const items = response.queueDatas.map((it) => it?.content).filter(Boolean);
if (!items) return false;
this.internalDispatch = true;
this._videoList.push(...videos);
this.queue?.dispatch({
type: 'ADD_ITEMS',
payload: {
nextQueueItemId:
this.queue.queue.store.store.getState().queue.nextQueueItemId,
index:
index ??
this.queue.queue.store.store.getState().queue.items.length ??
0,
items,
shuffleEnabled: false,
shouldAssignIds: true,
},
});
this.internalDispatch = false;
setTimeout(() => {
this.initQueue();
this.syncQueueOwner();
}, 0);
return true;
}
removeVideo(index: number) {
this.internalDispatch = true;
this._videoList.splice(index, 1);
this.queue?.dispatch({
type: 'REMOVE_ITEM',
payload: index,
});
this.internalDispatch = false;
setTimeout(() => {
this.initQueue();
this.syncQueueOwner();
}, 0);
}
setIndex(index: number) {
this.internalDispatch = true;
this.queue?.dispatch({
type: 'SET_INDEX',
payload: index,
});
this.internalDispatch = false;
}
moveItem(fromIndex: number, toIndex: number) {
this.internalDispatch = true;
const data = this._videoList.splice(fromIndex, 1)[0];
this._videoList.splice(toIndex, 0, data);
this.queue?.dispatch({
type: 'MOVE_ITEM',
payload: {
fromIndex,
toIndex,
},
});
this.internalDispatch = false;
setTimeout(() => {
this.initQueue();
this.syncQueueOwner();
}, 0);
}
clear() {
this.internalDispatch = true;
this._videoList = [];
this.queue?.dispatch({
type: 'CLEAR',
});
this.internalDispatch = false;
}
on(listener: QueueEventListener) {
this.listeners.push(listener);
}
off(listener: QueueEventListener) {
this.listeners = this.listeners.filter((it) => it !== listener);
}
rollbackInjection() {
if (!this.queue) {
console.error('Queue is not initialized!');
return;
}
if (this.originalDispatch)
this.queue.queue.store.store.dispatch = this
.originalDispatch as Store['dispatch'];
}
injection() {
if (!this.queue) {
console.error('Queue is not initialized!');
return;
}
this.originalDispatch = this.queue.queue.store.store.dispatch;
this.queue.queue.store.store.dispatch = (event) => {
if (!this.queue || !this.owner) {
console.error('Queue is not initialized!');
return;
}
if (!this.internalDispatch) {
if (event.type === 'CLEAR') {
this.ignoreFlag = true;
}
if (event.type === 'ADD_ITEMS') {
if (this.ignoreFlag) {
this.ignoreFlag = false;
const videoList = mapQueueItem(
(it) =>
({
videoId: it!.videoId,
ownerId: this.owner!.id,
}) satisfies VideoData,
(
event.payload! as {
items: QueueItem[];
}
).items,
);
const index = this._videoList.length + videoList.length - 1;
if (videoList.length > 0) {
this.broadcast({
// play
type: 'ADD_SONGS',
payload: {
videoList,
},
after: [
{
type: 'SYNC_PROGRESS',
payload: {
index,
},
},
],
});
}
} else if (
(
event.payload as {
items: unknown[];
}
).items.length === 1
) {
this.broadcast({
// add playlist
type: 'ADD_SONGS',
payload: {
// index: (event.payload as any).index,
videoList: mapQueueItem(
(it) =>
({
videoId: it!.videoId,
ownerId: this.owner!.id,
}) satisfies VideoData,
(
event.payload! as {
items: QueueItem[];
}
).items,
),
},
});
}
return;
}
if (event.type === 'MOVE_ITEM') {
this.broadcast({
type: 'MOVE_SONG',
payload: {
fromIndex: (
event.payload as {
fromIndex: number;
}
).fromIndex,
toIndex: (
event.payload as {
toIndex: number;
}
).toIndex,
},
});
return;
}
if (event.type === 'REMOVE_ITEM') {
this.broadcast({
type: 'REMOVE_SONG',
payload: {
index: event.payload as number,
},
});
return;
}
if (event.type === 'SET_INDEX') {
this.broadcast({
type: 'SYNC_PROGRESS',
payload: {
index: event.payload as number,
},
});
return;
}
if (event.type === 'SET_HEADER') event.payload = getHeaderPayload();
if (event.type === 'ADD_STEERING_CHIPS') {
event.type = 'CLEAR_STEERING_CHIPS';
event.payload = undefined;
}
if (event.type === 'SET_PLAYER_UI_STATE') {
if (
(event.payload as string) === 'INACTIVE' &&
this.videoList.length > 0
) {
return;
}
}
if (event.type === 'HAS_SHOWN_AUTOPLAY') return;
if (event.type === 'ADD_AUTOMIX_ITEMS') return;
}
const fakeContext = {
...this.queue,
queue: {
...this.queue.queue,
store: {
...this.queue.queue.store,
dispatch: this.originalDispatch,
},
},
};
this.originalDispatch?.call(
fakeContext,
event as {
type: string;
payload?: { items?: QueueItem[] | undefined } | undefined;
},
);
};
}
/* sync */
initQueue() {
if (!this.queue) return;
this.internalDispatch = true;
this.queue.dispatch({
type: 'HAS_SHOWN_AUTOPLAY',
payload: false,
});
this.queue.dispatch({
type: 'SET_HEADER',
payload: getHeaderPayload(),
});
this.queue.dispatch({
type: 'CLEAR_STEERING_CHIPS',
});
this.internalDispatch = false;
}
async syncVideo() {
const response = await getMusicQueueRenderer(
this._videoList.map((it) => it.videoId),
);
if (!response) return false;
const items = response.queueDatas.map((it) => it.content);
this.internalDispatch = true;
this.queue?.dispatch({
type: 'UPDATE_ITEMS',
payload: {
items: items,
nextQueueItemId:
this.queue.queue.store.store.getState().queue.nextQueueItemId,
shouldAssignIds: true,
currentIndex: -1,
},
});
this.internalDispatch = false;
setTimeout(() => {
this.initQueue();
this.syncQueueOwner();
}, 0);
return true;
}
syncQueueOwner() {
const allQueue = document.querySelectorAll('#queue');
allQueue.forEach((queue) => {
const list = Array.from(
queue?.querySelectorAll<HTMLElement>('ytmusic-player-queue-item') ?? [],
);
list.forEach((item, index: number | undefined) => {
if (typeof index !== 'number') return;
const id = this._videoList[index]?.ownerId;
const data = this.getProfile(id);
const profile =
item.querySelector<HTMLImageElement>('.music-together-owner') ??
document.createElement('img');
profile.classList.add('music-together-owner');
profile.dataset.id = id;
profile.dataset.index = index.toString();
const name =
item.querySelector<HTMLElement>('.music-together-name') ??
document.createElement('div');
name.classList.add('music-together-name');
name.textContent =
data?.name ?? t('plugins.music-together.internal.unknown-user');
if (data) {
profile.dataset.thumbnail = data.thumbnail ?? '';
profile.dataset.name = data.name ?? '';
profile.dataset.handleId = data.handleId ?? '';
profile.dataset.id = data.id ?? '';
profile.src = data.thumbnail ?? '';
profile.title = data.name ?? '';
profile.alt = data.handleId ?? '';
}
if (!profile.isConnected) item.append(profile);
if (!name.isConnected) item.append(name);
});
});
}
removeQueueOwner() {
const allQueue = document.querySelectorAll('#queue');
allQueue.forEach((queue) => {
const list = Array.from(
queue?.querySelectorAll<HTMLElement>('ytmusic-player-queue-item') ?? [],
);
list.forEach((item) => {
const profile = item.querySelector<HTMLImageElement>(
'.music-together-owner',
);
const name = item.querySelector<HTMLElement>('.music-together-name');
profile?.remove();
name?.remove();
});
});
}
/* private */
private broadcast(event: ConnectionEventUnion) {
this.listeners.forEach((listener) => listener(event));
}
}

View File

@ -1,7 +0,0 @@
export const SHA1Hash = async (str: string) => {
const enc = new TextEncoder();
const hash = await crypto.subtle.digest('SHA-1', enc.encode(str));
return Array.from(new Uint8Array(hash))
.map((v) => v.toString(16).padStart(2, '0'))
.join('');
};

View File

@ -1,50 +0,0 @@
import { extractToken, getAuthorizationHeader, getClient } from './client';
type QueueRendererResponse = {
queueDatas: {
content: unknown;
}[];
responseContext: unknown;
trackingParams: string;
};
export const getMusicQueueRenderer = async (
videoIds: string[],
): Promise<QueueRendererResponse | null> => {
const token = extractToken();
if (!token) return null;
const response = await fetch(
'https://music.youtube.com/youtubei/v1/music/get_queue?key=AIzaSyC9XL3ZjWddXya6X74dJoCTL-WEYFDNX30&prettyPrint=false',
{
method: 'POST',
credentials: 'include',
body: JSON.stringify({
context: {
client: getClient(),
request: {
useSsl: true,
internalExperimentFlags: [],
consistencyTokenJars: [],
},
user: {
lockedSafetyMode: false,
},
},
videoIds,
}),
headers: {
'Content-Type': 'application/json',
'Origin': 'https://music.youtube.com',
'Authorization': await getAuthorizationHeader(token),
},
},
);
const text = await response.text();
try {
return JSON.parse(text) as QueueRendererResponse;
} catch {}
return null;
};

View File

@ -1,26 +0,0 @@
import {
ItemPlaylistPanelVideoRenderer,
PlaylistPanelVideoWrapperRenderer,
QueueItem,
} from '@/types/datahost-get-state';
export const mapQueueItem = <T>(
map: (item?: ItemPlaylistPanelVideoRenderer) => T,
array: QueueItem[],
): T[] =>
array
.map((item) => {
if ('playlistPanelVideoWrapperRenderer' in item) {
const keys = Object.keys(
item.playlistPanelVideoWrapperRenderer!.primaryRenderer,
) as (keyof PlaylistPanelVideoWrapperRenderer['primaryRenderer'])[];
return item.playlistPanelVideoWrapperRenderer!.primaryRenderer[keys[0]];
}
if ('playlistPanelVideoRenderer' in item) {
return item.playlistPanelVideoRenderer;
}
console.error('Music Together: Unknown item', item);
return undefined;
})
.map(map);

View File

@ -0,0 +1,93 @@
import { createSignal, Show } from 'solid-js';
import { css } from 'solid-styled-components';
import { useFloating } from 'solid-floating-ui';
import { autoUpdate, flip, offset } from '@floating-ui/dom';
import { Portal } from 'solid-js/web';
import { cacheNoArgs } from '@/providers/decorators';
import { MusicTogetherPanel } from '@/plugins/music-together/src/Panel';
const buttonStyle = cacheNoArgs(
() => css`
display: inline-flex;
cursor: pointer;
margin-left: 8px;
margin-right: 16px;
& svg {
width: 24px;
height: 24px;
fill: rgba(255, 255, 255, 0.5);
}
&:hover svg:hover {
fill: #fff;
}
`,
);
const popupStyle = cacheNoArgs(
() => css`
position: fixed;
top: var(--offset-y, 0);
left: var(--offset-x, 0);
z-index: 1000;
`,
);
export const MusicTogetherButton = () => {
const [enabled, setEnabled] = createSignal(false);
const [anchor, setAnchor] = createSignal<HTMLElement | null>(null);
const [panel, setPanel] = createSignal<HTMLElement | null>(null);
const position = useFloating(anchor, panel, {
whileElementsMounted: autoUpdate,
strategy: 'fixed',
placement: 'bottom-end',
middleware: [
offset({
mainAxis: 4,
crossAxis: 0,
}),
flip({ fallbackStrategy: 'bestFit' }),
],
});
return (
<div
id="music-together-setting-button"
class={`${buttonStyle()} style-scope ytmusic-nav-bar`}
>
<svg
ref={setAnchor}
xmlns="http://www.w3.org/2000/svg"
height="24"
viewBox="0 -960 960 960"
width="24"
onClick={() => setEnabled(!enabled())}
>
<path d="M0-240v-63q0-43 44-70t116-27q13 0 25 .5t23 2.5q-14 21-21 44t-7 48v65H0Zm240 0v-65q0-32 17.5-58.5T307-410q32-20 76.5-30t96.5-10q53 0 97.5 10t76.5 30q32 20 49 46.5t17 58.5v65H240Zm540 0v-65q0-26-6.5-49T754-397q11-2 22.5-2.5t23.5-.5q72 0 116 26.5t44 70.5v63H780Zm-455-80h311q-10-20-55.5-35T480-370q-55 0-100.5 15T325-320ZM160-440q-33 0-56.5-23.5T80-520q0-34 23.5-57t56.5-23q34 0 57 23t23 57q0 33-23 56.5T160-440Zm640 0q-33 0-56.5-23.5T720-520q0-34 23.5-57t56.5-23q34 0 57 23t23 57q0 33-23 56.5T800-440Zm-320-40q-50 0-85-35t-35-85q0-51 35-85.5t85-34.5q51 0 85.5 34.5T600-600q0 50-34.5 85T480-480Zm0-80q17 0 28.5-11.5T520-600q0-17-11.5-28.5T480-640q-17 0-28.5 11.5T440-600q0 17 11.5 28.5T480-560Zm1 240Zm-1-280Z" />
</svg>
<Show when={enabled()}>
<Portal>
<div
ref={setPanel}
class={popupStyle()}
style={{
'--offset-x': `${position.x}px`,
'--offset-y': `${position.y}px`,
}}
>
<MusicTogetherPanel />
</div>
</Portal>
</Show>
</div>
);
};

View File

@ -0,0 +1,154 @@
import { css } from 'solid-styled-components';
import { createEffect, Match, Switch } from 'solid-js';
import { PanelItem } from './PanelItem';
import { MusicTogetherStatus } from './Status';
import {
IconConnect,
IconKey,
IconMusicCast,
IconOff,
IconTune,
} from './icons';
import { cacheNoArgs } from '@/providers/decorators';
import { t } from '@/i18n';
import { AppElement } from '@/types/queue';
import { Host } from '../api/host';
import { Guest } from '../api/guest';
import { Connection } from '../connection';
import { useToast } from '../context/ToastContext';
import { setStatus, status } from '../store/status';
import { connection, setConnection } from '../store/connection';
import { useRendererContext } from '../context/RendererContext';
const panelStyle = cacheNoArgs(
() => css`
border-radius: 10px !important;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
`,
);
const horizontalDividerStyle = cacheNoArgs(
() => css`
background-color: rgba(255, 255, 255, 0.15);
width: 100%;
height: 1px;
`,
);
export const MusicTogetherPanel = () => {
const show = useToast();
const { ipc } = useRendererContext();
const onHost = async () => {
setStatus('mode', 'connecting');
const result = new Connection();
await result.waitForReady();
setStatus('mode', 'host');
setConnection(result);
await onHostCopy();
};
const onHostCopy = async () => {
const id = connection()?.id;
if (!id) {
show(t('plugins.music-together.toast.id-copy-failed'));
return;
}
const success = await navigator.clipboard
.writeText(id)
.then(() => true)
.catch(() => false);
if (!success) {
show(t('plugins.music-together.toast.id-copy-failed'));
return;
}
show(t('plugins.music-together.toast.id-copied', { id }));
};
const onClose = () => {
setStatus('mode', 'disconnected');
connection()?.disconnect();
setConnection(null);
show(t('plugins.music-together.toast.closed'));
};
createEffect(() => {
const conn = connection();
const mode = status.mode;
const app = document.querySelector<AppElement>('ytmusic-app');
if (conn && app) {
if (mode === 'host') {
const listener = Host.buildListener(conn, {
ipc,
app,
});
conn.on(listener);
}
if (mode === 'guest') {
const listener = Guest.buildListener(conn, {
ipc,
app,
});
conn.on(listener);
}
}
});
return (
<tp-yt-paper-listbox
class={`style-scope ytmusic-menu-popup-renderer ${panelStyle()}`}
>
<MusicTogetherStatus />
<Switch>
<Match when={status.mode === 'disconnected'}>
<div class={horizontalDividerStyle()} />
<PanelItem
text={t('plugins.music-together.menu.host')}
icon={<IconMusicCast width={24} height={24} />}
onClick={onHost}
/>
<PanelItem
text={t('plugins.music-together.menu.join')}
icon={<IconConnect width={24} height={24} />}
/>
</Match>
<Match when={status.mode === 'host'}>
<div class={horizontalDividerStyle()} />
<PanelItem
text={t('plugins.music-together.menu.click-to-copy-id')}
icon={<IconKey width={24} height={24} />}
onClick={onHostCopy}
/>
<PanelItem
text={t('plugins.music-together.menu.set-permission', {
permission: t('plugins.music-together.menu.permission.host-only'),
})}
icon={<IconTune width={24} height={24} />}
/>
<div class={horizontalDividerStyle()} />
<PanelItem
text={t('plugins.music-together.menu.close')}
icon={<IconOff width={24} height={24} />}
onClick={onClose}
/>
</Match>
<Match when={status.mode === 'guest'}>
<div class={horizontalDividerStyle()} />
<PanelItem
text={t('plugins.music-together.menu.close')}
icon={<IconOff width={24} height={24} />}
onClick={onClose}
/>
</Match>
</Switch>
</tp-yt-paper-listbox>
);
};

View File

@ -0,0 +1,43 @@
import { JSX } from 'solid-js';
import { css } from 'solid-styled-components';
import { cacheNoArgs } from '@/providers/decorators';
const itemStyle = cacheNoArgs(
() => css`
display: flex;
height: 48px;
align-items: center;
padding: 0 8px;
--iron-icon-fill-color: #fff;
&:not([is-disabled]) {
cursor: pointer;
}
&:hover {
background-color: var(
--ytmusic-menu-item-hover-background-color,
rgba(255, 255, 255, 0.05)
);
}
`,
);
export type PanelItemProps = {
icon: JSX.Element;
text: string;
onClick?: () => void;
};
export const PanelItem = (props: PanelItemProps) => {
return (
<div class={`style-scope ${itemStyle()}`} onClick={props.onClick}>
<div class="icon style-scope ytmusic-menu-service-item-renderer">
{props.icon}
</div>
<div class="text style-scope ytmusic-menu-service-item-renderer">
{props.text}
</div>
</div>
);
};

View File

@ -0,0 +1,211 @@
import { For, Match, Show, Switch } from 'solid-js';
import { css } from 'solid-styled-components';
import { status } from '../store/status';
import { cacheNoArgs } from '@/providers/decorators';
import { user } from '@/plugins/music-together/store/user';
const panelStyle = cacheNoArgs(
() => css`
display: flex;
flex-direction: column;
align-items: stretch;
padding: 16px;
`,
);
const containerStyle = cacheNoArgs(
() => css`
flex: 1;
display: flex;
flex-direction: row;
align-items: flex-start;
gap: 16px;
`,
);
const profileStyle = cacheNoArgs(
() => css`
width: 24px;
height: 24px;
border-radius: 50%;
overflow: hidden;
flex-shrink: 0;
`,
);
const itemStyle = cacheNoArgs(
() => css`
display: inline-flex;
flex-direction: column;
align-items: flex-start;
gap: 4px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
font-size: 14px;
font-weight: 400;
`,
);
const userContainerStyle = cacheNoArgs(
() => css`
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
overflow: auto;
gap: 8px;
padding-top: 16px;
font-size: 14px;
`,
);
const emptyStyle = cacheNoArgs(
() => css`
width: 100%;
font-size: 14px;
color: rgba(255, 255, 255, 0.5);
text-align: center;
`,
);
const spinnerContainerStyle = cacheNoArgs(
() => css`
display: flex;
justify-content: center;
align-items: center;
`,
);
const horizontalDividerStyle = cacheNoArgs(
() => css`
background-color: rgba(255, 255, 255, 0.15);
width: 100%;
height: 1px;
`,
);
export const MusicTogetherStatus = () => {
return (
<div class={panelStyle()}>
<div class={containerStyle()}>
<img
class={profileStyle()}
style={{
width: '24px',
height: '24px',
}}
src={user.thumbnail}
alt="Profile Image"
/>
<div class={itemStyle()}>
<ytmd-trans key="plugins.music-together.name" />
<span id="music-together-status-label">
<Switch>
<Match when={status.mode === 'disconnected'}>
<ytmd-trans
key="plugins.music-together.menu.status.disconnected"
style={{ color: 'rgba(255, 255, 255, 0.5)' }}
/>
</Match>
<Match when={status.mode === 'host'}>
<ytmd-trans
key="plugins.music-together.menu.status.host"
style={{ color: 'rgba(255, 0, 0, 1)' }}
/>
</Match>
<Match when={status.mode === 'guest'}>
<ytmd-trans
key="plugins.music-together.menu.status.guest"
style={{ color: 'rgba(255, 255, 255, 1)' }}
/>
</Match>
<Match when={status.mode === 'connecting'}>
<ytmd-trans
key="plugins.music-together.menu.status.connecting"
style={{ color: 'rgba(255, 255, 255, 0.5)' }}
/>
</Match>
</Switch>
</span>
<Show
when={
status.mode !== 'connecting' && status.mode !== 'disconnected'
}
>
<marquee id="music-together-permission-label">
<Switch>
<Match when={status.permission === 'all'}>
<ytmd-trans
key="plugins.music-together.menu.permission.all"
style={{ color: 'rgba(255, 255, 255, 1)' }}
/>
</Match>
<Match when={status.permission === 'playlist'}>
<ytmd-trans
key="plugins.music-together.menu.permission.playlist"
style={{ color: 'rgba(255, 255, 255, 0.75)' }}
/>
</Match>
<Match when={status.permission === 'host-only'}>
<ytmd-trans
key="plugins.music-together.menu.permission.host-only"
style={{ color: 'rgba(255, 255, 255, 0.5)' }}
/>
</Match>
</Switch>
</marquee>
</Show>
</div>
</div>
<Show
when={status.mode !== 'connecting' && status.mode !== 'disconnected'}
fallback={
<Show when={status.mode === 'connecting'}>
<div
class={horizontalDividerStyle()}
style={{
'margin-top': '16px',
'margin-bottom': '32px',
}}
/>
<div class={spinnerContainerStyle()}>
<tp-yt-paper-spinner-lite
active
id="music-together-host-spinner"
class="loading-indicator style-scope music-together-spinner"
/>
</div>
</Show>
}
>
<div class={horizontalDividerStyle()} style="margin: 16px 0;" />
<div class={itemStyle()}>
<ytmd-trans
key="plugins.music-together.menu.connected-users"
attr:count={status.users.length}
/>
</div>
<div class={userContainerStyle()}>
<For
each={status.users}
fallback={
<span class={emptyStyle()}>
<ytmd-trans key="plugins.music-together.menu.empty-user" />
</span>
}
>
{(user) => (
<img
class={profileStyle()}
src={user.thumbnail}
title={user.name}
alt={`${user.name} (${user.id})`}
/>
)}
</For>
</div>
</Show>
</div>
);
};

View File

@ -0,0 +1,7 @@
import { IconProps } from './types';
export const IconConnect = (props: IconProps) => (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" {...props}>
<path d="M480-640 280-440l56 56 104-103v407h80v-407l104 103 56-56-200-200ZM146-260q-32-49-49-105T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 59-17 115t-49 105l-58-58q22-37 33-78t11-84q0-134-93-227t-227-93q-134 0-227 93t-93 227q0 43 11 84t33 78l-58 58Z" />
</svg>
);

View File

@ -0,0 +1,7 @@
import { IconProps } from './types';
export const IconKey = (props: IconProps) => (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" {...props}>
<path d="M280-400q-33 0-56.5-23.5T200-480q0-33 23.5-56.5T280-560q33 0 56.5 23.5T360-480q0 33-23.5 56.5T280-400Zm0 160q-100 0-170-70T40-480q0-100 70-170t170-70q67 0 121.5 33t86.5 87h352l120 120-180 180-80-60-80 60-85-60h-47q-32 54-86.5 87T280-240Zm0-80q56 0 98.5-34t56.5-86h125l58 41 82-61 71 55 75-75-40-40H435q-14-52-56.5-86T280-640q-66 0-113 47t-47 113q0 66 47 113t113 47Z"/>
</svg>
);

View File

@ -0,0 +1,7 @@
import { IconProps } from './types';
export const IconMusicCast = (props: IconProps) => (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" {...props}>
<path d="M560-160q-66 0-113-47t-47-113q0-66 47-113t113-47q23 0 42.5 5.5T640-458v-342h240v120H720v360q0 66-47 113t-113 47ZM80-320q0-99 38-186.5T221-659q65-65 152.5-103T560-800v80q-82 0-155 31.5t-127.5 86q-54.5 54.5-86 127T160-320H80Zm160 0q0-66 25.5-124.5t69-102Q378-590 436-615t124-25v80q-100 0-170 70t-70 170h-80Z"/>
</svg>
);

View File

@ -0,0 +1,7 @@
import { IconProps } from './types';
export const IconOff = (props: IconProps) => (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" {...props}>
<path d="M792-56 686-160H260q-92 0-156-64T40-380q0-77 47.5-137T210-594q3-8 6-15.5t6-16.5L56-792l56-56 736 736-56 56ZM260-240h346L284-562q-2 11-3 21t-1 21h-20q-58 0-99 41t-41 99q0 58 41 99t99 41Zm185-161Zm419 191-58-56q17-14 25.5-32.5T840-340q0-42-29-71t-71-29h-60v-80q0-83-58.5-141.5T480-720q-27 0-52 6.5T380-693l-58-58q35-24 74.5-36.5T480-800q117 0 198.5 81.5T760-520q69 8 114.5 59.5T920-340q0 39-15 72.5T864-210ZM593-479Z"/>
</svg>
);

View File

@ -0,0 +1,7 @@
import { IconProps } from './types';
export const IconTune = (props: IconProps) => (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" {...props}>
<path d="M440-120v-240h80v80h320v80H520v80h-80Zm-320-80v-80h240v80H120Zm160-160v-80H120v-80h160v-80h80v240h-80Zm160-80v-80h400v80H440Zm160-160v-240h80v80h160v80H680v80h-80Zm-480-80v-80h400v80H120Z"/>
</svg>
);

View File

@ -0,0 +1,5 @@
export * from './IconConnect';
export * from './IconKey';
export * from './IconMusicCast';
export * from './IconOff';
export * from './IconTune';

View File

@ -0,0 +1,5 @@
export type IconProps = {
width?: number;
height?: number;
fill?: string;
};

View File

@ -0,0 +1,56 @@
import { render } from 'solid-js/web';
import { MusicTogetherButton } from './Button';
import { AppElement } from '@/types/queue';
import { RendererContext } from '@/types/contexts';
import { MusicTogetherConfig } from '@/plugins/music-together/types';
import { ToastProvider } from '../context/ToastContext';
import { RendererContextProvider } from '../context/RendererContext';
import { setUser } from '../store/user';
export const onRendererLoad = (
context: RendererContext<MusicTogetherConfig>,
) => {
const container = document.createElement('div');
const target = document.querySelector<HTMLElement>(
'#right-content > ytmusic-settings-button',
);
const api = document.querySelector<AppElement>('ytmusic-app');
if (!target) {
console.warn('Music Together [renderer]: Cannot inject a button');
return;
}
const button = target.querySelector<HTMLElement>('tp-yt-paper-icon-button');
button?.click();
const interval = setInterval(() => {
const thumbnail = target?.querySelector<HTMLImageElement>('img')?.src;
const name = document.querySelector('#account-name')?.textContent;
if (name) {
setUser({ name, thumbnail });
clearInterval(interval);
setTimeout(() => {
button?.click();
target?.insertAdjacentElement('beforebegin', container);
render(
() => (
<RendererContextProvider context={context}>
<ToastProvider service={api!.toastService}>
<MusicTogetherButton />
</ToastProvider>
</RendererContextProvider>
),
container,
);
}, 0);
}
}, 1);
};

View File

@ -0,0 +1,7 @@
import { createSignal } from 'solid-js';
import { Connection } from '../connection';
export const [connection, setConnection] = createSignal<Connection | null>(
null,
);

View File

@ -0,0 +1,12 @@
import { createStore } from 'solid-js/store';
import { VideoData } from '../types';
export type QueueStoreType = {
queue: VideoData[];
title: string;
};
export const [queue, setQueue] = createStore<QueueStoreType>({
queue: [],
title: '',
});

View File

@ -0,0 +1,28 @@
import { createStore } from 'solid-js/store';
import { ConnectedState, Permission, User } from '../types';
// export const getDefaultProfile = (
// connectionID: string,
// id: string = Date.now().toString(36),
// ): User => {
// const name = `Guest ${id.slice(-6)}`;
//
// return {
// id: connectionID,
// handleId: `#music-together:${id}`,
// name,
// thumbnail: `https://ui-avatars.com/api/?name=${name}&background=random`,
// };
// };
export type StatusStoreType = {
mode: ConnectedState;
permission: Permission;
users: User[];
};
export const [status, setStatus] = createStore<StatusStoreType>({
mode: 'disconnected',
permission: 'all',
users: [],
});

View File

@ -0,0 +1,14 @@
import { createStore } from 'solid-js/store';
type ClinetUser = {
name: string;
thumbnail: string;
};
const id = Date.now().toString(36);
const name = `Guest ${id.slice(0, 4)}`;
const thumbnail = `https://ui-avatars.com/api/?name=${name}&background=random`;
export const [user, setUser] = createStore<ClinetUser>({
name,
thumbnail,
});

View File

@ -3,6 +3,7 @@
cursor: pointer;
margin-left: 8px;
margin-right: 16px;
& svg {
width: 24px;
@ -83,42 +84,15 @@
}
.music-together-status {
display: flex;
flex-direction: column;
align-items: stretch;
padding: 16px;
}
.music-together-profile {
width: 24px;
height: 24px;
border-radius: 50%;
overflow: hidden;
flex-shrink: 0;
}
.music-together-profile.big {
width: 32px;
height: 32px;
}
.music-together-status-container {
flex: 1;
display: flex;
flex-direction: row;
align-items: flex-start;
gap: 16px;
}
.music-together-status-item {
display: inline-flex;
flex-direction: column;
align-items: flex-start;
gap: 4px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
font-size: 14px;
font-weight: 400;
}
.music-together-user-container {
display: flex;
@ -131,11 +105,6 @@
font-size: 14px;
}
.music-together-empty {
width: 100%;
font-size: 14px;
color: rgba(255, 255, 255, .5);
text-align: center;
}
.music-together-owner {

View File

@ -1,8 +0,0 @@
<div class="style-scope music-together-item">
<div class="icon style-scope ytmusic-menu-service-item-renderer">
<!-- icon -->
</div>
<div class="text style-scope ytmusic-menu-service-item-renderer">
<!-- text -->
</div>
</div>

View File

@ -1,5 +0,0 @@
<div class="music-together-popup">
<tp-yt-paper-listbox class="style-scope ytmusic-menu-popup-renderer music-together-popup-container">
</tp-yt-paper-listbox>
</div>

View File

@ -1,7 +0,0 @@
<div id="music-together-setting-button" class="music-together-button style-scope ytmusic-nav-bar">
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24">
<path d="M0-240v-63q0-43 44-70t116-27q13 0 25 .5t23 2.5q-14 21-21 44t-7 48v65H0Zm240 0v-65q0-32 17.5-58.5T307-410q32-20 76.5-30t96.5-10q53 0 97.5 10t76.5 30q32 20 49 46.5t17 58.5v65H240Zm540 0v-65q0-26-6.5-49T754-397q11-2 22.5-2.5t23.5-.5q72 0 116 26.5t44 70.5v63H780Zm-455-80h311q-10-20-55.5-35T480-370q-55 0-100.5 15T325-320ZM160-440q-33 0-56.5-23.5T80-520q0-34 23.5-57t56.5-23q34 0 57 23t23 57q0 33-23 56.5T160-440Zm640 0q-33 0-56.5-23.5T720-520q0-34 23.5-57t56.5-23q34 0 57 23t23 57q0 33-23 56.5T800-440Zm-320-40q-50 0-85-35t-35-85q0-51 35-85.5t85-34.5q51 0 85.5 34.5T600-600q0 50-34.5 85T480-480Zm0-80q17 0 28.5-11.5T520-600q0-17-11.5-28.5T480-640q-17 0-28.5 11.5T440-600q0 17 11.5 28.5T480-560Zm1 240Zm-1-280Z"/>
</svg>
<tp-yt-paper-spinner-lite id="music-together-host-spinner" hidden class="loading-indicator style-scope music-together-spinner"></tp-yt-paper-spinner-lite>
</div>
<div class="music-together-divider"></div>

View File

@ -1,23 +0,0 @@
<div class="music-together-status">
<div class="music-together-status-container">
<img class="music-together-profile big" alt="Profile Image">
<div class="music-together-status-item">
<ytmd-trans key="plugins.music-together.name"></ytmd-trans>
<span id="music-together-status-label">
<ytmd-trans key="plugins.music-together.menu.status.disconnected"></ytmd-trans>
</span>
<marquee id="music-together-permission-label">
<ytmd-trans key="plugins.music-together.menu.permission.playlist" style="color: rgba(255, 255, 255, 0.75)"></ytmd-trans>
</marquee>
</div>
</div>
<div class="music-together-divider horizontal" style="margin: 16px 0;"></div>
<div class="music-together-status-item">
<ytmd-trans key="plugins.music-together.menu.connected-users"></ytmd-trans>
</div>
<div class="music-together-user-container">
<span class="music-together-empty">
<ytmd-trans key="plugins.music-together.menu.empty-user"></ytmd-trans>
</span>
</div>
</div>

View File

@ -1,4 +1,7 @@
export type Profile = {
export type MusicTogetherConfig = {
enabled: boolean;
};
export type User = {
id: string;
handleId: string;
name: string;
@ -8,18 +11,25 @@ export type VideoData = {
videoId: string;
ownerId: string;
};
export type ConnectedState = 'disconnected' | 'host' | 'guest' | 'connecting';
export type Permission = 'host-only' | 'playlist' | 'all';
export const getDefaultProfile = (
connectionID: string,
id: string = Date.now().toString(),
): Profile => {
const name = `Guest ${id.slice(0, 4)}`;
return {
id: connectionID,
handleId: `#music-together:${id}`,
name,
thumbnail: `https://ui-avatars.com/api/?name=${name}&background=random`,
};
export type ConnectionEventMap = {
ADD_SONGS: { videoList: VideoData[]; index?: number };
REMOVE_SONG: { index: number };
MOVE_SONG: { fromIndex: number; toIndex: number };
IDENTIFY: { user: User } | undefined;
SYNC_USER: { users: User[] } | undefined;
SYNC_QUEUE: { videoList: VideoData[] } | undefined;
SYNC_PROGRESS:
| { progress?: number; state?: number; index?: number }
| undefined;
PERMISSION: Permission | undefined;
};
export type ConnectionEventUnion = {
[Event in keyof ConnectionEventMap]: {
type: Event;
payload: ConnectionEventMap[Event];
after?: ConnectionEventUnion[];
};
}[keyof ConnectionEventMap];

View File

@ -1,42 +0,0 @@
import { ElementFromHtml } from '@/plugins/utils/renderer';
import { t } from '@/i18n';
import { Popup } from '../element';
import { createStatus } from '../ui/status';
import IconOff from '../icons/off.svg?raw';
export type GuestPopupProps = {
onItemClick: (id: string) => void;
};
export const createGuestPopup = (props: GuestPopupProps) => {
const status = createStatus();
status.setStatus('guest');
const result = Popup({
data: [
{
type: 'custom',
element: status.element,
},
{
type: 'divider',
},
{
type: 'item',
id: 'music-together-disconnect',
icon: ElementFromHtml(IconOff),
text: t('plugins.music-together.menu.disconnect'),
onClick: () => props.onItemClick('music-together-disconnect'),
},
],
anchorAt: 'bottom-right',
popupAt: 'top-right',
});
return {
...status,
...result,
};
};

View File

@ -1,62 +0,0 @@
import { t } from '@/i18n';
import { ElementFromHtml } from '@/plugins/utils/renderer';
import { Popup } from '../element';
import { createStatus } from '../ui/status';
import IconKey from '../icons/key.svg?raw';
import IconOff from '../icons/off.svg?raw';
import IconTune from '../icons/tune.svg?raw';
export type HostPopupProps = {
onItemClick: (id: string) => void;
};
export const createHostPopup = (props: HostPopupProps) => {
const status = createStatus();
status.setStatus('host');
const result = Popup({
data: [
{
type: 'custom',
element: status.element,
},
{
type: 'divider',
},
{
id: 'music-together-copy-id',
type: 'item',
icon: ElementFromHtml(IconKey),
text: t('plugins.music-together.menu.click-to-copy-id'),
onClick: () => props.onItemClick('music-together-copy-id'),
},
{
id: 'music-together-permission',
type: 'item',
icon: ElementFromHtml(IconTune),
text: t('plugins.music-together.menu.set-permission', {
permission: t('plugins.music-together.menu.permission.host-only'),
}),
onClick: () => props.onItemClick('music-together-permission'),
},
{
type: 'divider',
},
{
type: 'item',
id: 'music-together-close',
icon: ElementFromHtml(IconOff),
text: t('plugins.music-together.menu.close'),
onClick: () => props.onItemClick('music-together-close'),
},
],
anchorAt: 'bottom-right',
popupAt: 'top-right',
});
return {
...status,
...result,
};
};

View File

@ -1,49 +0,0 @@
import { Popup } from '@/plugins/music-together/element';
import { ElementFromHtml } from '@/plugins/utils/renderer';
import { createStatus } from './status';
import { t } from '@/i18n';
import IconMusicCast from '../icons/music-cast.svg?raw';
import IconConnect from '../icons/connect.svg?raw';
export type SettingPopupProps = {
onItemClick: (id: string) => void;
};
export const createSettingPopup = (props: SettingPopupProps) => {
const status = createStatus();
status.setStatus('disconnected');
const result = Popup({
data: [
{
type: 'custom',
element: status.element,
},
{
type: 'divider',
},
{
id: 'music-together-host',
type: 'item',
icon: ElementFromHtml(IconMusicCast),
text: t('plugins.music-together.menu.host'),
onClick: () => props.onItemClick('music-together-host'),
},
{
type: 'item',
icon: ElementFromHtml(IconConnect),
text: t('plugins.music-together.menu.join'),
onClick: () => props.onItemClick('music-together-join'),
},
],
anchorAt: 'bottom-right',
popupAt: 'top-right',
});
return {
...status,
...result,
};
};

View File

@ -1,102 +0,0 @@
import { ElementFromHtml } from '@/plugins/utils/renderer';
import { t } from '@/i18n';
import statusHTML from '../templates/status.html?raw';
import type { Permission, Profile } from '../types';
export const createStatus = () => {
const element = ElementFromHtml(statusHTML);
const icon = document.querySelector<HTMLImageElement>(
'ytmusic-settings-button > tp-yt-paper-icon-button > tp-yt-iron-icon#icon img',
);
const profile = element.querySelector<HTMLImageElement>(
'.music-together-profile',
)!;
const statusLabel = element.querySelector<HTMLSpanElement>(
'#music-together-status-label',
)!;
const permissionLabel = element.querySelector<HTMLMarqueeElement>(
'#music-together-permission-label',
)!;
profile.src = icon?.src ?? '';
const setStatus = (status: 'disconnected' | 'host' | 'guest') => {
if (status === 'disconnected') {
statusLabel.textContent = t(
'plugins.music-together.menu.status.disconnected',
);
statusLabel.style.color = 'rgba(255, 255, 255, 0.5)';
}
if (status === 'host') {
statusLabel.textContent = t('plugins.music-together.menu.status.host');
statusLabel.style.color = 'rgba(255, 0, 0, 1)';
}
if (status === 'guest') {
statusLabel.textContent = t('plugins.music-together.menu.status.guest');
statusLabel.style.color = 'rgba(255, 255, 255, 1)';
}
};
const setPermission = (permission: Permission) => {
if (permission === 'host-only') {
permissionLabel.textContent = t(
'plugins.music-together.menu.permission.host-only',
);
permissionLabel.style.color = 'rgba(255, 255, 255, 0.5)';
}
if (permission === 'playlist') {
permissionLabel.textContent = t(
'plugins.music-together.menu.permission.playlist',
);
permissionLabel.style.color = 'rgba(255, 255, 255, 0.75)';
}
if (permission === 'all') {
permissionLabel.textContent = t(
'plugins.music-together.menu.permission.all',
);
permissionLabel.style.color = 'rgba(255, 255, 255, 1)';
}
};
const setProfile = (src: string) => {
profile.src = src;
};
const setUsers = (users: Profile[]) => {
const container = element.querySelector<HTMLDivElement>(
'.music-together-user-container',
)!;
const empty = element.querySelector<HTMLElement>('.music-together-empty')!;
for (const child of Array.from(container.children)) {
if (child !== empty) child.remove();
}
if (users.length === 0) empty.style.display = 'block';
else empty.style.display = 'none';
for (const user of users) {
const img = document.createElement('img');
img.classList.add('music-together-profile');
img.src = user.thumbnail ?? '';
img.title = user.name;
img.alt = `${user.name} (${user.id})`;
container.append(img);
}
};
return {
element,
setStatus,
setUsers,
setProfile,
setPermission,
};
};

View File

@ -22,5 +22,27 @@ export default createPlugin({
node.remove();
}
}
// Remove the library button
const libraryIconPath =
'M16,6v2h-2v5c0,1.1-0.9,2-2,2s-2-0.9-2-2s0.9-2,2-2c0.37,0,0.7,0.11,1,0.28V6H16z M18,20H4V6H3v15h15V20z M21,3H6v15h15V3z M7,4h13v13H7V4z';
const observer = new MutationObserver(() => {
const menuEntries = document.querySelectorAll(
'#items ytmusic-guide-entry-renderer',
);
menuEntries.forEach((item) => {
const icon = item.querySelector('path');
if (icon) {
observer.disconnect();
if (icon.getAttribute('d') === libraryIconPath) {
item.remove();
}
}
});
});
observer.observe(document.documentElement, {
childList: true,
subtree: true,
});
},
});
});

View File

@ -12,12 +12,6 @@ export interface ScrobblerPluginConfig {
* @default true
*/
scrobbleOtherMedia: boolean;
/**
* Use alternative titles for scrobbling (Useful for non-roman song titles)
*
* @default false
*/
alternativeTitles: boolean;
scrobblers: {
lastfm: {
/**
@ -77,7 +71,6 @@ export interface ScrobblerPluginConfig {
export const defaultConfig: ScrobblerPluginConfig = {
enabled: false,
scrobbleOtherMedia: true,
alternativeTitles: false,
scrobblers: {
lastfm: {
enabled: false,

View File

@ -96,15 +96,6 @@ export const onMenu = async ({
setConfig(config);
},
},
{
label: t('plugins.scrobbler.menu.scrobble-alternative-title'),
type: 'checkbox',
checked: Boolean(config.alternativeTitles),
click(item) {
config.alternativeTitles = item.checked;
setConfig(config);
},
},
{
label: 'Last.fm',
submenu: [

View File

@ -127,13 +127,8 @@ export class LastFmScrobbler extends ScrobblerBase {
await this.createSession(config, setConfig);
}
const title =
config.alternativeTitles && songInfo.alternativeTitle !== undefined
? songInfo.alternativeTitle
: songInfo.title;
const postData: LastFmSongData = {
track: title,
track: songInfo.title,
duration: songInfo.songDuration,
artist: songInfo.artist,
...(songInfo.album ? { album: songInfo.album } : undefined), // Will be undefined if current song is a video

View File

@ -48,7 +48,7 @@ export class ListenbrainzScrobbler extends ScrobblerBase {
return;
}
const body = createRequestBody('playing_now', songInfo, config);
const body = createRequestBody('playing_now', songInfo);
submitListen(body, config);
}
@ -64,7 +64,7 @@ export class ListenbrainzScrobbler extends ScrobblerBase {
return;
}
const body = createRequestBody('single', songInfo, config);
const body = createRequestBody('single', songInfo);
body.payload[0].listened_at = Math.trunc(Date.now() / 1000);
submitListen(body, config);
@ -74,16 +74,10 @@ export class ListenbrainzScrobbler extends ScrobblerBase {
function createRequestBody(
listenType: string,
songInfo: SongInfo,
config: ScrobblerPluginConfig,
): ListenbrainzRequestBody {
const title =
config.alternativeTitles && songInfo.alternativeTitle !== undefined
? songInfo.alternativeTitle
: songInfo.title;
const trackMetadata = {
artist_name: songInfo.artist,
track_name: title,
track_name: songInfo.title,
release_name: songInfo.album ?? undefined,
additional_info: {
media_player: 'YouTube Music Desktop App',

View File

@ -20,8 +20,7 @@ export default createPlugin({
showTimeCodes: false,
defaultTextString: '♪',
lineEffect: 'fancy',
romanization: true,
} satisfies SyncedLyricsPluginConfig as SyncedLyricsPluginConfig,
} satisfies SyncedLyricsPluginConfig,
menu,
renderer,

View File

@ -1,6 +1,7 @@
import { MenuItemConstructorOptions } from 'electron';
import { t } from '@/i18n';
import type { MenuItemConstructorOptions } from 'electron';
import type { MenuContext } from '@/types/contexts';
import type { SyncedLyricsPluginConfig } from './types';
@ -135,17 +136,6 @@ export const menu = async (
},
],
},
{
label: t('plugins.synced-lyrics.menu.romanization.label'),
toolTip: t('plugins.synced-lyrics.menu.romanization.tooltip'),
type: 'checkbox',
checked: config.romanization,
click(item) {
ctx.setConfig({
romanization: item.checked,
});
},
},
{
label: t('plugins.synced-lyrics.menu.show-time-codes.label'),
toolTip: t('plugins.synced-lyrics.menu.show-time-codes.tooltip'),

Some files were not shown because too many files have changed in this diff Show More