Compare commits

..

2 Commits

Author SHA1 Message Date
9e97699e6c fix: seek 2023-11-03 10:09:40 +09:00
a810621762 feat: http api 2023-11-03 10:09:40 +09:00
303 changed files with 8238 additions and 31224 deletions

View File

@ -1 +1,3 @@
.eslintrc.js .eslintrc.js
rollup.main.config.ts
rollup.preload.config.ts

View File

@ -7,7 +7,7 @@ module.exports = {
'plugin:@typescript-eslint/recommended', 'plugin:@typescript-eslint/recommended',
'plugin:@typescript-eslint/recommended-requiring-type-checking', 'plugin:@typescript-eslint/recommended-requiring-type-checking',
], ],
plugins: ['prettier', '@typescript-eslint', 'import'], plugins: ['@typescript-eslint', 'import'],
parser: '@typescript-eslint/parser', parser: '@typescript-eslint/parser',
parserOptions: { parserOptions: {
project: './tsconfig.json', project: './tsconfig.json',
@ -26,7 +26,6 @@ module.exports = {
'import/newline-after-import': 'error', 'import/newline-after-import': 'error',
'import/no-default-export': 'off', 'import/no-default-export': 'off',
'import/no-duplicates': 'error', 'import/no-duplicates': 'error',
'import/no-unresolved': ['error', { ignore: ['^virtual:', '\\?inline$', '\\?raw$', '\\?asset&asarUnpack'] }],
'import/order': [ 'import/order': [
'error', 'error',
{ {
@ -67,14 +66,4 @@ module.exports = {
es6: true, es6: true,
}, },
ignorePatterns: ['dist', 'node_modules'], ignorePatterns: ['dist', 'node_modules'],
root: true,
settings: {
'import/parsers': {
'@typescript-eslint/parser': ['.ts']
},
'import/resolver': {
typescript: {},
exports: {},
},
},
}; };

View File

@ -24,12 +24,6 @@ body:
placeholder: 2.0.0 placeholder: 2.0.0
validations: validations:
required: true required: true
- type: checkboxes
attributes:
label: Checklists
options:
- label: I use the portable version of the YouTube Music Application.
- label: I can reproduce this issue in the [official YTM web version](https://music.youtube.com).
- type: dropdown - type: dropdown
attributes: attributes:
label: What operating system are you using? label: What operating system are you using?
@ -50,7 +44,7 @@ body:
required: true required: true
- type: dropdown - type: dropdown
attributes: attributes:
label: What CPU architecture are you using? label: What arch are you using?
options: options:
- x64 - x64
- ia32 - ia32
@ -63,17 +57,10 @@ body:
label: Last Known Working YouTube Music (Application) version label: Last Known Working YouTube Music (Application) version
description: (If applicable) What is the last version of YouTube Music this worked in? description: (If applicable) What is the last version of YouTube Music this worked in?
placeholder: 1.20.0 placeholder: 1.20.0
- type: textarea
attributes:
label: Reproduction steps
description: Provide steps to reproduce the issue.
placeholder: 1. Enable the X plugin.
validations:
required: true
- type: textarea - type: textarea
attributes: attributes:
label: Expected Behavior label: Expected Behavior
description: A clear and concise description of what you expected to happen. description: A clear and concise description of what you expected to happen. (Add a replication step if applicable)
validations: validations:
required: true required: true
- type: textarea - type: textarea
@ -82,13 +69,6 @@ body:
description: A clear description of what actually happens. description: A clear description of what actually happens.
validations: validations:
required: true required: true
- type: textarea
attributes:
label: Enabled plugins
description: Provide the list of plugins you enabled.
placeholder: 1. Album Color Theme
validations:
required: true
- type: textarea - type: textarea
attributes: attributes:
label: Additional Information label: Additional Information

View File

@ -15,7 +15,7 @@ body:
- type: textarea - type: textarea
attributes: attributes:
label: Problem Description label: Problem Description
description: A clear and concise description of the problem you are seeking to solve with this feature request. description: Please add a clear and concise description of the problem you are seeking to solve with this feature request.
validations: validations:
required: true required: true
- type: textarea - type: textarea
@ -33,6 +33,6 @@ body:
- type: textarea - type: textarea
attributes: attributes:
label: Additional Information label: Additional Information
description: Any other context about the problem. description: Add any other context about the problem here.
validations: validations:
required: false required: false

View File

@ -42,8 +42,8 @@ jobs:
- name: Install dependencies - name: Install dependencies
run: pnpm install --frozen-lockfile run: pnpm install --frozen-lockfile
# Only vite build without release if it is a fork, or it is a pull-request # Only rollup build without release if it is a fork
- name: Vite Build - name: Rollup Build
if: github.repository == 'th-ch/youtube-music' && github.event_name == 'pull_request' if: github.repository == 'th-ch/youtube-music' && github.event_name == 'pull_request'
run: | run: |
pnpm build pnpm build
@ -146,8 +146,6 @@ jobs:
Thanks to all contributors! 🏅 Thanks to all contributors! 🏅
(Note for Windows: `YouTube-Music-Web-Setup-${{ env.VERSION_TAG }}.exe` is an installer, and `YouTube-Music-${{ env.VERSION_TAG }}.exe` is a portable version)
- name: Update changelog - name: Update changelog
if: ${{ env.VERSION_HASH == '' }} if: ${{ env.VERSION_HASH == '' }}
run: | run: |

View File

@ -17,4 +17,4 @@ jobs:
- name: "Checkout Repository" - name: "Checkout Repository"
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: "Dependency Review" - name: "Dependency Review"
uses: actions/dependency-review-action@v4 uses: actions/dependency-review-action@v3

View File

@ -15,16 +15,12 @@ jobs:
name: Publish winget package name: Publish winget package
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Set winget version env
env:
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 - name: Submit package to Windows Package Manager Community Repository
uses: vedantmgoyal2009/winget-releaser@v2 uses: vedantmgoyal2009/winget-releaser@v2
with: with:
identifier: th-ch.YouTubeMusic identifier: th-ch.YouTubeMusic
installers-regex: '^YouTube-Music-Web-Setup-[\d\.]+\.exe$' installers-regex: '^YouTube-Music-Web-Setup-[\d\.]+\.exe$'
version: ${{ env.WINGET_TAG_NAME }} version: ${{ inputs.tag_name || github.event.release.tag_name }}
release-tag: ${{ inputs.tag_name || github.event.release.tag_name }} release-tag: ${{ inputs.tag_name || github.event.release.tag_name }}
token: ${{ secrets.WINGET_ACC_TOKEN }} token: ${{ secrets.WINGET_ACC_TOKEN }}
fork-user: youtube-music-winget fork-user: youtube-music-winget

1
.gitignore vendored
View File

@ -12,4 +12,3 @@ electron-builder.yml
!.yarn/releases !.yarn/releases
!.yarn/sdks !.yarn/sdks
!.yarn/versions !.yarn/versions
.vite-inspect

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

View File

@ -2,131 +2,8 @@
All notable changes to this project will be documented in this file. Dates are displayed in UTC. All notable changes to this project will be documented in this file. Dates are displayed in UTC.
#### [v3.2.2](https://github.com/th-ch/youtube-music/compare/v3.2.1...v3.2.2)
- feat(tray): Add song info and paused icon [`#1592`](https://github.com/th-ch/youtube-music/pull/1592)
- fix(skip-silences): fix audio distorted [`#1141`](https://github.com/th-ch/youtube-music/issues/1141)
- chore(deps): update dependency rollup to v4.9.3 [`0c3c380`](https://github.com/th-ch/youtube-music/commit/0c3c3805918adf2a185a7f1dc67ea3af8135863d)
- chore(i18n): Translated using Weblate (Turkish) [`64ea1fd`](https://github.com/th-ch/youtube-music/commit/64ea1fdb58fdf2766ae3284ac1a51bfac8894b36)
- fix(music-together): typing [`895386f`](https://github.com/th-ch/youtube-music/commit/895386f6f8c649f77ea15c88f6fb7ecc5b775554)
#### [v3.2.1](https://github.com/th-ch/youtube-music/compare/v3.2.0...v3.2.1)
> 1 January 2024
- fix: fix #1574 [`#1574`](https://github.com/th-ch/youtube-music/issues/1574)
- fix: fix #1575 [`#1575`](https://github.com/th-ch/youtube-music/issues/1575)
- chore(i18n): Translated using Weblate [`f5aa179`](https://github.com/th-ch/youtube-music/commit/f5aa179cd639eb4b8f70f1264b5b459ebcc16695)
- chore(i18n): Translated using Weblate (English) [`e409165`](https://github.com/th-ch/youtube-music/commit/e409165e1bed85f3d1aea3a565e7b9e462b1e05b)
- chore(i18n): Translated using Weblate (Czech) [`0ca4e34`](https://github.com/th-ch/youtube-music/commit/0ca4e34efd86e877314e5a245f266065b4cf0013)
#### [v3.2.0](https://github.com/th-ch/youtube-music/compare/v3.1.1...v3.2.0)
> 1 January 2024
- feat(album-color-theme): improve `Album Color Theme` style [`#1571`](https://github.com/th-ch/youtube-music/pull/1571)
- feat(menu): add more detail in Menu [`#1570`](https://github.com/th-ch/youtube-music/pull/1570)
- feat(music-together): Add new plugin `Music Together` [`#1562`](https://github.com/th-ch/youtube-music/pull/1562)
- chore(deps): update dependency rollup to v4.9.2 [`#1567`](https://github.com/th-ch/youtube-music/pull/1567)
- fix(deps): update dependency i18next to v23.7.13 [`#1569`](https://github.com/th-ch/youtube-music/pull/1569)
- feat: Add new plugin `Album actions` [`#1515`](https://github.com/th-ch/youtube-music/pull/1515)
- fix(deps): update dependency i18next to v23.7.12 [`#1564`](https://github.com/th-ch/youtube-music/pull/1564)
- fix: Only apply scale factor on Windows [`#1565`](https://github.com/th-ch/youtube-music/pull/1565)
- chore(deps): update dependency @typescript-eslint/eslint-plugin to v6.16.0 [`#1556`](https://github.com/th-ch/youtube-music/pull/1556)
- chore(deps): update pnpm to v8.13.1 [`#1557`](https://github.com/th-ch/youtube-music/pull/1557)
- chore(deps): update dependency ws to v8.16.0 [`#1559`](https://github.com/th-ch/youtube-music/pull/1559)
- fix(deps): update dependency youtubei.js to v8.1.0 [`#1560`](https://github.com/th-ch/youtube-music/pull/1560)
- fix(deps): update dependency node-html-parser to v6.1.12 [`#1554`](https://github.com/th-ch/youtube-music/pull/1554)
- Revert "fix(deps): update dependency @xhayper/discord-rpc to v1.1.2" [`#1552`](https://github.com/th-ch/youtube-music/pull/1552)
- feat(ambient-mode): support ambient mode on `Song section` [`#1555`](https://github.com/th-ch/youtube-music/issues/1555)
- fix: fixed an issue with the download button disappearing [`#1551`](https://github.com/th-ch/youtube-music/issues/1551)
- fix: fix `homebrew cask` [`#1514`](https://github.com/th-ch/youtube-music/issues/1514)
- fix: pnpm build error [`13ef856`](https://github.com/th-ch/youtube-music/commit/13ef8560ff43353030537403be7da82542ba535e)
- chore(i18n): Translated using Weblate (Czech) [`0dc9c6a`](https://github.com/th-ch/youtube-music/commit/0dc9c6a1a90bce6505614617b827e816cbaaf875)
- chore(deps): update dependency @typescript-eslint/eslint-plugin to v6.15.0 [`c5bcd89`](https://github.com/th-ch/youtube-music/commit/c5bcd89f164b51d7380486a8ae35edd0caeea842)
#### [v3.1.1](https://github.com/th-ch/youtube-music/compare/v3.1.0...v3.1.1)
> 18 December 2023
- fix: fix renderer plugin load timing [`#1522`](https://github.com/th-ch/youtube-music/issues/1522)
- chore(i18n): Translated using Weblate (Lithuanian) [`fc1a7cd`](https://github.com/th-ch/youtube-music/commit/fc1a7cda62b6e33e5f5d57a5a6e0adef6a32bf9a)
- chore(i18n): Translated using Weblate (Chinese (Simplified)) [`eba7026`](https://github.com/th-ch/youtube-music/commit/eba7026b89bbfdd3ac07cf728a66ba9bdd274ec0)
- chore(deps): update dependency rollup to v4.8.0 [`a601d0b`](https://github.com/th-ch/youtube-music/commit/a601d0b3d2dee0fabad79a18e1a7dd0ca84ccf01)
#### [v3.1.0](https://github.com/th-ch/youtube-music/compare/v3.0.2...v3.1.0)
> 11 December 2023
- chore(deps): update dependency electron to v28 [`#1498`](https://github.com/th-ch/youtube-music/pull/1498)
- Enable/Disable Navigation without restart [`#1507`](https://github.com/th-ch/youtube-music/pull/1507)
- Turkish(tr)_lang_file [`#1513`](https://github.com/th-ch/youtube-music/pull/1513)
- Skip Disliked Songs [`#1505`](https://github.com/th-ch/youtube-music/pull/1505)
- chore(deps): update dependency @typescript-eslint/eslint-plugin to v6.13.2 [`#1452`](https://github.com/th-ch/youtube-music/pull/1452)
- fix: Homebrew latest release url parsing [`#1496`](https://github.com/th-ch/youtube-music/pull/1496)
- fix: in-player adblocker inject timing issue [`#1478`](https://github.com/th-ch/youtube-music/issues/1478)
- fix(package.json): fix RPM version `libuuid` issue [`#1508`](https://github.com/th-ch/youtube-music/issues/1508)
- Translated using Weblate (Polish) [`7b78ba6`](https://github.com/th-ch/youtube-music/commit/7b78ba67613f14be65a45751efeb06431b405a91)
- Translated using Weblate (French) [`ebc0879`](https://github.com/th-ch/youtube-music/commit/ebc087963b23265ff00528c8305d51597abf587a)
- Translated using Weblate (Chinese (Traditional)) [`020bdc0`](https://github.com/th-ch/youtube-music/commit/020bdc0811ea45ad6c2853c62a05ae6695c5c4f9)
#### [v3.0.2](https://github.com/th-ch/youtube-music/compare/v3.0.1...v3.0.2)
> 3 December 2023
- fix(adblocker): fix In-Player adblocker [`#1478`](https://github.com/th-ch/youtube-music/issues/1478)
- fix(menu): crash on linux [`#1477`](https://github.com/th-ch/youtube-music/issues/1477)
- fix: update pnpm-lock.yaml [`9e2c6b1`](https://github.com/th-ch/youtube-music/commit/9e2c6b1afa33b5708853c8328946e68ec45b09c3)
- Translated using Weblate (Chinese (Traditional)) [`125b69f`](https://github.com/th-ch/youtube-music/commit/125b69fd75a05c3eb893886119e2d9f2332b3e56)
- Translated using Weblate (French) [`15c4551`](https://github.com/th-ch/youtube-music/commit/15c455105b5100a8ee2bd0a4631548d3d455f047)
#### [v3.0.1](https://github.com/th-ch/youtube-music/compare/v3.0.0...v3.0.1)
> 2 December 2023
- hotfix(adblocker): fix #1475 [`#1475`](https://github.com/th-ch/youtube-music/issues/1475)
- Translated using Weblate (French) [`7f02afc`](https://github.com/th-ch/youtube-music/commit/7f02afc5a6839adfe8437d4e2cc8dee13a93b311)
- Update changelog for v3.0.0 [`d8c8bd1`](https://github.com/th-ch/youtube-music/commit/d8c8bd17ecfbdf96ebd29eb4c5748c07876ee242)
- Translated using Weblate (German) [`0660f0b`](https://github.com/th-ch/youtube-music/commit/0660f0b7ce6895ef5800f48ade1da2d7f8e0c1f7)
### [v3.0.0](https://github.com/th-ch/youtube-music/compare/v2.2.0...v3.0.0)
> 2 December 2023
- Add text to Translation section [`#1470`](https://github.com/th-ch/youtube-music/pull/1470)
- fix(deps): update dependency youtubei.js to v8 [`#1473`](https://github.com/th-ch/youtube-music/pull/1473)
- chore(deps): update dependency electron to v27.1.3 [`#1471`](https://github.com/th-ch/youtube-music/pull/1471)
- fix(deps): update dependency @xhayper/discord-rpc to v1.1.1 [`#1472`](https://github.com/th-ch/youtube-music/pull/1472)
- feat: add support i18n [`#1468`](https://github.com/th-ch/youtube-music/pull/1468)
- chore(deps): update dependency electron to v27.1.2 [`#1441`](https://github.com/th-ch/youtube-music/pull/1441)
- Nicer Readme [`#1439`](https://github.com/th-ch/youtube-music/pull/1439)
- Windows Zoom, ScaleFactor [`#1402`](https://github.com/th-ch/youtube-music/pull/1402)
- chore(deps): bump axios from 1.5.1 to 1.6.1 [`#1400`](https://github.com/th-ch/youtube-music/pull/1400)
- Updated mac icon to better reflect the Mac styling [`#1395`](https://github.com/th-ch/youtube-music/pull/1395)
- feat: rename plugins to clarify context [`#1392`](https://github.com/th-ch/youtube-music/pull/1392)
- feat: refactor plugin utils [`#1391`](https://github.com/th-ch/youtube-music/pull/1391)
- feat: plugin auto-importer with `vite-plugin-resolve` [`#1385`](https://github.com/th-ch/youtube-music/pull/1385)
- feat: migrate from `rollup` to `electron-vite` [`#1364`](https://github.com/th-ch/youtube-music/pull/1364)
- feat: enable `context-isolation` [`#1361`](https://github.com/th-ch/youtube-music/pull/1361)
- fix: add workaround for `podcast` type video [`#1362`](https://github.com/th-ch/youtube-music/pull/1362)
- fix: fix broken menu-layout [`#1360`](https://github.com/th-ch/youtube-music/pull/1360)
- Add Homebrew cask install option for MacOS. [`#1357`](https://github.com/th-ch/youtube-music/pull/1357)
- feat: changed Zoom shortcuts to standard [`#1458`](https://github.com/th-ch/youtube-music/issues/1458)
- fix(in-app-menu): fix #1436 [`#1436`](https://github.com/th-ch/youtube-music/issues/1436)
- fix(discord): update application client-id [`#1431`](https://github.com/th-ch/youtube-music/issues/1431)
- chore(deps): update dependency electron to v27.0.4 [`#1324`](https://github.com/th-ch/youtube-music/issues/1324)
- fix(in-app-menu): panel should close with the window when it is closed [`#1389`](https://github.com/th-ch/youtube-music/issues/1389)
- fix: change titleBarOverlay height based on zoomFactor [`#1375`](https://github.com/th-ch/youtube-music/issues/1375)
- fix: fixed an issue if "Always on top" is enabled, the dialog is displayed below the window [`#1379`](https://github.com/th-ch/youtube-music/issues/1379)
- fix: fix winget version (fix #1363) [`#1363`](https://github.com/th-ch/youtube-music/issues/1363)
- feat: run prettier [`a3104fd`](https://github.com/th-ch/youtube-music/commit/a3104fda4b0d58b076d0c737111636a66e468acc)
- Translated using Weblate (Korean) [`b4b7ad8`](https://github.com/th-ch/youtube-music/commit/b4b7ad824b8c489ae483eba139b46e5b200231fc)
- Translated using Weblate (English) [`d2eabaa`](https://github.com/th-ch/youtube-music/commit/d2eabaa4bbccd89eae529eae52cec035e8e2620c)
#### [v2.2.0](https://github.com/th-ch/youtube-music/compare/v2.1.3...v2.2.0) #### [v2.2.0](https://github.com/th-ch/youtube-music/compare/v2.1.3...v2.2.0)
> 27 October 2023
- feat(ambient-mode): add config for `ambient-mode` plugin [`#1349`](https://github.com/th-ch/youtube-music/pull/1349) - feat(ambient-mode): add config for `ambient-mode` plugin [`#1349`](https://github.com/th-ch/youtube-music/pull/1349)
- bump deps [`4248d20`](https://github.com/th-ch/youtube-music/commit/4248d20e8ef926ce7b1d07eb83743755a341d9f6) - bump deps [`4248d20`](https://github.com/th-ch/youtube-music/commit/4248d20e8ef926ce7b1d07eb83743755a341d9f6)
- Update changelog for v2.1.3 [`dc73561`](https://github.com/th-ch/youtube-music/commit/dc73561c8a8acfc8ba91aff2dc78e4267869f2fd) - Update changelog for v2.1.3 [`dc73561`](https://github.com/th-ch/youtube-music/commit/dc73561c8a8acfc8ba91aff2dc78e4267869f2fd)

View File

@ -478,7 +478,7 @@
</a> </a>
</li> </li>
</ul> </ul>
<div class="footer-copyright">© 2024 th-ch</div> <div class="footer-copyright">© 2021 th-ch</div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,327 +0,0 @@
# 유튜브 뮤직 (YouTube Music)
<div align="center">
[![GitHub release](https://img.shields.io/github/release/th-ch/youtube-music.svg?style=for-the-badge&logo=youtube-music)](https://github.com/th-ch/youtube-music/releases/)
[![GitHub license](https://img.shields.io/github/license/th-ch/youtube-music.svg?style=for-the-badge)](https://github.com/th-ch/youtube-music/blob/master/LICENSE)
[![eslint code style](https://img.shields.io/badge/code_style-eslint-5ed9c7.svg?style=for-the-badge)](https://github.com/th-ch/youtube-music/blob/master/.eslintrc.js)
[![Build status](https://img.shields.io/github/actions/workflow/status/th-ch/youtube-music/build.yml?branch=master&style=for-the-badge&logo=youtube-music)](https://GitHub.com/th-ch/youtube-music/releases/)
[![GitHub All Releases](https://img.shields.io/github/downloads/th-ch/youtube-music/total?style=for-the-badge&logo=youtube-music)](https://GitHub.com/th-ch/youtube-music/releases/)
[![AUR](https://img.shields.io/aur/version/youtube-music-bin?color=blueviolet&style=for-the-badge&logo=youtube-music)](https://aur.archlinux.org/packages/youtube-music-bin)
[![Known Vulnerabilities](https://snyk.io/test/github/th-ch/youtube-music/badge.svg)](https://snyk.io/test/github/th-ch/youtube-music)
</div>
![Screenshot](../../web/screenshot.jpg "Screenshot")
<div align="center">
<a href="https://github.com/th-ch/youtube-music/releases/latest">
<img src="../../web/youtube-music.svg" width="400" height="100" alt="YouTube Music SVG">
</a>
</div>
**유튜브 뮤직의 Electron 래퍼; 기능:**
- 원래의 인터페이스를 유지하는 것을 목표로 하는 네이티브 디자인 및 느낌
- 맞춤 플러그인을 위한 프레임워크: 스타일, 콘텐츠, 기능 등 필요에 따라 유튜브 뮤직을 변경하고, 클릭 한 번으로 플러그인을 활성화/비활성화할 수 있습니다.
## 번역
[Hosted Weblate](https://hosted.weblate.org/projects/youtube-music/)에서 번역을 도울 수 있습니다.
<a href="https://hosted.weblate.org/engage/youtube-music/">
<img src="https://hosted.weblate.org/widget/youtube-music/i18n/multi-auto.svg" alt="번역 상태" />
<img src="https://hosted.weblate.org/widget/youtube-music/i18n/287x66-black.png" alt="번역 상태 2" />
</a>
## 다운로드
[최신 릴리즈](https://github.com/th-ch/youtube-music/releases/latest)를 확인하여 최신 버전을 빠르게 찾을 수 있습니다.
### Arch Linux
AUR에서 `youtube-music-bin` 패키지를 설치합니다. AUR 설치 지침은 [이 위키 페이지](https://wiki.archlinux.org/index.php/Arch_User_Repository#Installing_packages)를 참조하세요.
### MacOS
Homebrew를 사용하여 앱을 설치할 수 있습니다:
```bash
brew install --cask https://raw.githubusercontent.com/th-ch/youtube-music/master/youtube-music.rb
```
(앱을 수동으로 설치하고) 앱을 실행할 때 `손상되었기 때문에 열 수 없습니다.`라는 오류가 발생하면 터미널에서 다음을 실행하세요:
```bash
xattr -cr /Applications/YouTube\ Music.app
```
### Windows
[Scoop 패키지 매니저](https://scoop.sh)를 사용하여 [`extras` 버킷](https://github.com/ScoopInstaller/Extras)에서 `youtube-music` 패키지를 설치할 수 있습니다.
```bash
scoop bucket add extras
scoop install extras/youtube-music
```
또는 Windows 11의 공식 CLI 패키지 관리자인 [Winget](https://learn.microsoft.com/en-us/windows/package-manager/winget/)을 사용하여 `th-ch.YouTubeMusic` 패키지를 설치할 수 있습니다.
*참고: "알 수 없는 게시자"의 파일이기 때문에 Microsoft Defender의 SmartScreen에서 설치를 차단할 수 있습니다. 이는 GitHub에서 동일 파일을 수동으로 다운로드한 후 실행 파일(.exe)을 실행하려고 할 때도 마찬가지로 발생합니다.*
```bash
winget install th-ch.YouTubeMusic
```
#### (Windows에서) 네트워크에 연결하지 않고 설치하는 방법은 무엇인가요?
- [릴리즈 페이지](https://github.com/th-ch/youtube-music/releases/latest)에서 _본인 기기 아키텍처_에 맞는 `*.nsis.7z` 파일을 다운로드하세요.
- `x64`는 64비트 Windows 용입니다.
- `ia32`는 32비트 Windows 용입니다.
- `arm64`는 ARM64 Windows 용입니다.
- 릴리즈 페이지에서 설치기를 다운로드하세요. (`*-Setup.exe`)
- 두 파일을 **동일한 위치**에 놓아주세요.
- 설치기를 실행하세요.
## 기능:
- **일시 정지 시 자동 확인** (항상 활성화 됨): 일정 시간이 지나면 음악을 일시 정지하는 ["계속 시청하시겠습니까?"](https://user-images.githubusercontent.com/61631665/129977894-01c60740-7ec6-4bf0-9a2c-25da24491b0e.png) 팝업을 비활성화합니다.
- 이외에 더 많은 기능 ...
## 사용 가능한 플러그인:
- **애드블록**: 모든 광고와 트래커를 즉시 차단합니다
- **앨범 컬러 기반 테마**: 앨범 색상 팔레트를 기반으로 동적 테마 및 시각 효과를 적용합니다
- **앰비언트 모드**: 영상의 간접 조명을 화면 배경에 투사합니다.
- **오디오 컴프레서**: 오디오에 컴프레서를 적용합니다 (신호에서 가장 시끄러운 부분의 음량을 낮추고 가장 조용한 부분의 음량을 높임)
- **네비게이션 바 흐림 효과**: 내비게이션 바를 투명하고 흐릿하게 만듭니다
- **나이 제한 우회**: 유튜브의 나이 제한을 우회합니다
- **자막 선택기**: 자막을 활성화합니다
- **컴팩트 사이드바**: 사이드바를 항상 컴팩트 모드로 설정합니다
- **크로스페이드**: 노래 사이에 크로스페이드 효과를 적용합니다
- **자동 재생 해제**: 노래를 '일시 정지' 모드로 시작하게 합니다
- [**디스코드 활동 상태**](https://discord.com/): [활동 상태 (Rich Presence)](https://user-images.githubusercontent.com/28219076/104362104-a7a0b980-5513-11eb-9744-bb89eabe0016.png)를 사용하여 친구들에게 내가 듣는 음악을 보여주세요
- **다운로더**: UI에서 [직접](https://user-images.githubusercontent.com/61631665/129977677-83a7d067-c192-45e1-98ae-b5a4927393be.png) MP3/소스 오디오를 다운로드하세요
- **지수 볼륨**: 음량 슬라이더를 [지수적](https://greasyfork.org/en/scripts/397686-youtube-music-fix-volume-ratio/)으로 만들어 더 낮은 음량을 쉽게 선택할 수 있도록 합니다.
- **인앱 메뉴**: [메뉴 표시줄을 더 멋지게, 그리고 다크 또는 앨범의 색상으로 만듭니다](https://user-images.githubusercontent.com/78568641/112215894-923dbf00-8c29-11eb-95c3-3ce15db27eca.png)
> (이 플러그인 및 메뉴 숨기기 옵션을 활성화한 후 메뉴에 액세스하는 데 문제가 있는 경우 [이 글](https://github.com/th-ch/youtube-music/issues/410#issuecomment-952060709)을 참조하세요)
- [**Last.fm**](https://www.last.fm/): Last.fm에 대한 스크러블 지원을 추가합니다
- **Lumia Stream**: [Lumia Stream](https://lumiastream.com/) 지원을 추가합니다
- **Genius 가사**: 더 많은 곡에 대해 가사 지원을 추가합니다
- **네비게이션**: 브라우저에서처럼, UI에 직접 통합된 앞으로/뒤로 탐색하는 화살표를 추가합니다
- **Google 로그인 제거**: UI에서 Google 로그인 버튼 및 링크 제거하기
- **알림**: 노래 재생이 시작되면 알림을 표시 (Windows에서는 [대화형 알림](https://user-images.githubusercontent.com/78568641/114102651-63ce0e00-98d0-11eb-9dfe-c5a02bb54f9c.png) 사용 가능)
- **PiP**: 앱을 PiP 모드로 전환할 수 있게 허용합니다
- **재생 속도**: 빨리 듣거나, 천천히 들어보세요! [노래 속도를 제어하는 슬라이더를 추가합니다](https://user-images.githubusercontent.com/61631665/129976003-e55db5ba-bf42-448c-a059-26a009775e68.png)
- **정확한 음량**: 사용자 지정 HUD와 사용자 지정 음량 단계 및 마우스 휠/단축키를 사용하여 음량을 정확하게 제어하세요
- **영상 품질 체인저**: 영상 오버레이의 [버튼](https://user-images.githubusercontent.com/78568641/138574366-70324a5e-2d64-4f6a-acdd-dc2a2b9cecc5.png)으로 영상 품질을 변경할 수 있게 합니다
- **단축키 (& MPRIS)**: 재생을 위한 전역 단축키 설정 허용 (재생/일시 정지/다음/이전) + 미디어 키를 재정의하여 [미디어 osd](https://user-images.githubusercontent.com/84923831/128601225-afa38c1f-dea8-4209-9f72-0f84c1dd8b54.png) 비활성화 + Ctrl/CMD + F 검색 활성화 + 미디어 키에 대한 리눅스 MPRIS 지원 활성화 + [고급 사용자](https://github.com/th-ch/youtube-music/issues/106#issuecomment-952156902)를 위한 [사용자 지정 단축키](https://github.com/Araxeus/youtube-music/blob/1e591d6a3df98449bcda6e63baab249b28026148/providers/song-controls.js#L13-L50) 지원
- **무음 건너뛰기** - 노래의 무음 부분을 자동으로 건너뜁니다
- [**SponsorBlock**](https://github.com/ajayyy/SponsorBlock): 인트로/아웃트로와 같은 음악이 아닌 부분이나, 노래가 재생되지 않는 뮤직 비디오의 일부를 자동으로 건너뜁니다
- **작업표시줄 미디어 컨트롤**: [Windows 작업표시줄](https://user-images.githubusercontent.com/78568641/111916130-24a35e80-8a82-11eb-80c8-5021c1aa27f4.png)에서 재생을 제어하세요
- **TouchBar**: macOS 사용자를 위한 TouchBar 위젯을 추가합니다
- **Tuna-OBS**: [OBS](https://obsproject.com/)의 플러그인, [Tuna](https://obsproject.com/forum/resources/tuna.843/)와 통합을 활성화합니다
- **영상 전환**: 영상/노래 모드를 전환하는 [버튼](https://user-images.githubusercontent.com/28893833/173663950-63e6610e-a532-49b7-9afa-54cb57ddfc15.png)을 추가합니다. 선택적으로 전체 영상 탭을 제거할 수도 있습니다
- **비주얼라이저**: 플레이어에 시각화 도구 추가
## 테마
CSS 파일을 로드하여 애플리케이션의 모양을 변경할 수 있습니다(설정 > 시각적 변경 > 테마).
일부 사전 정의 테마는 https://github.com/kerichdev/themes-for-ytmdesktop-player 에서 사용할 수 있습니다.
## 개발
```bash
git clone https://github.com/th-ch/youtube-music
cd youtube-music
pnpm install --frozen-lockfile
pnpm dev
```
## 나만의 플러그인 만들기
플러그인을 사용하면 할 수 있는 것들:
- 앱 조작 - Electron에서 `BrowserWindow`가 플러그인 핸들러로 전달
- HTML/CSS를 조작하여 프론트엔드를 변경
### 플러그인 만들기
`plugins/나만의-플러그인-이름`에 폴더를 만듭니다:
- `index.ts`: 플러그인의 메인 파일입니다.
```typescript
import style from './style.css?inline'; // 스타일을 인라인으로 가져옵니다
import { createPlugin } from '@/utils';
export default createPlugin({
name: 'Plugin Label',
restartNeeded: true, // 값이 true면, YTM은 재시작 다이얼로그를 표시합니다
config: {
enabled: false,
}, // 나의 커스텀 config
stylesheets: [style], // 나의 스타일
menu: async ({ getConfig, setConfig }) => {
// 모든 *Config 메서드는 Promise<T>로 래핑됩니다
const config = await getConfig();
return [
{
label: 'menu',
submenu: [1, 2, 3].map((value) => ({
label: `value ${value}`,
type: 'radio',
checked: config.value === value,
click() {
setConfig({ value });
},
})),
},
];
},
backend: {
start({ window, ipc }) {
window.maximize();
// 이를 사용하여 렌더러 플러그인과 통신할 수 있습니다
ipc.handle('some-event', () => {
return 'hello';
});
},
// config가 변경되면 실행됩니다
onConfigChange(newConfig) { /* ... */ },
// 플러그인이 비활성화되면 실행됩니다
stop(context) { /* ... */ },
},
renderer: {
async start(context) {
console.log(await context.ipc.invoke('some-event'));
},
// 렌더러에서만 사용 가능한 훅입니다
onPlayerApiReady(api: YoutubePlayer, context: RendererContext<T>) {
// 플러그인의 config를 간단하게 설정할 수 있습니다
context.setConfig({ myConfig: api.getVolume() });
},
onConfigChange(newConfig) { /* ... */ },
stop(_context) { /* ... */ },
},
preload: {
async start({ getConfig }) {
const config = await getConfig();
},
onConfigChange(newConfig) {},
stop(_context) {},
},
});
```
### 일반적인 사용 예
- 사용자 정의 CSS 삽입: 같은 폴더에 `style.css` 파일을 생성합니다:
```typescript
// index.ts
import style from './style.css?inline'; // 스타일을 인라인으로 가져옵니다
import { createPlugin } from '@/utils';
export default createPlugin({
name: 'Plugin Label',
restartNeeded: true, // 값이 true면, YTM은 재시작 다이얼로그를 표시합니다
config: {
enabled: false,
}, // 나의 커스텀 config
stylesheets: [style], // 나의 커스텀 스타일
renderer() {} // 렌더러 훅 정의
});
```
- HTML을 변경하려는 경우:
```typescript
import { createPlugin } from '@/utils';
export default createPlugin({
name: 'Plugin Label',
restartNeeded: true, // 값이 true면, YTM은 재시작 다이얼로그를 표시합니다
config: {
enabled: false,
}, // 나의 커스텀 config
renderer() {
// 로그인 버튼을 제거합니다
document.querySelector(".sign-in-link.ytmusic-nav-bar").remove();
} // 렌더러 훅 정의
});
```
- 프론트엔드와 백엔드 간의 통신: Electron의 `ipcMain` 모듈을 사용하여 수행할 수 있습니다. `SponsorBlock` 플러그인의 `index.ts` 파일과 예제를 참조하세요.
## 빌드
1. 레포지토리를 복제 (clone) 합니다
2. [이 가이드](https://pnpm.io/installation)에 따라 `pnpm`을 설치합니다.
3. `pnpm install --frozen-lockfile`을 실행하여 종속성을 설치합니다.
4. `pnpm build:OS`을 실행합니다.
- `pnpm dist:win` - Windows
- `pnpm dist:linux` - Linux
- `pnpm dist:mac` - MacOS
[electron-builder](https://github.com/electron-userland/electron-builder)를 사용하여 macOS, Linux 및 Windows용 앱을 빌드합니다.
## 프로덕션 빌드 미리보기
```bash
pnpm start
```
## 테스트
```bash
pnpm test
```
[Playwright](https://playwright.dev/)를 사용하여 앱을 테스트합니다.
## 라이선스
MIT © [th-ch](https://github.com/th-ch/youtube-music)
## 자주 묻는 질문
### 앱 메뉴가 표시되지 않는 이유는 무엇인가요?
`메뉴 숨기기` 옵션이 켜져 있는 경우 - <kbd>alt</kbd> 키(또는 인앱 메뉴 플러그인을 사용하는 경우 <kbd>\`</kbd> [백틱] 키)로 메뉴를 표시할 수 있습니다.

View File

@ -1,160 +0,0 @@
import { resolve, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import { defineConfig, defineViteConfig } from 'electron-vite';
import builtinModules from 'builtin-modules';
import viteResolve from 'vite-plugin-resolve';
import Inspect from 'vite-plugin-inspect';
import { pluginVirtualModuleGenerator } from './vite-plugins/plugin-importer.mjs';
import pluginLoader from './vite-plugins/plugin-loader.mjs';
import type { UserConfig } from 'vite';
import { i18nImporter } from './vite-plugins/i18n-importer.mjs';
import solidPlugin from 'vite-plugin-solid';
const __dirname = dirname(fileURLToPath(import.meta.url));
const resolveAlias = {
'@': resolve(__dirname, './src'),
'@assets': resolve(__dirname, './assets'),
};
export default defineConfig({
main: defineViteConfig(({ mode }) => {
const commonConfig: UserConfig = {
plugins: [
pluginLoader('backend'),
viteResolve({
'virtual:i18n': i18nImporter(),
'virtual:plugins': pluginVirtualModuleGenerator('main'),
}),
],
publicDir: 'assets',
build: {
lib: {
entry: 'src/index.ts',
formats: ['cjs'],
},
outDir: 'dist/main',
commonjsOptions: {
ignoreDynamicRequires: true,
},
rollupOptions: {
external: ['electron', 'custom-electron-prompt', ...builtinModules],
input: './src/index.ts',
},
},
resolve: {
alias: resolveAlias,
},
};
if (mode === 'development') {
commonConfig.plugins?.push(
Inspect({ build: true, outputDir: '.vite-inspect/backend' }),
);
return commonConfig;
}
return {
...commonConfig,
build: {
...commonConfig.build,
minify: true,
cssMinify: true,
},
};
}),
preload: defineViteConfig(({ mode }) => {
const commonConfig: UserConfig = {
plugins: [
pluginLoader('preload'),
viteResolve({
'virtual:i18n': i18nImporter(),
'virtual:plugins': pluginVirtualModuleGenerator('preload'),
}),
],
build: {
lib: {
entry: 'src/preload.ts',
formats: ['cjs'],
},
outDir: 'dist/preload',
commonjsOptions: {
ignoreDynamicRequires: true,
},
rollupOptions: {
external: ['electron', 'custom-electron-prompt', ...builtinModules],
input: './src/preload.ts',
},
},
resolve: {
alias: resolveAlias,
},
};
if (mode === 'development') {
commonConfig.plugins?.push(
Inspect({ build: true, outputDir: '.vite-inspect/preload' }),
);
return commonConfig;
}
return {
...commonConfig,
build: {
...commonConfig.build,
minify: true,
cssMinify: true,
},
};
}),
renderer: defineViteConfig(({ mode }) => {
const commonConfig: UserConfig = {
plugins: [
pluginLoader('renderer'),
viteResolve({
'virtual:i18n': i18nImporter(),
'virtual:plugins': pluginVirtualModuleGenerator('renderer'),
}),
solidPlugin(),
],
root: './src/',
build: {
lib: {
entry: 'src/index.html',
formats: ['iife'],
name: 'renderer',
},
outDir: 'dist/renderer',
commonjsOptions: {
ignoreDynamicRequires: true,
},
rollupOptions: {
external: ['electron', ...builtinModules],
input: './src/index.html',
},
},
resolve: {
alias: resolveAlias,
},
};
if (mode === 'development') {
commonConfig.plugins?.push(
Inspect({ build: true, outputDir: '.vite-inspect/renderer' }),
);
return commonConfig;
}
return {
...commonConfig,
build: {
...commonConfig.build,
minify: true,
cssMinify: true,
},
};
}),
});

View File

@ -1,9 +1,9 @@
{ {
"name": "youtube-music", "name": "youtube-music",
"productName": "YouTube Music", "productName": "YouTube Music",
"version": "3.3.0", "version": "2.2.0",
"description": "YouTube Music Desktop App - including custom plugins", "description": "YouTube Music Desktop App - including custom plugins",
"main": "./dist/main/index.js", "main": "./dist/index.js",
"license": "MIT", "license": "MIT",
"repository": "th-ch/youtube-music", "repository": "th-ch/youtube-music",
"author": { "author": {
@ -17,7 +17,6 @@
"files": [ "files": [
"!*", "!*",
"dist", "dist",
"assets",
"license", "license",
"!node_modules", "!node_modules",
"node_modules/custom-electron-prompt/**", "node_modules/custom-electron-prompt/**",
@ -26,9 +25,6 @@
"!node_modules/**/*.map", "!node_modules/**/*.map",
"!node_modules/**/*.ts" "!node_modules/**/*.ts"
], ],
"asarUnpack": [
"assets"
],
"mac": { "mac": {
"identity": null, "identity": null,
"target": [ "target": [
@ -77,11 +73,6 @@
"rpm" "rpm"
] ]
}, },
"rpm": {
"depends": [
"/usr/lib64/libuuid.so.1"
]
},
"snap": { "snap": {
"slots": [ "slots": [
{ {
@ -98,127 +89,109 @@
"scripts": { "scripts": {
"test": "playwright test", "test": "playwright test",
"test:debug": "cross-env DEBUG=pw:*,-pw:test:protocol playwright test", "test:debug": "cross-env DEBUG=pw:*,-pw:test:protocol playwright test",
"build": "electron-vite build", "rollup:preload": "rollup -c rollup.preload.config.ts --configPlugin @rollup/plugin-typescript --bundleConfigAsCjs",
"vite:inspect": "pnpm clean && electron-vite build --mode development && pnpm exec serve .vite-inspect", "rollup:main": "rollup -c rollup.main.config.ts --configPlugin @rollup/plugin-typescript --bundleConfigAsCjs",
"start": "electron-vite preview", "build": "yarpm-pnpm run rollup:preload && yarpm-pnpm run rollup:main",
"start:debug": "cross-env ELECTRON_ENABLE_LOGGING=1 pnpm start", "start": "yarpm-pnpm run build && electron ./dist/index.js",
"dev": "electron-vite dev --watch", "start:debug": "cross-env ELECTRON_ENABLE_LOGGING=1 yarpm-pnpm run start",
"dev:debug": "cross-env ELECTRON_ENABLE_LOGGING=1 pnpm dev", "clean": "del-cli dist && del-cli pack",
"clean": "del-cli dist && del-cli pack && del-cli .vite-inspect", "dist": "yarpm-pnpm run clean && yarpm-pnpm run build && electron-builder --win --mac --linux -p never",
"dist": "pnpm clean && pnpm build && pnpm electron-builder --win --mac --linux -p never", "dist:linux": "yarpm-pnpm run clean && yarpm-pnpm run build && electron-builder --linux -p never",
"dist:linux": "pnpm clean && pnpm build && pnpm electron-builder --linux -p never", "dist:mac": "yarpm-pnpm run clean && yarpm-pnpm run build && electron-builder --mac dmg:x64 -p never",
"dist:mac": "pnpm clean && pnpm build && pnpm electron-builder --mac dmg:x64 -p never", "dist:mac:arm64": "yarpm-pnpm run clean && yarpm-pnpm run build && electron-builder --mac dmg:arm64 -p never",
"dist:mac:arm64": "pnpm clean && pnpm build && pnpm electron-builder --mac dmg:arm64 -p never", "dist:win": "yarpm-pnpm run clean && yarpm-pnpm run build && electron-builder --win -p never",
"dist:win": "pnpm clean && pnpm build && pnpm electron-builder --win -p never", "dist:win:x64": "yarpm-pnpm run clean && yarpm-pnpm run build && electron-builder --win nsis-web:x64 -p never",
"dist:win:x64": "pnpm clean && pnpm build && pnpm electron-builder --win nsis-web:x64 -p never",
"lint": "eslint .", "lint": "eslint .",
"changelog": "npx --yes auto-changelog", "changelog": "npx --yes auto-changelog",
"release:linux": "pnpm clean && pnpm build && pnpm electron-builder --linux -p always -c.snap.publish=github", "release:linux": "yarpm-pnpm run clean && yarpm-pnpm run build && electron-builder --linux -p always -c.snap.publish=github",
"release:mac": "pnpm clean && pnpm build && pnpm electron-builder --mac -p always", "release:mac": "yarpm-pnpm run clean && yarpm-pnpm run build && electron-builder --mac -p always",
"release:win": "pnpm clean && pnpm build && pnpm electron-builder --win -p always", "release:win": "yarpm-pnpm run clean && yarpm-pnpm run build && electron-builder --win -p always",
"typecheck": "tsc -p tsconfig.json --noEmit" "typecheck": "tsc -p tsconfig.json --noEmit"
}, },
"engines": { "engines": {
"node": ">=18.0.0" "node": ">=16.0.0"
}, },
"pnpm": { "pnpm": {
"overrides": { "overrides": {
"usocket": "1.0.1", "rollup": "4.2.0",
"node-gyp": "10.0.1", "node-gyp": "10.0.1",
"xml2js": "0.6.2", "xml2js": "0.6.2",
"node-fetch": "3.3.2", "node-fetch": "3.3.2",
"@electron/universal": "2.0.1", "@electron/universal": "1.4.4",
"@babel/runtime": "7.23.8" "@babel/runtime": "7.23.2"
},
"patchedDependencies": {
"vudio@2.1.1": "patches/vudio@2.1.1.patch",
"@xhayper/discord-rpc@1.1.2": "patches/@xhayper__discord-rpc@1.1.2.patch"
} }
}, },
"overrides": {
"rollup": "4.2.0",
"node-gyp": "10.0.1",
"xml2js": "0.6.2",
"node-fetch": "3.3.2",
"@electron/universal": "1.4.4",
"@babel/runtime": "7.23.2"
},
"dependencies": { "dependencies": {
"@cliqz/adblocker-electron": "1.26.15", "@cliqz/adblocker-electron": "1.26.10",
"@cliqz/adblocker-electron-preload": "1.26.15", "@cliqz/adblocker-electron-preload": "1.26.10",
"@electron-toolkit/tsconfig": "1.0.1",
"@electron/remote": "2.1.2",
"@ffmpeg.wasm/core-mt": "0.12.0", "@ffmpeg.wasm/core-mt": "0.12.0",
"@ffmpeg.wasm/main": "0.12.0", "@ffmpeg.wasm/main": "0.12.0",
"@floating-ui/dom": "1.6.3", "@foobar404/wave": "2.0.4",
"@foobar404/wave": "2.0.5",
"@jellybrick/electron-better-web-request": "1.0.4", "@jellybrick/electron-better-web-request": "1.0.4",
"@jellybrick/mpris-service": "2.1.4", "@jellybrick/mpris-service": "2.1.4",
"@xhayper/discord-rpc": "1.1.2", "@xhayper/discord-rpc": "1.0.24",
"async-mutex": "0.4.1", "async-mutex": "0.4.0",
"butterchurn": "3.0.0-beta.4", "butterchurn": "3.0.0-beta.4",
"butterchurn-presets": "3.0.0-beta.4", "butterchurn-presets": "3.0.0-beta.4",
"color": "4.2.3",
"conf": "10.2.0", "conf": "10.2.0",
"custom-electron-prompt": "1.5.7", "custom-electron-prompt": "1.5.7",
"dbus-next": "0.10.2",
"deepmerge-ts": "5.1.0",
"electron-debug": "3.2.0", "electron-debug": "3.2.0",
"electron-is": "3.0.0", "electron-is": "3.0.0",
"electron-localshortcut": "3.2.1", "electron-localshortcut": "3.2.1",
"electron-store": "8.1.0", "electron-store": "8.1.0",
"electron-unhandled": "4.0.1", "electron-unhandled": "4.0.1",
"electron-updater": "6.1.7", "electron-updater": "6.1.4",
"fast-average-color": "9.4.0", "fast-average-color": "9.4.0",
"fast-equals": "5.0.1",
"filenamify": "6.0.0", "filenamify": "6.0.0",
"howler": "2.2.4", "howler": "2.2.4",
"html-to-text": "9.0.5", "html-to-text": "9.0.5",
"i18next": "23.8.3",
"keyboardevent-from-electron-accelerator": "2.0.0", "keyboardevent-from-electron-accelerator": "2.0.0",
"keyboardevents-areequal": "0.2.2", "keyboardevents-areequal": "0.2.2",
"node-html-parser": "6.1.12",
"node-id3": "0.2.6", "node-id3": "0.2.6",
"peerjs": "1.5.2", "simple-youtube-age-restriction-bypass": "git+https://github.com/organization/Simple-YouTube-Age-Restriction-Bypass.git#v2.5.8",
"semver": "7.6.0",
"serve": "14.2.1",
"simple-youtube-age-restriction-bypass": "github:organization/Simple-YouTube-Age-Restriction-Bypass#v2.5.9",
"solid-floating-ui": "0.3.1",
"solid-js": "1.8.15",
"solid-styled-components": "0.28.5",
"solid-transition-group": "0.2.3",
"ts-morph": "21.0.1",
"vudio": "2.1.1", "vudio": "2.1.1",
"x11": "2.3.0", "x11": "2.3.0",
"youtubei.js": "9.0.2" "youtubei.js": "7.0.0"
}, },
"devDependencies": { "devDependencies": {
"@playwright/test": "1.41.2", "@playwright/test": "1.39.0",
"@rollup/plugin-commonjs": "25.0.7",
"@rollup/plugin-image": "3.0.3",
"@rollup/plugin-json": "6.0.1",
"@rollup/plugin-node-resolve": "15.2.3",
"@rollup/plugin-terser": "0.4.4",
"@rollup/plugin-typescript": "11.1.5",
"@rollup/plugin-wasm": "6.2.2",
"@total-typescript/ts-reset": "0.5.1", "@total-typescript/ts-reset": "0.5.1",
"@types/color": "3.0.6", "@types/electron-localshortcut": "3.1.2",
"@types/electron-localshortcut": "3.1.3", "@types/howler": "2.2.10",
"@types/howler": "2.2.11", "@types/html-to-text": "9.0.3",
"@types/html-to-text": "9.0.4", "@typescript-eslint/eslint-plugin": "6.9.1",
"@types/semver": "7.5.7", "builtin-modules": "^3.3.0",
"@typescript-eslint/eslint-plugin": "7.0.1",
"bufferutil": "4.0.8",
"builtin-modules": "3.3.0",
"cross-env": "7.0.3", "cross-env": "7.0.3",
"del-cli": "5.1.0", "del-cli": "5.1.0",
"discord-api-types": "0.37.70", "electron": "27.0.3",
"electron": "28.2.3", "electron-builder": "24.6.4",
"electron-builder": "24.9.1",
"electron-devtools-installer": "3.2.0", "electron-devtools-installer": "3.2.0",
"electron-vite": "2.0.0", "eslint": "8.52.0",
"esbuild": "0.20.0", "eslint-plugin-import": "2.29.0",
"eslint": "8.56.0", "eslint-plugin-prettier": "5.0.1",
"eslint-import-resolver-exports": "1.0.0-beta.5",
"eslint-import-resolver-typescript": "3.6.1",
"eslint-plugin-import": "2.29.1",
"eslint-plugin-prettier": "5.1.3",
"glob": "10.3.10",
"node-gyp": "10.0.1", "node-gyp": "10.0.1",
"playwright": "1.41.2", "playwright": "1.39.0",
"rollup": "4.12.0", "rollup": "4.2.0",
"typescript": "5.3.3", "rollup-plugin-copy": "3.5.0",
"utf-8-validate": "6.0.3", "rollup-plugin-import-css": "3.3.5",
"vite": "5.1.3", "rollup-plugin-string": "3.0.0",
"vite-plugin-inspect": "0.8.3", "typescript": "5.2.2",
"vite-plugin-resolve": "2.5.1", "yarpm": "1.2.0"
"vite-plugin-solid": "2.10.1",
"ws": "8.16.0"
}, },
"auto-changelog": { "auto-changelog": {
"hideCredit": true, "hideCredit": true,
@ -226,5 +199,5 @@
"unreleased": true, "unreleased": true,
"output": "changelog.md" "output": "changelog.md"
}, },
"packageManager": "pnpm@8.15.3" "packageManager": "pnpm@8.10.2"
} }

View File

@ -1,17 +0,0 @@
diff --git a/package.json b/package.json
index 40db5dfbd8a4455ce2987d8115eca9882e1f9f14..414fc6986b9c0cc288908eb0107b90c4bfd916b2 100644
--- a/package.json
+++ b/package.json
@@ -25,11 +25,7 @@
},
"dependencies": {
"axios": "^1.6.2",
- "ws": "^8.15.1"
- },
- "optionalDependencies": {
- "bufferutil": "^4.0.8",
- "utf-8-validate": "^6.0.3"
+ "ws": "^8.16.0"
},
"devDependencies": {
"@types/node": "^14.*",

View File

@ -1,20 +0,0 @@
diff --git a/umd/vudio.js b/umd/vudio.js
index d0d1127e57125ad4e77442af2db4a26998c7b385..c0b66bd4327c65c31dc6e588bfa4ae6ec70bd3b8 100644
--- a/umd/vudio.js
+++ b/umd/vudio.js
@@ -147,7 +147,6 @@
source.connect(this.analyser);
this.analyser.fftSize = this.option.accuracy * 2;
- this.analyser.connect(audioContext.destination);
this.freqByteData = new Uint8Array(this.analyser.frequencyBinCount);
@@ -207,7 +206,6 @@
source.connect(this.analyser);
this.analyser.fftSize = this.option.accuracy * 2;
- this.analyser.connect(audioContext.destination);
},
__rebuildData : function (freqByteData, horizontalAlign) {

3603
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -6,44 +6,26 @@
[![GitHub license](https://img.shields.io/github/license/th-ch/youtube-music.svg?style=for-the-badge)](https://github.com/th-ch/youtube-music/blob/master/LICENSE) [![GitHub license](https://img.shields.io/github/license/th-ch/youtube-music.svg?style=for-the-badge)](https://github.com/th-ch/youtube-music/blob/master/LICENSE)
[![eslint code style](https://img.shields.io/badge/code_style-eslint-5ed9c7.svg?style=for-the-badge)](https://github.com/th-ch/youtube-music/blob/master/.eslintrc.js) [![eslint code style](https://img.shields.io/badge/code_style-eslint-5ed9c7.svg?style=for-the-badge)](https://github.com/th-ch/youtube-music/blob/master/.eslintrc.js)
[![Build status](https://img.shields.io/github/actions/workflow/status/th-ch/youtube-music/build.yml?branch=master&style=for-the-badge&logo=youtube-music)](https://GitHub.com/th-ch/youtube-music/releases/) [![Build status](https://img.shields.io/github/actions/workflow/status/th-ch/youtube-music/build.yml?branch=master&style=for-the-badge&logo=youtube-music)](https://GitHub.com/th-ch/youtube-music/releases/)
[![Known Vulnerabilities](https://img.shields.io/snyk/vulnerabilities/github/th-ch/youtube-music?style=for-the-badge)](https://snyk.io/test/github/th-ch/youtube-music)
[![GitHub All Releases](https://img.shields.io/github/downloads/th-ch/youtube-music/total?style=for-the-badge&logo=youtube-music)](https://GitHub.com/th-ch/youtube-music/releases/) [![GitHub All Releases](https://img.shields.io/github/downloads/th-ch/youtube-music/total?style=for-the-badge&logo=youtube-music)](https://GitHub.com/th-ch/youtube-music/releases/)
[![AUR](https://img.shields.io/aur/version/youtube-music-bin?color=blueviolet&style=for-the-badge&logo=youtube-music)](https://aur.archlinux.org/packages/youtube-music-bin) [![AUR](https://img.shields.io/aur/version/youtube-music-bin?color=blueviolet&style=for-the-badge&logo=youtube-music)](https://aur.archlinux.org/packages/youtube-music-bin)
[![Known Vulnerabilities](https://snyk.io/test/github/th-ch/youtube-music/badge.svg)](https://snyk.io/test/github/th-ch/youtube-music)
</div> </div>
![Screenshot](web/screenshot.jpg "Screenshot") ![Screenshot](web/screenshot.jpg "Screenshot")
<div align="center"> <div align="center">
<a href="https://github.com/th-ch/youtube-music/releases/latest"> <a href="https://github.com/th-ch/youtube-music/releases/latest">
<img src="web/youtube-music.svg" width="400" height="100" alt="YouTube Music SVG"> <img src="web/youtube-music.svg" width="400" height="100" alt="YouTube Music SVG">
</a> </a>
</div> </div>
Read this in other languages: [🇰🇷](./docs/readme/README-ko.md)
**Electron wrapper around YouTube Music featuring:** **Electron wrapper around YouTube Music featuring:**
- Native look & feel, aims at keeping the original interface - Native look & feel, aims at keeping the original interface
- Framework for custom plugins: change YouTube Music to your needs (style, content, features), enable/disable plugins in - Framework for custom plugins: change YouTube Music to your needs (style, content, features), enable/disable plugins in
one click one click
## Demo Image
| Player Screen (album color theme & ambient light) |
|:---------------------------------------------------------------------------------------------------------:|
|![Screenshot2](https://github.com/th-ch/youtube-music/assets/16558115/28ed8f08-c8c4-48ad-811b-7722093e9d81)|
## Translation
You can help with translation on [Hosted Weblate](https://hosted.weblate.org/projects/youtube-music/).
<a href="https://hosted.weblate.org/engage/youtube-music/">
<img src="https://hosted.weblate.org/widget/youtube-music/i18n/multi-auto.svg" alt="translation status" />
<img src="https://hosted.weblate.org/widget/youtube-music/i18n/287x66-black.png" alt="translation status 2" />
</a>
## Download ## Download
You can check out the [latest release](https://github.com/th-ch/youtube-music/releases/latest) to quickly find the You can check out the [latest release](https://github.com/th-ch/youtube-music/releases/latest) to quickly find the
@ -56,10 +38,9 @@ this [wiki page](https://wiki.archlinux.org/index.php/Arch_User_Repository#Insta
### MacOS ### MacOS
You can install the app using Homebrew (see the [cask definition](https://github.com/th-ch/homebrew-youtube-music)): You can install the app using Homebrew:
```bash ```bash
brew install th-ch/youtube-music/youtube-music brew install --cask https://raw.githubusercontent.com/th-ch/youtube-music/master/youtube-music.rb
``` ```
If you install the app manually and get an error "is damaged and cant be opened." when launching the app, run the following in the Terminal: If you install the app manually and get an error "is damaged and cant be opened." when launching the app, run the following in the Terminal:
@ -99,40 +80,30 @@ winget install th-ch.YouTubeMusic
- Place them in the **same directory**. - Place them in the **same directory**.
- Run the installer. - Run the installer.
## Features:
- **Auto confirm when paused** (Always Enabled): disable
the ["Continue Watching?"](https://user-images.githubusercontent.com/61631665/129977894-01c60740-7ec6-4bf0-9a2c-25da24491b0e.png)
popup that pause music after a certain time
- And more ...
## Available plugins: ## Available plugins:
- **Ad Blocker**: Block all ads and tracking out of the box - **Ad Blocker**: Block all ads and tracking out of the box
- **Album Actions**: Adds Undislike, Dislike, Like, and Unlike buttons to apply this to all songs in a playlist or album
- **Album Color Theme**: Applies a dynamic theme and visual effects based on the album color palette - **Album Color Theme**: Applies a dynamic theme and visual effects based on the album color palette
- **Ambient Mode**: Applies a lighting effect by casting gentle colors from the video, into your screens background - **Ambient Mode**: Applies a lighting effect by casting gentle colors from the video, into your screens background.
- **Audio Compressor**: Apply compression to audio (lowers the volume of the loudest parts of the signal and raises the - **Audio Compressor**: Apply compression to audio (lowers the volume of the loudest parts of the signal and raises the
volume of the softest parts) volume of the softest parts)
- **Blur Navigation Bar**: makes navigation bar transparent and blurry - **Blur Nav Bar**: makes navigation bar transparent and blurry
- **Bypass Age Restrictions**: bypass YouTube's age verification - **Bypass age restrictions**: bypass YouTube's age verification
- **Captions Selector**: Enable captions - **Captions selector**: Enable captions
- **Compact Sidebar**: Always set the sidebar in compact mode - **Compact sidebar**: Always set the sidebar in compact mode
- **Crossfade**: Crossfade between songs - **Crossfade**: Crossfade between songs
- **Disable Autoplay**: Makes every song start in "paused" mode - **Disable Autoplay**: Makes every song start in "paused" mode
- **[Discord](https://discord.com/) Rich Presence**: Show your friends what you listen to - [**Discord**](https://discord.com/): Show your friends what you listen to
with [Rich Presence](https://user-images.githubusercontent.com/28219076/104362104-a7a0b980-5513-11eb-9744-bb89eabe0016.png) with [Rich Presence](https://user-images.githubusercontent.com/28219076/104362104-a7a0b980-5513-11eb-9744-bb89eabe0016.png)
- **Downloader**: downloads - **Downloader**: downloads
@ -140,21 +111,19 @@ winget install th-ch.YouTubeMusic
- **Exponential Volume**: Makes the volume - **Exponential Volume**: Makes the volume
slider [exponential](https://greasyfork.org/en/scripts/397686-youtube-music-fix-volume-ratio/) so it's easier to slider [exponential](https://greasyfork.org/en/scripts/397686-youtube-music-fix-volume-ratio/) so it's easier to
select lower volumes select lower volumes.
- **In-App Menu**: [gives bars a fancy, dark look](https://user-images.githubusercontent.com/78568641/112215894-923dbf00-8c29-11eb-95c3-3ce15db27eca.png) - **In-App Menu**: [gives bars a fancy, dark look](https://user-images.githubusercontent.com/78568641/112215894-923dbf00-8c29-11eb-95c3-3ce15db27eca.png)
> (see [this post](https://github.com/th-ch/youtube-music/issues/410#issuecomment-952060709) if you have problem > (see [this post](https://github.com/th-ch/youtube-music/issues/410#issuecomment-952060709) if you have problem
accessing the menu after enabling this plugin and hide-menu option) accessing the menu after enabling this plugin and hide-menu option)
- **Scrobbler**: Adds scrobbling support for [Last.fm](https://www.last.fm/) and [ListenBrainz](https://listenbrainz.org/) - [**Last.fm**](https://www.last.fm/): Scrobbles support
- **Lumia Stream**: Adds [Lumia Stream](https://lumiastream.com/) support - **Lumia Stream**: Adds [Lumia Stream](https://lumiastream.com/) support
- **Lyrics Genius**: Adds lyrics support for most songs - **Lyrics Genius**: Adds lyrics support for most songs
- **Music Together**: Share a playlist with others. When the host plays a song, everyone else will hear the same song
- **Navigation**: Next/Back navigation arrows directly integrated in the interface, like in your favorite browser - **Navigation**: Next/Back navigation arrows directly integrated in the interface, like in your favorite browser
- **No Google Login**: Remove Google login buttons and links from the interface - **No Google Login**: Remove Google login buttons and links from the interface
@ -163,7 +132,7 @@ winget install th-ch.YouTubeMusic
playing ([interactive notifications](https://user-images.githubusercontent.com/78568641/114102651-63ce0e00-98d0-11eb-9dfe-c5a02bb54f9c.png) playing ([interactive notifications](https://user-images.githubusercontent.com/78568641/114102651-63ce0e00-98d0-11eb-9dfe-c5a02bb54f9c.png)
are available on windows) are available on windows)
- **Picture-in-picture**: allows to switch the app to picture-in-picture mode - **Picture in picture**: allows to switch the app to picture-in-picture mode
- **Playback Speed**: Listen fast, listen - **Playback Speed**: Listen fast, listen
slow! [Adds a slider that controls song speed](https://user-images.githubusercontent.com/61631665/129976003-e55db5ba-bf42-448c-a059-26a009775e68.png) slow! [Adds a slider that controls song speed](https://user-images.githubusercontent.com/61631665/129976003-e55db5ba-bf42-448c-a059-26a009775e68.png)
@ -171,15 +140,17 @@ winget install th-ch.YouTubeMusic
- **Precise Volume**: Control the volume precisely using mousewheel/hotkeys, with a custom hud and customizable volume - **Precise Volume**: Control the volume precisely using mousewheel/hotkeys, with a custom hud and customizable volume
steps steps
- **Shortcuts (& MPRIS)**: Allows setting global hotkeys for playback (play/pause/next/previous) + - **Quality Changer**: Allows changing the video quality with
a [button](https://user-images.githubusercontent.com/78568641/138574366-70324a5e-2d64-4f6a-acdd-dc2a2b9cecc5.png) on
the video overlay
- **Shortcuts**: Allows setting global hotkeys for playback (play/pause/next/previous) +
disable [media osd](https://user-images.githubusercontent.com/84923831/128601225-afa38c1f-dea8-4209-9f72-0f84c1dd8b54.png) disable [media osd](https://user-images.githubusercontent.com/84923831/128601225-afa38c1f-dea8-4209-9f72-0f84c1dd8b54.png)
by overriding media keys + enable Ctrl/CMD + F to search + enable linux mpris support for by overriding media keys + enable Ctrl/CMD + F to search + enable linux mpris support for
mediakeys + [custom hotkeys](https://github.com/Araxeus/youtube-music/blob/1e591d6a3df98449bcda6e63baab249b28026148/providers/song-controls.js#L13-L50) mediakeys + [custom hotkeys](https://github.com/Araxeus/youtube-music/blob/1e591d6a3df98449bcda6e63baab249b28026148/providers/song-controls.js#L13-L50)
for [advanced users](https://github.com/th-ch/youtube-music/issues/106#issuecomment-952156902) for [advanced users](https://github.com/th-ch/youtube-music/issues/106#issuecomment-952156902)
- **Skip Disliked Song**: Skips disliked songs - **Skip-Silences** - Automatically skip silenced sections
- **Skip Silences**: Automatically skip silenced sections
- [**SponsorBlock**](https://github.com/ajayyy/SponsorBlock): Automatically Skips non-music parts like intro/outro or - [**SponsorBlock**](https://github.com/ajayyy/SponsorBlock): Automatically Skips non-music parts like intro/outro or
parts of music videos where the song isn't playing parts of music videos where the song isn't playing
@ -187,21 +158,26 @@ winget install th-ch.YouTubeMusic
- **Taskbar Media Control**: Control playback from - **Taskbar Media Control**: Control playback from
your [Windows taskbar](https://user-images.githubusercontent.com/78568641/111916130-24a35e80-8a82-11eb-80c8-5021c1aa27f4.png) your [Windows taskbar](https://user-images.githubusercontent.com/78568641/111916130-24a35e80-8a82-11eb-80c8-5021c1aa27f4.png)
- **TouchBar**: Custom TouchBar layout for macOS - **Touchbar**: Custom TouchBar layout for macOS
- **Tuna OBS**: Integration with [OBS](https://obsproject.com/)'s - **Tuna-OBS**: Integration with [OBS](https://obsproject.com/)'s
plugin [Tuna](https://obsproject.com/forum/resources/tuna.843/) plugin [Tuna](https://obsproject.com/forum/resources/tuna.843/)
- **Video Quality Changer**: Allows changing the video quality with
a [button](https://user-images.githubusercontent.com/78568641/138574366-70324a5e-2d64-4f6a-acdd-dc2a2b9cecc5.png) on
the video overlay
- **Video Toggle**: Adds - **Video Toggle**: Adds
a [button](https://user-images.githubusercontent.com/28893833/173663950-63e6610e-a532-49b7-9afa-54cb57ddfc15.png) to a [button](https://user-images.githubusercontent.com/28893833/173663950-63e6610e-a532-49b7-9afa-54cb57ddfc15.png) to
switch between Video/Song mode. can also optionally remove the whole video tab switch between Video/Song mode. can also optionally remove the whole video tab
- **Visualizer**: Different music visualizers - **Visualizer**: Different music visualizers
---
- **Auto confirm when paused** (Always Enabled): disable
the ["Continue Watching?"](https://user-images.githubusercontent.com/61631665/129977894-01c60740-7ec6-4bf0-9a2c-25da24491b0e.png)
popup that pause music after a certain time
> If `Hide Menu` option is on - you can show the menu with the <kbd>alt</kbd> key (or <kbd>\`</kbd> [backtick] if using
> the in-app-menu plugin)
## Themes ## Themes
You can load CSS files to change the look of the application (Options > Visual Tweaks > Themes). You can load CSS files to change the look of the application (Options > Visual Tweaks > Themes).
@ -214,7 +190,7 @@ Some predefined themes are available in https://github.com/kerichdev/themes-for-
git clone https://github.com/th-ch/youtube-music git clone https://github.com/th-ch/youtube-music
cd youtube-music cd youtube-music
pnpm install --frozen-lockfile pnpm install --frozen-lockfile
pnpm dev pnpm start
``` ```
## Build your own plugins ## Build your own plugins
@ -226,72 +202,49 @@ Using plugins, you can:
### Creating a plugin ### Creating a plugin
Create a folder in `src/plugins/YOUR-PLUGIN-NAME`: Create a folder in `plugins/YOUR-PLUGIN-NAME`:
- if you need to manipulate the BrowserWindow, create a file with the following template:
- `index.ts`: the main file of the plugin
```typescript ```typescript
import style from './style.css?inline'; // import style as inline // file: back.ts
export default (win: Electron.BrowserWindow, config: ConfigType<'YOUR-PLUGIN-NAME'>) => {
// something
};
```
import { createPlugin } from '@/utils'; then, register the plugin in `index.ts`:
export default createPlugin({ ```typescript
name: 'Plugin Label', import yourPlugin from './plugins/YOUR-PLUGIN-NAME/back';
restartNeeded: true, // if value is true, ytmusic show restart dialog
config: {
enabled: false,
}, // your custom config
stylesheets: [style], // your custom style,
menu: async ({ getConfig, setConfig }) => {
// All *Config methods are wrapped Promise<T>
const config = await getConfig();
return [
{
label: 'menu',
submenu: [1, 2, 3].map((value) => ({
label: `value ${value}`,
type: 'radio',
checked: config.value === value,
click() {
setConfig({ value });
},
})),
},
];
},
backend: {
start({ window, ipc }) {
window.maximize();
// you can communicate with renderer plugin // ...
ipc.handle('some-event', () => {
return 'hello'; const mainPlugins = {
}); // ...
}, 'YOUR-PLUGIN-NAME': yourPlugin,
// it fired when config changed };
onConfigChange(newConfig) { /* ... */ }, ```
// it fired when plugin disabled
stop(context) { /* ... */ }, - if you need to change the front, create a file with the following template:
},
renderer: { ```typescript
async start(context) { // file: front.ts
console.log(await context.ipc.invoke('some-event')); export default (config: ConfigType<'YOUR-PLUGIN-NAME'>) => {
}, // This function will be called as a preload script
// Only renderer available hook // So you can use front features like `document.querySelector`
onPlayerApiReady(api: YoutubePlayer, context: RendererContext) { };
// set plugin config easily ```
context.setConfig({ myConfig: api.getVolume() });
}, then, register the plugin in `preload.ts`:
onConfigChange(newConfig) { /* ... */ },
stop(_context) { /* ... */ }, ```typescript
}, import yourPlugin from './plugins/YOUR-PLUGIN-NAME/front';
preload: {
async start({ getConfig }) { const rendererPlugins: PluginMapper<'renderer'> = {
const config = await getConfig(); // ...
}, 'YOUR-PLUGIN-NAME': yourPlugin,
onConfigChange(newConfig) {}, };
stop(_context) {},
},
});
``` ```
### Common use cases ### Common use cases
@ -299,42 +252,27 @@ export default createPlugin({
- injecting custom CSS: create a `style.css` file in the same folder then: - injecting custom CSS: create a `style.css` file in the same folder then:
```typescript ```typescript
// index.ts import path from 'node:path';
import style from './style.css?inline'; // import style as inline import { injectCSS } from '../utils';
import { createPlugin } from '@/utils'; // back.ts
export default (win: Electron.BrowserWindow) => {
export default createPlugin({ injectCSS(win.webContents, path.join(__dirname, 'style.css'));
name: 'Plugin Label', };
restartNeeded: true, // if value is true, ytmusic will show a restart dialog
config: {
enabled: false,
}, // your custom config
stylesheets: [style], // your custom style
renderer() {} // define renderer hook
});
``` ```
- If you want to change the HTML: - changing the HTML:
```typescript ```typescript
import { createPlugin } from '@/utils'; // front.ts
export default () => {
export default createPlugin({ // Remove the login button
name: 'Plugin Label', document.querySelector(".sign-in-link.ytmusic-nav-bar").remove();
restartNeeded: true, // if value is true, ytmusic will show the restart dialog };
config: {
enabled: false,
}, // your custom config
renderer() {
// Remove the login button
document.querySelector(".sign-in-link.ytmusic-nav-bar").remove();
} // define renderer hook
});
``` ```
- communicating between the front and back: can be done using the ipcMain module from electron. See `index.ts` file and - communicating between the front and back: can be done using the ipcMain module from electron. See `utils.js` file and
example in `sponsorblock` plugin. example in `navigation` plugin.
## Build ## Build
@ -350,12 +288,6 @@ export default createPlugin({
Builds the app for macOS, Linux, and Windows, Builds the app for macOS, Linux, and Windows,
using [electron-builder](https://github.com/electron-userland/electron-builder). using [electron-builder](https://github.com/electron-userland/electron-builder).
## Production Preview
```bash
pnpm start
```
## Tests ## Tests
```bash ```bash
@ -367,10 +299,3 @@ Uses [Playwright](https://playwright.dev/) to test the app.
## License ## License
MIT © [th-ch](https://github.com/th-ch/youtube-music) MIT © [th-ch](https://github.com/th-ch/youtube-music)
## Most asked questions
### Why apps menu isn't showing up?
If `Hide Menu` option is on - you can show the menu with the <kbd>alt</kbd> key (or <kbd>\`</kbd> [backtick] if using
the in-app-menu plugin)

View File

@ -1,8 +0,0 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"config:base"
],
"labels": ["dependencies"],
"postUpdateOptions": ["pnpmDedupe"]
}

60
rollup.main.config.ts Normal file
View File

@ -0,0 +1,60 @@
import { defineConfig } from 'rollup';
import builtinModules from 'builtin-modules';
import typescript from '@rollup/plugin-typescript';
import commonjs from '@rollup/plugin-commonjs';
import nodeResolvePlugin from '@rollup/plugin-node-resolve';
import json from '@rollup/plugin-json';
import terser from '@rollup/plugin-terser';
import { string } from 'rollup-plugin-string';
import css from 'rollup-plugin-import-css';
import wasmPlugin from '@rollup/plugin-wasm';
import copy from 'rollup-plugin-copy';
export default defineConfig({
plugins: [
typescript({
module: 'ESNext',
}),
nodeResolvePlugin({
browser: false,
preferBuiltins: true,
exportConditions: ['node', 'default', 'module', 'import'],
}),
commonjs({
ignoreDynamicRequires: true,
}),
wasmPlugin({
maxFileSize: 0,
targetEnv: 'browser',
}),
json(),
string({
include: '**/*.html',
}),
css(),
copy({
targets: [
{ src: 'src/error.html', dest: 'dist/' },
{ src: 'assets', dest: 'dist/' },
],
}),
terser({
ecma: 2020,
}),
{
closeBundle() {
if (!process.env.ROLLUP_WATCH) {
setTimeout(() => process.exit(0));
}
},
name: 'force-close',
},
],
input: './src/index.ts',
output: {
format: 'cjs',
name: '[name].js',
dir: './dist',
},
external: ['electron', 'custom-electron-prompt', ...builtinModules],
});

54
rollup.preload.config.ts Normal file
View File

@ -0,0 +1,54 @@
import { defineConfig } from 'rollup';
import builtinModules from 'builtin-modules';
import typescript from '@rollup/plugin-typescript';
import commonjs from '@rollup/plugin-commonjs';
import nodeResolvePlugin from '@rollup/plugin-node-resolve';
import json from '@rollup/plugin-json';
import terser from '@rollup/plugin-terser';
import { string } from 'rollup-plugin-string';
import css from 'rollup-plugin-import-css';
import wasmPlugin from '@rollup/plugin-wasm';
import image from '@rollup/plugin-image';
export default defineConfig({
plugins: [
typescript({
module: 'ESNext',
}),
nodeResolvePlugin({
browser: false,
preferBuiltins: true,
}),
commonjs({
ignoreDynamicRequires: true,
}),
json(),
string({
include: '**/*.html',
}),
css(),
wasmPlugin({
maxFileSize: 0,
targetEnv: 'browser',
}),
image({ dom: true }),
terser({
ecma: 2020,
}),
{
closeBundle() {
if (!process.env.ROLLUP_WATCH) {
setTimeout(() => process.exit(0));
}
},
name: 'force-close',
},
],
input: './src/preload.ts',
output: {
format: 'cjs',
name: '[name].js',
dir: './dist',
},
external: ['electron', 'custom-electron-prompt', ...builtinModules],
});

View File

@ -1,20 +1,24 @@
import { blockers } from '../plugins/adblocker/blocker-types';
import { DefaultPresetList } from '../plugins/downloader/types';
export interface WindowSizeConfig { export interface WindowSizeConfig {
width: number; width: number;
height: number; height: number;
} }
export interface WindowPositionConfig {
x: number;
y: number;
}
export interface DefaultConfig { export interface DefaultConfig {
'window-size': WindowSizeConfig; 'window-size': {
width: number;
height: number;
}
'window-maximized': boolean; 'window-maximized': boolean;
'window-position': WindowPositionConfig; 'window-position': {
x: number;
y: number;
}
url: string; url: string;
options: { options: {
language?: string;
tray: boolean; tray: boolean;
appVisible: boolean; appVisible: boolean;
autoUpdates: boolean; autoUpdates: boolean;
@ -32,13 +36,11 @@ export interface DefaultConfig {
proxy: string; proxy: string;
startingPage: string; startingPage: string;
overrideUserAgent: boolean; overrideUserAgent: boolean;
usePodcastParticipantAsArtist: boolean;
themes: string[]; themes: string[];
}; }
plugins: Record<string, unknown>;
} }
const defaultConfig: DefaultConfig = { const defaultConfig = {
'window-size': { 'window-size': {
width: 1100, width: 1100,
height: 550, height: 550,
@ -67,10 +69,230 @@ const defaultConfig: DefaultConfig = {
proxy: '', proxy: '',
startingPage: '', startingPage: '',
overrideUserAgent: false, overrideUserAgent: false,
usePodcastParticipantAsArtist: false, themes: [] as string[],
themes: [], },
/** please order alphabetically */
'plugins': {
'adblocker': {
enabled: true,
cache: true,
blocker: blockers.InPlayer as string,
additionalBlockLists: [], // Additional list of filters, e.g "https://raw.githubusercontent.com/uBlockOrigin/uAssets/master/filters/filters.txt"
disableDefaultLists: false,
},
'album-color-theme': {},
'ambient-mode': {
enabled: false,
quality: 50,
buffer: 30,
interpolationTime: 1500,
blur: 100,
size: 100,
opacity: 1,
fullscreen: false,
},
'audio-compressor': {},
'blur-nav-bar': {},
'bypass-age-restrictions': {},
'captions-selector': {
enabled: false,
disableCaptions: false,
autoload: false,
lastCaptionsCode: '',
},
'compact-sidebar': {},
'crossfade': {
enabled: false,
fadeInDuration: 1500, // Ms
fadeOutDuration: 5000, // Ms
secondsBeforeEnd: 10, // S
fadeScaling: 'linear', // 'linear', 'logarithmic' or a positive number in dB
},
'disable-autoplay': {
applyOnce: false,
},
'discord': {
enabled: false,
autoReconnect: true, // If enabled, will try to reconnect to discord every 5 seconds after disconnecting or failing to connect
activityTimoutEnabled: true, // If enabled, the discord rich presence gets cleared when music paused after the time specified below
activityTimoutTime: 10 * 60 * 1000, // 10 minutes
playOnYouTubeMusic: true, // Add a "Play on YouTube Music" button to rich presence
hideGitHubButton: false, // Disable the "View App On GitHub" button
hideDurationLeft: false, // Hides the start and end time of the song to rich presence
},
'downloader': {
enabled: false,
downloadFolder: undefined as string | undefined, // Custom download folder (absolute path)
selectedPreset: 'mp3 (256kbps)', // Selected preset
customPresetSetting: DefaultPresetList['mp3 (256kbps)'], // Presets
skipExisting: false,
playlistMaxItems: undefined as number | undefined,
},
'http-api': {},
'exponential-volume': {},
'in-app-menu': {
/**
* true in Windows, false in Linux and macOS (see youtube-music/config/store.ts)
*/
enabled: false,
hideDOMWindowControls: false,
},
'last-fm': {
enabled: false,
token: undefined as string | undefined, // Token used for authentication
session_key: undefined as string | undefined, // Session key used for scrobbling
api_root: 'http://ws.audioscrobbler.com/2.0/',
api_key: '04d76faaac8726e60988e14c105d421a', // Api key registered by @semvis123
secret: 'a5d2a36fdf64819290f6982481eaffa2',
},
'lumiastream': {},
'lyrics-genius': {
romanizedLyrics: false,
},
'navigation': {
enabled: true,
},
'no-google-login': {},
'notifications': {
enabled: false,
unpauseNotification: false,
urgency: 'normal', // Has effect only on Linux
// the following has effect only on Windows
interactive: true,
toastStyle: 1, // See plugins/notifications/utils for more info
refreshOnPlayPause: false,
trayControls: true,
hideButtonText: false,
},
'picture-in-picture': {
'enabled': false,
'alwaysOnTop': true,
'savePosition': true,
'saveSize': false,
'hotkey': 'P',
'pip-position': [10, 10],
'pip-size': [450, 275],
'isInPiP': false,
'useNativePiP': false,
},
'playback-speed': {},
'precise-volume': {
enabled: false,
steps: 1, // Percentage of volume to change
arrowsShortcut: true, // Enable ArrowUp + ArrowDown local shortcuts
globalShortcuts: {
volumeUp: '',
volumeDown: '',
},
savedVolume: undefined as number | undefined, // Plugin save volume between session here
},
'quality-changer': {},
'shortcuts': {
enabled: false,
overrideMediaKeys: false,
global: {
previous: '',
playPause: '',
next: '',
} as Record<string, string>,
local: {
previous: '',
playPause: '',
next: '',
} as Record<string, string>,
},
'skip-silences': {
onlySkipBeginning: false,
},
'sponsorblock': {
enabled: false,
apiURL: 'https://sponsor.ajay.app',
categories: [
'sponsor',
'intro',
'outro',
'interaction',
'selfpromo',
'music_offtopic',
],
},
'taskbar-mediacontrol': {},
'touchbar': {},
'tuna-obs': {},
'video-toggle': {
enabled: false,
hideVideo: false,
mode: 'custom',
forceHide: false,
align: '',
},
'visualizer': {
enabled: false,
type: 'butterchurn',
// Config per visualizer
butterchurn: {
preset: 'martin [shadow harlequins shape code] - fata morgana',
renderingFrequencyInMs: 500,
blendTimeInSeconds: 2.7,
},
vudio: {
effect: 'lighting',
accuracy: 128,
lighting: {
maxHeight: 160,
maxSize: 12,
lineWidth: 1,
color: '#49f3f7',
shadowBlur: 2,
shadowColor: 'rgba(244,244,244,.5)',
fadeSide: true,
prettify: false,
horizontalAlign: 'center',
verticalAlign: 'middle',
dottify: true,
},
},
wave: {
animations: [
{
type: 'Cubes',
config: {
bottom: true,
count: 30,
cubeHeight: 5,
fillColor: { gradient: ['#FAD961', '#F76B1C'] },
lineColor: 'rgba(0,0,0,0)',
radius: 20,
},
},
{
type: 'Cubes',
config: {
top: true,
count: 12,
cubeHeight: 5,
fillColor: { gradient: ['#FAD961', '#F76B1C'] },
lineColor: 'rgba(0,0,0,0)',
radius: 10,
},
},
{
type: 'Circles',
config: {
lineColor: {
gradient: ['#FAD961', '#FAD961', '#F76B1C'],
rotate: 90,
},
lineWidth: 4,
diameter: 20,
count: 10,
frequencyBand: 'base',
},
},
],
},
},
}, },
'plugins': {},
}; };
export default defaultConfig; export default defaultConfig;

241
src/config/dynamic.ts Normal file
View File

@ -0,0 +1,241 @@
/* eslint-disable @typescript-eslint/require-await */
import { ipcMain, ipcRenderer } from 'electron';
import defaultConfig from './defaults';
import { getOptions, setMenuOptions, setOptions } from './plugins';
import { sendToFront } from '../providers/app-controls';
import { Entries } from '../utils/type-utils';
export type DefaultPluginsConfig = typeof defaultConfig.plugins;
export type OneOfDefaultConfigKey = keyof DefaultPluginsConfig;
export type OneOfDefaultConfig = typeof defaultConfig.plugins[OneOfDefaultConfigKey];
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const activePlugins: { [key in OneOfDefaultConfigKey]?: PluginConfig<any> } = {};
/**
* [!IMPORTANT!]
* The method is **sync** in the main process and **async** in the renderer process.
*/
export const getActivePlugins
= process.type === 'renderer'
? async () => ipcRenderer.invoke('get-active-plugins')
: () => activePlugins;
if (process.type === 'browser') {
ipcMain.handle('get-active-plugins', getActivePlugins);
}
/**
* [!IMPORTANT!]
* The method is **sync** in the main process and **async** in the renderer process.
*/
export const isActive
= process.type === 'renderer'
? async (plugin: string) =>
plugin in (await ipcRenderer.invoke('get-active-plugins'))
: (plugin: string): boolean => plugin in activePlugins;
interface PluginConfigOptions {
enableFront: boolean;
initialOptions?: OneOfDefaultConfig;
}
/**
* This class is used to create a dynamic synced config for plugins.
*
* [!IMPORTANT!]
* The methods are **sync** in the main process and **async** in the renderer process.
*
* @param {string} name - The name of the plugin.
* @param {boolean} [options.enableFront] - Whether the config should be available in front.js. Default: false.
* @param {object} [options.initialOptions] - The initial options for the plugin. Default: loaded from store.
*
* @example
* const { PluginConfig } = require("../../config/dynamic");
* const config = new PluginConfig("plugin-name", { enableFront: true });
* module.exports = { ...config };
*
* // or
*
* module.exports = (win, options) => {
* const config = new PluginConfig("plugin-name", {
* enableFront: true,
* initialOptions: options,
* });
* setupMyPlugin(win, config);
* };
*/
export type ConfigType<T extends OneOfDefaultConfigKey> = typeof defaultConfig.plugins[T];
type ValueOf<T> = T[keyof T];
type Mode<T, Mode extends 'r' | 'm'> = Mode extends 'r' ? Promise<T> : T;
export class PluginConfig<T extends OneOfDefaultConfigKey> {
private readonly name: string;
private readonly config: ConfigType<T>;
private readonly defaultConfig: ConfigType<T>;
private readonly enableFront: boolean;
private subscribers: { [key in keyof ConfigType<T>]?: (config: ConfigType<T>) => void } = {};
private allSubscribers: ((config: ConfigType<T>) => void)[] = [];
constructor(
name: T,
options: PluginConfigOptions = {
enableFront: false,
},
) {
const pluginDefaultConfig = defaultConfig.plugins[name] ?? {};
const pluginConfig = options.initialOptions || getOptions(name) || {};
this.name = name;
this.enableFront = options.enableFront;
this.defaultConfig = pluginDefaultConfig;
this.config = { ...pluginDefaultConfig, ...pluginConfig };
if (this.enableFront) {
this.setupFront();
}
activePlugins[name] = this;
}
get<Key extends keyof ConfigType<T> = keyof ConfigType<T>>(key: Key): ConfigType<T>[Key] {
return this.config?.[key];
}
set(key: keyof ConfigType<T>, value: ValueOf<ConfigType<T>>) {
this.config[key] = value;
this.onChange(key);
this.save();
}
getAll(): ConfigType<T> {
return { ...this.config };
}
setAll(options: Partial<ConfigType<T>>) {
if (!options || typeof options !== 'object') {
throw new Error('Options must be an object.');
}
let changed = false;
for (const [key, value] of Object.entries(options) as Entries<typeof options>) {
if (this.config[key] !== value) {
if (value !== undefined) this.config[key] = value;
this.onChange(key, false);
changed = true;
}
}
if (changed) {
for (const fn of this.allSubscribers) {
fn(this.config);
}
}
this.save();
}
getDefaultConfig() {
return this.defaultConfig;
}
/**
* Use this method to set an option and restart the app if `appConfig.restartOnConfigChange === true`
*
* Used for options that require a restart to take effect.
*/
setAndMaybeRestart(key: keyof ConfigType<T>, value: ValueOf<ConfigType<T>>) {
this.config[key] = value;
setMenuOptions(this.name, this.config);
this.onChange(key);
}
subscribe(valueName: keyof ConfigType<T>, fn: (config: ConfigType<T>) => void) {
this.subscribers[valueName] = fn;
}
subscribeAll(fn: (config: ConfigType<T>) => void) {
this.allSubscribers.push(fn);
}
/** Called only from back */
private save() {
setOptions(this.name, this.config);
}
private onChange(valueName: keyof ConfigType<T>, single: boolean = true) {
this.subscribers[valueName]?.(this.config[valueName] as ConfigType<T>);
if (single) {
for (const fn of this.allSubscribers) {
fn(this.config);
}
}
}
private setupFront() {
const ignoredMethods = ['subscribe', 'subscribeAll'];
if (process.type === 'renderer') {
for (const [fnName, fn] of Object.entries(this) as Entries<this>) {
if (typeof fn !== 'function' || fn.name in ignoredMethods) {
return;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any,@typescript-eslint/no-unsafe-return
this[fnName] = (async (...args: any) => await ipcRenderer.invoke(
`${this.name}-config-${String(fnName)}`,
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
...args,
)) as typeof this[keyof this];
this.subscribe = (valueName, fn: (config: ConfigType<T>) => void) => {
if (valueName in this.subscribers) {
console.error(`Already subscribed to ${String(valueName)}`);
}
this.subscribers[valueName] = fn;
ipcRenderer.on(
`${this.name}-config-changed-${String(valueName)}`,
(_, value: ConfigType<T>) => {
fn(value);
},
);
ipcRenderer.send(`${this.name}-config-subscribe`, valueName);
};
this.subscribeAll = (fn: (config: ConfigType<T>) => void) => {
ipcRenderer.on(`${this.name}-config-changed`, (_, value: ConfigType<T>) => {
fn(value);
});
ipcRenderer.send(`${this.name}-config-subscribe-all`);
};
}
} else if (process.type === 'browser') {
for (const [fnName, fn] of Object.entries(this) as Entries<this>) {
if (typeof fn !== 'function' || fn.name in ignoredMethods) {
return;
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument,@typescript-eslint/no-unsafe-return
ipcMain.handle(`${this.name}-config-${String(fnName)}`, (_, ...args) => fn(...args));
}
ipcMain.on(`${this.name}-config-subscribe`, (_, valueName: keyof ConfigType<T>) => {
this.subscribe(valueName, (value) => {
sendToFront(`${this.name}-config-changed-${String(valueName)}`, value);
});
});
ipcMain.on(`${this.name}-config-subscribe-all`, () => {
this.subscribeAll((value) => {
sendToFront(`${this.name}-config-changed`, value);
});
});
}
}
}

View File

@ -1,24 +1,15 @@
import Store from 'electron-store'; import Store from 'electron-store';
import { deepmergeCustom } from 'deepmerge-ts';
import defaultConfig from './defaults'; import defaultConfig from './defaults';
import store from './store';
import plugins from './plugins'; import plugins from './plugins';
import store from './store';
import { restart } from '@/providers/app-controls'; import { restart } from '../providers/app-controls';
const deepmerge = deepmergeCustom({
mergeArrays: false,
});
const set = (key: string, value: unknown) => { const set = (key: string, value: unknown) => {
store.set(key, value); store.set(key, value);
}; };
const setPartial = (key: string, value: object, defaultValue?: object) => {
const newValue = deepmerge(defaultValue ?? {}, store.get(key) ?? {}, value);
store.set(key, newValue);
};
function setMenuOption(key: string, value: unknown) { function setMenuOption(key: string, value: unknown) {
set(key, value); set(key, value);
@ -29,65 +20,34 @@ function setMenuOption(key: string, value: unknown) {
// MAGIC OF TYPESCRIPT // MAGIC OF TYPESCRIPT
type Prev = [ type Prev = [never, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
never, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, ...0[]]
0, type Join<K, P> = K extends string | number ?
1, P extends string | number ?
2, `${K}${'' extends P ? '' : '.'}${P}`
3, : never : never;
4, type Paths<T, D extends number = 10> = [D] extends [never] ? never : T extends object ?
5, { [K in keyof T]-?: K extends string | number ?
6, `${K}` | Join<K, Paths<T[K], Prev[D]>>
7,
8,
9,
10,
11,
12,
13,
14,
15,
16,
17,
18,
19,
20,
...0[],
];
type Join<K, P> = K extends string | number
? P extends string | number
? `${K}${'' extends P ? '' : '.'}${P}`
: never : never
: never; }[keyof T] : ''
type Paths<T, D extends number = 10> = [D] extends [never]
? never
: T extends object
? {
[K in keyof T]-?: K extends string | number
? `${K}` | Join<K, Paths<T[K], Prev[D]>>
: never;
}[keyof T]
: '';
type SplitKey<K> = K extends `${infer A}.${infer B}` ? [A, B] : [K, string]; type SplitKey<K> = K extends `${infer A}.${infer B}` ? [A, B] : [K, string];
type PathValue<T, K extends string> = SplitKey<K> extends [ type PathValue<T, K extends string> =
infer A extends keyof T, SplitKey<K> extends [infer A extends keyof T, infer B extends string]
infer B extends string, ? PathValue<T[A], B>
] : T;
? PathValue<T[A], B> const get = <Key extends Paths<typeof defaultConfig>>(key: Key) => store.get(key) as PathValue<typeof defaultConfig, typeof key>;
: T;
const get = <Key extends Paths<typeof defaultConfig>>(key: Key) =>
store.get(key) as PathValue<typeof defaultConfig, typeof key>;
export default { export default {
defaultConfig, defaultConfig,
get, get,
set, set,
setPartial,
setMenuOption, setMenuOption,
edit: () => store.openInEditor(), edit: () => store.openInEditor(),
watch(cb: Parameters<Store['onDidAnyChange']>[0]) { watch(cb: Parameters<Store['onDidChange']>[1]) {
store.onDidAnyChange(cb); store.onDidChange('options', cb);
store.onDidChange('plugins', cb);
}, },
plugins, plugins,
}; };

View File

@ -1,21 +1,24 @@
import { deepmerge } from 'deepmerge-ts';
import { allPlugins } from 'virtual:plugins';
import store from './store'; import store from './store';
import defaultConfig from './defaults';
import { restart } from '@/providers/app-controls'; import { restart } from '../providers/app-controls';
import { Entries } from '../utils/type-utils';
import type { PluginConfig } from '@/types/plugins'; interface Plugin {
enabled: boolean;
}
export function getPlugins() { type DefaultPluginsConfig = typeof defaultConfig.plugins;
return store.get('plugins') as Record<string, PluginConfig>;
export function getEnabled() {
const plugins = store.get('plugins') as DefaultPluginsConfig;
return (Object.entries(plugins) as Entries<DefaultPluginsConfig>).filter(([plugin]) =>
isEnabled(plugin),
);
} }
export function isEnabled(plugin: string) { export function isEnabled(plugin: string) {
const pluginConfig = deepmerge( const pluginConfig = (store.get('plugins') as Record<string, Plugin>)[plugin];
allPlugins[plugin].config ?? { enabled: false },
(store.get('plugins') as Record<string, PluginConfig>)[plugin] ?? {},
);
return pluginConfig !== undefined && pluginConfig.enabled; return pluginConfig !== undefined && pluginConfig.enabled;
} }
@ -25,11 +28,7 @@ export function isEnabled(plugin: string) {
* @param options Options to set * @param options Options to set
* @param exclude Options to exclude from the options object * @param exclude Options to exclude from the options object
*/ */
export function setOptions<T>( export function setOptions<T>(plugin: string, options: T, exclude: string[] = ['enabled']) {
plugin: string,
options: T,
exclude: string[] = ['enabled'],
) {
const plugins = store.get('plugins') as Record<string, T>; const plugins = store.get('plugins') as Record<string, T>;
// HACK: This is a workaround for preventing changed options from being overwritten // HACK: This is a workaround for preventing changed options from being overwritten
exclude.forEach((key) => { exclude.forEach((key) => {
@ -46,11 +45,7 @@ export function setOptions<T>(
}); });
} }
export function setMenuOptions<T>( export function setMenuOptions<T>(plugin: string, options: T, exclude: string[] = ['enabled']) {
plugin: string,
options: T,
exclude: string[] = ['enabled'],
) {
setOptions(plugin, options, exclude); setOptions(plugin, options, exclude);
if (store.get('options.restartOnConfigChanges')) { if (store.get('options.restartOnConfigChanges')) {
restart(); restart();
@ -71,7 +66,7 @@ export function disable(plugin: string) {
export default { export default {
isEnabled, isEnabled,
getPlugins, getEnabled,
enable, enable,
disable, disable,
setOptions, setOptions,

View File

@ -1,82 +1,25 @@
import Store from 'electron-store'; import Store from 'electron-store';
import Conf from 'conf'; import Conf from 'conf';
import is from 'electron-is';
import defaults from './defaults'; import defaults from './defaults';
import { DefaultPresetList, type Preset } from '@/plugins/downloader/types'; import { DefaultPresetList, type Preset } from '../plugins/downloader/types';
const getDefaults = () => {
if (is.windows()) {
defaults.plugins['in-app-menu'].enabled = true;
}
return defaults;
};
const setDefaultPluginOptions = (store: Conf<Record<string, unknown>>, plugin: keyof typeof defaults.plugins) => {
if (!store.get(`plugins.${plugin}`)) {
store.set(`plugins.${plugin}`, defaults.plugins[plugin]);
}
};
const migrations = { const migrations = {
'>=3.3.0'(store: Conf<Record<string, unknown>>) {
const lastfmConfig = store.get('plugins.lastfm') as {
enabled?: boolean;
token?: string;
session_key?: string;
api_root?: string;
api_key?: string;
secret?: string;
};
if (lastfmConfig) {
let scrobblerConfig = store.get(
'plugins.scrobbler',
) as {
enabled?: boolean;
scrobblers?: {
lastfm?: {
enabled?: boolean;
token?: string;
sessionKey?: string;
apiRoot?: string;
apiKey?: string;
secret?: string;
};
};
} | undefined;
if (!scrobblerConfig) {
scrobblerConfig = {
enabled: lastfmConfig.enabled,
};
}
if (!scrobblerConfig.scrobblers) {
scrobblerConfig.scrobblers = {
lastfm: {},
};
}
scrobblerConfig.scrobblers.lastfm = {
enabled: lastfmConfig.enabled,
token: lastfmConfig.token,
sessionKey: lastfmConfig.session_key,
apiRoot: lastfmConfig.api_root,
apiKey: lastfmConfig.api_key,
secret: lastfmConfig.secret,
};
store.set('plugins.scrobbler', scrobblerConfig);
}
},
'>=3.0.0'(store: Conf<Record<string, unknown>>) {
const discordConfig = store.get('plugins.discord') as Record<
string,
unknown
>;
if (discordConfig) {
const oldActivityTimoutEnabled = store.get(
'plugins.discord.activityTimoutEnabled',
) as boolean | undefined;
const oldActivityTimoutTime = store.get(
'plugins.discord.activityTimoutTime',
) as number | undefined;
if (oldActivityTimoutEnabled !== undefined) {
discordConfig.activityTimeoutEnabled = oldActivityTimoutEnabled;
store.set('plugins.discord', discordConfig);
}
if (oldActivityTimoutTime !== undefined) {
discordConfig.activityTimeoutTime = oldActivityTimoutTime;
store.set('plugins.discord', discordConfig);
}
}
},
'>=2.1.3'(store: Conf<Record<string, unknown>>) { '>=2.1.3'(store: Conf<Record<string, unknown>>) {
const listenAlong = store.get('plugins.discord.listenAlong'); const listenAlong = store.get('plugins.discord.listenAlong');
if (listenAlong !== undefined) { if (listenAlong !== undefined) {
@ -85,24 +28,19 @@ const migrations = {
} }
}, },
'>=2.1.0'(store: Conf<Record<string, unknown>>) { '>=2.1.0'(store: Conf<Record<string, unknown>>) {
const originalPreset = store.get('plugins.downloader.preset') as const originalPreset = store.get('plugins.downloader.preset') as string | undefined;
| string
| undefined;
if (originalPreset) { if (originalPreset) {
if (originalPreset !== 'opus') { if (originalPreset !== 'opus') {
store.set('plugins.downloader.selectedPreset', 'Custom'); store.set('plugins.downloader.selectedPreset', 'Custom');
store.set('plugins.downloader.customPresetSetting', { store.set('plugins.downloader.customPresetSetting', {
extension: 'mp3', extension: 'mp3',
ffmpegArgs: ffmpegArgs: store.get('plugins.downloader.ffmpegArgs') as string[] ?? DefaultPresetList['mp3 (256kbps)'].ffmpegArgs,
(store.get('plugins.downloader.ffmpegArgs') as string[]) ??
DefaultPresetList['mp3 (256kbps)'].ffmpegArgs,
} satisfies Preset); } satisfies Preset);
} else { } else {
store.set('plugins.downloader.selectedPreset', 'Source'); store.set('plugins.downloader.selectedPreset', 'Source');
store.set('plugins.downloader.customPresetSetting', { store.set('plugins.downloader.customPresetSetting', {
extension: null, extension: null,
ffmpegArgs: ffmpegArgs: store.get('plugins.downloader.ffmpegArgs') as string[] ?? [],
(store.get('plugins.downloader.ffmpegArgs') as string[]) ?? [],
} satisfies Preset); } satisfies Preset);
} }
store.delete('plugins.downloader.preset'); store.delete('plugins.downloader.preset');
@ -110,11 +48,12 @@ const migrations = {
} }
}, },
'>=1.20.0'(store: Conf<Record<string, unknown>>) { '>=1.20.0'(store: Conf<Record<string, unknown>>) {
store.delete('plugins.visualizer'); // default value is now in the plugin setDefaultPluginOptions(store, 'visualizer');
if (store.get('plugins.notifications.toastStyle') === undefined) { if (store.get('plugins.notifications.toastStyle') === undefined) {
const pluginOptions = store.get('plugins.notifications') || {}; const pluginOptions = store.get('plugins.notifications') || {};
store.set('plugins.notifications', { store.set('plugins.notifications', {
...defaults.plugins.notifications,
...pluginOptions, ...pluginOptions,
}); });
} }
@ -125,7 +64,7 @@ const migrations = {
} }
}, },
'>=1.17.0'(store: Conf<Record<string, unknown>>) { '>=1.17.0'(store: Conf<Record<string, unknown>>) {
store.delete('plugins.picture-in-picture'); // default value is now in the plugin setDefaultPluginOptions(store, 'picture-in-picture');
if (store.get('plugins.video-toggle.mode') === undefined) { if (store.get('plugins.video-toggle.mode') === undefined) {
store.set('plugins.video-toggle.mode', 'custom'); store.set('plugins.video-toggle.mode', 'custom');
@ -149,41 +88,31 @@ const migrations = {
} }
}, },
'>=1.12.0'(store: Conf<Record<string, unknown>>) { '>=1.12.0'(store: Conf<Record<string, unknown>>) {
const options = store.get('plugins.shortcuts') as const options = store.get('plugins.shortcuts') as Record<string, {
| Record< action: string;
string, shortcut: unknown;
| { }[] | Record<string, unknown>>;
action: string; let updated = false;
shortcut: unknown; for (const optionType of ['global', 'local']) {
}[] if (Array.isArray(options[optionType])) {
| Record<string, unknown> const optionsArray = options[optionType] as {
> action: string;
| undefined; shortcut: unknown;
if (options) { }[];
let updated = false; const updatedOptions: Record<string, unknown> = {};
for (const optionType of ['global', 'local']) { for (const optionObject of optionsArray) {
if ( if (optionObject.action && optionObject.shortcut) {
Object.hasOwn(options, optionType) && updatedOptions[optionObject.action] = optionObject.shortcut;
Array.isArray(options[optionType])
) {
const optionsArray = options[optionType] as {
action: string;
shortcut: unknown;
}[];
const updatedOptions: Record<string, unknown> = {};
for (const optionObject of optionsArray) {
if (optionObject.action && optionObject.shortcut) {
updatedOptions[optionObject.action] = optionObject.shortcut;
}
} }
options[optionType] = updatedOptions;
updated = true;
} }
options[optionType] = updatedOptions;
updated = true;
} }
if (updated) { }
store.set('plugins.shortcuts', options);
} if (updated) {
store.set('plugins.shortcuts', options);
} }
}, },
'>=1.11.0'(store: Conf<Record<string, unknown>>) { '>=1.11.0'(store: Conf<Record<string, unknown>>) {
@ -226,10 +155,7 @@ const migrations = {
}; };
export default new Store({ export default new Store({
defaults: { defaults: getDefaults(),
...defaults,
// README: 'plugin' uses deepmerge to populate the default values, so it is not necessary to include it here
},
clearInvalidConfig: false, clearInvalidConfig: false,
migrations, migrations,
}); });

View File

@ -53,45 +53,33 @@ declare module 'custom-electron-prompt' {
export interface CounterPromptOptions extends BasePromptOptions<'counter'> { export interface CounterPromptOptions extends BasePromptOptions<'counter'> {
counterOptions: CounterOptions; counterOptions: CounterOptions;
} }
export interface MultiInputPromptOptions export interface MultiInputPromptOptions extends BasePromptOptions<'multiInput'> {
extends BasePromptOptions<'multiInput'> {
multiInputOptions: InputOptions[]; multiInputOptions: InputOptions[];
} }
export interface KeybindPromptOptions extends BasePromptOptions<'keybind'> { export interface KeybindPromptOptions extends BasePromptOptions<'keybind'> {
keybindOptions: KeybindOptions[]; keybindOptions: KeybindOptions[];
} }
export type PromptOptions<T extends string> = T extends 'input' export type PromptOptions<T extends string> = (
? InputPromptOptions T extends 'input' ? InputPromptOptions :
: T extends 'select' T extends 'select' ? SelectPromptOptions :
? SelectPromptOptions T extends 'counter' ? CounterPromptOptions :
: T extends 'counter' T extends 'keybind' ? KeybindPromptOptions :
? CounterPromptOptions T extends 'multiInput' ? MultiInputPromptOptions :
: T extends 'keybind' never
? KeybindPromptOptions );
: T extends 'multiInput'
? MultiInputPromptOptions
: never;
type PromptResult<T extends string> = T extends 'input' type PromptResult<T extends string> = T extends 'input' ? string :
? string T extends 'select' ? string :
: T extends 'select' T extends 'counter' ? number :
? string T extends 'keybind' ? {
: T extends 'counter' value: string;
? number accelerator: string
: T extends 'keybind' }[] :
? { T extends 'multiInput' ? string[] :
value: string; never;
accelerator: string;
}[]
: T extends 'multiInput'
? string[]
: never;
const prompt: <T extends Type>( const prompt: <T extends Type>(options?: PromptOptions<T> & { type: T }, parent?: BrowserWindow) => Promise<PromptResult<T> | null>;
options?: PromptOptions<T> & { type: T },
parent?: BrowserWindow,
) => Promise<PromptResult<T> | null>;
export default prompt; export default prompt;
} }

View File

@ -1,50 +1,50 @@
<!doctype html> <!DOCTYPE html>
<html> <html>
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8"/>
<title>Cannot load YouTube Music</title> <title>Cannot load YouTube Music</title>
<style> <style>
body { body {
background: #000; background: #000;
} }
.container { .container {
margin: 0; margin: 0;
font-family: Roboto, Arial, sans-serif; font-family: Roboto, Arial, sans-serif;
font-size: 20px; font-size: 20px;
font-weight: 500; font-weight: 500;
color: rgba(255, 255, 255, 0.5); color: rgba(255, 255, 255, 0.5);
position: absolute; position: absolute;
top: 50%; top: 50%;
left: 50%; left: 50%;
margin-right: -50%; margin-right: -50%;
transform: translate(-50%, -50%); transform: translate(-50%, -50%);
text-align: center; text-align: center;
} }
.button { .button {
background: #065fd4; background: #065fd4;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
color: white; color: white;
font: inherit; font: inherit;
text-transform: uppercase; text-transform: uppercase;
text-decoration: none; text-decoration: none;
border-radius: 2px; border-radius: 2px;
font-size: 16px; font-size: 16px;
font-weight: normal; font-weight: normal;
text-align: center; text-align: center;
padding: 8px 22px; padding: 8px 22px;
display: inline-block; display: inline-block;
} }
</style> </style>
</head> </head>
<body> <body>
<div class="container"> <div class="container">
<p>Cannot load YouTube Music… Internet disconnected?</p> <p>Cannot load YouTube Music… Internet disconnected?</p>
<a class="button" href="#" onclick="reload()">Retry</a> <a class="button" href="#" onclick="reload()">Retry</a>
</div> </div>
</body> </body>
</html> </html>

View File

@ -1,18 +0,0 @@
import i18next, { init, t as i18t, changeLanguage } from 'i18next';
import { languageResources } from 'virtual:i18n';
export const loadI18n = async () =>
await init({
resources: languageResources,
lng: 'en',
fallbackLng: 'en',
interpolation: {
escapeValue: false,
},
});
export const setLanguage = async (language: string) =>
await changeLanguage(language);
export const t = i18t.bind(i18next);

View File

@ -1,11 +0,0 @@
export interface LanguageResources {
[lang: string]: {
translation: Record<string, unknown> & {
language?: {
name: string;
'local-name': string;
code: string;
};
};
};
}

View File

@ -1,46 +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": "bg",
"local-name": "Български",
"name": "Bulgarian"
},
"main": {
"console": {
"did-finish-load": {
"dev-tools": "Завърши зареждането. DevTools отворени"
},
"i18n": {
"loaded": "i18n заредено"
},
"second-instance": {
"receive-command": "Получена команда чрез протокол: \"{{command}}\""
},
"theme": {
"css-file-not-found": "CSS файл \"{{cssFile}}\" не съществува, ингнорира се"
},
"unresponsive": {
"details": "Грешка без отговор!\n{{error}}"
},
"when-ready": {
"clearing-cache-after-20s": "Изчистване на кешът на аппа"
},
"window": {
"tried-to-render-offscreen": "Прозореца се опита да се изрисува извън екрана, windowSize={{windowSize}}, displaySize={{displaySize}}, position={{position}}"
}
}
}
}

View File

@ -1,632 +0,0 @@
{
"common": {
"console": {
"plugins": {
"execute-failed": "Selhalo spuštění pluginu {{pluginName}}::{{contextName}}",
"executed-at-ms": "Plugin {{pluginName}}::{{contextName}} spuštěn za {{ms}}ms",
"initialize-failed": "Selhalo zapnutí \"{{pluginName}}\" pluginu",
"load-all": "Načítání všech pluginů",
"load-failed": "Selhalo načtení \"{{pluginName}}\" pluginu",
"loaded": "Plugin \"{{pluginName}}\" načten",
"unload-failed": "Selhalo unload \"{{pluginName}}\" pluginu",
"unloaded": "Plugin {{pluginName}} byl odnačten"
}
}
},
"language": {
"code": "cs",
"local-name": "Čeština",
"name": "Czech"
},
"main": {
"console": {
"did-finish-load": {
"dev-tools": "Načítání dokončeno. Vývojářské nástroje se otevřely"
},
"i18n": {
"loaded": "i18n načteno"
},
"second-instance": {
"receive-command": "Přijmut příkaz přes protokol: \"{{command}}\""
},
"theme": {
"css-file-not-found": "CSS soubor \"{{cssFile}}\" neexistuje, ignorováno"
},
"unresponsive": {
"details": "Chyba - Aplikace nereaguje!\n{{error}}"
},
"when-ready": {
"clearing-cache-after-20s": "Čištění mezipaměti aplikace"
},
"window": {
"tried-to-render-offscreen": "Okno se pokusilo vykreslit na pozadí, velikost okna = {{windowSize}}, display velikost = {{displaySize}}, pozice = {{position}}"
}
},
"dialog": {
"hide-menu-enabled": {
"detail": "Menu je skryté, stiskněte 'Alt' k jeho zobrazení (nebo 'Escape', pokud používáte in-app-menu)",
"message": "Skrýt menu je povoleno",
"title": "Skrýt menu Povolené"
},
"need-to-restart": {
"buttons": {
"later": "Později",
"restart-now": "Restartovat nyní"
},
"detail": "\"{{pluginName}}\" plugin vyžaduje restart, aby se projevil",
"message": "\"{{pluginName}}\" potřebuje restartovat",
"title": "Restart vyžadován"
},
"unresponsive": {
"buttons": {
"quit": "Ukončit",
"relaunch": "Spustit znovu",
"wait": "Počkat"
},
"detail": "Omlouváme se za způsobené nepříjemnosti! prosím vyberte, co dělat:",
"message": "Aplikace nereaguje",
"title": "Okno nereaguje"
},
"update-available": {
"buttons": {
"disable": "Vypnout aktualizace",
"download": "Stáhnout",
"ok": "OK"
},
"detail": "Nová verze je k dispozici a lze ji stáhnout na {{downloadLink}}",
"message": "Nová verze je dostupná",
"title": "Aktualizace je k dispozici"
}
},
"menu": {
"about": "O Aplikaci",
"navigation": {
"label": "Navigace",
"submenu": {
"copy-current-url": "Zkopírovat aktuální URL adresu",
"go-back": "Jít zpátky",
"go-forward": "Jít dopředu",
"quit": "Ukončit",
"restart": "Restartovat aplikaci"
}
},
"options": {
"label": "Možnosti",
"submenu": {
"advanced-options": {
"label": "Pokročilé možnosti",
"submenu": {
"auto-reset-app-cache": "Při spuštění aplikace se resetuje její mezipaměť",
"disable-hardware-acceleration": "Vypnout hardware zrychlení",
"edit-config-json": "Upravit config.json",
"override-user-agent": "Přepsat uživatelského agenta",
"restart-on-config-changes": "Restartovat aplikaci na změny v konfiguraci",
"set-proxy": {
"label": "Nastavit proxy",
"prompt": {
"label": "Zadejte adresu proxy: (k vypnutí nechte pole prázdné)",
"placeholder": "Příklad: SOCKS5://127.0.0.1:9999",
"title": "Nastavit proxy"
}
},
"toggle-dev-tools": "Přepínat vývojářské nástroje"
}
},
"always-on-top": "Vždy na vrchu",
"auto-update": "Automatické aktualizace",
"hide-menu": {
"dialog": {
"message": "Menu bude skryto na dalším spuštěním, použijte [Alt] k jeho zobrazení (nebo backtick [`] pokud používáte in-app-menu)",
"title": "Skrýt menu Povoleno"
},
"label": "Skrýt menu"
},
"language": {
"dialog": {
"message": "Jazyk bude změněn po restartu",
"title": "Jazyk změněn"
},
"label": "Jazyk",
"submenu": {
"to-help-translate": "Chcete pomoc s překladem? Klikněte zde"
}
},
"resume-on-start": "Při spuštění aplikace, pokračovat na poslední písničce",
"single-instance-lock": "Zámek pro jednu instanci",
"start-at-login": "Zapnutí aplikace po přihlášení",
"starting-page": {
"label": "Úvodní stránka",
"unset": "Nenastaveno"
},
"tray": {
"label": "Tray",
"submenu": {
"disabled": "Vypnuto",
"enabled-and-hide-app": "Povolit a skrýt aplikaci",
"enabled-and-show-app": "Enabled a show aplikaci",
"play-pause-on-click": "Přehrát/Pozastavit na kliknutí"
}
},
"visual-tweaks": {
"label": "Vzhledové vylepšení",
"submenu": {
"like-buttons": {
"default": "Výchozí",
"force-show": "Vynutit zobrazení",
"hide": "Skrýt",
"label": "Like tlačítka"
},
"remove-upgrade-button": "Odebrat upgrade tlačítko",
"theme": {
"label": "Motiv",
"submenu": {
"import-css-file": "Vložit vlastní CSS soubor",
"no-theme": "Žádný motiv"
}
}
}
}
}
},
"plugins": {
"enabled": "Povoleno",
"label": "Pluginy",
"new": "NOVÉ"
},
"view": {
"label": "Zobrazení",
"submenu": {
"force-reload": "Vynutit znovu načtení",
"reload": "Obnovit",
"reset-zoom": "Skutečná velikost",
"toggle-fullscreen": "Přepnout režim celé obrazovky",
"zoom-in": "Přiblížit",
"zoom-out": "Oddálit"
}
}
},
"tray": {
"next": "Další",
"play-pause": "Přehrát/Pozastavit",
"previous": "Minulý",
"quit": "Ukončit",
"restart": "Restartovat aplikaci",
"show": "Zobrazit okno"
}
},
"plugins": {
"adblocker": {
"description": "Blokuje všechny reklamy a sledování ihned od začátku",
"menu": {
"blocker": "Blokátor"
},
"name": "Blokovač reklam"
},
"album-actions": {
"description": "Přidává Undislike, Dislike, Like, a Unlike tlačítka k apply this ke všem písničkám v seznamu písniček nebo albumu.",
"name": "Album akce"
},
"album-color-theme": {
"description": "Používá dynamický motiv a vizuální efekty na základě palety barev alba",
"name": "Motiv podle barvy Alba"
},
"ambient-mode": {
"description": "Applies bleskové efekty pomocí casting jemných barev z videa, do vašeho pozadí obrazovky.",
"menu": {
"blur-amount": {
"label": "Množství rozmazání",
"submenu": {
"pixels": "{{blurAmount}} pixelů"
}
},
"buffer": {
"label": "Vyrovnávací paměť",
"submenu": {
"buffer": "{{buffer}}"
}
},
"opacity": {
"label": "Neprůhlednost",
"submenu": {
"percent": "{{opacity}}%"
}
},
"quality": {
"label": "Kvalita",
"submenu": {
"pixels": "{{quality}} pixelů"
}
},
"size": {
"label": "Velikost",
"submenu": {
"percent": "{{size}}%"
}
},
"smoothness-transition": {
"label": "Plynulý přechod",
"submenu": {
"during": "Během {{interpolationTime}} s"
}
},
"use-fullscreen": {
"label": "Používání režimu celé obrazovky"
}
},
"name": "Ambientní režim"
},
"audio-compressor": {
"description": "Apply compression k audiu (snižuje hlasitost nejhlasitěších částí signálu and zvyšuje hlasitost nejjemnějších částí)",
"name": "Audio kompresor"
},
"blur-nav-bar": {
"description": "Udělá navigační panel průhledný a rozmazaný",
"name": "Rozmazaný navigační panel"
},
"bypass-age-restrictions": {
"description": "Obejít ověření věku na YouTube",
"name": "Obejít věková omezení"
},
"captions-selector": {
"description": "Titulkový selector pro zvukové stopy v YouTube Music",
"menu": {
"autoload": "Automaticky vybrat naposledy použité titulky",
"disable-captions": "Žádné titulky ve vychozím nastavení"
},
"name": "Titulkový selector",
"prompt": {
"selector": {
"label": "Aktuální jazyk titulků: {{language}}",
"none": "Žádný",
"title": "Vybrat jazyk titulků"
}
},
"templates": {
"title": "Otevřít titulový selector"
}
},
"compact-sidebar": {
"description": "Vždy nastavit postranní panel do kompaktního režimu",
"name": "Kompaktní postranní panel"
},
"crossfade": {
"description": "Prolínání mezi písničkami",
"menu": {
"advanced": "Pokročilý"
},
"name": "Prolínání [Beta]",
"prompt": {
"options": {
"multi-input": {
"fade-scaling": {
"linear": "Lineární",
"logarithmic": "Logaritmické"
},
"seconds-before-end": "Crossfade N sekund před koncem"
},
"title": "Možnosti prolínání"
}
}
},
"disable-autoplay": {
"description": "Spustí písničku v režimu \"pozastaveno\"",
"menu": {
"apply-once": "Applies jenom na spuštění aplikace"
},
"name": "Vypnout automatické přehrávání"
},
"discord": {
"backend": {
"already-connected": "Pokusilo se spojit s aktivním spojením",
"connected": "Připojeno k Discordu",
"disconnected": "Odpojeno od Discordu"
},
"description": "Ukažte svým přátelům, co posloucháte s Bohatou přítomností",
"menu": {
"auto-reconnect": "Automaticky znovu připojit",
"clear-activity": "Vymazat aktivitu",
"clear-activity-after-timeout": "Vymazat aktivitu po timeout",
"connected": "Připojeno",
"disconnected": "Odpojeno",
"hide-duration-left": "Skrýt zbývající duration",
"hide-github-button": "Skrýt tlačítko s odkazem na GitHub",
"play-on-youtube-music": "Hrát na YouTube Music",
"set-inactivity-timeout": "Nastavit timeout pro neaktivitu"
},
"name": "Discord Bohatá přítomnost",
"prompt": {
"set-inactivity-timeout": {
"label": "Zadejte timeout neaktivity v sekundách:",
"title": "Nastavit timeout pro neaktivitu"
}
}
},
"downloader": {
"backend": {
"dialog": {
"error": {
"buttons": {
"ok": "OK"
},
"message": "Argh! Omlouvám se, stáhnutí selhalo…",
"title": "Chyba ve stáhování!"
},
"start-download-playlist": {
"buttons": {
"ok": "OK"
},
"detail": "({{playlistSize}} písničky)",
"message": "Stahování seznamu písniček {{playlistTitle}}",
"title": "Stahování začalo"
}
},
"feedback": {
"conversion-progress": "Konverze: {{percent}}%",
"done": "Hotovo: {{filePath}}",
"download-info": "Stahování {{artist}} - {{title}} [{{videoId}}",
"download-progress": "Stahování: {{percent}}%",
"downloading": "Stahování…",
"downloading-counter": "Stahování {{current}}/{{total}}…",
"downloading-playlist": "Stahování seznamu písniček \"{{playlistTitle}}\" - {{playlistSize}} písničky ({{playlistId}})",
"error-while-downloading": "Chyba při stahování \"{{author}} - {{title}}\": {{error}}",
"folder-already-exists": "Složka {{playlistFolder}} již existuje",
"getting-playlist-info": "Získávání informací o seznamu písniček…",
"loading": "Načítání…",
"playlist-has-only-one-song": "Seznam písniček má pouze jednu položku, stahuje se přímo",
"playlist-id-not-found": "Žádné ID seznamu písnček nenalezeno",
"playlist-is-empty": "Seznam písniček je prázdný",
"playlist-is-mix-or-private": "Chyba při získávání informací o seznamu písniček: ujistite se, že se nejedná o soukromý nebo \"Namíchaný pro vás\" seznam písniček\n\n{{error}}",
"preparing-file": "Připravování souboru…",
"saving": "Ukládání…",
"trying-to-get-playlist-id": "Trying se získat ID seznamu písniček: {{playlistId}}",
"video-id-not-found": "Video nebylo nalezeno",
"writing-id3": "Psaní ID3 značek…"
}
},
"description": "Stahuje MP3 / source audio přímo z rozhraní",
"menu": {
"choose-download-folder": "Vybrat složku pro stahování",
"download-playlist": "Stáhnout seznam písniček",
"presets": "Předvolby",
"skip-existing": "Přeskočit existující soubory"
},
"name": "Stahovač",
"renderer": {
"can-not-update-progress": "Progress nemůže být aktualizován"
},
"templates": {
"button": "Stáhnout"
}
},
"exponential-volume": {
"description": "Dělá posuvník hlasitosti exponenciální, takže je snazší vybrat nižší hlasitost.",
"name": "Exponenciální hlasitost"
},
"in-app-menu": {
"description": "Dává menu panelům fancy, tmavý nebo album-color vzhled",
"menu": {
"hide-dom-window-controls": "Skrýt DOM window controls"
}
},
"lumiastream": {
"description": "Přidává Lumia Stream podporu",
"name": "Lumia Stream [Beta]"
},
"lyrics-genius": {
"description": "Přidává lyrics podporu pro většinu písniček",
"renderer": {
"fetched-lyrics": "Fetched lyrics pro Genius"
}
},
"music-together": {
"description": "Sdílejte seznam písniček s ostatními. Když the host hraje písničku, uslyší jí i všichni ostatní.",
"dialog": {
"enter-host": "Zadejte Host ID"
},
"internal": {
"save": "Uložit",
"unknown-user": "Neznámý uživatel"
},
"menu": {
"click-to-copy-id": "Zkopírovat Host ID",
"close": "Zavřít Hudba Spolu",
"connected-users": "Připojení uživatelé",
"disconnect": "Odpojit od Hudby Spolu",
"empty-user": "Žadní připojení uživatelé",
"host": "Hudba Spolu Host",
"join": "Připojit se k Hudbě Spolu",
"permission": {
"all": "Povolit hostům ovládat seznam písniček a přehrávač",
"host-only": "Jenom host může ovládat seznam písniček a přehrávač",
"playlist": "Povolit hostům ovládat seznam písniček"
},
"set-permission": "Změnit ovládací oprávnění",
"status": {
"disconnected": "Odpojen",
"guest": "Připojený/á jako Guest",
"host": "Připojený/á jako Host"
}
},
"name": "Hudba Spolu [Beta]",
"toast": {
"add-song-failed": "Selhalo přidání písničky",
"closed": "Hudba Spolu zavřena",
"disconnected": "Hudba Spolu odpojena",
"host-failed": "Selhalo hostování Hudby Spolu",
"id-copied": "Host ID zkopírováno do schránky",
"join-failed": "Selhalo připojení k Hudba Spolu",
"joined": "Připojil/a jste se k Hudbě Spolu",
"permission-changed": "Oprávnění Hudby Spolu se změnilo na \"{{permission}}\"",
"remove-song-failed": "Selhalo odstranění písničky",
"user-connected": "{{name}} se připojil/a k Hudbě Spolu",
"user-disconnected": "{{name}} odpustil/a Hudba Spolu"
}
},
"navigation": {
"description": "Další/Zpátky navigační šipky přímo integrovány do rozhraní, jako ve vašem oblíbeném prohlížeči",
"name": "Navigace"
},
"no-google-login": {
"description": "Odstranit tlačítka Google přihlášení a odkazy z rozhraní",
"name": "Žádné Google přihlášení"
},
"notifications": {
"description": "Zobrazit oznámení, když písnička začne hrát (interaktivní notifikace jsou dostupné na Windows)",
"menu": {
"interactive": "Interaktivní oznámení",
"interactive-settings": {
"label": "Interactive Nastavení",
"submenu": {
"hide-button-text": "Skrýt text tlačítka",
"refresh-on-play-pause": "Refresh na Přehrát/Pozastavit",
"tray-controls": "Otevřít/Zavřít aplikaci na kliknutí na tray ikonu"
}
},
"priority": "Priorita Oznámení",
"unpause-notification": "Zobrazit oznámení na unpause"
},
"name": "Oznámení"
},
"picture-in-picture": {
"description": "Povoluje switch aplikaci do režimu obrázek v obrázku",
"menu": {
"always-on-top": "Vždy na vrchu",
"hotkey": {
"label": "Klávesová zkratka",
"prompt": {
"keybind-options": {
"hotkey": "Klávesová zkratka"
},
"label": "Vybrat klávesovou zkratku pro přepínání obrázek v obrázku",
"title": "klávesová zkratka pro obrázek v obrázku"
}
},
"save-window-position": "Uložit pozici okna",
"save-window-size": "Uložit velikost okna",
"use-native-pip": "Použít browser native PiP"
},
"name": "Obrázek v obrázku",
"templates": {
"button": "Obrázek v obrázku"
}
},
"playback-speed": {
"description": "Poslouchej rychle, poslouchej pomalu! Přidává slider, který kontroluje rychlost písníčky",
"name": "Rychlost přehrávání",
"templates": {
"button": "Rychlost"
}
},
"precise-volume": {
"description": "Přesná kontrola hlasitosti pomocí kolečka myši/klávesnicových zkratek, s vlastní HUD a customizable hlasitostních steps",
"menu": {
"custom-volume-steps": "Nastavit vlastní hlasitostní steps",
"global-shortcuts": "Globální klávesové zkratky"
},
"name": "Přesná hlasitost",
"prompt": {
"global-shortcuts": {
"keybind-options": {
"decrease": "Snížit hlasitost",
"increase": "Zvýšit hlasitost"
},
"label": "Vybrat globální klávesnicové zkratky:",
"title": "Globální klávesnicové zkratky hlasitosti"
},
"volume-steps": {
"label": "Vybrat Zvýšení/Snížení hlasitost Steps",
"title": "Hlasitostní steps"
}
}
},
"quality-changer": {
"backend": {
"dialog": {
"quality-changer": {
"detail": "Aktuální kvalita: {{quality}}",
"message": "Vybrat kvalitu videa:",
"title": "Vybrat kvalitu videa"
}
}
},
"description": "Umožňuje měnit kvalitu videa pomocí tlačítka na video overlay",
"name": "Měnič kvality videa"
},
"shortcuts": {
"description": "Dovoluje nastavit globální klávesové zkratky pro playback (přehrát/pozastavit/další/předchozí) a vypínání media OSD pomocí přepisování media klíčů, zapínání Ctrl/CMD + F k vyhledávání, zapínání Linux MPRIS podporu pro media klíče, a vlastní klávesové zkratky pro pokročilé uživatele.",
"menu": {
"override-media-keys": "Přepsat media klíče",
"set-keybinds": "Nastavit globální Controls písniček"
},
"name": "Zkratky (& MPRIS)",
"prompt": {
"keybind": {
"keybind-options": {
"next": "Další",
"play-pause": "Přehrát / Pozastavit",
"previous": "Předchozí"
},
"label": "Vybrat globální klávesnicové zkratky pro ovládání písniček:",
"title": "Globální klávesnicové zkratky"
}
}
},
"skip-disliked-songs": {
"description": "Přeskakovat disliked písničky",
"name": "Přeskočit Disliked písničky"
},
"skip-silences": {
"description": "Automaticky přeskakovat tichá místa v písničkách",
"name": "Přeskakovat Tichá místa"
},
"sponsorblock": {
"description": "Automaticky přeskakuje nehudební části jako intro/outro nebo části hudebních videí, kde nehraje písnčka",
"name": "SponsorBlock"
},
"taskbar-mediacontrol": {
"description": "Ovládejte přehrávání z vašeho Windows hlavního panelu",
"name": "Hlavní panel Media Control"
},
"touchbar": {
"description": "Přidává Touch Bar widget pro macOS uživatele",
"name": "Touch Bar"
},
"tuna-obs": {
"description": "Integrace s OBS's plugin Tuna",
"name": "Tuna OBS"
},
"video-toggle": {
"description": "Přidává tlačítko k switch mezi video/písničko režimem. Může také odstranit celou video kartu",
"menu": {
"align": {
"label": "Zarovnání",
"submenu": {
"left": "Vlevo",
"middle": "Uprostřed",
"right": "Pravo"
}
},
"force-hide": "Vynutit odstranění karty videa",
"mode": {
"label": "Režim",
"submenu": {
"custom": "Vlastní přepínač",
"disabled": "Vypnuto",
"native": "Původní přepínač"
}
}
},
"name": "Přepínač videa",
"templates": {
"button": "Písnička"
}
},
"visualizer": {
"description": "Přidá vizualizér do přehrávače",
"menu": {
"visualizer-type": "Typ vizualizéru"
},
"name": "Vizualizér"
}
}
}

View File

@ -1,672 +0,0 @@
{
"common": {
"console": {
"plugins": {
"execute-failed": "Erweiterung {{pluginName}}::{{contextName}} konnte nicht ausgeführt werden",
"executed-at-ms": "Erweiterung {{pluginName}}::{{contextName}} ausgeführt in {{ms}}ms",
"initialize-failed": "Initialisierung der Erweiterung \"{{pluginName}}\" fehlgeschlagen",
"load-all": "Lade alle Erweiterungen",
"load-failed": "Laden der Erweiterung \"{{pluginName}}\" fehlgeschlagen",
"loaded": "Erweiterung \"{{pluginName}}\" geladen",
"unload-failed": "Entladen der Erweiterung \"{{pluginName}}\" fehlgeschlagen",
"unloaded": "Erweiterung \"{{pluginName}}\" entladen"
}
}
},
"language": {
"code": "de",
"local-name": "Deutsch",
"name": "German"
},
"main": {
"console": {
"did-finish-load": {
"dev-tools": "Laden fertiggestellt. Entwicklerwerkzeuge geöffnet"
},
"i18n": {
"loaded": "i18n geladen"
},
"second-instance": {
"receive-command": "Befehl über Protokoll empfangen: \"{{command}}\""
},
"theme": {
"css-file-not-found": "CSS-Datei \"{{cssFile}}\" existiert nicht, ignoriere"
},
"unresponsive": {
"details": "Nicht reagierender Fehler!\n{{error}}"
},
"when-ready": {
"clearing-cache-after-20s": "Leere Anwendungscache"
},
"window": {
"tried-to-render-offscreen": "Fenster vesucht außerhalb des Bildschirms zu rendern, windowSize={{windowSize}}, displaySize={{displaySize}}, position={{position}}"
}
},
"dialog": {
"hide-menu-enabled": {
"detail": "Das Menü ist versteckt, nutze 'Alt', um es zu aufzurufen (oder 'Escape' beim Verwenden des In-App-Menüs)",
"message": "Menü verstecken ist aktiviert",
"title": "Menü Verstecken Aktiviert"
},
"need-to-restart": {
"buttons": {
"later": "Später",
"restart-now": "Jetzt neustarten"
},
"detail": "\"{{pluginName}}\"-Erweiterung erfordert einen Neustart, um in Kraft zu treten",
"message": "\"{{pluginName}}\" muss neugestartet werden",
"title": "Neustart Erforderlich"
},
"unresponsive": {
"buttons": {
"quit": "Verlassen",
"relaunch": "Neustarten",
"wait": "Warten"
},
"detail": "Wir entschuldigen uns für die Unannehmlichkeiten! Bitte entscheide, was du tun möchtest:",
"message": "Die Anwendung reagiert nicht",
"title": "Fenster reagiert nicht"
},
"update-available": {
"buttons": {
"disable": "Aktualisierungen deaktivieren",
"download": "Herunterladen",
"ok": "OK"
},
"detail": "Eine neue Version ist verfügbar und kann unter {{downloadLink}} heruntergeladen werden",
"message": "Eine neue Version ist verfügbar",
"title": "Aktualisierung Verfügbar"
}
},
"menu": {
"about": "Über",
"navigation": {
"label": "Navigation",
"submenu": {
"copy-current-url": "Aktuelle URL kopieren",
"go-back": "Zurück gehen",
"go-forward": "Vorwärts gehen",
"quit": "Beenden",
"restart": "Anwendung Neustarten"
}
},
"options": {
"label": "Einstellungen",
"submenu": {
"advanced-options": {
"label": "Erweiterte Einstellungen",
"submenu": {
"auto-reset-app-cache": "Anwendungscache beim Start der Anwendung zurücksetzen",
"disable-hardware-acceleration": "Hardware-Beschleunigung deaktivieren",
"edit-config-json": "config.json ändern",
"override-user-agent": "User-Agent außer Kraft setzen",
"restart-on-config-changes": "Neustarten bei Änderungen der Konfiguration",
"set-proxy": {
"label": "Proxy setzen",
"prompt": {
"label": "Proxy-Adresse eingeben: (leer lassen zum Ausschalten)",
"placeholder": "Beispiel: SOCKS5://127.0.0.1:9999",
"title": "Proxy setzen"
}
},
"toggle-dev-tools": "Entwicklerwerkzeuge umschalten"
}
},
"always-on-top": "Immer im Vordergrund",
"auto-update": "Automatisch Aktualisieren",
"hide-menu": {
"dialog": {
"message": "Menü wird beim nächsten Start versteckt, verwende [Alt], um es zu zeigen (oder Backtick [`], wenn du das In-App-Menü benutzt)",
"title": "Menü Verstecken Aktiviert"
},
"label": "Menü Verstecken"
},
"language": {
"dialog": {
"message": "Sprache wird nach Neustart geändert",
"title": "Sprache Geändert"
},
"label": "Sprache",
"submenu": {
"to-help-translate": "Willst du beim Übersetzen helfen? Klicke hier"
}
},
"resume-on-start": "Letztes Lied weiter abspielen, wenn Anwendung startet",
"single-instance-lock": "Sperren einer einzelnen Instanz",
"start-at-login": "Start beim Einschalten",
"starting-page": {
"label": "Startseite",
"unset": "Ungesetzt"
},
"tray": {
"label": "Tray",
"submenu": {
"disabled": "Deaktiviert",
"enabled-and-hide-app": "Aktiviert und verstecke Anwendung",
"enabled-and-show-app": "Aktiviert und zeige Anwendung",
"play-pause-on-click": "Abspielen/Pausieren durch Klick"
}
},
"visual-tweaks": {
"label": "Visuelle Optimierungen",
"submenu": {
"like-buttons": {
"default": "Standard",
"force-show": "Zeigen erzwungen",
"hide": "Versteckt",
"label": "Gefällt mir-Knopf"
},
"remove-upgrade-button": "Upgrade-Schaltfläche entfernen",
"theme": {
"label": "Thema",
"submenu": {
"import-css-file": "Importiere eigene CSS-Datei",
"no-theme": "Kein Thema"
}
}
}
}
}
},
"plugins": {
"enabled": "Aktiviert",
"label": "Erweiterungen",
"new": "NEU"
},
"view": {
"label": "Ansicht",
"submenu": {
"force-reload": "Neuladen erzwingen",
"reload": "Neu laden",
"reset-zoom": "Tatsächliche Größe",
"toggle-fullscreen": "Vollbild umschalten",
"zoom-in": "Vergrößern",
"zoom-out": "Verkleinern"
}
}
},
"tray": {
"next": "Nächstes",
"play-pause": "Weiter/Pause",
"previous": "Vorheriges",
"quit": "Beenden",
"restart": "Anwendung neu starten",
"show": "Fenster anzeigen",
"tooltip": {
"default": "YouTube Musik",
"with-song-info": "YouTube Musik: {{artist}} - {{title}}"
}
}
},
"plugins": {
"adblocker": {
"description": "Blockiere jegliche Werbung und Tracker",
"menu": {
"blocker": "Abfangmethode"
},
"name": "Werbeblocker"
},
"album-actions": {
"description": "Fügt Undislike, Dislike, Like und Unlike-Knöpfe hinzu, welche sich auf alle Lieder in einer Playlist oder Album auswirken",
"name": "Album-Aktionen"
},
"album-color-theme": {
"description": "Wendet ein dynamisches Farbthema und visuelle Effekte auf Basis der Farbpalette des Albumcovers an",
"name": "Thema aus Albumfarbe"
},
"ambient-mode": {
"description": "Fügt einen Lichteffekt durch sanftes Abstreifen der Farben des Videos in deinen Bildschirmhintergrund hinzu",
"menu": {
"blur-amount": {
"label": "Unschärfemenge",
"submenu": {
"pixels": "{{blurAmount}} Pixel"
}
},
"buffer": {
"label": "Puffer",
"submenu": {
"buffer": "{{buffer}}"
}
},
"opacity": {
"label": "Durchsichtigkeit",
"submenu": {
"percent": "{{opacity}}%"
}
},
"quality": {
"label": "Qualität",
"submenu": {
"pixels": "{{quality}} Pixel"
}
},
"size": {
"label": "Größe",
"submenu": {
"percent": "{{size}}%"
}
},
"smoothness-transition": {
"label": "Glatter Übergang",
"submenu": {
"during": "Während {{interpolationTime}}s"
}
},
"use-fullscreen": {
"label": "Vollbild nutzen"
}
},
"name": "Ambiente-Modus"
},
"audio-compressor": {
"description": "Kompressor auf Audio anwenden (senkt die Lautstärke der lautesten Teile des Signals und hebt die Lautstärke der leisesten Teile an)",
"name": "Audio-Komprimierer"
},
"blur-nav-bar": {
"description": "Macht Navigationsleiste durchsichtig und unscharf",
"name": "Verschwommene Navigationsleiste"
},
"bypass-age-restrictions": {
"description": "Youtubes Altersbestätigung umgehen",
"name": "Altersbeschränkungen umgehen"
},
"captions-selector": {
"description": "Untertitelwähler für YouTube Music-Audio-Lieder",
"menu": {
"autoload": "Wähle automatisch den zuletzt verwendeten Untertitel",
"disable-captions": "Standartmäßig keine Untertitel"
},
"name": "Untertitelwähler",
"prompt": {
"selector": {
"label": "Aktuelle Untertitelsprache: {{language}}",
"none": "Keine",
"title": "Wähle Untertitelsprache"
}
},
"templates": {
"title": "Untertitelwähler öffnen"
}
},
"compact-sidebar": {
"description": "Seitenleiste immer in den kompakten Modus setzen",
"name": "Kompakte Seitenleiste"
},
"crossfade": {
"description": "Übergang zwischen Liedern",
"menu": {
"advanced": "Erweitert"
},
"name": "Übergang [Beta]",
"prompt": {
"options": {
"multi-input": {
"fade-in-duration": "Einblendezeit (Millisekunden)",
"fade-out-duration": "Ausblendezeit (Millisekunden)",
"fade-scaling": {
"label": "Übergangsskalierung",
"linear": "Linear",
"logarithmic": "Logarithmisch"
},
"seconds-before-end": "Übergang N Sekunden vor dem Ende starten"
},
"title": "Übergangseinstellungen"
}
}
},
"disable-autoplay": {
"description": "Startet Lied im pausierten Modus",
"menu": {
"apply-once": "Nur beim Start der Anwendung anwenden"
},
"name": "Deaktiviere automatisches Abspielen"
},
"discord": {
"backend": {
"already-connected": "Verbindungsaufbau bei aktiver Verbindung versucht",
"connected": "Mit Discord verbunden",
"disconnected": "Verbindung zu Discord getrennt"
},
"description": "Zeige deinen Freunden, was du hörst mit Discords Aktivitätsstatus",
"menu": {
"auto-reconnect": "Automatisch erneut verbinden",
"clear-activity": "Aktivität leeren",
"clear-activity-after-timeout": "Aktivität nach Timeout leeren",
"connected": "Verbunden",
"disconnected": "Getrennt",
"hide-duration-left": "Verbleibende Zeit verstecken",
"hide-github-button": "Knopf mit Link zu GitHub ausblenden",
"play-on-youtube-music": "Auf YouTube Music abspielen",
"set-inactivity-timeout": "Inaktivitätstimeout setzen"
},
"name": "Discords Aktivitätsstatus",
"prompt": {
"set-inactivity-timeout": {
"label": "Inaktivitätstimeout in Sekunden eingeben:",
"title": "Inaktivitätstimeout setzen"
}
}
},
"downloader": {
"backend": {
"dialog": {
"error": {
"buttons": {
"ok": "OK"
},
"message": "Argh! Entschuldigung, herunterladen fehlgeschlagen…",
"title": "Fehler beim Herunterladen!"
},
"start-download-playlist": {
"buttons": {
"ok": "OK"
},
"detail": "({{playlistSize}} Lieder)",
"message": "Lade Playlist {{playlistTitle}} herunter",
"title": "Download begonnen"
}
},
"feedback": {
"conversion-progress": "Konvertieren: {{percent}}%",
"converting": "Konvertiere…",
"done": "Abgeschlossen: {{filePath}}",
"download-info": "Lade {{artist}} - {{title}} [{{videoId}} herunter",
"download-progress": "Herunterladen: {{percent}}%",
"downloading": "Lade herunter…",
"downloading-counter": "Lade herunter {{current}}/{{total}}…",
"downloading-playlist": "Lade Playlist \"{{playlistTitle}}\" herunter - {{playlistSize}} Lieder ({{playlistId}})",
"error-while-downloading": "Fehler beim Herunterladen \"{{author}} - {{title}}\": {{error}}",
"folder-already-exists": "Der Ordner {{playlistFolder}} existiert bereits",
"getting-playlist-info": "Hole Playlist-Informationen…",
"loading": "Lade…",
"playlist-has-only-one-song": "Playlist hat nur ein Element, wird direkt heruntergeladen",
"playlist-id-not-found": "Keine Playlist-ID gefunden",
"playlist-is-empty": "Playlist ist leer",
"playlist-is-mix-or-private": "Fehler beim Sammeln der Playlist-Informationen: stelle sicher, dass es keine private oder \"Mixed for you\"-Playlist ist\n\n{{error}}",
"preparing-file": "Bereite Datei vor…",
"saving": "Speichere…",
"trying-to-get-playlist-id": "Versuche Playlist-ID zu bekommen: {{playlistId}}",
"video-id-not-found": "Video nicht gefunden",
"writing-id3": "Schreibe ID3 tags…"
}
},
"description": "Lädt MP3-/Original-Audio direkt von der Schnittstelle herunter",
"menu": {
"choose-download-folder": "Downloadordner wählen",
"download-playlist": "Wiedergabeliste herunterladen",
"presets": "Voreinstellungen",
"skip-existing": "Vorhandene Dateien überspringen"
},
"name": "Downloader",
"renderer": {
"can-not-update-progress": "Fortschritt kann nicht aktualisiert werden"
},
"templates": {
"button": "Herunterladen"
}
},
"exponential-volume": {
"description": "Macht den Lautstärkeregler exponentiell, damit es einfacher ist leise Lautstärken zu wählen.",
"name": "Exponentielle Lautstärke"
},
"in-app-menu": {
"description": "Verleiht den Menüleisten ein schickes, dunkles oder albumfarbenes Aussehen",
"menu": {
"hide-dom-window-controls": "DOM-Fenster-Steuerelemente ausblenden"
},
"name": "In-App Menü"
},
"lumiastream": {
"description": "Fügt Unterstützung für Lumia Stream hinzu",
"name": "Lumia Stream [Beta]"
},
"lyrics-genius": {
"description": "Für Songtextunterstützung für die meisten Lieder hinzu",
"menu": {
"romanized-lyrics": "Romanisierte Songtexte"
},
"name": "Liedtexte von Genius",
"renderer": {
"fetched-lyrics": "Liedtexte für Genius abgerufen"
}
},
"music-together": {
"description": "Teile eine Wiedergabeliste mit anderen. Wenn der Host ein Lied abspielt, hören alle anderen das gleiche Lied",
"dialog": {
"enter-host": "Host ID eingeben"
},
"internal": {
"save": "Speichern",
"track-source": "Quelle verfolgen",
"unknown-user": "Unbekannter Nutzer"
},
"menu": {
"click-to-copy-id": "Host ID kopieren",
"close": "Music Together schließen",
"connected-users": "Verbundene Benutzer",
"disconnect": "Verbindung zu Music Together trennen",
"empty-user": "Keine verbundenen Benutzer",
"host": "Host für Music Together",
"join": "Music Together beitreten",
"permission": {
"all": "Gästen erlauben, Wiederhabeliste und Player zu bedienen",
"host-only": "Nur der Host kann die Playlist und den Player kontrollieren",
"playlist": "Gäste das Kontrollieren der Playlist erlauben"
},
"set-permission": "Kontrollberechtigung ändern",
"status": {
"disconnected": "Verbindung getrennt",
"guest": "Als Gast verbunden",
"host": "Als Host verbunden"
}
},
"name": "Music Together [Beta]",
"toast": {
"add-song-failed": "Song hinzufügen gescheitert",
"closed": "Music Together geschlossen",
"disconnected": "Verbindung zu Music Together getrennt",
"host-failed": "Hosten von Music Together gescheitert",
"id-copied": "Host ID in die Zwischenablage kopiert",
"id-copy-failed": "Kopieren der Host ID in die Zwischenablage gescheitert",
"join-failed": "Beitreten zu Music Together gescheitert",
"joined": "Music Together beigetreten",
"permission-changed": "Music Together-Berechtigung zu \"{{permission}}\" geändert",
"remove-song-failed": "Entfernen des Liedes gescheitert",
"user-connected": "{{name}} ist Music Together beigetreten",
"user-disconnected": "{{name}} hat Music Together verlassen"
}
},
"navigation": {
"description": "Vorwärts/Zurück Navigationspfeile direkt in die Oberfläche integriert - wie in deinem geliebten Browser",
"name": "Navigation"
},
"no-google-login": {
"description": "Googles Anmelden-Knöpfe und -Links von der Oberfläche entfernen",
"name": "Keine Google-Anmeldung"
},
"notifications": {
"description": "Zeige eine Benachrichtigung, wenn ein Lied beginnt zu spielen (interaktive Benachrichtigungen sind unter Windows verfügbar)",
"menu": {
"interactive": "Interaktive Benachrichtigungen",
"interactive-settings": {
"label": "Interaktivitätseinstellungen",
"submenu": {
"hide-button-text": "Text der Knöpfe verstecken",
"refresh-on-play-pause": "Aktualisieren bei Wiedergabe/Pause",
"tray-controls": "Öffnen/Schließen beim Klicken des Tray-Icons"
}
},
"priority": "Benachrichtigungspriorität",
"toast-style": "Toast-Stil",
"unpause-notification": "Benachrichtigungen beim Pausieren anzeigen"
},
"name": "Benachrichtigungen"
},
"picture-in-picture": {
"description": "Erlaubt die App in den Bild-im-Bild-Modus zu wechseln",
"menu": {
"always-on-top": "Immer im Vordergrund",
"hotkey": {
"label": "Tastenkürzel",
"prompt": {
"keybind-options": {
"hotkey": "Tastenkürzel"
},
"label": "Tastenkürzel für Bild-im-Bild wählen",
"title": "Bild-im-Bild Tastenkürzel"
}
},
"save-window-position": "Fensterposition speichern",
"save-window-size": "Fenstergröße speichern",
"use-native-pip": "Browsereigenes PiP verwenden"
},
"name": "Bild-im-Bild",
"templates": {
"button": "Bild-im-Bild"
}
},
"playback-speed": {
"description": "Schnell hören, langsam hören! Fügt einen Schieberegler zur Steuerung der Songgeschwindigkeit hinzu",
"name": "Wiedergabegeschwindigkeit",
"templates": {
"button": "Geschwindigkeit"
}
},
"precise-volume": {
"description": "Präzise Steuerung der Lautstärke mit dem Mausrad/Numpad mit einem benutzerdefinierten HUD und benutzerdefinierten Lautstärkestufen",
"menu": {
"arrows-shortcuts": "Lokale Pfeiltasten als Steuerung",
"custom-volume-steps": "Eigene Lautstärkestufen setzen",
"global-shortcuts": "Globale Tastenkürzel"
},
"name": "Genaue Lautstärke",
"prompt": {
"global-shortcuts": {
"keybind-options": {
"decrease": "Lautstärke senken",
"increase": "Lautstärke erhöhen"
},
"label": "Wähle globale Tastenkombinationen für Lautstärke:",
"title": "Globale Lautstärketastenbelegungen"
},
"volume-steps": {
"label": "Wähle Schritte zur Lautstärkehebung/-senkung",
"title": "Lautstärkestufen"
}
}
},
"quality-changer": {
"backend": {
"dialog": {
"quality-changer": {
"detail": "Aktuelle Videoqualität: {{quality}}",
"message": "Wähle Videoqualität:",
"title": "Videoqualität wählen"
}
}
},
"description": "Erlaubt die Videoqualität über einen Knopf auf dem Video",
"name": "Videoqualitätsänderer"
},
"scrobbler": {
"description": "Scrobbling-Unterstützung aktivieren (z.B. für last.fm, Listenbrainz)",
"menu": {
"lastfm": {
"api-settings": "Last.fm API Einstellungen"
},
"listenbrainz": {
"token": "ListenBrainz-Benutzer-Token eintragen"
}
},
"name": "Scrobbler",
"prompt": {
"lastfm": {
"api-key": "Last.fm API-Schlüssel",
"api-secret": "Last.fm API secret"
},
"listenbrainz": {
"token": {
"label": "ListenBrainz-Benutzer-Token eintragen"
}
}
}
},
"shortcuts": {
"description": "Ermöglicht das Festlegen globaler Hotkeys für die Wiedergabe (Abspielen/Pause/Nächster/Vorheriger) + Deaktivieren des Medien-OSD durch Überschreiben der Medientasten + Aktivieren von Strg/CMD + F zum Suchen + Aktivieren der Linux mpris-Unterstützung für Medientasten + Angepasste Tastenkürzel für fortgeschrittene Benutzer.",
"menu": {
"override-media-keys": "Medientasten überschreiben",
"set-keybinds": "Globale Liedsteuerung setzen"
},
"name": "Abkürzungen (& MPRIS)",
"prompt": {
"keybind": {
"keybind-options": {
"next": "Nächstes",
"play-pause": "Weiter / Pause",
"previous": "Vorheriges"
},
"label": "Wähle globale Tastenkombinationen für die Liedsteuerung:",
"title": "Globale Tastenkombinationen"
}
}
},
"skip-disliked-songs": {
"description": "Überspringt Lieder, die ihnen nicht gefallen",
"name": "Überspring Lieder, die ihnen nicht gefallen"
},
"skip-silences": {
"description": "Automatisch stille Abschnitte in Liedern überspringen",
"name": "Stille überspringen"
},
"sponsorblock": {
"description": "Überspringt automatisch nicht-musikalische Teile wie Intro/Outro oder Teile von Musikvideos, in denen der Song nicht gespielt wird",
"name": "SponsorBlock"
},
"taskbar-mediacontrol": {
"description": "Wiedergabe aus der Windows Taskleiste kontrollieren",
"name": "Mediensteuerung in der Taskleiste"
},
"touchbar": {
"description": "Fügt ein TouchBar-Widget für macOS-Benutzer hinzu",
"name": "TouchBar"
},
"tuna-obs": {
"description": "Integration mit dem OBS-Plugin Tuna",
"name": "Tuna OBS"
},
"video-toggle": {
"description": "Fügt einen Knopf hinzu, um zwischen Video-/Liedmodus zu wechseln. kann auch genutzt werden, um den ganzen Videoabschnitt zu entfernen",
"menu": {
"align": {
"label": "Ausrichtung",
"submenu": {
"left": "Links",
"middle": "Mitte",
"right": "Rechts"
}
},
"force-hide": "Entfernen des Videoabschnitts erzwingen",
"mode": {
"label": "Modus",
"submenu": {
"custom": "Angepasster Schalter",
"disabled": "Deaktiviert",
"native": "Eingebauter Schalter"
}
}
},
"name": "Videoumschalter",
"templates": {
"button": "Lied"
}
},
"visualizer": {
"description": "Fügt einen Visualisierer zum Player hinzu",
"menu": {
"visualizer-type": "Visualisierertyp"
},
"name": "Visualisierer"
}
}
}

View File

@ -1,246 +0,0 @@
{
"common": {
"console": {
"plugins": {
"execute-failed": "Αποτυχία εκτέλεσης προσθέτου {{pluginName}}::{{contextName}}"
}
}
},
"language": {
"code": "el",
"local-name": "Ελληνικά",
"name": "Greek"
},
"main": {
"dialog": {
"hide-menu-enabled": {
"message": "Απόκρυψη μενού είναι ενεργοποιημένο"
},
"update-available": {
"buttons": {
"download": "Download",
"ok": "OK"
}
}
},
"menu": {
"navigation": {
"label": "Navigation"
},
"options": {
"label": "Options",
"submenu": {
"advanced-options": {
"submenu": {
"set-proxy": {
"label": "Set proxy",
"prompt": {
"title": "Set proxy"
}
}
}
},
"auto-update": "Auto Update",
"language": {
"label": "Γλώσσα"
},
"start-at-login": "Start at login",
"tray": {
"label": "Tray",
"submenu": {
"enabled-and-hide-app": "Ενεργοποιημένο και απόκρυψη της εφαρμογής",
"play-pause-on-click": "Play/Pause στο πάτημα"
}
},
"visual-tweaks": {
"submenu": {
"like-buttons": {
"default": "Default"
},
"theme": {
"label": "Theme",
"submenu": {
"no-theme": "No theme"
}
}
}
}
}
},
"plugins": {
"label": "Plugins"
},
"view": {
"label": "View"
}
}
},
"plugins": {
"adblocker": {
"description": "Αποκλεισμός όλων των διαφημίσεων και tracker",
"menu": {
"blocker": "Μέθοδος αποκλεισμού"
},
"name": "Adblocker"
},
"album-color-theme": {
"description": "Εφαρμόζει ένα δυναμικό θέμα και εφέ με βάση τη χρωματική παλέτα του άλμπουμ",
"name": "Album Color Theme"
},
"ambient-mode": {
"description": "Εφαρμόζει ένα εφέ φωτισμού ρίχνοντας απαλά χρώματα από το βίντεο, στο φόντο της οθόνης σας.",
"menu": {
"blur-amount": {
"submenu": {
"pixels": "{{blurAmount}} pixels"
}
},
"buffer": {
"label": "Buffer",
"submenu": {
"buffer": "{{buffer}}"
}
},
"opacity": {
"label": "Ποσότητα αδιαφάνειας",
"submenu": {
"percent": "{{opacity}}%"
}
},
"quality": {
"submenu": {
"pixels": "{{quality}} pixels"
}
},
"size": {
"submenu": {
"percent": "{{size}}%"
}
},
"smoothness-transition": {
"submenu": {
"during": "Σε {{interpolationTime}} δεύτερα"
}
}
}
},
"audio-compressor": {
"description": "Συμπίεση ήχου (μειώνει την ένταση των πιο δυνατών τμημάτων του κύματος και αυξάνει την ένταση των πιο μαλακών τμημάτων)"
},
"blur-nav-bar": {
"description": "Κάνει τη γραμμή πλοήγησης διαφανή και θολή"
},
"bypass-age-restrictions": {
"description": "Παράκαμψη της επαλήθευσης ηλικίας του YouTube"
},
"captions-selector": {
"prompt": {
"selector": {
"none": "None"
}
}
},
"compact-sidebar": {
"description": "Να είναι πάντα συμπαγές το sidebar"
},
"crossfade": {
"menu": {
"advanced": "Για προχωρημένους"
},
"prompt": {
"options": {
"multi-input": {
"fade-scaling": {
"linear": "Γραμμική",
"logarithmic": "Λογαριθμική"
}
}
}
}
},
"disable-autoplay": {
"description": "Κάνει τα τραγούδια να είναι αυτόματα σε παύση",
"menu": {
"apply-once": "Εφαρμόζεται μόνο στο πρώτο τραγούδι"
}
},
"discord": {
"description": "Δείξτε στους φίλους σας τι ακούτε με το Rich Presence",
"menu": {
"hide-duration-left": "Απόκρυψη της διάρκειας που απομένει",
"hide-github-button": "Απόκρυψη του συνδέσμου προς GitHub",
"set-inactivity-timeout": "Ορισμός χρονικού ορίου αδράνειας"
}
},
"downloader": {
"backend": {
"dialog": {
"error": {
"buttons": {
"ok": "OK"
},
"title": "Error in download!"
},
"start-download-playlist": {
"buttons": {
"ok": "OK"
},
"message": "Λήψη λίστας αναπαραγωγής {{playlistTitle}}",
"title": "Λήψη ξεκίνησε"
}
},
"feedback": {
"conversion-progress": "Μετατροπή: {{percent}}%",
"download-progress": "Download: {{percent}}%",
"preparing-file": "Προετοιμασία αρχείου…"
}
},
"templates": {
"button": "Download"
}
},
"navigation": {
"name": "Navigation"
},
"no-google-login": {
"name": "No Google Login"
},
"notifications": {
"name": "Notifications"
},
"shortcuts": {
"prompt": {
"keybind": {
"keybind-options": {
"next": "Next"
}
}
}
},
"sponsorblock": {
"name": "SponsorBlock"
},
"touchbar": {
"name": "TouchBar"
},
"tuna-obs": {
"name": "Tuna OBS"
},
"video-toggle": {
"menu": {
"align": {
"submenu": {
"middle": "Middle",
"right": "Right"
}
},
"mode": {
"label": "Mode"
}
},
"templates": {
"button": "Song"
}
}
}
}

View File

@ -1,682 +0,0 @@
{
"common": {
"console": {
"plugins": {
"execute-failed": "Failed to execute plugin {{pluginName}}::{{contextName}}",
"executed-at-ms": "Plugin {{pluginName}}::{{contextName}} executed at {{ms}}ms",
"initialize-failed": "Failed to initialize plugin \"{{pluginName}}\"",
"load-all": "Loading all plugins",
"load-failed": "Failed to load plugin \"{{pluginName}}\"",
"loaded": "Plugin \"{{pluginName}}\" loaded",
"unload-failed": "Failed to unload plugin \"{{pluginName}}\"",
"unloaded": "Plugin \"{{pluginName}}\" unloaded"
}
}
},
"language": {
"code": "en",
"local-name": "English",
"name": "English"
},
"main": {
"console": {
"did-finish-load": {
"dev-tools": "Finished loading. DevTools opened"
},
"i18n": {
"loaded": "i18n loaded"
},
"second-instance": {
"receive-command": "Received command over protocol: \"{{command}}\""
},
"theme": {
"css-file-not-found": "CSS file \"{{cssFile}}\" does not exist, ignoring"
},
"unresponsive": {
"details": "Unresponsive Error!\n{{error}}"
},
"when-ready": {
"clearing-cache-after-20s": "Clearing app cache"
},
"window": {
"tried-to-render-offscreen": "Window tried to render offscreen, windowSize={{windowSize}}, displaySize={{displaySize}}, position={{position}}"
}
},
"dialog": {
"hide-menu-enabled": {
"detail": "Menu is hidden, use 'Alt' to show it (or 'Escape' if using In-App Menu)",
"message": "Hide Menu is enabled",
"title": "Hide Menu Enabled"
},
"need-to-restart": {
"buttons": {
"later": "Later",
"restart-now": "Restart Now"
},
"detail": "\"{{pluginName}}\" plugin requires a restart to take effect",
"message": "\"{{pluginName}}\" needs to restart",
"title": "Restart Required"
},
"unresponsive": {
"buttons": {
"quit": "Quit",
"relaunch": "Relaunch",
"wait": "Wait"
},
"detail": "We are sorry for the inconvenience! please choose what to do:",
"message": "The Application is Unresponsive",
"title": "Window Unresponsive"
},
"update-available": {
"buttons": {
"disable": "Disable Updates",
"download": "Download",
"ok": "OK"
},
"detail": "A new version is available and can be downloaded at {{downloadLink}}",
"message": "A new version is available",
"title": "Update Available"
}
},
"menu": {
"about": "About",
"navigation": {
"label": "Navigation",
"submenu": {
"copy-current-url": "Copy current URL",
"go-back": "Go back",
"go-forward": "Go forward",
"quit": "Exit",
"restart": "Restart App"
}
},
"options": {
"label": "Options",
"submenu": {
"advanced-options": {
"label": "Advanced options",
"submenu": {
"auto-reset-app-cache": "Reset app cache when app starts",
"disable-hardware-acceleration": "Disable hardware acceleration",
"edit-config-json": "Edit config.json",
"override-user-agent": "Override User-Agent",
"restart-on-config-changes": "Restart on config changes",
"set-proxy": {
"label": "Set proxy",
"prompt": {
"label": "Enter Proxy Address: (leave empty to disable)",
"placeholder": "Example: SOCKS5://127.0.0.1:9999",
"title": "Set proxy"
}
},
"toggle-dev-tools": "Toggle DevTools"
}
},
"always-on-top": "Always on top",
"auto-update": "Auto Update",
"hide-menu": {
"dialog": {
"message": "Menu will be hidden on next launch, use [Alt] to show it (or backtick [`] if using in-app-menu)",
"title": "Hide Menu Enabled"
},
"label": "Hide Menu"
},
"language": {
"dialog": {
"message": "Language will be changed after restart",
"title": "Language Changed"
},
"label": "Language",
"submenu": {
"to-help-translate": "Want to help translate? Click here"
}
},
"resume-on-start": "Resume last song when app starts",
"single-instance-lock": "Single Instance Lock",
"start-at-login": "Start at login",
"starting-page": {
"label": "Starting page",
"unset": "Unset"
},
"tray": {
"label": "Tray",
"submenu": {
"disabled": "Disabled",
"enabled-and-hide-app": "Enabled and hide app",
"enabled-and-show-app": "Enabled and show app",
"play-pause-on-click": "Play/Pause on click"
}
},
"visual-tweaks": {
"label": "Visual Tweaks",
"submenu": {
"like-buttons": {
"default": "Default",
"force-show": "Force show",
"hide": "Hide",
"label": "Like buttons"
},
"remove-upgrade-button": "Remove upgrade button",
"theme": {
"label": "Theme",
"submenu": {
"import-css-file": "Import custom CSS file",
"no-theme": "No theme"
}
}
}
}
}
},
"plugins": {
"enabled": "Enabled",
"label": "Plugins",
"new": "NEW"
},
"view": {
"label": "View",
"submenu": {
"force-reload": "Force Reload",
"reload": "Reload",
"reset-zoom": "Actual Size",
"toggle-fullscreen": "Toggle Full Screen",
"zoom-in": "Zoom In",
"zoom-out": "Zoom Out"
}
}
},
"tray": {
"next": "Next",
"play-pause": "Play/Pause",
"previous": "Previous",
"quit": "Exit",
"restart": "Restart App",
"show": "Show window",
"tooltip": {
"default": "YouTube Music",
"with-song-info": "YouTube Music: {{artist}} - {{title}}"
}
}
},
"plugins": {
"adblocker": {
"description": "Block all ads and tracking out of the box",
"menu": {
"blocker": "Blocker"
},
"name": "Ad Blocker"
},
"album-actions": {
"description": "Adds Undislike, Dislike, Like, and Unlike buttons to apply this to all songs in a playlist or album",
"name": "Album Actions"
},
"album-color-theme": {
"description": "Applies a dynamic theme and visual effects based on the album color palette",
"name": "Album Color Theme",
"menu": {
"color-mix-ratio": {
"label": "Color mix ratio",
"submenu": {
"percent": "{{ratio}}%"
}
}
}
},
"ambient-mode": {
"description": "Applies a lighting effect by casting gentle colors from the video, into your screens background",
"menu": {
"blur-amount": {
"label": "Blur amount",
"submenu": {
"pixels": "{{blurAmount}} pixels"
}
},
"buffer": {
"label": "Buffer",
"submenu": {
"buffer": "{{buffer}}"
}
},
"opacity": {
"label": "Opacity",
"submenu": {
"percent": "{{opacity}}%"
}
},
"quality": {
"label": "Quality",
"submenu": {
"pixels": "{{quality}} pixels"
}
},
"size": {
"label": "Size",
"submenu": {
"percent": "{{size}}%"
}
},
"smoothness-transition": {
"label": "Smoothness transition",
"submenu": {
"during": "During {{interpolationTime}} s"
}
},
"use-fullscreen": {
"label": "Using fullscreen"
}
},
"name": "Ambient Mode"
},
"audio-compressor": {
"description": "Apply compression to audio (lowers the volume of the loudest parts of the signal and raises the volume of the softest parts)",
"name": "Audio Compressor"
},
"blur-nav-bar": {
"description": "Makes navigation bar transparent and blurry",
"name": "Blur Navigation Bar"
},
"bypass-age-restrictions": {
"description": "Bypass YouTube's age verification",
"name": "Bypass Age Restrictions"
},
"captions-selector": {
"description": "Caption selector for YouTube Music audio tracks",
"menu": {
"autoload": "Automatically select last used caption",
"disable-captions": "No captions by default"
},
"name": "Captions Selector",
"prompt": {
"selector": {
"label": "Current caption language: {{language}}",
"none": "None",
"title": "Select caption language"
}
},
"templates": {
"title": "Open captions selector"
}
},
"compact-sidebar": {
"description": "Always set the sidebar in compact mode",
"name": "Compact Sidebar"
},
"crossfade": {
"description": "Crossfade between songs",
"menu": {
"advanced": "Advanced"
},
"name": "Crossfade [Beta]",
"prompt": {
"options": {
"multi-input": {
"fade-in-duration": "Fade in duration (ms)",
"fade-out-duration": "Fade out duration (ms)",
"fade-scaling": {
"label": "Fade scaling",
"linear": "Linear",
"logarithmic": "Logarithmic"
},
"seconds-before-end": "Crossfade N seconds before end"
},
"title": "Crossfade options"
}
}
},
"disable-autoplay": {
"description": "Makes song start in \"paused\" mode",
"menu": {
"apply-once": "Applies only on startup"
},
"name": "Disable Autoplay"
},
"discord": {
"backend": {
"already-connected": "Attempted to connect with active connection",
"connected": "Connected to Discord",
"disconnected": "Disconnected from Discord"
},
"description": "Show your friends what you listen to with Rich Presence",
"menu": {
"auto-reconnect": "Auto reconnect",
"clear-activity": "Clear activity",
"clear-activity-after-timeout": "Clear activity after timeout",
"connected": "Connected",
"disconnected": "Disconnected",
"hide-duration-left": "Hide duration left",
"hide-github-button": "Hide GitHub link Button",
"play-on-youtube-music": "Play on YouTube Music",
"set-inactivity-timeout": "Set inactivity timeout"
},
"name": "Discord Rich Presence",
"prompt": {
"set-inactivity-timeout": {
"label": "Enter inactivity timeout in seconds:",
"title": "Set inactivity timeout"
}
}
},
"downloader": {
"backend": {
"dialog": {
"error": {
"buttons": {
"ok": "OK"
},
"message": "Argh! Apologies, download failed…",
"title": "Error in download!"
},
"start-download-playlist": {
"buttons": {
"ok": "OK"
},
"detail": "({{playlistSize}} songs)",
"message": "Downloading Playlist {{playlistTitle}}",
"title": "Download started"
}
},
"feedback": {
"conversion-progress": "Conversion: {{percent}}%",
"converting": "Converting…",
"done": "Done: {{filePath}}",
"download-info": "Downloading {{artist}} - {{title}} [{{videoId}}",
"download-progress": "Download: {{percent}}%",
"downloading": "Downloading…",
"downloading-counter": "Downloading {{current}}/{{total}}…",
"downloading-playlist": "Downloading playlist \"{{playlistTitle}}\" - {{playlistSize}} songs ({{playlistId}})",
"error-while-downloading": "Error downloading \"{{author}} - {{title}}\": {{error}}",
"folder-already-exists": "The folder {{playlistFolder}} already exists",
"getting-playlist-info": "Getting playlist info…",
"loading": "Loading…",
"playlist-has-only-one-song": "Playlist has only one item, downloading it directly",
"playlist-id-not-found": "No playlist ID found",
"playlist-is-empty": "Playlist is empty",
"playlist-is-mix-or-private": "Error getting playlist info: make sure it isn't a private or \"Mixed for you\" playlist\n\n{{error}}",
"preparing-file": "Preparing file…",
"saving": "Saving…",
"trying-to-get-playlist-id": "Trying to get playlist ID: {{playlistId}}",
"video-id-not-found": "Video not found",
"writing-id3": "Writing ID3 tags…"
}
},
"description": "Downloads MP3 / source audio directly from the interface",
"menu": {
"choose-download-folder": "Choose download folder",
"download-playlist": "Download playlist",
"presets": "Presets",
"skip-existing": "Skip existing files"
},
"name": "Downloader",
"renderer": {
"can-not-update-progress": "Cannot update progress"
},
"templates": {
"button": "Download"
}
},
"exponential-volume": {
"description": "Makes the volume slider exponential so it's easier to select lower volumes.",
"name": "Exponential Volume"
},
"in-app-menu": {
"description": "Gives menu-bars a fancy, dark or album-color look",
"menu": {
"hide-dom-window-controls": "Hide DOM window controls"
},
"name": "In-App Menu"
},
"lumiastream": {
"description": "Adds Lumia Stream support",
"name": "Lumia Stream [Beta]"
},
"lyrics-genius": {
"description": "Adds lyrics support for most songs",
"menu": {
"romanized-lyrics": "Romanized Lyrics"
},
"name": "Lyrics Genius",
"renderer": {
"fetched-lyrics": "Fetched lyrics for Genius"
}
},
"music-together": {
"description": "Share a playlist with others. When the host plays a song, everyone else will hear the same song",
"dialog": {
"enter-host": "Enter Host ID"
},
"internal": {
"save": "Save",
"track-source": "Track Source",
"unknown-user": "Unknown User"
},
"menu": {
"click-to-copy-id": "Copy Host ID",
"close": "Close Music Together",
"connected-users": "Connected Users",
"disconnect": "Disconnect Music Together",
"empty-user": "No connected users",
"host": "Music Together Host",
"join": "Join Music Together",
"permission": {
"all": "Allow guests to control playlist and player",
"host-only": "Only the host can control playlist and player",
"playlist": "Allow guests to control playlist"
},
"set-permission": "Change Control Permission",
"status": {
"disconnected": "Disconnected",
"guest": "Connected as Guest",
"host": "Connected as Host"
}
},
"name": "Music Together [Beta]",
"toast": {
"add-song-failed": "Failed to add song",
"closed": "Music Together closed",
"disconnected": "Music Together disconnected",
"host-failed": "Failed to host Music Together",
"id-copied": "Host ID copied to clipboard",
"id-copy-failed": "Failed to copy Host ID to clipboard",
"join-failed": "Failed to join Music Together",
"joined": "Joined Music Together",
"permission-changed": "Music Together permission changed to \"{{permission}}\"",
"remove-song-failed": "Failed to remove song",
"user-connected": "{{name}} joined Music Together",
"user-disconnected": "{{name}} left Music Together"
}
},
"navigation": {
"description": "Next/Back navigation arrows directly integrated in the interface, like in your favorite browser",
"name": "Navigation"
},
"no-google-login": {
"description": "Remove Google login buttons and links from the interface",
"name": "No Google Login"
},
"notifications": {
"description": "Display a notification when a song starts playing (interactive notifications are available on Windows)",
"menu": {
"interactive": "Interactive Notifications",
"interactive-settings": {
"label": "Interactive Settings",
"submenu": {
"hide-button-text": "Hide button text",
"refresh-on-play-pause": "Refresh on Play/Pause",
"tray-controls": "Open/Close on tray click"
}
},
"priority": "Notification Priority",
"toast-style": "Toast style",
"unpause-notification": "Show notification on unpause"
},
"name": "Notifications"
},
"picture-in-picture": {
"description": "Allows to switch the app to picture-in-picture mode",
"menu": {
"always-on-top": "Always on top",
"hotkey": {
"label": "Hotkey",
"prompt": {
"keybind-options": {
"hotkey": "Hotkey"
},
"label": "Choose a hotkey to toggle picture-in-picture",
"title": "Picture-in-picture Hotkey"
}
},
"save-window-position": "Save window position",
"save-window-size": "Save window size",
"use-native-pip": "Use browser native PiP"
},
"name": "Picture-in-picture",
"templates": {
"button": "Picture-in-picture"
}
},
"playback-speed": {
"description": "Listen fast, listen slow! Adds a slider that controls song speed",
"name": "Playback Speed",
"templates": {
"button": "Speed"
}
},
"precise-volume": {
"description": "Control the volume precisely using mousewheel/hotkeys, with a custom HUD and customizable volume steps",
"menu": {
"arrows-shortcuts": "Local Arrow-keys Controls",
"custom-volume-steps": "Set Custom Volume Steps",
"global-shortcuts": "Global Hotkeys"
},
"name": "Precise Volume",
"prompt": {
"global-shortcuts": {
"keybind-options": {
"decrease": "Decrease Volume",
"increase": "Increase Volume"
},
"label": "Choose Global Volume Keybinds:",
"title": "Global Volume Keybinds"
},
"volume-steps": {
"label": "Choose Volume Increase/Decrease Steps",
"title": "Volume Steps"
}
}
},
"quality-changer": {
"backend": {
"dialog": {
"quality-changer": {
"detail": "Current Quality: {{quality}}",
"message": "Choose Video Quality:",
"title": "Choose Video Quality"
}
}
},
"description": "Allows changing the video quality with a button on the video overlay",
"name": "Video Quality Changer"
},
"scrobbler": {
"description": "Add scrobbling support (etc. last.fm, Listenbrainz)",
"menu": {
"scrobble-other-media": "Scrobble other media",
"lastfm": {
"api-settings": "Last.fm API Settings"
},
"listenbrainz": {
"token": "Enter ListenBrainz user token"
}
},
"name": "Scrobbler",
"prompt": {
"lastfm": {
"api-key": "Last.fm API key",
"api-secret": "Last.fm API secret"
},
"listenbrainz": {
"token": {
"label": "Enter your ListenBrainz user token:",
"title": "ListenBrainz token"
}
}
}
},
"shortcuts": {
"description": "Allows setting global hotkeys for playback (play/pause/next/previous) and turning off media OSD by overriding media keys, turning on Ctrl/CMD + F to search, turning on Linux MPRIS support for media keys, and custom hotkeys for advanced users",
"menu": {
"override-media-keys": "Override Media Keys",
"set-keybinds": "Set Global Song Controls"
},
"name": "Shortcuts (& MPRIS)",
"prompt": {
"keybind": {
"keybind-options": {
"next": "Next",
"play-pause": "Play / Pause",
"previous": "Previous"
},
"label": "Choose Global Keybinds for Songs Control:",
"title": "Global Keybinds"
}
}
},
"skip-disliked-songs": {
"description": "Skips disliked songs",
"name": "Skip Disliked Songs"
},
"skip-silences": {
"description": "Automatically skip silences sections in songs",
"name": "Skip Silences"
},
"sponsorblock": {
"description": "Automatically Skips non-music parts like intro/outro or parts of music videos where the song isn't playing",
"name": "SponsorBlock"
},
"taskbar-mediacontrol": {
"description": "Control playback from your Windows taskbar",
"name": "Taskbar Media Control"
},
"touchbar": {
"description": "Adds a TouchBar widget for macOS users",
"name": "TouchBar"
},
"tuna-obs": {
"description": "Integration with OBS's plugin Tuna",
"name": "Tuna OBS"
},
"video-toggle": {
"description": "Adds a button to switch between Video/Song mode. can also optionally remove the whole video tab",
"menu": {
"align": {
"label": "Alignment",
"submenu": {
"left": "Left",
"middle": "Middle",
"right": "Right"
}
},
"force-hide": "Force remove video tab",
"mode": {
"label": "Mode",
"submenu": {
"custom": "Custom toggle",
"disabled": "Disabled",
"native": "Native toggle"
}
}
},
"name": "Video Toggle",
"templates": {
"button": "Song"
}
},
"visualizer": {
"description": "Adds a visualizer to the player",
"menu": {
"visualizer-type": "Visualizer Type"
},
"name": "Visualizer"
}
}
}

View File

@ -1,682 +0,0 @@
{
"common": {
"console": {
"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 plugin \"{{pluginName}}\"",
"load-all": "Cargando todos los plugins",
"load-failed": "Error al cargar el plugin \"{{pluginName}}\"",
"loaded": "Plugin \"{{pluginName}}\" cargado",
"unload-failed": "No se ha podido descargar el plugin \"{{pluginName}}\"",
"unloaded": "Plugin \"{{pluginName}}\" descargado"
}
}
},
"language": {
"code": "es",
"local-name": "Inglés",
"name": "Spanish"
},
"main": {
"console": {
"did-finish-load": {
"dev-tools": "Carga finalizada. DevTools abiertos"
},
"i18n": {
"loaded": "i18n cargado"
},
"second-instance": {
"receive-command": "Comando recibido sobre el protocolo: \"{{command}}\""
},
"theme": {
"css-file-not-found": "El archivo CSS \"{{cssFile}}\" no existe, ignorando"
},
"unresponsive": {
"details": "¡Error sin repuesta!\n{{error}}"
},
"when-ready": {
"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}}"
}
},
"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": "Menu oculto esta deshabilitado",
"title": "Menú oculto activado"
},
"need-to-restart": {
"buttons": {
"later": "Más tarde",
"restart-now": "Reiniciar ahora"
},
"detail": "\"{{pluginName}}\" se requiere reiniciar para que el plugin tome efecto",
"message": "\"{{pluginName}}\" necesita reiniciar",
"title": "Se requiere reinicio"
},
"unresponsive": {
"buttons": {
"quit": "Dejar",
"relaunch": "Volver a abrir",
"wait": "Espera"
},
"detail": "Sentimos las molestias. Por favor, elija qué hacer:",
"message": "La aplicación no responde",
"title": "La ventana no responde"
},
"update-available": {
"buttons": {
"disable": "Desactivar actualizaciones",
"download": "Descargar",
"ok": "OK"
},
"detail": "Una nueva versión está disponible y puede descargarse en {{downloadLink}}",
"message": "Ya está disponible una nueva versión",
"title": "Actualización disponible"
}
},
"menu": {
"about": "Acerca de",
"navigation": {
"label": "Navegación",
"submenu": {
"copy-current-url": "Copiar la URL actual",
"go-back": "Regresar",
"go-forward": "Adelante",
"quit": "Salir",
"restart": "Reiniciar la aplicación"
}
},
"options": {
"label": "Opciones",
"submenu": {
"advanced-options": {
"label": "Opciones avanzadas",
"submenu": {
"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": "Reinicie al cambiar la configuración",
"set-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",
"title": "Establecer proxy"
}
},
"toggle-dev-tools": "Activar DevTools"
}
},
"always-on-top": "Siempre arriba",
"auto-update": "Actualización automática",
"hide-menu": {
"dialog": {
"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 cambio el idioma"
},
"label": "Idioma",
"submenu": {
"to-help-translate": "¿Quieres ayudar a traducir? Haz clic aquí"
}
},
"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"
},
"tray": {
"label": "Bandeja",
"submenu": {
"disabled": "Desactivado",
"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"
}
},
"visual-tweaks": {
"label": "Ajustes visuales",
"submenu": {
"like-buttons": {
"default": "Predeterminado",
"force-show": "Forzar la visualización",
"hide": "Ocultar",
"label": "Botones de \"Me Gusta\""
},
"remove-upgrade-button": "Remover el botón de Actualización",
"theme": {
"label": "Tema",
"submenu": {
"import-css-file": "Importar archivo CSS personalizado",
"no-theme": "Sin temas"
}
}
}
}
}
},
"plugins": {
"enabled": "Habilitado",
"label": "Plugins",
"new": "NUEVO"
},
"view": {
"label": "Ver",
"submenu": {
"force-reload": "Forzar la recarga",
"reload": "Recargar",
"reset-zoom": "Tamaño actual",
"toggle-fullscreen": "Alternar pantalla completa",
"zoom-in": "Acercar",
"zoom-out": "Alejar"
}
}
},
"tray": {
"next": "Siguiente",
"play-pause": "Reproducir/Pausar",
"previous": "Anterior",
"quit": "Salir",
"restart": "Reiniciar la aplicación",
"show": "Mostrar ventana",
"tooltip": {
"default": "YouTube Music",
"with-song-info": "YouTube Music: {{artist}} - {{title}}"
}
}
},
"plugins": {
"adblocker": {
"description": "Bloquear todos los anuncios y el rastreo",
"menu": {
"blocker": "Bloqueador"
},
"name": "Bloqueador de anuncios"
},
"album-actions": {
"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": {
"description": "Aplica un tema dinámico y efectos visuales basados en la paleta de colores del álbum",
"menu": {
"color-mix-ratio": {
"label": "Proporción de la mezcla de color",
"submenu": {
"percent": "{{ratio}}%"
}
}
},
"name": "Color del álbum"
},
"ambient-mode": {
"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}} pixeles"
}
},
"buffer": {
"label": "Buffer",
"submenu": {
"buffer": "{{buffer}}"
}
},
"opacity": {
"label": "Transparencia",
"submenu": {
"percent": "{{opacity}}%"
}
},
"quality": {
"label": "Calidad",
"submenu": {
"pixels": "{{quality}} píxeles"
}
},
"size": {
"label": "Tamaño",
"submenu": {
"percent": "{{size}}%"
}
},
"smoothness-transition": {
"label": "Transición suave",
"submenu": {
"during": "Durante {{interpolationTime}} s"
}
},
"use-fullscreen": {
"label": "Usando Pantalla Completa"
}
},
"name": "Modo ambiente"
},
"audio-compressor": {
"description": "Aplicar compresión al audio (reduce la diferencia entre las partes más fuertes y más suaves de una pista para que tenga un nivel más consistente)",
"name": "Compresor de audio"
},
"blur-nav-bar": {
"description": "Hace que la barra de navegación sea transparente y borrosa",
"name": "Desenfocar barra de navegación"
},
"bypass-age-restrictions": {
"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",
"menu": {
"autoload": "Seleccionar automáticamente el último subtítulo utilizado",
"disable-captions": "Sin subtítulos por defecto"
},
"name": "Selector de subtítulos",
"prompt": {
"selector": {
"label": "Idioma actual: {{language}}",
"none": "Ninguno",
"title": "Seleccionar idioma de los subtítulos"
}
},
"templates": {
"title": "Abra el selector de subtítulos"
}
},
"compact-sidebar": {
"description": "Poner siempre la barra lateral en modo compacto",
"name": "Barra lateral compacta"
},
"crossfade": {
"description": "Crossfade entre canciones",
"menu": {
"advanced": "Avanzado"
},
"name": "Crossfade [Beta]",
"prompt": {
"options": {
"multi-input": {
"fade-in-duration": "Duración del fundido (ms)",
"fade-out-duration": "Duración del fundido de salida (ms)",
"fade-scaling": {
"label": "Escala de fundido",
"linear": "Lineal",
"logarithmic": "Logarítmico"
},
"seconds-before-end": "Crossfade N segundos antes del final"
},
"title": "Opciones de crossfade"
}
}
},
"disable-autoplay": {
"description": "Hace que la canción comience en modo \"pausado\"",
"menu": {
"apply-once": "Sólo se aplica al inicio"
},
"name": "Desactivar reproducción automática"
},
"discord": {
"backend": {
"already-connected": "Se intentó conectar con una conexión activa",
"connected": "Conectado a Discord",
"disconnected": "Desconectado de Discord"
},
"description": "Muestra a tus amigos lo que escuchas con Rich Presence",
"menu": {
"auto-reconnect": "Reconectar automáticamente",
"clear-activity": "Borrar actividad",
"clear-activity-after-timeout": "Borrar actividad después de un tiempo",
"connected": "Conectado",
"disconnected": "Desconectado",
"hide-duration-left": "Ocultar la duración restante",
"hide-github-button": "Ocultar el botón de enlace a GitHub",
"play-on-youtube-music": "Reproducir en YouTube Music",
"set-inactivity-timeout": "Establecer tiempo de inactividad"
},
"name": "Estado de actividad de Discord",
"prompt": {
"set-inactivity-timeout": {
"label": "Introduzca el tiempo de inactividad en segundos:",
"title": "Establecer tiempo de inactividad"
}
}
},
"downloader": {
"backend": {
"dialog": {
"error": {
"buttons": {
"ok": "OK"
},
"message": "¡Argh! Lo siento, la descarga falló…",
"title": "¡Error en la descarga!"
},
"start-download-playlist": {
"buttons": {
"ok": "OK"
},
"detail": "({{playlistSize}} canciones)",
"message": "Descargar Playlist {{playlistTitle}}",
"title": "Descarga iniciada"
}
},
"feedback": {
"conversion-progress": "Conversión: {{percent}}%",
"converting": "Convirtiendo…",
"done": "Listo: {{filePath}}",
"download-info": "Descargando {{artist}} - {{title}} [{{videoId}}",
"download-progress": "Descarga: {{percent}}%",
"downloading": "Descargando…",
"downloading-counter": "Descargando {{current}}/{{total}}…",
"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, 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 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 el ID de la lista de reproducción: {{playlistId}}",
"video-id-not-found": "Video no encontrado",
"writing-id3": "Escribiendo las etiquetas ID3…"
}
},
"description": "Descarga MP3 / audio fuente directamente desde la interfaz",
"menu": {
"choose-download-folder": "Elija la carpeta de descarga",
"download-playlist": "Descargar lista de reproducción",
"presets": "Preajustes",
"skip-existing": "Saltar archivos existentes"
},
"name": "Descargador",
"renderer": {
"can-not-update-progress": "No se puede actualizar el progreso"
},
"templates": {
"button": "Descargar"
}
},
"exponential-volume": {
"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": {
"description": "Da a las barras de menú un aspecto elegante, oscuro o del color de un álbum",
"menu": {
"hide-dom-window-controls": "Ocultar controles de ventana DOM"
},
"name": "Menú de aplicación"
},
"lumiastream": {
"description": "Agrega soporte para Lumia Stream",
"name": "Lumia Stream [Beta]"
},
"lyrics-genius": {
"description": "Añade el soporte para las letras para la mayoría de las canciones",
"menu": {
"romanized-lyrics": "Letras Romanizadas"
},
"name": "Lyrics Genius",
"renderer": {
"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 el ID del host"
},
"internal": {
"save": "Guardar",
"track-source": "Fuente de la pista",
"unknown-user": "Usuario desconocido"
},
"menu": {
"click-to-copy-id": "Copiar el ID del host",
"close": "Cerrar Music Together",
"connected-users": "Usuarios conectados",
"disconnect": "Desactivar Music Together",
"empty-user": "No hay usuarios conectados",
"host": "Host de Music Together",
"join": "Únase a Music Together",
"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": "Permita que los invitados controlen la lista de reproducción"
},
"set-permission": "Permiso de control de cambios",
"status": {
"disconnected": "Desconectado",
"guest": "Conectado como invitado",
"host": "Conectado como anfitrión"
}
},
"name": "Music Together [Beta]",
"toast": {
"add-song-failed": "No se puede añadir la canción",
"closed": "Music Together cerrado",
"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",
"user-connected": "{{name}} se unió a Music Together",
"user-disconnected": "{{name}} dejó Music Together"
}
},
"navigation": {
"description": "Flechas de navegación Siguiente/Atrás directamente integradas en la interfaz, como en tu navegador favorito",
"name": "Navegación"
},
"no-google-login": {
"description": "Eliminar los botones y enlaces de inicio de sesión de Google de la interfaz",
"name": "Sin inicio de sesión de Google"
},
"notifications": {
"description": "Mostrar una notificación cuando empiece a sonar una canción (las notificaciones interactivas están disponibles en Windows)",
"menu": {
"interactive": "Notificaciones interactivas",
"interactive-settings": {
"label": "Ajustes interactivos",
"submenu": {
"hide-button-text": "Ocultar el texto del botón",
"refresh-on-play-pause": "Actualizar al reproducir/pausar",
"tray-controls": "Abrir/Cerrar al hacer clic en la bandeja"
}
},
"priority": "Prioridad de notificación",
"toast-style": "Estilo de mensaje emergente",
"unpause-notification": "Mostrar notificación al reanudar"
},
"name": "Notificaciones"
},
"picture-in-picture": {
"description": "Permite cambiar la aplicación al modo de imagen en imagen",
"menu": {
"always-on-top": "Siempre encima",
"hotkey": {
"label": "Tecla de acceso rápido",
"prompt": {
"keybind-options": {
"hotkey": "Tecla de acceso rápido"
},
"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 \"Dos imágenes a la vez\" PiP nativo del navegador"
},
"name": "Imagen en imagen",
"templates": {
"button": "Imagen en imagen"
}
},
"playback-speed": {
"description": "Escucha rápido, escucha despacio! Añade un control deslizante que ajusta la velocidad de la canción",
"name": "Velocidad de reproducción",
"templates": {
"button": "Velocidad"
}
},
"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 pasos de volumen personalizables",
"menu": {
"arrows-shortcuts": "Controles de teclas de flechas locales",
"custom-volume-steps": "Establecer niveles de volumen personalizados",
"global-shortcuts": "Teclas de acceso rápido globales"
},
"name": "Volumen preciso",
"prompt": {
"global-shortcuts": {
"keybind-options": {
"decrease": "Disminuir el volumen",
"increase": "Aumentar el volumen"
},
"label": "Elija combinaciones de teclas para el volumen:",
"title": "Combinaciones de teclas para el volumen"
},
"volume-steps": {
"label": "Escoge los pasos de aumento o disminución del volumen",
"title": "Niveles de volumen"
}
}
},
"quality-changer": {
"backend": {
"dialog": {
"quality-changer": {
"detail": "Calidad actual: {{quality}}",
"message": "Elija la calidad de vídeo:",
"title": "Elija la calidad de vídeo"
}
}
},
"description": "Permite cambiar la calidad del vídeo con un botón sobre puesto en el vídeo",
"name": "Ajustador de calidad de vídeo"
},
"scrobbler": {
"description": "Añadir soporte para scrobbling (last.fm, Listenbrainz, etc.)",
"menu": {
"lastfm": {
"api-settings": "Ajustes de la API de Last.fm"
},
"listenbrainz": {
"token": "Introduzca el token de usuario de ListenBrainz"
},
"scrobble-other-media": "Scrobble en otros medios"
},
"name": "Scrobbler",
"prompt": {
"lastfm": {
"api-key": "Clave de la API de Last.fm",
"api-secret": "Clave secreta de la API de Last.fm"
},
"listenbrainz": {
"token": {
"label": "Introduzca su token de usuario de ListenBrainz:",
"title": "Token de ListenBrainz"
}
}
}
},
"shortcuts": {
"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"
},
"name": "Atajos (& MPRIS)",
"prompt": {
"keybind": {
"keybind-options": {
"next": "Siguiente",
"play-pause": "Reproducir / Pausa",
"previous": "Anterior"
},
"label": "Elija combinaciones de teclas para el control de las canciones:",
"title": "Atajos de teclado globales"
}
}
},
"skip-disliked-songs": {
"description": "Omite las canciones que no le gustan",
"name": "Saltar canciones que no me gustan"
},
"skip-silences": {
"description": "Salta automáticamente las secciones silenciosas de las canciones",
"name": "Saltar silencios"
},
"sponsorblock": {
"description": "Salta automáticamente las partes no musicales como la introducción/final o secciones de videos musicales donde la canción no está sonando",
"name": "SponsorBlock"
},
"taskbar-mediacontrol": {
"description": "Controla la reproducción desde la barra de tareas de Windows",
"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 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",
"menu": {
"align": {
"label": "Alineación",
"submenu": {
"left": "Izquierda",
"middle": "Medio",
"right": "Derecha"
}
},
"force-hide": "Forzar eliminación de la pestaña de vídeo",
"mode": {
"label": "Modo",
"submenu": {
"custom": "Alternador personalizado",
"disabled": "Desactivado",
"native": "Alternador nativo"
}
}
},
"name": "Alternador de vídeo",
"templates": {
"button": "Canción"
}
},
"visualizer": {
"description": "Añadir un visualizador al reproductor",
"menu": {
"visualizer-type": "Tipo de visualizador"
},
"name": "Visualizador"
}
}
}

View File

@ -1,603 +0,0 @@
{
"common": {
"console": {
"plugins": {
"execute-failed": "Échec de l'exécution du plugin {{pluginName}}::{{contextName}}",
"executed-at-ms": "Plugin {{pluginName}}::{{contextName}} exécuté en {{ms}}ms",
"initialize-failed": "Échec de l'initialisation du plugin \"{{pluginName}}\"",
"load-all": "Chargement des plugins",
"load-failed": "Échec du chargement du plugin \"{{pluginName}}\"",
"loaded": "Plugin \"{{pluginName}}\" chargé",
"unload-failed": "Échec du déchargement du plugin \"{{pluginName}}\"",
"unloaded": "Plugin \"{{pluginName}}\" déchargé"
}
}
},
"language": {
"code": "fr",
"local-name": "Français",
"name": "French"
},
"main": {
"console": {
"did-finish-load": {
"dev-tools": "Chargement terminé. DevTools ouvert"
},
"i18n": {
"loaded": "i18n chargé"
},
"second-instance": {
"receive-command": "Received command over protocol: \"{{command}}\""
},
"theme": {
"css-file-not-found": "Le fichier de CSS \"{{cssFile}}\" n'existe pas, ignorer"
},
"unresponsive": {
"details": "Erreur: ne répond pas!\n{{error}}"
},
"when-ready": {
"clearing-cache-after-20s": "Effacement du cache de l'application"
},
"window": {
"tried-to-render-offscreen": "La fenêtre a essayé d'effectuer un rendu hors écran, windowSize={{windowSize}}, displaySize={{displaySize}}, position={{position}}"
}
},
"dialog": {
"hide-menu-enabled": {
"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é"
},
"need-to-restart": {
"buttons": {
"later": "Plus tard",
"restart-now": "Redémarrer maintenant"
},
"detail": "\"{{pluginName}}\" plugin nécessite un redémarrage pour qu'il soit pris en compte",
"message": "\"{{pluginName}}\" a besoin d'un redémarrage",
"title": "Redémarrage requis"
},
"unresponsive": {
"buttons": {
"quit": "Quitté",
"relaunch": "Relancer",
"wait": "Attendre"
},
"detail": "Nous sommes désolés du dérangement! veuillez choisir quoi faire:",
"message": "L'application ne répond pas",
"title": "La fenêtre ne répond pas"
},
"update-available": {
"buttons": {
"disable": "Désactiver les mises à jour",
"download": "Télécharger",
"ok": "Ok"
},
"detail": "Une nouvelle version est disponible et peut-être télécharger sur {{downloadLink}}",
"message": "Une nouvelle version est disponible",
"title": "Mise à jour disponible"
}
},
"menu": {
"about": "À-propos",
"navigation": {
"label": "Navigation",
"submenu": {
"copy-current-url": "Copier l'URL actuelle",
"go-back": "Retour",
"go-forward": "Avancer",
"quit": "Quitter",
"restart": "Redémarrer l'application"
}
},
"options": {
"label": "Paramètres",
"submenu": {
"advanced-options": {
"label": "Options avancée",
"submenu": {
"auto-reset-app-cache": "Réinitialiser le cache de l'application au démarrage",
"disable-hardware-acceleration": "Désactiver les accélérations matérielles",
"edit-config-json": "Modifier config.json",
"override-user-agent": "Remplacer le User-Agent",
"restart-on-config-changes": "Redémarrer quand la configuration change",
"set-proxy": {
"label": "Définir un proxy",
"prompt": {
"label": "Entrez l'adresse proxy : (laissez vide pour désactiver)",
"placeholder": "Exemple: socks5://127.0.0.1:9999",
"title": "Définir un proxy"
}
},
"toggle-dev-tools": "Ouvrir/fermer les outils de développement"
}
},
"always-on-top": "Toujours au dessus",
"auto-update": "Mise à jour automatique",
"hide-menu": {
"dialog": {
"message": "Le menu sera masqué au prochain lancement, utilisez [Alt] pour l'afficher (ou backtick [`] si vous utilisez le menu intégré à l'application)",
"title": "Masquer le menu activé"
},
"label": "Cacher le menu"
},
"language": {
"dialog": {
"message": "La langue sera changée après le redémarrage",
"title": "Langue modifiée"
},
"label": "Langue",
"submenu": {
"to-help-translate": "Envie d'aider à la traduction ? Cliquer ici"
}
},
"resume-on-start": "Reprendre la dernière chanson quand l'application démarre",
"single-instance-lock": "Verrouillage d'instance unique",
"start-at-login": "Démarrer à la connexion",
"starting-page": {
"label": "Page de démarrage",
"unset": "Définir à vide"
},
"tray": {
"label": "Plateau",
"submenu": {
"disabled": "Désactivé",
"enabled-and-hide-app": "Activé et cacher l'app",
"enabled-and-show-app": "Activé et afficher l'application",
"play-pause-on-click": "Lecture/Pause au clic"
}
},
"visual-tweaks": {
"label": "Ajustements visuels",
"submenu": {
"like-buttons": {
"default": "Par défaut",
"force-show": "Forcer à apparaître",
"hide": "Cacher",
"label": "Boutons « J'aime »"
},
"remove-upgrade-button": "Supprimer le bouton de mise à niveau",
"theme": {
"label": "Thème",
"submenu": {
"import-css-file": "Importer fichier CSS personnalisé",
"no-theme": "Pas de thème"
}
}
}
}
}
},
"plugins": {
"enabled": "Activé",
"label": "Extensions",
"new": "NOUVELLE"
},
"view": {
"label": "Vue",
"submenu": {
"force-reload": "Forcer l'actualisation",
"reload": "Actualiser",
"reset-zoom": "Taille réelle",
"toggle-fullscreen": "Basculer en plein écran",
"zoom-in": "Zoom avant",
"zoom-out": "Zoom arrière"
}
}
},
"tray": {
"next": "Suivant",
"play-pause": "Lecture/Pause",
"previous": "Précédent",
"quit": "Quitter",
"restart": "Redémarrer l'application",
"show": "Afficher la fenêtre",
"tooltip": {
"default": "YouTube Music",
"with-song-info": "YouTube Music: {{artist}} - {{title}}"
}
}
},
"plugins": {
"adblocker": {
"description": "Bloquer toutes les annonces et le suivi par défaut",
"menu": {
"blocker": "Bloqueur"
},
"name": "Bloqueur de publicités"
},
"album-actions": {
"description": "Ajoute les boutons Dislike, Undislike, Like, et Unlike à appliquer sur toutes les chansons dans un playlist ou un album.",
"name": "Actions d'Album"
},
"album-color-theme": {
"description": "Applique un thème dynamique et des effets visuels basés sur la palette des couleurs de l'album",
"name": "Thème de couleur d'album"
},
"ambient-mode": {
"description": "Applique un effet d'éclairage en jetant des couleurs douces de la vidéo, dans le fond de votre écran.",
"menu": {
"blur-amount": {
"label": "Quantité de flou",
"submenu": {
"pixels": "{{blurAmount}} pixels"
}
},
"buffer": {
"label": "Tampon",
"submenu": {
"buffer": "{{buffer}}"
}
},
"opacity": {
"label": "Opacité",
"submenu": {
"percent": "{{opacity}}%"
}
},
"quality": {
"label": "Qualité",
"submenu": {
"pixels": "{{quality}} pixels"
}
},
"size": {
"label": "Taille",
"submenu": {
"percent": "{{size}}%"
}
},
"smoothness-transition": {
"label": "Transition en douceur",
"submenu": {
"during": "Pendant {{interpolationTime}} s"
}
},
"use-fullscreen": {
"label": "Utilisation du mode plein écran"
}
},
"name": "Mode ambiant"
},
"audio-compressor": {
"description": "Appliquer une compression à l'audio (diminue le volume des parties les plus fortes du signal et augmente le volume des parties les plus faibles)",
"name": "Compresseur audio"
},
"blur-nav-bar": {
"description": "Rend la barre de navigation transparente et floue",
"name": "Barre de navigation floue"
},
"bypass-age-restrictions": {
"description": "Contourner la vérification de l'âge de YouTube",
"name": "Contourner les restrictions dâge"
},
"captions-selector": {
"description": "Sélecteur de sous-titres pour les pistes audio YouTube Music",
"menu": {
"autoload": "Sélectionner automatiquement la dernière légende utilisée",
"disable-captions": "Pas de sous-titres par défaut"
},
"name": "Sélecteur de sous-titres",
"prompt": {
"selector": {
"label": "Langue de sous-titrage actuelle : {{language}}",
"none": "Aucun",
"title": "Sélectionnez la langue des sous-titres"
}
},
"templates": {
"title": "Ouvrir le sélecteur de sous-titres"
}
},
"compact-sidebar": {
"description": "Toujours définir la barre latérale en mode compact",
"name": "Barre latérale compacte"
},
"crossfade": {
"description": "Fondu enchaîné entre les chansons",
"menu": {
"advanced": "Avancé"
},
"name": "Fondu enchaîné [Bêta]",
"prompt": {
"options": {
"multi-input": {
"fade-in-duration": "Durée du fondu (millisecondes)",
"fade-out-duration": "Durée du fondu (millisecondes)",
"fade-scaling": {
"label": "Mise à l'échelle du fondu",
"linear": "Linéaire",
"logarithmic": "Logarithmique"
},
"seconds-before-end": "Fondu enchaîné N secondes avant la fin"
},
"title": "Options de fondu enchaîné"
}
}
},
"disable-autoplay": {
"description": "Fait démarrer la chanson en mode \"pause\"",
"menu": {
"apply-once": "S'applique seulement au démarrage"
},
"name": "Désactiver la lecture automatique"
},
"discord": {
"backend": {
"already-connected": "Tentative de connexion avec une connexion active",
"connected": "Connecté à Discord",
"disconnected": "Déconnecté de Discord"
},
"description": "Montrez à vos amis ce que vous écoutez avec Rich Presence",
"menu": {
"auto-reconnect": "Reconnexion automatique",
"clear-activity": "Effacer l'activité",
"clear-activity-after-timeout": "Effacer lactivité après un délai dattente",
"connected": "Connecté",
"disconnected": "Déconnecté",
"hide-duration-left": "Masquer la durée restante",
"hide-github-button": "Masquer le bouton du lien GitHub",
"play-on-youtube-music": "Jouer sur YouTube Music",
"set-inactivity-timeout": "Définir le délai d'inactivité"
},
"name": "Discord Rich Presence",
"prompt": {
"set-inactivity-timeout": {
"label": "Entrez le délai d'inactivité en secondes :",
"title": "Définir le délai d'inactivité"
}
}
},
"downloader": {
"backend": {
"dialog": {
"error": {
"buttons": {
"ok": "Ok"
},
"message": "Argh ! Désolé, le téléchargement a échoué…",
"title": "Erreur de téléchargement !"
},
"start-download-playlist": {
"buttons": {
"ok": "Ok"
},
"detail": "({{playlistSize}} chansons)",
"message": "Téléchargement de la playlist {{playlistTitle}}",
"title": "Téléchargement a commencé"
}
},
"feedback": {
"conversion-progress": "Conversion : {{percent}} %",
"converting": "Conversion…",
"done": "Terminé : {{filePath}}",
"download-info": "Téléchargement {{artist}} - {{title}} [{{videoId}}",
"download-progress": "Télécharger: {{percent}}%",
"downloading": "Télécharge…",
"downloading-counter": "Télécharge {{current}}/{{total}}…",
"downloading-playlist": "Téléchargement de la playlist \"{{playlistTitle}}\"  {{playlistSize}} chansons ({{playlistId}})",
"error-while-downloading": "Erreur lors du téléchargement de \"{{author}} - {{title}}\" : {{error}}",
"folder-already-exists": "Le dossier {{playlistFolder}} existe déjà",
"getting-playlist-info": "Obtention d'informations sur la liste de lecture…",
"loading": "Chargement…",
"playlist-has-only-one-song": "La liste de lecture ne contient qu'un seul élément, téléchargement du morceau seul",
"playlist-id-not-found": "Aucun ID de liste de lecture trouvé",
"playlist-is-empty": "La liste de lecture est vide",
"playlist-is-mix-or-private": "Erreur lors de l'obtention des informations sur la liste de lecture: assurez-vous qu'il ne s'agit pas d'une liste privée ou \"Mixée pour vous\"\n\n{{error}}",
"preparing-file": "Péparer des fichier…",
"saving": "Sauvegarde…",
"trying-to-get-playlist-id": "Obtention de l'ID de la liste de lecture: {{playlistId}}",
"video-id-not-found": "Vidéo non trouvée",
"writing-id3": "Écriture des balises ID3…"
}
},
"description": "Télécharge les fichiers MP3/source audio directement depuis l'interface",
"menu": {
"choose-download-folder": "Choisissez le dossier de téléchargement",
"download-playlist": "Télécharger la liste de lecture",
"presets": "Préconfigurations",
"skip-existing": "Passer les fichiers existants"
},
"name": "Téléchargeur",
"renderer": {
"can-not-update-progress": "Impossible de mettre à jour la progression"
},
"templates": {
"button": "Télécharger"
}
},
"exponential-volume": {
"description": "Rend le curseur de volume exponentiel afin qu'il soit plus facile de sélectionner des volumes plus faibles.",
"name": "Volume exponentiel"
},
"in-app-menu": {
"description": "Donne aux barres de menus un aspect élégant, sombre ou aux couleurs de l'album",
"menu": {
"hide-dom-window-controls": "Masquer les contrôles de la fenêtre DOM"
},
"name": "Menu intégré à l'application"
},
"lumiastream": {
"description": "Ajoute la prise en charge de Lumia Stream",
"name": "Lumia Stream [Bêta]"
},
"lyrics-genius": {
"description": "Ajoute la prise en charge des paroles pour la plupart des chansons",
"menu": {
"romanized-lyrics": "Paroles romanisées"
},
"name": "Paroles Genius",
"renderer": {
"fetched-lyrics": "Paroles récupérées pour Genius"
}
},
"navigation": {
"description": "Flèches de navigation Suivant/Retour directement intégrées dans l'interface, comme dans votre navigateur préféré",
"name": "Navigation"
},
"no-google-login": {
"description": "Supprimer les boutons et liens de connexion Google de l'interface",
"name": "Pas de connexion Google"
},
"notifications": {
"description": "Afficher une notification quand une chanson commence à jouer (les notifications interactives sont disponibles sur Windows)",
"menu": {
"interactive": "Notifications interactives",
"interactive-settings": {
"label": "Paramètres interactifs",
"submenu": {
"hide-button-text": "Masquer le texte du bouton",
"refresh-on-play-pause": "Actualiser lors de la lecture/pause",
"tray-controls": "Ouvrir/Fermer sur le plateau, cliquez"
}
},
"priority": "Priorité des notifications",
"toast-style": "Style des notifications \"Toast\"",
"unpause-notification": "Afficher la notification lors de la reprise"
},
"name": "Notifications"
},
"picture-in-picture": {
"description": "Permet de basculer lapplication en mode image dans image",
"menu": {
"always-on-top": "Toujours en haut",
"hotkey": {
"label": "Raccourci clavier",
"prompt": {
"keybind-options": {
"hotkey": "Raccourci clavier"
},
"label": "Choisissez un raccourci clavier pour activer l'image dans l'image",
"title": "Touche de raccourci Image dans l'image"
}
},
"save-window-position": "Enregistrer la position de la fenêtre",
"save-window-size": "Enregistrer la taille de la fenêtre",
"use-native-pip": "Utiliser le mode image dans image natif du navigateur"
},
"name": "Image dans l'image",
"templates": {
"button": "Image dans l'image"
}
},
"playback-speed": {
"description": "Écoutez vite, écoutez lentement ! Ajoute un curseur qui contrôle la vitesse de la chanson",
"name": "Vitesse de lecture",
"templates": {
"button": "Vitesse"
}
},
"precise-volume": {
"description": "Contrôlez le volume avec précision à l'aide de la molette de la souris/des raccourcis clavier, avec une interface personnalisée et des étapes de volume personnalisables",
"menu": {
"arrows-shortcuts": "Contrôles avec les touches fléchées",
"custom-volume-steps": "Définir des étapes de volume personnalisées",
"global-shortcuts": "Raccourcis clavier globaux"
},
"name": "Volume précis",
"prompt": {
"global-shortcuts": {
"keybind-options": {
"decrease": "Diminuer le volume",
"increase": "Augmenter le volume"
},
"label": "Choisissez les raccourcis clavier du volume global :",
"title": "Raccourcis clavier de volume global"
},
"volume-steps": {
"label": "Choisissez les étapes d'augmentation/diminution du volume",
"title": "Étapes de volume"
}
}
},
"quality-changer": {
"backend": {
"dialog": {
"quality-changer": {
"detail": "Qualité actuelle: {{quality}}",
"message": "Choisissez la qualité vidéo :",
"title": "Choisissez la qualité vidéo"
}
}
},
"description": "Permet de changer la qualité vidéo avec un bouton sur la vidéo",
"name": "Changeur de qualité vidéo"
},
"shortcuts": {
"description": "Permet de définir des raccourcis clavier globaux pour la lecture (lecture/pause/suivant/précédent) + désactiver l'OSD multimédia en remplaçant les touches multimédias + activer Ctrl/CMD + F pour rechercher + activer la prise en charge Linux MPRIS pour les touches multimédias + raccourcis clavier personnalisés pour les utilisateurs avancés",
"menu": {
"override-media-keys": "Remplacer les touches multimédias",
"set-keybinds": "Définir les contrôles globaux des morceaux"
},
"name": "Raccourcis (& MPRIS)",
"prompt": {
"keybind": {
"keybind-options": {
"next": "Suivant",
"play-pause": "Lecture / Pause",
"previous": "Précédent"
},
"label": "Choisissez les raccourcis clavier globaux pour le contrôle des morceaux :",
"title": "Raccourcis clavier globaux"
}
}
},
"skip-disliked-songs": {
"description": "Passer les musiques que je n'aime pas",
"name": "Passer Chansons Déplaisantes"
},
"skip-silences": {
"description": "Ignorer automatiquement les sections de silence dans les chansons",
"name": "Passer les silences"
},
"sponsorblock": {
"description": "Saute automatiquement les parties non musicales comme l'intro/outro ou les parties de clips vidéo où la chanson n'est pas lue",
"name": "SponsorBlock"
},
"taskbar-mediacontrol": {
"description": "Contrôlez la lecture depuis votre barre des tâches Windows",
"name": "Contrôle multimédia de la barre des tâches"
},
"touchbar": {
"description": "Ajoute un widget TouchBar pour les utilisateurs de macOS",
"name": "TouchBar"
},
"tuna-obs": {
"description": "Intégration avec le plugin OBS Tuna",
"name": "Tuna OBS"
},
"video-toggle": {
"description": "Ajoute un bouton pour basculer entre le mode Vidéo/Chanson. peut également supprimer tout l'onglet vidéo",
"menu": {
"align": {
"label": "Alignement",
"submenu": {
"left": "Gauche",
"middle": "Milieu",
"right": "Droite"
}
},
"force-hide": "Forcer la suppression de l'onglet vidéo",
"mode": {
"label": "Mode",
"submenu": {
"custom": "Bascule personnalisée",
"disabled": "Désactivé",
"native": "Bascule native"
}
}
},
"name": "Basculer la vidéo",
"templates": {
"button": "Musique"
}
},
"visualizer": {
"description": "Ajoute un visualiseur au lecteur",
"menu": {
"visualizer-type": "Type de visualiseur"
},
"name": "Visualiseur"
}
}
}

View File

@ -1,682 +0,0 @@
{
"common": {
"console": {
"plugins": {
"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",
"load-failed": "Gagal memuat plugin \"{{pluginName}}\"",
"loaded": "Plugin \"{{pluginName}}\" dimuat",
"unload-failed": "Gagal untuk memuat plugin \"{{pluginName}}\"",
"unloaded": "Plugin \"{{pluginName}}\" telah dikeluarkan"
}
}
},
"language": {
"code": "id",
"local-name": "Bahasa Indonesia",
"name": "Indonesian"
},
"main": {
"console": {
"did-finish-load": {
"dev-tools": "Selesai memuat. DevTools terbuka"
},
"i18n": {
"loaded": "i18n selesai dimuat"
},
"second-instance": {
"receive-command": "Menerima instruksi lewat protokol: \"{{command}}\""
},
"theme": {
"css-file-not-found": "CSS file \"{{cssFile}}\" tidak ada, mengabaikan"
},
"unresponsive": {
"details": "Kesalahan Tidak Responsif!\n{{error}}"
},
"when-ready": {
"clearing-cache-after-20s": "Menghapus cache aplikasi"
},
"window": {
"tried-to-render-offscreen": "Window mencoba membuat render di luar layar, windowUkuran={{windowSize}}, displaySize={{displaySize}}, posisi={{position}}"
}
},
"dialog": {
"hide-menu-enabled": {
"detail": "Menu tersembunyi, gunakan 'Alt' untuk menampilkannya (atau 'Escape' jika menggunakan Menu Dalam Aplikasi)",
"message": "Menu Sembunyikan diaktifkan",
"title": "Sembunyikan Menu Diaktifkan"
},
"need-to-restart": {
"buttons": {
"later": "Kemudian",
"restart-now": "Restart Sekarang"
},
"detail": "\"{{pluginName}}\" Plugin memerlukan pengaktifan ulang agar dapat diterapkan",
"message": "\"{{pluginName}}\" harus dimulai ulang",
"title": "Diperlukan Restart"
},
"unresponsive": {
"buttons": {
"quit": "Keluar",
"relaunch": "Luncurkan kembali",
"wait": "Tunggu"
},
"detail": "Kami mohon maaf atas ketidaknyamanan ini. silakan pilih apa yang harus dilakukan:",
"message": "Aplikasi Tidak Responsif",
"title": "Jendela Tidak Responsif"
},
"update-available": {
"buttons": {
"disable": "Nonaktifkan Pembaruan",
"download": "Unduh",
"ok": "OK"
},
"detail": "Versi baru tersedia dan dapat diunduh di {{downloadLink}}",
"message": "Versi baru tersedia",
"title": "Pembaruan Tersedia"
}
},
"menu": {
"about": "Tentang",
"navigation": {
"label": "Navigasi",
"submenu": {
"copy-current-url": "Salin URL saat ini",
"go-back": "Kembali",
"go-forward": "Maju",
"quit": "Keluar",
"restart": "Restart Aplikasi"
}
},
"options": {
"label": "Option",
"submenu": {
"advanced-options": {
"label": "Opsi lanjutan",
"submenu": {
"auto-reset-app-cache": "Mengatur ulang cache aplikasi saat aplikasi dimulai",
"disable-hardware-acceleration": "Menonaktifkan akselerasi perangkat keras",
"edit-config-json": "Ubah config.json",
"override-user-agent": "Mengesampingkan User-Agent",
"restart-on-config-changes": "Mulai ulang pada perubahan konfigurasi",
"set-proxy": {
"label": "Atur Proxy",
"prompt": {
"label": "Masukkan Alamat Proxy: (biarkan kosong untuk menonaktifkan)",
"placeholder": "Contoh: SOCKS5://127.0.0.1:9999",
"title": "Atur proxy"
}
},
"toggle-dev-tools": "Beralih ke DevTools"
}
},
"always-on-top": "Selalu di atas",
"auto-update": "Pembaruan Otomatis",
"hide-menu": {
"dialog": {
"message": "Menu akan disembunyikan pada peluncuran berikutnya, gunakan [Alt] untuk menampilkannya (atau centang [`] jika menggunakan menu dalam aplikasi)",
"title": "Sembunyikan Menu Diaktifkan"
},
"label": "Sembunyikan Menu"
},
"language": {
"dialog": {
"message": "Bahasa akan berubah setelah restart",
"title": "Bahasa Berubah"
},
"label": "Bahasa",
"submenu": {
"to-help-translate": "Ingin membantu menerjemahkan? Klik di sini"
}
},
"resume-on-start": "Melanjutkan lagu terakhir saat aplikasi dimulai",
"single-instance-lock": "Kunci Instance Tunggal",
"start-at-login": "Mulai saat masuk",
"starting-page": {
"label": "Halaman awal",
"unset": "Tidak ditetapkan"
},
"tray": {
"label": "Bilah",
"submenu": {
"disabled": "Dinonaktifkan",
"enabled-and-hide-app": "Mengaktifkan dan menyembunyikan aplikasi",
"enabled-and-show-app": "Mengaktifkan dan menampilkan aplikasi",
"play-pause-on-click": "Putar/Jeda dengan klik"
}
},
"visual-tweaks": {
"label": "Penyesuaian Visual",
"submenu": {
"like-buttons": {
"default": "Standar",
"force-show": "Pertunjukan paksa",
"hide": "Sembunyikan",
"label": "Tombol suka"
},
"remove-upgrade-button": "Hapus tombol peningkatan",
"theme": {
"label": "Tema",
"submenu": {
"import-css-file": "Impor file CSS khusus",
"no-theme": "Tidak ada tema"
}
}
}
}
}
},
"plugins": {
"enabled": "Diaktifkan",
"label": "Plugin",
"new": "Baru"
},
"view": {
"label": "Lihat",
"submenu": {
"force-reload": "Paksa Reload",
"reload": "Muat ulang",
"reset-zoom": "Ukuran sebenarnya",
"toggle-fullscreen": "Alihkan Layar Penuh",
"zoom-in": "Perbesar",
"zoom-out": "Perkecil"
}
}
},
"tray": {
"next": "Selanjutnya",
"play-pause": "Putar/Jeda",
"previous": "Sebelumnya",
"quit": "Keluar",
"restart": "Restart aplikasi",
"show": "Tampilkan jendela",
"tooltip": {
"default": "YouTube Musik",
"with-song-info": "YouTube Music: {{artist}} - {{title}}"
}
}
},
"plugins": {
"adblocker": {
"description": "Blokir semua iklan dan pelacakan di luar kotak",
"menu": {
"blocker": "Pemblokir"
},
"name": "Pemblokir Iklan"
},
"album-actions": {
"description": "Tambah tombol Suka, Batal Suka, Tidak Suka dan Batal Tidak Suka untuk diterapkan ke semua lagu dalam daftar putar atau album",
"name": "Tindakan Album"
},
"album-color-theme": {
"description": "Menerapkan tema dinamis dan efek visual berdasarkan palet warna album",
"menu": {
"color-mix-ratio": {
"label": "Rasio campuran warna",
"submenu": {
"percent": "{{ratio}}%"
}
}
},
"name": "Tema Warna Album"
},
"ambient-mode": {
"description": "Menerapkan efek pencahayaan dengan memancarkan warna-warna lembut dari video, ke dalam latar belakang layar Anda",
"menu": {
"blur-amount": {
"label": "Jumlah kabur",
"submenu": {
"pixels": "{{blurAmount}} piksel"
}
},
"buffer": {
"label": "Buffer",
"submenu": {
"buffer": "{{buffer}}"
}
},
"opacity": {
"label": "Keburaman",
"submenu": {
"percent": "{{opacity}}%"
}
},
"quality": {
"label": "Kualitas",
"submenu": {
"pixels": "{{quality}} piksel"
}
},
"size": {
"label": "Ukuran",
"submenu": {
"percent": "{{size}}%"
}
},
"smoothness-transition": {
"label": "Kehalusan transisi",
"submenu": {
"during": "Selama {{interpolationTime}} s"
}
},
"use-fullscreen": {
"label": "Gunakan layar penuh"
}
},
"name": "Mode ambient"
},
"audio-compressor": {
"description": "Menerapkan kompresi pada audio (mengurangi volume pada bagian paling keras dari sinyal dan meningkatkan volume pada bagian paling lembut)",
"name": "Kompresi suara"
},
"blur-nav-bar": {
"description": "Jadikan bar navigasi blur dan transparan",
"name": "buramkan bar navigasi"
},
"bypass-age-restrictions": {
"description": "Lewati verifikasi umur dari YouTube",
"name": "Lewati batasan umur"
},
"captions-selector": {
"description": "pemilih caption untuk trek audio YouTube Music",
"menu": {
"autoload": "pilih caption terakhir secara otomatis",
"disable-captions": "bawaannya tanpa caption"
},
"name": "pemilih caption",
"prompt": {
"selector": {
"label": "bahasa caption yang dipakai sekarang: {{language}}",
"none": "tidak ada",
"title": "pilih bahasa caption"
}
},
"templates": {
"title": "buka pemilih caption"
}
},
"compact-sidebar": {
"description": "Selalu atur sidebar dalam mode kompak",
"name": "sidebar ringkas"
},
"crossfade": {
"description": "Crossfade antar lagu",
"menu": {
"advanced": "Lanjutan"
},
"name": "Crossfade [Beta]",
"prompt": {
"options": {
"multi-input": {
"fade-in-duration": "durasi fade in (ms)",
"fade-out-duration": "durasi fade out (ms)",
"fade-scaling": {
"label": "redup perlahan",
"linear": "Linear",
"logarithmic": "Logaritmik"
},
"seconds-before-end": "Crossfade N detik sebelum berakhir"
},
"title": "Pilihan crossfade"
}
}
},
"disable-autoplay": {
"description": "Buat lagu mulai dalam mode \"jeda\"",
"menu": {
"apply-once": "Hanya terapkan pada saat startup"
},
"name": "Matikan Autoplay"
},
"discord": {
"backend": {
"already-connected": "Percobaan untuk terhubung dengan koneksi yang aktif",
"connected": "Terhubung dengan Discord",
"disconnected": "Terputus dari Discord"
},
"description": "tunjukan apa yang kamu dengarkan dengan Rich Presence",
"menu": {
"auto-reconnect": "Reconnect otomatis",
"clear-activity": "Hapus riwayat aktifitas",
"clear-activity-after-timeout": "hapus riwayat aktifitas setelah timeout",
"connected": "terhubung",
"disconnected": "tidak terhubung",
"hide-duration-left": "sembunyikan sisa durasi",
"hide-github-button": "sembunyikan tombol link GitHub",
"play-on-youtube-music": "Mainkan di YouTube Music",
"set-inactivity-timeout": "Tetapkan batas waktu tidak aktif"
},
"name": "Rich Presence Discord",
"prompt": {
"set-inactivity-timeout": {
"label": "Masukkan batas waktu tidak aktif dalam detik:",
"title": "Tetapkan batas waktu tidak aktif"
}
}
},
"downloader": {
"backend": {
"dialog": {
"error": {
"buttons": {
"ok": "Oke"
},
"message": "Argh! Maaf, dowloadnya gagal…",
"title": "Downloadnya error!"
},
"start-download-playlist": {
"buttons": {
"ok": "Oke"
},
"detail": "({{playlistSize}} lagu-lagu)",
"message": "Mengunduh Playlist {{playlistTitle}}",
"title": "Download dimulai"
}
},
"feedback": {
"conversion-progress": "Konversi: {{percent}}%",
"converting": "Mengkonversi…",
"done": "Selesai: {{filePath}}",
"download-info": "Mengunduh {{artist}} - {{title}} {{videoId}}",
"download-progress": "Mengunduh: {{percent}}%",
"downloading": "Mengunduh…",
"downloading-counter": "Mengunduh {{current}}/{{total}}…",
"downloading-playlist": "Mengunduh playlist \"{{playlistTitle}}\" - {{playlistSize}} lagu ({{playlistId}})",
"error-while-downloading": "Gagal mengunduh \"{{author}} - {{title}}\": {{error}}",
"folder-already-exists": "Folder {{playlistFolder}} sudah ada",
"getting-playlist-info": "Mendapatkan informasi playlist…",
"loading": "Memuat…",
"playlist-has-only-one-song": "Daftar putar hanya memiliki satu item, mengunduhnya secara langsung",
"playlist-id-not-found": "ID playlist tidak ditemukan",
"playlist-is-empty": "Playlist kosong",
"playlist-is-mix-or-private": "Kesalahan mendapatkan info playlist: pastikan bukan playlist pribadi atau \"Campuran untuk Anda\"\n\n{{error}}",
"preparing-file": "Menyiapkan file…",
"saving": "Menyimpan…",
"trying-to-get-playlist-id": "Mencoba mendapatkan ID playlist: {{playlistId}}",
"video-id-not-found": "Video tidak ditemukan",
"writing-id3": "Menulis tanda ID3…"
}
},
"description": "Unduh MP3 / sumber suara secara langsung via antarmuka",
"menu": {
"choose-download-folder": "Pilih folder unduhan",
"download-playlist": "Unduh daftar putar",
"presets": "Prasetel",
"skip-existing": "Lewati berkas yang sudah ada"
},
"name": "Pengunduh",
"renderer": {
"can-not-update-progress": "Tidak dapat memperbarui proses"
},
"templates": {
"button": "Unduh"
}
},
"exponential-volume": {
"description": "Buat penggeser volume menjadi eksponen sehingga memudahkan memilih volume yang lebih rendah.",
"name": "Volume Eksponen"
},
"in-app-menu": {
"description": "Buat bilah-menu terlihat indah, gelap atau serupa dengan album",
"menu": {
"hide-dom-window-controls": "Sembunyikan DOM pengendali jendela"
},
"name": "Menu di Aplikasi"
},
"lumiastream": {
"description": "Tambah dukungan Lumia Stream",
"name": "Lumia Stream [Beta]"
},
"lyrics-genius": {
"description": "Tambah dukungan lirik untuk kebanyakan lagu",
"menu": {
"romanized-lyrics": "Romanisasi Lirik"
},
"name": "Lirik Genius",
"renderer": {
"fetched-lyrics": "Lirik yang diambil untuk Genius"
}
},
"music-together": {
"description": "Bagikan daftar putar dengan yang lain. Saat host memainkan lagu, semua orang akan mendengarkan lagu yang sama",
"dialog": {
"enter-host": "Masukkan ID Host"
},
"internal": {
"save": "Simpan",
"track-source": "Sumber Trek",
"unknown-user": "Pengguna Tidak Diketahui"
},
"menu": {
"click-to-copy-id": "Salin ID Host",
"close": "Tutup Musik Bersama",
"connected-users": "Pengguna Terhubung",
"disconnect": "Putuskan Musik Bersama",
"empty-user": "Tidak ada pengguna terhubung",
"host": "Host Musik Bersama",
"join": "Gabung Musik Bersama",
"permission": {
"all": "Izinkan tamu untuk mengendalikan daftar putar dan pemutar",
"host-only": "Hanya host yang dapat mengendalikan daftar putar dan pemutar",
"playlist": "Izinkan tamu untuk mengendalikan daftar putar"
},
"set-permission": "Ubah Pengendali Izin",
"status": {
"disconnected": "Terputus",
"guest": "Terhubung sebagai Tamu",
"host": "Terhubung sebagai Host"
}
},
"name": "Musik Bersama [Beta]",
"toast": {
"add-song-failed": "Gagal untuk menambahkan lagu",
"closed": "Musik Bersama ditutup",
"disconnected": "Musik Bersama terputus",
"host-failed": "Gagal untuk memulai Musik Bersama",
"id-copied": "ID Host tersalin ke papan klip",
"id-copy-failed": "Gagal menyalin ID Host ke papan klip",
"join-failed": "Gagal untuk bergabung ke Musik Bersama",
"joined": "Bergabung ke Musik Bersama",
"permission-changed": "Perizinan Musik Bersama diubah ke \"{{permission}}\"",
"remove-song-failed": "Gagal menghapus lagu",
"user-connected": "{{name}} bergabung ke Musik Bersama",
"user-disconnected": "{{name}} meninggalkan Musik Bersama"
}
},
"navigation": {
"description": "panah navigasi Selanjutnya/Sebelumnya terintegrasi pada antarmuka, layaknya peramban kesukaan Anda",
"name": "Navigasi"
},
"no-google-login": {
"description": "Hapus tombol dan tautan masuk Google dari antarmuka",
"name": "Tanpa Google Login"
},
"notifications": {
"description": "Tampilkan pemberitahuan saat lagu dimainkan (pemberitahuan interaktif tersedia di Windows)",
"menu": {
"interactive": "Pemberitahuan Interaktif",
"interactive-settings": {
"label": "Pengaturan Interaktif",
"submenu": {
"hide-button-text": "Sembunyikan teks tombol",
"refresh-on-play-pause": "Segarkan saat Putar/Jeda",
"tray-controls": "Buka/Tutup saat baki ditekan"
}
},
"priority": "Prioritas Pemberitahuan",
"toast-style": "Gaya Toast",
"unpause-notification": "Tampilkan pemberitahuan saat tidak dijeda"
},
"name": "Pemberitahuan"
},
"picture-in-picture": {
"description": "Izinkan untuk memindahkan aplikasi ke mode gambar-dalam-gambar",
"menu": {
"always-on-top": "Selalu di atas",
"hotkey": {
"label": "Pintasan",
"prompt": {
"keybind-options": {
"hotkey": "Pintasan"
},
"label": "Pilih pintasan untuk beralih ke gambar-dalam-gambar",
"title": "Pintasan gambar-dalam-gambar"
}
},
"save-window-position": "Simpan posisi jendela",
"save-window-size": "Simpan ukuran jendela",
"use-native-pip": "Gunakan PiP bawaan peramban"
},
"name": "Gambar-dalam-gambar",
"templates": {
"button": "Gambar-dalam-gambar"
}
},
"playback-speed": {
"description": "Dengarkan cepat, dengarkan perlahan! Tambahkan penggeser untuk mengendalikan kecepatan lagu",
"name": "Kecepatan Pemutar",
"templates": {
"button": "Kecepatan"
}
},
"precise-volume": {
"description": "Kendalikan volume secara presisi menggunakan roda tetikus/pintasan, dengan HUD kustom dan langkah volume yang dapat diatur",
"menu": {
"arrows-shortcuts": "Kendali Tombol Panah Lokal",
"custom-volume-steps": "Atur Langkah Volume Kustom",
"global-shortcuts": "Pintasan Global"
},
"name": "Volume Presisi",
"prompt": {
"global-shortcuts": {
"keybind-options": {
"decrease": "Kurangi Volume",
"increase": "Tingkatkan Volume"
},
"label": "Pilih Pintasan Volume Global:",
"title": "Pintasan Volume Global"
},
"volume-steps": {
"label": "Pilih Langkah Peningkatan/Pengurangan Volume",
"title": "Langkah Volume"
}
}
},
"quality-changer": {
"backend": {
"dialog": {
"quality-changer": {
"detail": "Kualitas Terkini: {{quality}}",
"message": "Pilih Kualitas Video:",
"title": "Pilih Kualitas Video"
}
}
},
"description": "Izinkan untuk mengubah kualitas video dengan tombol pada hamparan video",
"name": "Pengubah Kualitas Video"
},
"scrobbler": {
"description": "Tambahkan dukungan scrobbling (mis. last.fm, Listenbrainz)",
"menu": {
"lastfm": {
"api-settings": "Pengaturan API Last.fm"
},
"listenbrainz": {
"token": "Masukkan token pengguna ListenBrainz"
},
"scrobble-other-media": "Scrobble media lain"
},
"name": "Scrobbler",
"prompt": {
"lastfm": {
"api-key": "Kunci API Last.fm",
"api-secret": "Secret API Last.fm"
},
"listenbrainz": {
"token": {
"label": "Masukkan token pengguna ListenBrainz Anda:",
"title": "Token ListenBrainz"
}
}
}
},
"shortcuts": {
"description": "Izinkan pengaturan pintasan global untuk pemutar (main/jeda/selanjutnya/sebelumnya) dan mematikan OSD media dengan mengesampingkan tombol media, mengaktifkan Ctrl/CMD + F untuk pencarian, mengaktifkan dukungan MPRIS Linux untuk tombol media, dan tombol pintasan kustom untuk pengguna lanjutan",
"menu": {
"override-media-keys": "Timpa Tombol Media",
"set-keybinds": "Atur Pengendali Lagu Global"
},
"name": "Pintasan (& MPRIS)",
"prompt": {
"keybind": {
"keybind-options": {
"next": "Selanjutnya",
"play-pause": "Main / Jeda",
"previous": "Sebelumnya"
},
"label": "Pilih Pintasan Global untuk Pengendali Lagu:",
"title": "Pintasan Global"
}
}
},
"skip-disliked-songs": {
"description": "Lewati lagu yang tidak disukai",
"name": "Lewati Lagu yang Tidak Disukai"
},
"skip-silences": {
"description": "Otomatis lewati bagian hening dari lagu",
"name": "Lewati Keheningan"
},
"sponsorblock": {
"description": "Otomatis Melewati bagian yang bukan musik seperti intro/outro atau bagian dari video musik di mana lagu tidak dimainkan",
"name": "SponsorBlock"
},
"taskbar-mediacontrol": {
"description": "Kendalikan pemutaran dari bilah alat Windows",
"name": "Pengendali Media di Bilah Alat"
},
"touchbar": {
"description": "Tambahkan widget TouchBar untuk pengguna macOS",
"name": "TouchBar"
},
"tuna-obs": {
"description": "Integrasi dengan plugin Tuna OBS",
"name": "Tuna OBS"
},
"video-toggle": {
"description": "Tambahkan tombol untuk beralih antara mode Lagu/Video. secara opsional juga dapat menghapus keseluruhan tab video",
"menu": {
"align": {
"label": "Perataan",
"submenu": {
"left": "Kiri",
"middle": "Tengah",
"right": "Kanan"
}
},
"force-hide": "Paksa hapus tab video",
"mode": {
"label": "Mode",
"submenu": {
"custom": "Peralih kustom",
"disabled": "Mati",
"native": "Peralih bawaan"
}
}
},
"name": "Peralih Video",
"templates": {
"button": "Lagu"
}
},
"visualizer": {
"description": "Tambahkan visualisator ke pemutar",
"menu": {
"visualizer-type": "Tipe Visualisator"
},
"name": "Visualisator"
}
}
}

View File

@ -1,673 +0,0 @@
{
"common": {
"console": {
"plugins": {
"execute-failed": "È stato impossibile eseguire il plugin {{pluginName}}::{{contextName}}",
"executed-at-ms": "Il plugin {{pluginName}}:{{contextName}} è stato eseguito a {{ms}}ms",
"initialize-failed": "Inizializzazione del plugin \"{{pluginName}}\" fallita",
"load-all": "Carica tutti i plugin",
"load-failed": "Caricamento del plugin \"{{pluginName}}\" non riuscito",
"loaded": "Plugin \"{{pluginName}}\" caricato",
"unload-failed": "Rimozione del plugin \"{{pluginName}}\" fallita",
"unloaded": "Plugin \"{{pluginName}}\" rimosso"
}
}
},
"language": {
"code": "it",
"local-name": "Italiano",
"name": "Italian"
},
"main": {
"console": {
"did-finish-load": {
"dev-tools": "Caricamento concluso. DevTools aperto"
},
"i18n": {
"loaded": "i18n caricato"
},
"second-instance": {
"receive-command": "Comando ricevuto tramite protocollo: \"{{command}}\""
},
"theme": {
"css-file-not-found": "Il file CSS \"{{cssFile}}\" non esiste, ignorato"
},
"unresponsive": {
"details": "Errore di mancata risposta!\n{{error}}"
},
"when-ready": {
"clearing-cache-after-20s": "Sto liberando la cache dell'app"
},
"window": {
"tried-to-render-offscreen": "La finestra ha cercato di renderizzare fuori schermo, windowSize={{windowSize}}, displaySize={{displaySize}}, position={{position}}\""
}
},
"dialog": {
"hide-menu-enabled": {
"detail": "Il menu è nascosto, utilizza 'Alt' per visualizzarlo (o 'Escape' se si utilizza il Menu In-App)\"",
"message": "'Nascondi menu' è attivo",
"title": "'Nascondi menu' attivo"
},
"need-to-restart": {
"buttons": {
"later": "In seguito",
"restart-now": "Riavvia ora"
},
"detail": "Riavviare per attivare il plugin\"{{pluginName}}\"",
"message": "\"{{pluginName}}\" deve essere riavviato",
"title": "Riavvio richiesto"
},
"unresponsive": {
"buttons": {
"quit": "Arresta",
"relaunch": "Riavvia",
"wait": "Attendi"
},
"detail": "Ci dispiace per l'inconveniente! Scegli cosa fare:",
"message": "L'applicazione non risponde",
"title": "La finestra non risponde"
},
"update-available": {
"buttons": {
"disable": "Disattiva gli aggiornamenti",
"download": "Download",
"ok": "OK"
},
"detail": "È disponibile una nuova versione scaricabile all'indirizzo {{downloadLink}}",
"message": "È disponibile una nuova versione",
"title": "Aggiornamento disponibile"
}
},
"menu": {
"about": "Informazioni",
"navigation": {
"label": "Navigazione",
"submenu": {
"copy-current-url": "Copia l'URL corrente",
"go-back": "Pagina indietro",
"go-forward": "Pagina avanti",
"quit": "Esci",
"restart": "Riavvia l'app"
}
},
"options": {
"label": "Opzioni",
"submenu": {
"advanced-options": {
"label": "Opzioni avanzate",
"submenu": {
"auto-reset-app-cache": "Reimposta la cache dell'app quando viene riavviata",
"disable-hardware-acceleration": "Disabilita l'accelerazione hardware",
"edit-config-json": "Modificare config.json",
"override-user-agent": "Sovrascrivi User-Agent",
"restart-on-config-changes": "Riavvia alla modifica delle impostazioni",
"set-proxy": {
"label": "Imposta il proxy",
"prompt": {
"label": "Inserisci l'indirizzo proxy: (lascia vuoto per disabilitare)",
"placeholder": "Esempio: SOKS5://127.0.0.1:9999",
"title": "Imposta il proxy"
}
},
"toggle-dev-tools": "Attiva/disattiva DevTools"
}
},
"always-on-top": "Sempre in cima",
"auto-update": "Aggiornamento automatico",
"hide-menu": {
"dialog": {
"message": "Il menu verrà nascosto al prossimo avvio. Utilizzare [Alt] per mostrarlo (o backtick [`] se si utilizza il Menu In-App)",
"title": "Nascondi menu abilitato"
},
"label": "Nascondi menu"
},
"language": {
"dialog": {
"message": "La lingua verrà cambiata dopo il riavvio",
"title": "Lingua cambiata"
},
"label": "Lingua",
"submenu": {
"to-help-translate": "Vuoi aiutare a tradurre? Clicca qui"
}
},
"resume-on-start": "Riprendi a riprodurre l'ultimo brano all'avvio dell'app",
"single-instance-lock": "Permetti una sola istanza dell'app",
"start-at-login": "Avvia al login",
"starting-page": {
"label": "Pagina iniziale",
"unset": "Non impostato"
},
"tray": {
"label": "Mostra icona nel tray",
"submenu": {
"disabled": "Disabilita",
"enabled-and-hide-app": "Abilita e nascondi l'app",
"enabled-and-show-app": "Abilita e mostra l'app",
"play-pause-on-click": "Riproduci/Pausa al click sull'icona"
}
},
"visual-tweaks": {
"label": "Miglioramenti visivi",
"submenu": {
"like-buttons": {
"default": "Predefinito",
"force-show": "Forza la visualizzazione",
"hide": "Nascondi",
"label": "Pulsanti Like"
},
"remove-upgrade-button": "Rimuovi il pulsante aggiorna",
"theme": {
"label": "Tema",
"submenu": {
"import-css-file": "Importa file CSS personalizzato",
"no-theme": "Nessun tema"
}
}
}
}
}
},
"plugins": {
"enabled": "Attivato",
"label": "Plugin",
"new": "NUOVO"
},
"view": {
"label": "Visualizzazione",
"submenu": {
"force-reload": "Forza l'aggiornamento",
"reload": "Aggiorna",
"reset-zoom": "Ripristina dimensione",
"toggle-fullscreen": "Attiva/disattiva Schermo Intero",
"zoom-in": "Zoom in",
"zoom-out": "Zoom out"
}
}
},
"tray": {
"next": "Prossimo",
"play-pause": "Riproduci/Pausa",
"previous": "Precedente",
"quit": "Esci",
"restart": "Riavvia l'app",
"show": "Mostra finestra",
"tooltip": {
"default": "YouTube Music",
"with-song-info": "YouTube Music: {{artist}} - {{title}}"
}
}
},
"plugins": {
"adblocker": {
"description": "Blocca tutti gli annunci e i tracker",
"menu": {
"blocker": "Blocco"
},
"name": "Ad blocker"
},
"album-actions": {
"description": "Aggiunge i pulsanti Undislike, Dislike, Like e Unlike a tutti i brani di una playlist o di un album",
"name": "Azioni album"
},
"album-color-theme": {
"description": "Applica un tema dinamico e degli effetti visivi basandosi sul colore dell'album",
"name": "Tema abbinato a colore album"
},
"ambient-mode": {
"description": "Applica un effetto di illuminazione proiettando i colori delicati del video sullo sfondo dello schermo",
"menu": {
"blur-amount": {
"label": "Intensità sfocatura",
"submenu": {
"pixels": "{{blurAmount}} pixel"
}
},
"buffer": {
"label": "Buffer",
"submenu": {
"buffer": "{{buffer}}"
}
},
"opacity": {
"label": "Trasparenza",
"submenu": {
"percent": "{{opacity}}%"
}
},
"quality": {
"label": "Qualità",
"submenu": {
"pixels": "{{quality}} pixel"
}
},
"size": {
"label": "Dimensione",
"submenu": {
"percent": "{{size}}%"
}
},
"smoothness-transition": {
"label": "Fluidità transizione",
"submenu": {
"during": "Per {{interpolationTime}} _s"
}
},
"use-fullscreen": {
"label": "Utilizzo di schermo intero"
}
},
"name": "Modalità Ambiente"
},
"audio-compressor": {
"description": "Attiva la compressione audio (abbassa il volume delle parti più alte e alza quello delle parti più basse del segnale)",
"name": "Compressore audio"
},
"blur-nav-bar": {
"description": "Rende la barra di navigazione trasparente e sfuocata",
"name": "Barra di navigazione trasparente"
},
"bypass-age-restrictions": {
"description": "Bypassa la verifica dell'età di YouTube",
"name": "Aggira i limiti d'età"
},
"captions-selector": {
"description": "Selettore sottotitolo per le tracce audio di YouTube",
"menu": {
"autoload": "Seleziona automaticamente l'ultimo sottotitolo utilizzato",
"disable-captions": "Disattiva i sottotitoli"
},
"name": "Selettore Sottotitoli",
"prompt": {
"selector": {
"label": "Lingua del sottotitolo attuale: {{language}}",
"none": "Nessuno",
"title": "Scegli la lingua del sottotitolo"
}
},
"templates": {
"title": "Apri il selettore dei sottotitoli"
}
},
"compact-sidebar": {
"description": "Imposta sempre la barra laterale in modalità compatta",
"name": "Barra laterale compatta"
},
"crossfade": {
"description": "Crossfade tra i brani",
"menu": {
"advanced": "Impostazioni avanzate"
},
"name": "Crossfade [beta]",
"prompt": {
"options": {
"multi-input": {
"fade-in-duration": "Durata dissolvenza in entrata (ms)",
"fade-out-duration": "Durata dissolvenza in uscita (ms)",
"fade-scaling": {
"label": "Transizione dissolvenza",
"linear": "Lineare",
"logarithmic": "Logaritmica"
},
"seconds-before-end": "N° secondi di crossfade prima della fine"
},
"title": "Opzioni crossfade"
}
}
},
"disable-autoplay": {
"description": "Fa iniziare i brani in modalità \"pausa\"",
"menu": {
"apply-once": "Solo all'avvio"
},
"name": "Disattiva autoplay"
},
"discord": {
"backend": {
"already-connected": "Tenta di connettersi con connessione attiva",
"connected": "Connesso a Discord",
"disconnected": "Scollegato da Discord"
},
"description": "Mostra ai tuoi amici cosa ascolti con Rich Presence",
"menu": {
"auto-reconnect": "Riconnessione automatica",
"clear-activity": "Rimuovi attività",
"clear-activity-after-timeout": "Cancella attività dopo il timeout",
"connected": "Connesso",
"disconnected": "Disconnesso",
"hide-duration-left": "Nascondi la durata rimasta",
"hide-github-button": "Nascondi il pulsante link a GitHub",
"play-on-youtube-music": "Riproduci su YouTube Music",
"set-inactivity-timeout": "Imposta il timeout di inattività"
},
"name": "Discord Rich Presence",
"prompt": {
"set-inactivity-timeout": {
"label": "Inserisci il timeout di inattività in secondi:",
"title": "Imposta il timeout di inattività"
}
}
},
"downloader": {
"backend": {
"dialog": {
"error": {
"buttons": {
"ok": "OK"
},
"message": "Mi dispiace, download fallito…",
"title": "Errore nel download!"
},
"start-download-playlist": {
"buttons": {
"ok": "OK"
},
"detail": "({{playlistSize}} canzoni)",
"message": "Scarica Playlist {{playlistTitle}}",
"title": "Download iniziato"
}
},
"feedback": {
"conversion-progress": "Conversione: {{percent}}%",
"converting": "Sto convertendo…",
"done": "Fatto: {{filePath}}",
"download-info": "Sto scaricando {{artist}} - {{title}} [{{videoId}}",
"download-progress": "Download: {{percent}}%",
"downloading": "Sto scaricando…",
"downloading-counter": "Sto scaricando {{current}}/{{total}}…",
"downloading-playlist": "Sto scaricando la playlist \"{{playlistTitle}}\" - {{playlistSize}} brani({{playlistId}})",
"error-while-downloading": "Errore di download \"{{author}} - {{title}}\": {{error}}",
"folder-already-exists": "La cartella {{playlistFolder}} è già esistente",
"getting-playlist-info": "Sto ottenendo le info sulla playlist…",
"loading": "Caricamento…",
"playlist-has-only-one-song": "La playlist ha un solo elemento, lo sto scaricando direttamente",
"playlist-id-not-found": "Nessun ID playlist trovato",
"playlist-is-empty": "La playlist è vuota",
"playlist-is-mix-or-private": "Errore nell'ottenere info sulla playlist: assicurati che non sia una playlist privata o un \"Mixtape per te\"\n\n{{error}}",
"preparing-file": "Sto preparando il file…",
"saving": "Sto salvando…",
"trying-to-get-playlist-id": "Sto cercando di ottenere l'ID della playlist: {{playlistId}}",
"video-id-not-found": "Video non trovato",
"writing-id3": "Sto scrivendo i tag ID3…"
}
},
"description": "Download MP3 / sorgenti audio direttamente dall'interfaccia",
"menu": {
"choose-download-folder": "Scegli cartella download",
"download-playlist": "Scarica la playlist",
"presets": "Preimpostazioni",
"skip-existing": "Salta i file esistenti"
},
"name": "Downloader",
"renderer": {
"can-not-update-progress": "Impossibile aggiornare l'avanzamento"
},
"templates": {
"button": "Scarica"
}
},
"exponential-volume": {
"description": "Rende esponenziale il cursore del volume, in modo da facilitare la selezione di volumi più bassi.",
"name": "Volume esponenziale"
},
"in-app-menu": {
"description": "Migliora l'aspetto delle barre del menu con un look scuro o basato sul colore dell'album",
"menu": {
"hide-dom-window-controls": "Nascondi i controlli delle finestre DOM"
},
"name": "Menu In-App"
},
"lumiastream": {
"description": "Aggiungi supporto per Lumia Stream",
"name": "Lumia Stream [Beta]"
},
"lyrics-genius": {
"description": "Aggiunge il supporto dei testi per la maggior parte delle canzoni",
"menu": {
"romanized-lyrics": "Alfabeto latino per i brani con testo in caratteri orientali"
},
"name": "Lyrics Genius",
"renderer": {
"fetched-lyrics": "Testi recuperati per Genius"
}
},
"music-together": {
"description": "Condividi una playlist con altri. Quando l'Host riproduce un brano, tutti gli altri ascolteranno lo stesso brano",
"dialog": {
"enter-host": "Inserisci l'ID dell'Host"
},
"internal": {
"save": "Salva",
"track-source": "Traccia sorgente",
"unknown-user": "Utente sconosciuto"
},
"menu": {
"click-to-copy-id": "Copia l'ID dell'Host",
"close": "Chiudi Music Together",
"connected-users": "Utenti connessi",
"disconnect": "Disconetti Music Together",
"empty-user": "Utenti non connessi",
"host": "Music Together Host",
"join": "Unisciti a Music Together",
"permission": {
"all": "Consenti ai Guest di controllare la playlist e il player",
"host-only": "Solo l'Host può controllare la playlist e il player",
"playlist": "Consenti ai Guest di controllare la playlist"
},
"set-permission": "Cambia autorizzazione di controllo",
"status": {
"disconnected": "Disconnesso",
"guest": "Connesso come Guest",
"host": "Connesso come Host"
}
},
"name": "Music Together [Beta]",
"toast": {
"add-song-failed": "Impossibile aggiungere il brano",
"closed": "Music Together chiuso",
"disconnected": "Music Together disconnesso",
"host-failed": "Impossibile ospitare Music Together",
"id-copied": "L'ID dell Host è stato copiato negli appunti",
"id-copy-failed": "Impossibile copiare l'ID dell'host negli appunti",
"join-failed": "Impossibile unirsi a Music Together",
"joined": "Unito a Music Together",
"permission-changed": "L'autorizzazione di Music Together è cambiata in {{permission}}",
"remove-song-failed": "Impossibile rimuovere il brano",
"user-connected": "{{name}} si è unito a Music Together",
"user-disconnected": "{{name}} ha lasciato Music Together"
}
},
"navigation": {
"description": "Frecce di navigazione Avanti/Indietro integrate direttamente nell'interfaccia, come nel tuo browser preferito",
"name": "Navigazione"
},
"no-google-login": {
"description": "Rimuovi i pulsanti di accesso e i link di Google dall'interfaccia",
"name": "Nessun login di Google"
},
"notifications": {
"description": "Mostra una notifica quando viene riprodotto un brano (le notifiche interattive sono disponibili su Windows)",
"menu": {
"interactive": "Notifiche interattive",
"interactive-settings": {
"label": "Impostazioni dell'interazione",
"submenu": {
"hide-button-text": "Nascondi il testo del pulsante",
"refresh-on-play-pause": "Refresh quando si preme Riproduci/Pausa",
"tray-controls": "Apri/chiudi cliccando l'icona nel tray"
}
},
"priority": "Priorità di notifica",
"toast-style": "Stile Toast",
"unpause-notification": "Mostra notifica quando riprendi ascolto"
},
"name": "Notifiche"
},
"picture-in-picture": {
"description": "Consente di far passare l'app alla modalità Picture-in-Picture",
"menu": {
"always-on-top": "Sempre in primo piano",
"hotkey": {
"label": "Hotkey",
"prompt": {
"keybind-options": {
"hotkey": "Hotkey"
},
"label": "Scegliere un'hotkey per attivare Picture-in-picture",
"title": "Picture-in-picture Hotkey"
}
},
"save-window-position": "Salva la posizione della finestra",
"save-window-size": "Salva la dimensione della finestra",
"use-native-pip": "Usa il PiP nativo del browser"
},
"name": "Picture-in-Picture",
"templates": {
"button": "Picture-in-Picture"
}
},
"playback-speed": {
"description": "Ascolto veloce, ascolto lento! Aggiunge un cursore che controlla la velocità di riproduzione del brano",
"name": "Velocità riproduzione",
"templates": {
"button": "Velocità"
}
},
"precise-volume": {
"description": "Controlla con precisione il volume utilizzando la rotella del mouse, le hotkey o i tasti freccia e usando incrementi di volume personalizzabili",
"menu": {
"arrows-shortcuts": "Controlla con i tasti freccia",
"custom-volume-steps": "Imposta incrementi di volume personalizzati",
"global-shortcuts": "Hotkey globali"
},
"name": "Volume preciso",
"prompt": {
"global-shortcuts": {
"keybind-options": {
"decrease": "Diminuisci volume",
"increase": "Aumenta volume"
},
"label": "Scegli i tasti di scelta rapida regolazione volume:",
"title": "Tasti di scelta rapida regolazione volume"
},
"volume-steps": {
"label": "Seleziona l'incremento/decremento del volume",
"title": "Incrementi volume"
}
}
},
"quality-changer": {
"backend": {
"dialog": {
"quality-changer": {
"detail": "Qualità attuale: {{quality}}",
"message": "Qualità Video:",
"title": "Scegli la qualità video"
}
}
},
"description": "Permette di cambiare la qualità del video con un pulsante in sovrimpressione",
"name": "Cambia qualità video"
},
"scrobbler": {
"description": "Aggiunge il supporto per lo scrobbling (Last.fm, Listenbrainz ecc.)",
"menu": {
"lastfm": {
"api-settings": "Impostazione Last.fm API"
},
"listenbrainz": {
"token": "Inserire il token utente per ListenBrainz"
}
},
"name": "Scrobbler",
"prompt": {
"lastfm": {
"api-key": "API key per Last.fm",
"api-secret": "API secret per Last.fm"
},
"listenbrainz": {
"token": {
"label": "Inserisci il tuo token utente ListenBrainz:",
"title": "Token ListenBrainz"
}
}
}
},
"shortcuts": {
"description": "Consente di impostare tasti di scelta rapida globali per la riproduzione (riproduci/pausa/successivo/precedente) + disabilita l'OSD multimediale sovrascrivendo i tasti multimediali + abilita Ctrl/CMD + F per la ricerca + abilita il supporto Linux MPRIS per i tasti multimediali + tasti di scelta rapida personalizzati per utenti avanzati",
"menu": {
"override-media-keys": "Ridefinisci i tasti multimediali",
"set-keybinds": "Imposta i controlli brano globali"
},
"name": "Scorciatoie (& MPRIS)",
"prompt": {
"keybind": {
"keybind-options": {
"next": "Prossimo",
"play-pause": "Riproduci / Pausa",
"previous": "Precedente"
},
"label": "Scegli combinazioni di tasti per il controllo dei brani:",
"title": "Combinazioni di tasti"
}
}
},
"skip-disliked-songs": {
"description": "Salta i brani che non ti piacciono",
"name": "Salta i brani che non ti piacciono"
},
"skip-silences": {
"description": "Salta automaticamente le parti silenziose nei brani",
"name": "Salta silenzi"
},
"sponsorblock": {
"description": "Salta automaticamente le parti non musicali, come l'intro/outro delle canzoni o le parti dei video musicali in cui non viene riprodotto il brano",
"name": "Blocco sponsor"
},
"taskbar-mediacontrol": {
"description": "Controlla riproduzione dalla taskbar di Windows",
"name": "Controlli multimediali sulla taskbar"
},
"touchbar": {
"description": "Aggiunge un widget TouchBar per gli utenti macOS",
"name": "Touch Bar (per MacOS)"
},
"tuna-obs": {
"description": "Integrazione con il plugin OBS Tuna",
"name": "Tuna OBS"
},
"video-toggle": {
"description": "Aggiunge un pulsante per passare dalla modalità Video a quella Brano. Può anche rimuovere l'intera scheda Brano/Video",
"menu": {
"align": {
"label": "Allineamento",
"submenu": {
"left": "Sinistra",
"middle": "Centro",
"right": "Destra"
}
},
"force-hide": "Rimuovi la scheda Brano/Video",
"mode": {
"label": "Modalità",
"submenu": {
"custom": "Brano/Video personalizzato",
"disabled": "Disattivato",
"native": "Brano/Video nativo"
}
}
},
"name": "Selettore Brano/Video",
"templates": {
"button": "Brano"
}
},
"visualizer": {
"description": "Sostituisce al Video un visualizzatore grafico",
"menu": {
"visualizer-type": "Tipo di visualizzazione"
},
"name": "Visualizzatore grafico"
}
}
}

View File

@ -1,682 +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": "ja",
"local-name": "日本語",
"name": "Japanese"
},
"main": {
"console": {
"did-finish-load": {
"dev-tools": "ロード完了。デベロッパーツールが開きました"
},
"i18n": {
"loaded": "翻訳ロード完了"
},
"second-instance": {
"receive-command": "プロトコルより命令を受けました:”{{command}}”"
},
"theme": {
"css-file-not-found": "CSSファイル”{{cssFile}}”が存在しません。無視します"
},
"unresponsive": {
"details": "応答なしエラー!\n{{error}}"
},
"when-ready": {
"clearing-cache-after-20s": "アプリのキャッシュを削除中"
},
"window": {
"tried-to-render-offscreen": "ウィンドウは画面外をレンダリングしようとしました, windowSize={{windowSize}}, displaySize={{displaySize}}, position={{position}}"
}
},
"dialog": {
"hide-menu-enabled": {
"detail": "メニューは非表示です。'Alt'で表示します。(アプリ内メニューには'Escape'を使用します)",
"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": "OK"
},
"detail": "新しいバージョンが利用可能です。{{downloadLink}} からダウンロードできます",
"message": "新しいバージョンが利用可能",
"title": "アップデートが利用可能"
}
},
"menu": {
"about": "このアプリについて",
"navigation": {
"label": "移動",
"submenu": {
"copy-current-url": "現在の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": "DevToolsの切り替え"
}
},
"always-on-top": "常に最前面に表示",
"auto-update": "自動アップデート",
"hide-menu": {
"dialog": {
"message": "メニューは次の起動から非表示になります。表示するには[Alt]キーを使用します (in-app-menuを使用している場合は[`]を使用します)",
"title": "メニューの非表示が有効"
},
"label": "メニューの非表示"
},
"language": {
"dialog": {
"message": "言語は再起動後に変更されます",
"title": "言語が変更されました"
},
"label": "言語設定",
"submenu": {
"to-help-translate": "翻訳をサポートしたいですか?こちらをクリック"
}
},
"resume-on-start": "起動時に最後の曲を再開する",
"single-instance-lock": "単一インスタンスロック",
"start-at-login": "windowsのログイン時に起動",
"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": {
"label": "テーマ",
"submenu": {
"import-css-file": "CSSファイルをインポート",
"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": "YouTube ミュージック",
"with-song-info": "YouTube ミュージック: {{artist}} - {{title}}"
}
}
},
"plugins": {
"adblocker": {
"description": "すべての広告とトラッカーをブロックj",
"menu": {
"blocker": "ブロッカー"
},
"name": "広告ブロッカー"
},
"album-actions": {
"description": "「Undislike嫌いではない」「Dislike嫌い」「Like好き」「Unlike好きではない」ボタンを追加し、プレイリストやアルバム内のすべての曲にこれらを適用します",
"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}}秒間切り替え"
}
},
"use-fullscreen": {
"label": "全体画面モード使用"
}
},
"name": "アンビエント モード"
},
"audio-compressor": {
"description": "オーディオにコンプレッサーを適用します(信号での一番大きい部分の音量を下げ、小さい部分の音量を上げる)",
"name": "オーディオコンプレッサー"
},
"blur-nav-bar": {
"description": "ナビゲーションバーを透明かつぼやけにします",
"name": "ナビゲーションバーの曇り効果"
},
"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": "ディスコードに接続中",
"disconnected": "Discordから切断されました"
},
"description": "アクティビティ ステータスで、あなたが聴いている曲を友達に見せましょう",
"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": "Discordアクティビティステータス",
"prompt": {
"set-inactivity-timeout": {
"label": "非アクティブ時のタイムアウトを秒単位で入力:",
"title": "非アクティブタイムアウト"
}
}
},
"downloader": {
"backend": {
"dialog": {
"error": {
"buttons": {
"ok": "OK"
},
"message": "ダウンロード失敗!ごめんね…",
"title": "ダウンロード中にエラーが発生しました!"
},
"start-download-playlist": {
"buttons": {
"ok": "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": "プレイリストに1曲しかありません。直接ダウンロードします",
"playlist-id-not-found": "プレイリストIDが見つかりません",
"playlist-is-empty": "プレイリストは空です",
"playlist-is-mix-or-private": "プレイリスト情報をダウンロード中にエラーが発生しました: プレイリストが非公開ではないこと、\"Mixed for you\"ではないことを確認してください\n\n{{error}}",
"preparing-file": "ファイルを準備中…",
"saving": "保存中…",
"trying-to-get-playlist-id": "プレイリストIDを取得中{{playlistId}}",
"video-id-not-found": "動画が見つかりません",
"writing-id3": "ID3タグ作成中…"
}
},
"description": "UIから直にMP3・ソースオーディオをダウンロードします",
"menu": {
"choose-download-folder": "ダウンロードフォルダ",
"download-playlist": "プレイリストをダウンロード",
"presets": "プリセット",
"skip-existing": "存在するファイルをスキップ"
},
"name": "ダウンローダー",
"renderer": {
"can-not-update-progress": "進捗を更新できません"
},
"templates": {
"button": "ダウンロード"
}
},
"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": "ホストIDを入力"
},
"internal": {
"save": "保存",
"track-source": "トラックソース",
"unknown-user": "不明なユーザー"
},
"menu": {
"click-to-copy-id": "ホストIDをコピー",
"close": "一緒に音楽を閉じる",
"connected-users": "接続されているユーザー",
"disconnect": "一緒に音楽を切断する",
"empty-user": "接続中のユーザーはいません",
"host": "Music Together ホスト",
"join": "一緒に音楽に参加",
"permission": {
"all": "ゲストがプレイリストとプレーヤーを制御できるようにする",
"host-only": "ホストのみがプレイリストとプレーヤーを制御できます",
"playlist": "ゲストによるプレイリストの制御を許可する"
},
"set-permission": "制御権限を変更",
"status": {
"disconnected": "切断されました",
"guest": "ゲストとして接続しました",
"host": "ホストとして接続されています"
}
},
"name": "一緒に音楽 [ベータ版]",
"toast": {
"add-song-failed": "曲の追加に失敗しました",
"closed": "一緒に音楽が閉じられました",
"disconnected": "一緒に音楽が切断されました",
"host-failed": "一緒に音楽のホストに失敗しました",
"id-copied": "ホストIDがクリップボードにコピーされました",
"id-copy-failed": "ホストIDをクリップボードにコピー出来ませんでした",
"join-failed": "一緒に音楽に参加出来ませんでした",
"joined": "一緒に音楽に参加しました",
"permission-changed": "一緒に音楽の権限が \"{{permission}}\" に変更されました",
"remove-song-failed": "曲の削除に失敗しました",
"user-connected": "{{name}} が一緒に音楽に参加しました",
"user-disconnected": "{{name}} が一緒に音楽を退出しました"
}
},
"navigation": {
"description": "ブラウザの戻る・進むボタンのようにUIからコントロールできるボタン",
"name": "ナビゲーション"
},
"no-google-login": {
"description": "インターフェースからGoogleのログインボタンとリンクを削除",
"name": "No Google Login"
},
"notifications": {
"description": "曲の再生開始時に通知を表示する(Windowsではインタラクティブ通知が利用可能)",
"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": "ブラウザ標準の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": "スクロブリング対応を追加しますlast.fm、Listenbrainzなど",
"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トークン"
}
}
}
},
"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"
},
"taskbar-mediacontrol": {
"description": "Windowsタスクバーから再生をコントロール",
"name": "タスクバーメディアコントロール"
},
"touchbar": {
"description": "masOSユーザー向けにTouchBarウィジェットを追加",
"name": "TouchBar"
},
"tuna-obs": {
"description": "OBSのプラグインTunaの統合",
"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

@ -1,682 +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": "ko",
"local-name": "한국어",
"name": "Korean"
},
"main": {
"console": {
"did-finish-load": {
"dev-tools": "로드가 완료되었습니다. 개발자 도구가 열렸습니다"
},
"i18n": {
"loaded": "국제화 로드됨"
},
"second-instance": {
"receive-command": "프로토콜을 통해 명령을 받았습니다: \"{{command}}\""
},
"theme": {
"css-file-not-found": "CSS 파일 \"{{cssFile}}\"이(가) 존재하지 않습니다. 무시합니다"
},
"unresponsive": {
"details": "응답 없음 오류!\n{{error}}"
},
"when-ready": {
"clearing-cache-after-20s": "앱 캐시 지우기"
},
"window": {
"tried-to-render-offscreen": "창이 오프스크린 렌더링을 시도했습니다. windowSize={{windowSize}}, displaySize={{displaySize}}, position={{position}}"
}
},
"dialog": {
"hide-menu-enabled": {
"detail": "'Alt' 키를 눌러 숨겨진 메뉴를 표시할 수 있습니다 (인앱 메뉴를 사용하는 경우 'Esc' 키를 사용)",
"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": "현재 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": "User-Agent 재정의",
"restart-on-config-changes": "설정 변경 시 재시작",
"set-proxy": {
"label": "프록시 설정",
"prompt": {
"label": "프록시 주소를 입력하세요: (비워두면 비활성화됨)",
"placeholder": "예제: SOCKS5://127.0.0.1:9999",
"title": "프록시 설정"
}
},
"toggle-dev-tools": "DevTools 열기"
}
},
"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": {
"label": "테마",
"submenu": {
"import-css-file": "사용자 정의 CSS 파일 가져오기",
"no-theme": "테마 없음"
}
}
}
}
}
},
"plugins": {
"enabled": "활성화",
"label": "확장",
"new": "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": "유튜브 뮤직: {{artist}} - {{title}}"
}
}
},
"plugins": {
"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}}초 동안 전환"
}
},
"use-fullscreen": {
"label": "전체 화면 모드 사용"
}
},
"name": "앰비언트 모드"
},
"audio-compressor": {
"description": "오디오에 컴프레서를 적용합니다 (신호에서 가장 시끄러운 부분의 음량을 낮추고 가장 조용한 부분의 음량을 높임)",
"name": "오디오 컴프레서"
},
"blur-nav-bar": {
"description": "탐색 바를 투명하고 흐릿하게 만듭니다",
"name": "탐색 바 흐림 효과"
},
"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": "디스코드에 연결됨",
"disconnected": "디스코드에서 연결이 끊김"
},
"description": "활동 상태를 사용하여 친구들에게 내가 듣는 음악을 보여주세요",
"menu": {
"auto-reconnect": "자동 연결",
"clear-activity": "활동 제거",
"clear-activity-after-timeout": "시간 초과 시 활동 제거",
"connected": "연결 됨",
"disconnected": "연결 해제 됨",
"hide-duration-left": "남은 재생 시간 숨기기",
"hide-github-button": "GitHub 링크 버튼 숨기기",
"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": "재생목록 ID를 찾을 수 없습니다",
"playlist-is-empty": "재생목록이 비어있습니다",
"playlist-is-mix-or-private": "재생목록 정보 가져오는 중 오류 발생: 비공개 재생목록 또는 '유튜브 Mix' 재생목록이 아닌지 확인하세요\n\n{{error}}",
"preparing-file": "파일 준비 중…",
"saving": "저장 중…",
"trying-to-get-playlist-id": "재생목록 ID를 가져오는 중: {{playlistId}}",
"video-id-not-found": "영상을 찾을 수 없습니다",
"writing-id3": "ID3 태그 작성 중…"
}
},
"description": "UI에서 직접 MP3/소스 오디오를 다운로드하세요",
"menu": {
"choose-download-folder": "다운로드 폴더 선택",
"download-playlist": "재생목록 다운로드",
"presets": "프리셋",
"skip-existing": "이미 존재하는 파일 넘기기"
},
"name": "다운로더",
"renderer": {
"can-not-update-progress": "진행 상황을 업데이트 할 수 없음"
},
"templates": {
"button": "다운로드"
}
},
"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": "Music Together 닫기",
"connected-users": "연결된 사용자",
"disconnect": "Music Together 연결 끊기",
"empty-user": "연결된 사용자 없음",
"host": "Music Together 호스트",
"join": "Music Together 참여",
"permission": {
"all": "게스트가 모두 제어 가능",
"host-only": "호스트만 제어 가능",
"playlist": "게스트가 재생목록 제어 가능"
},
"set-permission": "제어 권한 변경",
"status": {
"disconnected": "연결 끊김",
"guest": "게스트로 연결됨",
"host": "호스트로 연결됨"
}
},
"name": "Music Together [베타]",
"toast": {
"add-song-failed": "노래 추가 실패",
"closed": "Music Together가 닫혔습니다",
"disconnected": "Music Together 연결이 끊어졌습니다",
"host-failed": "Music Together를 열 수 없습니다",
"id-copied": "호스트 아이디가 클립보드에 복사되었습니다",
"id-copy-failed": "호스트 ID를 클립보드에 복사하지 못했습니다",
"join-failed": "Music Together에 참여할 수 없습니다",
"joined": "Music Together에 참여했습니다",
"permission-changed": "Music Together 제어 권한이 \"{{permission}}\"(으)로 변경되었습니다",
"remove-song-failed": "노래 제거 실패",
"user-connected": "{{name}}님이 Music Together에 참여했습니다",
"user-disconnected": "{{name}}님이 Music Together에서 나갔습니다"
}
},
"navigation": {
"description": "브라우저에서처럼, UI에 직접 통합된 앞으로/뒤로 탐색하는 화살표",
"name": "탐색"
},
"no-google-login": {
"description": "UI에서 Google 로그인 버튼 및 링크 제거하기",
"name": "Google 로그인 제거"
},
"notifications": {
"description": "노래 재생이 시작되면 알림을 표시 (Windows에서는 대화형 알림 사용 가능)",
"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": "앱을 PiP 모드로 전환할 수 있게 허용합니다",
"menu": {
"always-on-top": "항상 맨 위에 표시",
"hotkey": {
"label": "단축키",
"prompt": {
"keybind-options": {
"hotkey": "단축키"
},
"label": "PiP를 전환하기 위한 단축키를 선택하세요",
"title": "PiP 단축키"
}
},
"save-window-position": "창 위치 저장",
"save-window-size": "창 크기 저장",
"use-native-pip": "브라우저 내장 PiP 사용"
},
"name": "PiP",
"templates": {
"button": "PiP"
}
},
"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": "스크로블링 지원을 추가합니다 (예: last.fm, Listenbrainz)",
"menu": {
"lastfm": {
"api-settings": "Last.fm API 설정"
},
"listenbrainz": {
"token": "ListenBrainz 유저 토큰 입력"
},
"scrobble-other-media": "다른 미디어 스크로블하기"
},
"name": "스크로블러",
"prompt": {
"lastfm": {
"api-key": "Last.fm API 키",
"api-secret": "Last.fm API 비밀 키"
},
"listenbrainz": {
"token": {
"label": "ListenBrainz 유저 토큰을 입력하세요:",
"title": "ListenBrainz 토큰"
}
}
}
},
"shortcuts": {
"description": "재생을 위한 전역 단축키 설정 허용 (재생/일시 정지/다음/이전), 미디어 키를 재정의하여 미디어 OSD 비활성화, Ctrl/CMD + F 검색을 활성화, 미디어키 지원을 위해 리눅스 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"
},
"taskbar-mediacontrol": {
"description": "Windows 작업 표시줄에서 재생을 제어하세요",
"name": "작업표시줄 미디어 컨트롤"
},
"touchbar": {
"description": "macOS 사용자를 위한 TouchBar 위젯을 추가합니다",
"name": "TouchBar"
},
"tuna-obs": {
"description": "OBS의 확장인 Tuna와의 통합을 활성화합니다",
"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

@ -1,594 +0,0 @@
{
"common": {
"console": {
"plugins": {
"execute-failed": "Nepavyko įvykdyti įskiepio {{pluginName}}::{{contextName}}",
"executed-at-ms": "Įskiepis {{pluginName}}::{{contextName}} įvykdytas per {{ms}}ms",
"initialize-failed": "Nepavyko inicijuoti įskiepio \"{{pluginName}}\"",
"load-all": "Kraunama visus įskiepius",
"load-failed": "Nepavyko užkrauti įskiepio \"{{pluginName}}\"",
"loaded": "Įskiepis \"{{pluginName}}\" užkrautas",
"unload-failed": "Nepavyko iškrauti įskiepio \"{{pluginName}}\"",
"unloaded": "Įskiepis \"{{pluginName}}\" iškrautas"
}
}
},
"language": {
"code": "lt",
"local-name": "Lietuvių kalba",
"name": "Lithuanian"
},
"main": {
"console": {
"did-finish-load": {
"dev-tools": "Baigta krauti. \"DevTools\" atidaryta"
},
"i18n": {
"loaded": "\"i18n\" užkrauta"
},
"second-instance": {
"receive-command": "Gauta komanda per protokolą: \"{{command}}\""
},
"theme": {
"css-file-not-found": "CSS failas \"{{cssFile}}\" neegzistuoja, ignoruojama"
},
"unresponsive": {
"details": "Nereguojanti paklaida\n{{error}}"
},
"when-ready": {
"clearing-cache-after-20s": "Išvaloma programos talpykla"
},
"window": {
"tried-to-render-offscreen": "Langas bandė vaizduotis už ekrano ribų, langoDydis={{windowSize}}, ekranoDydis={{displaySize}}, pozicija={{position}}"
}
},
"dialog": {
"hide-menu-enabled": {
"detail": "Meniu yra paslėpta, naudokite 'Alt', kad ją parodyti (arba 'Escape' jei naudojama programos meniu)",
"message": "\"Paslėpti Meniu\" yra įjungta",
"title": "Įjungta \"Paslėpti Meniu\""
},
"need-to-restart": {
"buttons": {
"later": "Vėliau",
"restart-now": "Perkrauti Dabar"
},
"detail": "\"{{pluginName}}\" įskiepis reikalauja perkrovimą, kad veiktų",
"message": "\"{{pluginName}}\" reikia perkrovimo",
"title": "Reikiamas perkrovimas"
},
"unresponsive": {
"buttons": {
"quit": "Išeiti",
"relaunch": "Perleisti",
"wait": "Palaukti"
},
"detail": "Mes apgailestaujame dėl nepatogumų! prašau pasirinkti ką daryti:",
"message": "Programa Neatsako",
"title": "Langas Neatsako"
},
"update-available": {
"buttons": {
"disable": "Išjungti Atnaujinimus",
"download": "Atsisiųsti",
"ok": "Gerai"
},
"detail": "Nauja versija yra prieinama ir gali būti atsisiųsta {{downloadLink}}",
"message": "Nauja versija yra prieinama",
"title": "Prieinamas Atnaujinimas"
}
},
"menu": {
"about": "Apie",
"navigation": {
"label": "Navigacija",
"submenu": {
"copy-current-url": "Nukopijuoti dabartinį URL",
"go-back": "Grįžti Atgal",
"go-forward": "Eiti į priekį",
"quit": "Išeiti",
"restart": "Perkrauti programą"
}
},
"options": {
"label": "Nustatymai",
"submenu": {
"advanced-options": {
"label": "Išplėstiniai nustatymai",
"submenu": {
"auto-reset-app-cache": "Perkrauti programos talpyklą, kai programa paleidžiama",
"disable-hardware-acceleration": "Išjungti aparatūros pagreitį",
"edit-config-json": "Redaguoti config.json",
"override-user-agent": "Perrašyti \"User-Agent\"",
"restart-on-config-changes": "Perkrauti po config pasikeitimo",
"set-proxy": {
"label": "Nustatyti įgaliotajį serverį",
"prompt": {
"label": "Įvesti Įgaliotojo serverio adresą: (palikti tuščią, kad išjungti)",
"placeholder": "Pavyzdys: SOCKS5://127.0.0.1:9999",
"title": "Nustatyti įgaliotajį serverį"
}
},
"toggle-dev-tools": "Įjungti/Išjungti DevTools"
}
},
"always-on-top": "Visada viršuje",
"auto-update": "Automatinis Atnaujinimas",
"hide-menu": {
"dialog": {
"message": "Meniu bus paslėpta per kitą paleidimą, naudokite [Alt], kad ją parodyti (arba kairinio kirčio ženklą [`] jei naudojama programos meniu)",
"title": "\"Paslėpti Meniu\" įjungtas"
},
"label": "Paslėpti Meniu"
},
"language": {
"dialog": {
"message": "Kalba bus pakeista po perkrovimo",
"title": "Kalba Pakeista"
},
"label": "Kalba",
"submenu": {
"to-help-translate": "Norite padėti išversti? Paspauskite čia"
}
},
"resume-on-start": "Tęsti paskutinę dainą, kai programa bus paleista",
"single-instance-lock": "Vienkartinis užraktas",
"start-at-login": "Pradėti nuo prisijungimo",
"starting-page": {
"label": "Pradžios puslapis",
"unset": "Nenustatyta"
},
"tray": {
"label": "Padėklas",
"submenu": {
"disabled": "Išjungta",
"enabled-and-hide-app": "Įjungta ir slėpti programos langą",
"enabled-and-show-app": "Įjungta ir rodyti programos langą",
"play-pause-on-click": "Paleisti/Pristabdyti ant paspaudimo"
}
},
"visual-tweaks": {
"label": "Vizualiniai patobulinimai",
"submenu": {
"like-buttons": {
"default": "Numatytasis",
"force-show": "Priversti rodyti",
"hide": "Slėpti",
"label": "\"Patinka\" mygtukai"
},
"remove-upgrade-button": "Nerodyti \"Patobulinti\" mygtuko",
"theme": {
"label": "Tema",
"submenu": {
"import-css-file": "Įkelti pasirinktinį CSS failą",
"no-theme": "Be temos"
}
}
}
}
}
},
"plugins": {
"enabled": "Įjungta",
"label": "Įskiepiai"
},
"view": {
"label": "Vaizdas",
"submenu": {
"force-reload": "Priverstinai perkrauti",
"reload": "Perkrauti",
"reset-zoom": "Tikras dydis",
"toggle-fullscreen": "Įjungti/Išjungti Pilną Ekraną",
"zoom-in": "Priartinti",
"zoom-out": "Nutolinti"
}
}
},
"tray": {
"next": "Kitas",
"play-pause": "Paleisti/Pristabdyti",
"previous": "Ankstesnis",
"quit": "Išeiti",
"restart": "Perkrauti programą",
"show": "Rodyti langą"
}
},
"plugins": {
"adblocker": {
"description": "Blokuoti visas reklamas ir seklius",
"menu": {
"blocker": "Blokuotojas"
},
"name": "Reklamų blokuotojas"
},
"album-color-theme": {
"description": "Pritaiko dinamišką temą ir vizualinius efektus pagal albumo spalvų paletę",
"name": "Albumo Spalvų Tema"
},
"ambient-mode": {
"description": "Pritaiko apšvietimo efektą, perteikdamas švelnias vaizdo įrašo spalvas į ekrano foną.",
"menu": {
"blur-amount": {
"label": "Suliejimo kiekis",
"submenu": {
"pixels": "{{blurAmount}} pikseliai"
}
},
"buffer": {
"label": "Buferis",
"submenu": {
"buffer": "{{buffer}}"
}
},
"opacity": {
"label": "Skaidrumas",
"submenu": {
"percent": "{{opacity}}%"
}
},
"quality": {
"label": "Kokybė",
"submenu": {
"pixels": "{{quality}} pikseliai"
}
},
"size": {
"label": "Dydis",
"submenu": {
"percent": "{{size}}%"
}
},
"smoothness-transition": {
"label": "Perliejimo švelnumas",
"submenu": {
"during": "Per {{interpolationTime}}s"
}
},
"use-fullscreen": {
"label": "Naudojamas visas ekranas"
}
},
"name": "Aplinkos rėžimas"
},
"audio-compressor": {
"description": "Pritaikyti garso kompresiją (sumažina garsiausių signalo dalių garsumą ir padidina švelniausių dalių garsumą)",
"name": "Garso Kompresorius"
},
"blur-nav-bar": {
"description": "Padaro navigacijos lentą permatomą ir susiliejusią",
"name": "Sulieti Navigacijos Lentą"
},
"bypass-age-restrictions": {
"description": "Apeiti \"Youtube\" amžiaus patikrinimą",
"name": "Apeiti Amžiaus Apribojimus"
},
"captions-selector": {
"description": "„YouTube Music“ Garso takelių antraščių parinkiklis",
"menu": {
"autoload": "Automatiškai pasirinkti paskutinę naudotą antraštę",
"disable-captions": "Pagal numatytuosius nustatymus išjungti antraštės"
},
"name": "Antraščių parinkiklis",
"prompt": {
"selector": {
"label": "Dabartinė antraščių kalba: {{language}}",
"none": "Joks",
"title": "Pasirinkti antraščių kalbą"
}
},
"templates": {
"title": "Atidaryti antraščių parinkiklį"
}
},
"compact-sidebar": {
"description": "Visada nustatyti šoninę juostą kompaktiniame rėžime",
"name": "Kompaktinė šoninė juosta"
},
"crossfade": {
"description": "Perliejimas tarp dainų",
"menu": {
"advanced": "Išplėstinė"
},
"name": "Perliejimas [Beta]",
"prompt": {
"options": {
"multi-input": {
"fade-in-duration": "Išblukimo trukmė (ms)",
"fade-out-duration": "Išnykimo trukmė (ms)",
"fade-scaling": {
"label": "Išblukimo stiprumas",
"linear": "Linijinis",
"logarithmic": "Logaritminis"
},
"seconds-before-end": "Pradėti lieti dainas N sekundžių prieš pabaigą"
},
"title": "Perliejimo nustatymai"
}
}
},
"disable-autoplay": {
"description": "Pradeda dainą pristabdytame rėžime",
"menu": {
"apply-once": "Pritaiko tik per programos paleidimą"
},
"name": "Išjungti Automatinį leidimą"
},
"discord": {
"backend": {
"already-connected": "Bandyta prisijungti naudojant aktyvų ryšį",
"connected": "Prisijungta prie \"Discord\"",
"disconnected": "Atsijungta nuo \"Discord\""
},
"description": "Parodyk savo draugams ko tu klausaisi su \"Turtingas Buvimas\" (Rich Presence)",
"menu": {
"auto-reconnect": "Automatiškai prisijungti",
"clear-activity": "Išvalyti veiklą",
"clear-activity-after-timeout": "Išvalyti veiklą po skirtojo laiko",
"connected": "Prisijungta",
"disconnected": "Atsijungta",
"hide-duration-left": "Slėpti kiek liko laiko",
"hide-github-button": "Slėpti \"GitHub\" nuorodos mygtuką",
"play-on-youtube-music": "Leisti ant \"Youtube Music\"",
"set-inactivity-timeout": "Nustatyti neveiklumo laiką"
},
"name": "\"Discord\" Turtingas Buvimas (Rich Presence)",
"prompt": {
"set-inactivity-timeout": {
"label": "Įveskite neveiklumo skirtąjį laiką sekundėmis:",
"title": "Nustatyti neveiklumo laiką"
}
}
},
"downloader": {
"backend": {
"dialog": {
"error": {
"buttons": {
"ok": "Gerai"
},
"message": "Aaa! Apgailestaujame, nepavyko atsisiųsti…",
"title": "Paklaida atsisiunčiant!"
},
"start-download-playlist": {
"buttons": {
"ok": "Gerai"
},
"detail": "({{playlistSize}} dainos)",
"message": "Atsisiunčiama {{playlistTitle}} grojaraštį",
"title": "Prasidėjo atsisiuntimas"
}
},
"feedback": {
"conversion-progress": "Konversija: {{percent}}%",
"converting": "Konvertuojama…",
"done": "Baigta: {{filePath}}",
"download-info": "Atsiunčiama {{artist}} - {{title}} {{videoId}}",
"download-progress": "Atsisiuntimas: {{percent}}%",
"downloading": "Atsisiunčiama…",
"downloading-counter": "Atsisiunčiama {{current}}/{{total}}…",
"downloading-playlist": "Atsisiunčiamas grojaraštis \"{{playlistTitle}}\" - {{playlistSize}} dainų {{playlistId}}",
"error-while-downloading": "Paklaida atsisiunčiant \"{{author}} - {{title}}\": {{error}}",
"folder-already-exists": "Aplankas {{playlistFolder}} jau egzistuoja",
"getting-playlist-info": "Gaunama grojaraščio informacija…",
"loading": "Kraunama…",
"playlist-has-only-one-song": "Grojaraštis turi tik vieną elementą, jis atsisiunčiamas tiesiogiai",
"playlist-id-not-found": "Grojaraščio ID nerastas",
"playlist-is-empty": "Grojaraštis yra tuščias",
"playlist-is-mix-or-private": "Paklaida gaunant grojaraščio informaciją: Pasitikrink, kad jis nėra privatus ar \"Surinkta specialiai jums\" grojaraštis\n\n{{error}}",
"preparing-file": "Failas paruošiamas…",
"saving": "Išsaugojama…",
"trying-to-get-playlist-id": "Bandoma gauti grojaraščio ID: {{playlistId}}",
"video-id-not-found": "Vaizdo įrašas nerastas",
"writing-id3": "Rašoma ID3 žymes…"
}
},
"description": "Atsisiunčia MP3 / šaltinio garsą tiesiogiai iš sąsajos",
"menu": {
"choose-download-folder": "Pasirinkti atsisiuntimų aplanką",
"download-playlist": "Atsisiųsti grojaraštį",
"presets": "Iš anksto nustatyti nustatymai",
"skip-existing": "Praleisti egzistuojančius failus"
},
"name": "Atsiuntėjas",
"renderer": {
"can-not-update-progress": "Nepavyko atnaujinti eigos"
},
"templates": {
"button": "Atsisiųsti"
}
},
"exponential-volume": {
"description": "Padaro garsumo slankiklį eksponentinį, kad būtų lengviau pasirinkti mažesnį garsumą.",
"name": "Eksponentinis garsas"
},
"in-app-menu": {
"description": "Duoda meniu lentoms įmantrią, tamsią ar albumo spalvos išvaizdą",
"menu": {
"hide-dom-window-controls": "Slėpti DOM lango kontroles"
},
"name": "Programos Meniu"
},
"lumiastream": {
"description": "Prideda \"Lumia Stream\" palaikymą",
"name": "Lumia Stream [Beta]"
},
"lyrics-genius": {
"description": "Prideda daugumai dainių žodžių tekstus",
"menu": {
"romanized-lyrics": "Romanizuoti dainų tekstai"
},
"name": "\"Genius\" Žodžių tekstai",
"renderer": {
"fetched-lyrics": "Gauti žodžiai iš „Genius“"
}
},
"navigation": {
"description": "Kitas/Ankstenis navigacijos rodyklės tiesiogiai integruotos sąsajoje, kaip tavo mėgstamiausioje naršyklėje",
"name": "Navigacija"
},
"no-google-login": {
"description": "Pašalinti \"Google\" prisijungimo mygtukus ir nuorodas iš sąsjos",
"name": "Be \"Google\" Prisijungimo"
},
"notifications": {
"description": "Rodyti pranešimą, kai pradeda groti daina (interaktyvūs pranešimai pasiekiami sistemoje \"Windows\")",
"menu": {
"interactive": "Interaktyvūs pranešimai",
"interactive-settings": {
"label": "Interaktyvūs nustatymai",
"submenu": {
"hide-button-text": "Paslėpti mygtuko tekstą",
"refresh-on-play-pause": "Atnaujinti ant Paleidimo/Pristabdymo",
"tray-controls": "Atidaryti/Uždaryti ant padėklo paspaudimo"
}
},
"priority": "Pranešimų pirminybė",
"toast-style": "Skrudintas stilius",
"unpause-notification": "Rodyti pranešimus po dainos paleidimo"
},
"name": "Pranešimai"
},
"picture-in-picture": {
"description": "Leidžia pakeisti programą į \"picture-in-picture\" rėžimą",
"menu": {
"always-on-top": "Visada ant viršaus",
"hotkey": {
"label": "Spartusis klavišas",
"prompt": {
"keybind-options": {
"hotkey": "Spartusis klavišas"
},
"label": "Pasirinkti spartujį klaviša, kad įjungti/išjungti \"picture-in-picture\"",
"title": "\"Picture-in-picture\" Spartusis klavišas"
}
},
"save-window-position": "Išsaugoti lango poziciją",
"save-window-size": "Išsaugoti lango dydį",
"use-native-pip": "Naudoti naršyklės savajį PiP"
},
"name": "Picture-in-picture",
"templates": {
"button": "Picture-in-picture"
}
},
"playback-speed": {
"description": "Klausyk greitai, klausyk lėtai! Prideda slankiklį, kuris valdo dainos greitį",
"name": "Atkūrimo Greitis",
"templates": {
"button": "Greitis"
}
},
"precise-volume": {
"description": "Tiksliai valdykite garsumą naudodami pelės ratuką / sparčiuosius klavišus, naudodami pritaikytą HUD ir pritaikomus garsumo žingsnius",
"menu": {
"arrows-shortcuts": "Vietiniai rodyklių klavišai valdikliai",
"custom-volume-steps": "Nustatykite Pasirinktinius Garsumo Žingsnius",
"global-shortcuts": "Pasauliniai spartieji klavišai"
},
"name": "Tikslus Garsas",
"prompt": {
"global-shortcuts": {
"keybind-options": {
"decrease": "Pamažinti Garsą",
"increase": "Padidinti Garsą"
},
"label": "Pasirinkti Pasaulinius garso klavišus:",
"title": "Pasauliniai Garso Klavišai"
},
"volume-steps": {
"label": "Pasirinkti Garso Didinimo/Mažinimo Žingsnius",
"title": "Garso Žingsniai"
}
}
},
"quality-changer": {
"backend": {
"dialog": {
"quality-changer": {
"detail": "Dabartinė Kokybė: {{quality}}",
"message": "Pasirinkite Vaizdo Kokybę:",
"title": "Pasirinkite Vaizdo Kokybę"
}
}
},
"description": "Leidžia pakeisti vaizdo kokybę su mygtuku ant vaizdo perdangos",
"name": "Vaizdo Kokybės Pakeitėjas"
},
"shortcuts": {
"description": "Leidžia nustatyti visuotinius atkūrimo sparčiuosius klavišus (paleisti / pristabdyti / kitą / ankstesnį) ir išjungti medijos OSD nepaisant medijos klavišų, įjungti Ctrl / CMD + F ieškoti, įjungti Linux MPRIS palaikymą medijos klavišams ir pasirinktinius sparčiuosius klavišus pažengusiems vartotojams.",
"menu": {
"override-media-keys": "Perrašyti Medijos klavišus",
"set-keybinds": "Nustatyti Pasaulines Dainų Kontroles"
},
"name": "Spartieji klavišai (& MPRIS)",
"prompt": {
"keybind": {
"keybind-options": {
"next": "Kitas",
"play-pause": "Paleisti / Pristabdyti",
"previous": "Ankstesnis"
},
"label": "Pasirinkti Pasaulinius Klavišus Dainų Kontroliavimui:",
"title": "Pasauliniai Klavišai"
}
}
},
"skip-disliked-songs": {
"description": "Praleidžia nepatinkančias dainas",
"name": "Praleisti Nepatinkančias Dainas"
},
"skip-silences": {
"description": "Automatiškai praleisti tylos dalis dainose",
"name": "Praleisti Tylumas"
},
"sponsorblock": {
"description": "Automatiškai praleidžia ne muzikines dalis, pvz., įžangą/užvedimą arba muzikinių vaizdo įrašų dalis, kuriose daina negrojama",
"name": "Rėmėjų blokuotojas"
},
"taskbar-mediacontrol": {
"description": "Valdykite atkūrimą iš „Windows“ užduočių juostos",
"name": "Užduočių juostos medijos valdymas"
},
"touchbar": {
"description": "Pridedamas jutiklinės juostos valdiklis MacOS vartotojams",
"name": "TouchBar"
},
"tuna-obs": {
"description": "Integracija su OBS papildiniu \"Tuna\"",
"name": "Tuna OBS"
},
"video-toggle": {
"description": "Pridedamas mygtukas, skirtas perjungti vaizdo įrašo/dainos režimą. taip pat galite pasirinktinai pašalinti visą vaizdo įrašo skirtuką",
"menu": {
"align": {
"label": "Lygiavimas",
"submenu": {
"left": "Kairė",
"middle": "Vidurys",
"right": "Dešinė"
}
},
"force-hide": "Priverstinai pašalinti vaizdo įrašo skirtuką",
"mode": {
"label": "Rėžimas",
"submenu": {
"custom": "Pasirinktinis perjungimas",
"disabled": "Išjungta",
"native": "Vietinis perjungimas"
}
}
},
"name": "Vaizdo įrašo perjungimas",
"templates": {
"button": "Daina"
}
},
"visualizer": {
"description": "Prie grotuvo pridedamas vizualizatorius",
"menu": {
"visualizer-type": "Vizualizatoriaus tipas"
},
"name": "Vizualizatorius"
}
}
}

View File

@ -1,591 +0,0 @@
{
"common": {
"console": {
"plugins": {
"execute-failed": "Klarte ikke å kjøre programtillegg {{pluginName}}::{{contextName}}",
"executed-at-ms": "Det tok {{ms}} ms å kjøre {{pluginName}}::{{contextName}}",
"initialize-failed": "Klarte ikke å igangsette «{{pluginName}}»",
"load-all": "Laster inn alle programtillegg",
"load-failed": "Klarte ikke å laste inn {{pluginName}}-programtillegget",
"loaded": "Lastet inn {{pluginName}}-programtillegget",
"unload-failed": "Kunne ikke skru av {{pluginName}}-programtillegget",
"unloaded": "{{pluginName}}-programtillegg avskrudd"
}
}
},
"language": {
"code": "nb_NO",
"local-name": "Norsk bokmål",
"name": "Norwegian Bokmål"
},
"main": {
"console": {
"did-finish-load": {
"dev-tools": "Innlasting fullført. Utviklerverktøy åpnet."
},
"i18n": {
"loaded": "språkstøtte innlastet"
},
"second-instance": {
"receive-command": "Mottok kommando over protokoll: \"{{command}}\""
},
"theme": {
"css-file-not-found": "CSS-filen «{{cssFile}}» finnes ikke. Ignorerer."
},
"unresponsive": {
"details": "Svarer ikke\n{{error}}"
},
"when-ready": {
"clearing-cache-after-20s": "Tømmer programhurtiglager"
},
"window": {
"tried-to-render-offscreen": "Prøvde å tegne vindu utenfor skjermen. Størrelse={{windowSize}}, skjermstørrelse={{displaySize}}, posisjon={{position}}"
}
},
"dialog": {
"hide-menu-enabled": {
"detail": "Menyen er skjult. Bruk «Alt» for å vise den, (ller «Esc» for å bruke menyen i programmet).",
"message": "Meny skjult",
"title": "Meny vist"
},
"need-to-restart": {
"buttons": {
"later": "Senere",
"restart-now": "Start på ny nå"
},
"detail": "{{pluginName}}-programtillegget krever programomstart før bruk.",
"message": "{{pluginName}}-programtillegget krever programomstart.",
"title": "Programomstart kreves"
},
"unresponsive": {
"buttons": {
"quit": "Avslutt",
"relaunch": "Start igjen",
"wait": "Vent"
},
"detail": "Velg blandt følgende:",
"message": "Programmet svarer ikke",
"title": "Vinduet svarer ikke"
},
"update-available": {
"buttons": {
"disable": "Skru av oppgraderinger",
"download": "Last ned",
"ok": "OK"
},
"detail": "En ny versjon er tilgjengelig og kan lastes ned fra {{downloadLink}}",
"message": "En ny versjon er tilgjengelig",
"title": "Oppgradering tilgjengelig"
}
},
"menu": {
"about": "Om",
"navigation": {
"label": "Navigasjon",
"submenu": {
"copy-current-url": "Kopier nåværende nettadresse",
"go-back": "Tilbake",
"go-forward": "Forover",
"quit": "Avslutt",
"restart": "Programomstart"
}
},
"options": {
"label": "Alternativer",
"submenu": {
"advanced-options": {
"label": "Avanserte alternativer",
"submenu": {
"auto-reset-app-cache": "Tilbakestill programhurtiglager når programmet startes",
"disable-hardware-acceleration": "Skru av maskinvareakselerasjon",
"edit-config-json": "Rediger config.json",
"override-user-agent": "Overstyr brukeragent",
"restart-on-config-changes": "Omstart ved oppsettsendringer",
"set-proxy": {
"label": "Sett mellomtjener",
"prompt": {
"label": "Skriv inn mellomtjeneradresse: (la stå tom for å skru av)",
"placeholder": "Eksempel: socks5://127.0.0.1:9999",
"title": "Sett mellomtjener"
}
},
"toggle-dev-tools": "Skru av/på utviklerverktøy"
}
},
"always-on-top": "Alltid på toppen",
"auto-update": "Auto-oppdatering",
"hide-menu": {
"dialog": {
"message": "Menyen vil bli skjult ved neste programstart. Bruk [Alt] for å vise den, eller gravistegn [`] hvis du bruker menyen i programmet).",
"title": "Skjuler meny"
},
"label": "Skjul meny"
},
"language": {
"dialog": {
"message": "Nytt språk vises når programmet startes på ny",
"title": "Språk endret"
},
"label": "Språk",
"submenu": {
"to-help-translate": "Klikk her for å bistå oversettelsen"
}
},
"resume-on-start": "Gjenoppta siste spor ved programstart",
"single-instance-lock": "Sperr én instans",
"start-at-login": "Start ved innlogging",
"starting-page": {
"label": "Startside",
"unset": "Opphev"
},
"tray": {
"label": "Systemkurv",
"submenu": {
"disabled": "Avskrudd",
"play-pause-on-click": "Spill av/pause ved klikk"
}
},
"visual-tweaks": {
"label": "Visuelle tilpasninger",
"submenu": {
"like-buttons": {
"default": "Forvalg",
"force-show": "Tving visning",
"hide": "Skjul",
"label": "Begunstningsknapper"
},
"remove-upgrade-button": "Fjern oppgraderingsknapp",
"theme": {
"label": "Drakt",
"submenu": {
"import-css-file": "Importer egendefinert CSS-fil",
"no-theme": "Ingen"
}
}
}
}
}
},
"plugins": {
"enabled": "Påskrudd",
"label": "Programtillegg"
},
"view": {
"label": "Vis",
"submenu": {
"force-reload": "Tving gjeninnlasting",
"reload": "Gjeninnlast",
"reset-zoom": "Faktisk størrelse",
"toggle-fullscreen": "Veksle fullskjermsvisning",
"zoom-in": "Forstørr",
"zoom-out": "Forminsk"
}
}
},
"tray": {
"next": "Neste",
"play-pause": "Spill av/pause",
"previous": "Forrige",
"quit": "Avslutt",
"restart": "Programomstart",
"show": "Vis vindu"
}
},
"plugins": {
"adblocker": {
"description": "Stenger ute reklame og sporing",
"menu": {
"blocker": "Blokkering"
},
"name": "Reklameblokkering"
},
"album-color-theme": {
"description": "Ifører dynamisk drakt og visuelle effekter basert på albumsfargepaletten",
"name": "Albumsfargedrakt"
},
"ambient-mode": {
"description": "Ifører lyseffekt ved å hente myke farger fra videoen inn i skjermens bakgrunn.",
"menu": {
"blur-amount": {
"label": "Tilsløringsmengde",
"submenu": {
"pixels": "{{blurAmount}} piksler"
}
},
"buffer": {
"label": "Mellomlager",
"submenu": {
"buffer": "{{buffer}}"
}
},
"opacity": {
"label": "Dekkevne",
"submenu": {
"percent": "{{opacity}}%"
}
},
"quality": {
"label": "Kvalitet",
"submenu": {
"pixels": "{{quality}} piksler"
}
},
"size": {
"label": "Størrelse",
"submenu": {
"percent": "{{size}}%"
}
},
"smoothness-transition": {
"label": "Mykhetsovergang",
"submenu": {
"during": "I løpet av {{interpolationTime}} s"
}
},
"use-fullscreen": {
"label": "Ved bruk av fullskjerm"
}
},
"name": "Omgivelsesmodus"
},
"audio-compressor": {
"description": "Anvend kompresjon for lyd (senker lydstyrken for de kraftigste delene av signalet og øker nivået i de svakeste)",
"name": "Lydkompressor"
},
"blur-nav-bar": {
"description": "Gjør navigeringsbjelken gjennomsiktig og tilslørt",
"name": "Tilslør navigasjonsfelt"
},
"bypass-age-restrictions": {
"description": "Omgå YouTube sin aldersgrenser",
"name": "Omgå aldersgrense"
},
"captions-selector": {
"description": "Undertekstverktøy for lydspor i YouTube Music",
"menu": {
"autoload": "Auto-velg sist brukte undertekst",
"disable-captions": "Ingen undertekst som forvalg"
},
"name": "Undertekstvelger",
"prompt": {
"selector": {
"label": "Nåværende tekstingsspråk: {{language}}",
"none": "Ingen",
"title": "Velg tekstingsspråk"
}
},
"templates": {
"title": "Åpne undertekstvelger"
}
},
"compact-sidebar": {
"description": "Alltid sett sidefeltet i kompakt modus",
"name": "Kompakt sidefelt"
},
"crossfade": {
"description": "Overgang mellom spor",
"menu": {
"advanced": "Avansert"
},
"name": "Overgang [Beta]",
"prompt": {
"options": {
"multi-input": {
"fade-in-duration": "Inntoningsvarighet (ms)",
"fade-out-duration": "Uttoningsvarighet (ms)",
"fade-scaling": {
"label": "Overgangsskalering",
"linear": "Lineær",
"logarithmic": "Logaritmisk"
},
"seconds-before-end": "Overgang antall sekunder før slutt"
},
"title": "Overgangsalternativer"
}
}
},
"disable-autoplay": {
"description": "Merkerer sporstart i «pauset» modus",
"menu": {
"apply-once": "Har kun innvirkning ved oppstart"
},
"name": "Skru av autospilling"
},
"discord": {
"backend": {
"already-connected": "Forsøkte å koble til med aktiv tilkobling",
"connected": "Tilkoblet Discord",
"disconnected": "Frakoblet fra Discord"
},
"description": "Vis venne dine hva du lytter til med rik tilstedeværelse",
"menu": {
"auto-reconnect": "Automatisk retilkobling",
"clear-activity": "Tøm aktivitet",
"clear-activity-after-timeout": "Tøm aktivitet etter tidsavbrudd",
"connected": "Tilkoblet",
"disconnected": "Frakoblet",
"hide-duration-left": "Skjul gjenværende tid",
"hide-github-button": "Skjul GitHub-lenkeknapp",
"play-on-youtube-music": "Spill på YouTube Music",
"set-inactivity-timeout": "Sett tid før tidsavbrudd"
},
"name": "Rik tilstedeværelse for Discord",
"prompt": {
"set-inactivity-timeout": {
"label": "Skriv inn antall sekunder for inaktivitetstidsavbrudd:",
"title": "Sett inaktivitetstidsavbrudd"
}
}
},
"downloader": {
"backend": {
"dialog": {
"error": {
"buttons": {
"ok": "OK"
},
"message": "Nedlasting mislyktes …",
"title": "Feil i nedlastning."
},
"start-download-playlist": {
"buttons": {
"ok": "OK"
},
"detail": "({{playlistSize}} spor)",
"message": "Laster ned {{playlistTitle}}-spillelisten …",
"title": "Nedlasting startet"
}
},
"feedback": {
"conversion-progress": "Konvertering: {{percent}}%",
"converting": "Konverterer …",
"done": "Ferdig: {{filePath}}",
"download-info": "Laster ned {{artist}} — {{title}} [{{videoId}} …",
"download-progress": "Nedlastet: {{percent}}%",
"downloading": "Laster ned …",
"downloading-counter": "Laster ned {{current}}/{{total}} …",
"downloading-playlist": "Laster ned {{playlistTitle}}-spillelisten — {{playlistSize}} spor ({{playlistId}})",
"error-while-downloading": "Kunne ikke laste ned «{{author}} — {{title}}»: {{error}}",
"folder-already-exists": "{{playlistFolder}}-mappen finnes allerede",
"getting-playlist-info": "Henter spillelisteinfo …",
"loading": "Laster inn …",
"playlist-has-only-one-song": "Spillelisten har kun ett element. Laster ned direkte.",
"playlist-id-not-found": "Fant ingen spilleliste-ID",
"playlist-is-empty": "Tom spilleliste",
"playlist-is-mix-or-private": "Kunne ikke hente spillelisteinfo. Forsikre deg om at den ikke er privat eller «Mikset for deg».\n\n{{error}}",
"preparing-file": "Forbereder fil …",
"saving": "Lagrer …",
"trying-to-get-playlist-id": "Prøver å hente spilleliste-ID: {{playlistId}}",
"video-id-not-found": "Fant ikke videoen",
"writing-id3": "Skriver ID3-tagger …"
}
},
"description": "Laster ned MP3/kildelyd direkte fra grensesnittet",
"menu": {
"choose-download-folder": "Velg nedlastningsmappe",
"download-playlist": "Last ned spilleliste",
"presets": "Forhåndsinnstillinger",
"skip-existing": "Hopp over eksisterende filer"
},
"name": "Nedlaster",
"renderer": {
"can-not-update-progress": "Kan ikke oppdatere framdrift"
},
"templates": {
"button": "Last ned"
}
},
"exponential-volume": {
"description": "Gjør lydstyrkekontrollen eksponentiell, slik at det er enklere velge lavere lydstyrker.",
"name": "Eksponentiell lydstyrke"
},
"in-app-menu": {
"description": "Gir menybjelkene stilig, mørk, eller albumfarget utseende",
"menu": {
"hide-dom-window-controls": "Skjul DOM-vinduskontroller"
},
"name": "Meny i programmet"
},
"lumiastream": {
"description": "Legger til Lumia Stream-støtte",
"name": "Lumia Stream [Beta]"
},
"lyrics-genius": {
"description": "Gir sangtekststøtte for de fleste spor",
"menu": {
"romanized-lyrics": "Romaniserte sangtekster"
},
"name": "Sangtekster fra Genius",
"renderer": {
"fetched-lyrics": "Henter sangtekster for Genius"
}
},
"navigation": {
"description": "Direkte integrering av neste/tilbake-navigasjonspilene i grensesnittet, som din favorittnettleser",
"name": "Navigasjon"
},
"no-google-login": {
"description": "Fjern Google-innloggingsknapper og lenker fra grensesnittet",
"name": "Ingen Google-innlogging"
},
"notifications": {
"description": "Vis en merknad når et spor starter avspilling (interaktive merknader er tilgjengelig på Windows)",
"menu": {
"interactive": "Interaktive merknader",
"interactive-settings": {
"label": "Interaktive innstillinger",
"submenu": {
"hide-button-text": "Skjul knappetekst",
"refresh-on-play-pause": "Gjenoppfrisk ved avspilling/pause",
"tray-controls": "Åpne/lukk med klikk i systemkurven"
}
},
"priority": "Merknadsprioritet",
"unpause-notification": "Vis merknad ved oppheving av pause"
},
"name": "Merknader"
},
"picture-in-picture": {
"description": "Tillater å bytte programmet til bilde-i-bilde modus",
"menu": {
"always-on-top": "Alltid på toppen",
"hotkey": {
"label": "Hurtigtast",
"prompt": {
"keybind-options": {
"hotkey": "Hurtigtast"
},
"label": "Velg en hurtigtast for veksling av bilde-i-bilde",
"title": "Hurtigtast for bilde-i-bilde"
}
},
"save-window-position": "Lagre vindusposisjon",
"save-window-size": "Lagre vindusstørrelse",
"use-native-pip": "Bruk nettleserens innebygde bilde-i-bilde"
},
"name": "Bilde-i-bilde",
"templates": {
"button": "Bilde-i-bilde"
}
},
"playback-speed": {
"description": "Legger til glidebryter som kontrollerer avspillingshastighet",
"name": "Avspillingshastighet",
"templates": {
"button": "Hastighet"
}
},
"precise-volume": {
"description": "Kontroller lydstyrken presist ved bruk av musehjul/hurtigtaster, med egendefinert skjermoverlag og tilpassbare steg",
"menu": {
"arrows-shortcuts": "Kontroller for lokale piltaster",
"custom-volume-steps": "Sett egendefinerte lydstyrkesteg",
"global-shortcuts": "Hurtigtaster i hele programmet"
},
"name": "Presis lydstyrkejustering",
"prompt": {
"global-shortcuts": {
"keybind-options": {
"decrease": "Senk lydstyrke",
"increase": "Øk lydstyrke"
},
"label": "Velg tastetilknytninger for lydstyrkejusteringskontroller i hele programmet",
"title": "Tastetilknytninger for lydstyrkejusteringskontroller i hele programmet"
},
"volume-steps": {
"label": "Velg steg for økning/senking av lydstyrke",
"title": "Lydstyrkesteg"
}
}
},
"quality-changer": {
"backend": {
"dialog": {
"quality-changer": {
"detail": "Nåværende kvalitet: {{quality}}",
"message": "Velg videokvalitet:",
"title": "Velg videokvalitet"
}
}
},
"description": "Tillat endring av videokvalitet med en knapp i videooverlaget",
"name": "Vindukvalitetsvelger"
},
"shortcuts": {
"description": "Tillater bruk av hurtigtaster for hele programmet til avspilling (spill/pause/neste/forrige) + skru av media-videooverlag ved å overstyre mediataster + skru på Ctrl+CMD+F for å søke, pluss å egge til MPRIS støtte på linux for mediataster pluss egendefinerte hurtigtaster for avanserte brukere",
"menu": {
"override-media-keys": "Overstyr mediataster",
"set-keybinds": "Sett kontroller for spor i hele programmet"
},
"name": "Snarveier (og MPRIS)",
"prompt": {
"keybind": {
"keybind-options": {
"next": "Neste",
"play-pause": "Spill av/pause",
"previous": "Forrige"
},
"label": "Velg kontroller for spor i hele programmet:",
"title": "Tastaturtilknytninger i hele programmet"
}
}
},
"skip-disliked-songs": {
"description": "Hopper over mislikte spor",
"name": "Hopp over mislikte spor"
},
"skip-silences": {
"description": "Hopp over stille deler av spor",
"name": "Hopp over pauser"
},
"sponsorblock": {
"description": "Hopper over ikke-musikalske deler, som intro/sluttsats, eller deler av musikkvideoer der ingen musikk spilles",
"name": "SponsorBlock"
},
"taskbar-mediacontrol": {
"description": "Kontroller avspilling fra din Windows-oppgavelinje",
"name": "Oppgavelinje-mediakontroll"
},
"touchbar": {
"description": "Legger til et pekefelt-miniprogram for macOS-brukere",
"name": "Pekefelt"
},
"tuna-obs": {
"description": "Integrasjon med Tuna-programtillegget i OBS",
"name": "OBS Tuna"
},
"video-toggle": {
"description": "Leger til en knapp for å bytte mellom video/spormodus. Kan også alternativt fjerne hele videofanen.",
"menu": {
"align": {
"label": "Justering",
"submenu": {
"left": "Venstre",
"middle": "Midten",
"right": "Høyre"
}
},
"force-hide": "Påtving fjerning av videofane",
"mode": {
"label": "Modus",
"submenu": {
"custom": "Egendefinert veksling",
"disabled": "Avskrudd",
"native": "Innebygd veksling"
}
}
},
"name": "Videoveksling",
"templates": {
"button": "Spor"
}
},
"visualizer": {
"description": "Legger til en visualisator i avspilleren",
"menu": {
"visualizer-type": "Visualisatortype"
},
"name": "Visualisator"
}
}
}

View File

@ -1,674 +0,0 @@
{
"common": {
"console": {
"plugins": {
"execute-failed": "Błąd uruchamiania wtyczki {{pluginName}}::{{contextName}}",
"executed-at-ms": "Wtyczka {{pluginName}}::{{contextName}} uruchomiona w {{ms}}ms",
"initialize-failed": "Błąd inicjalizacji wtyczki \"{{pluginName}}\"",
"load-all": "Ładowanie wszystkich wtyczek",
"load-failed": "Błąd w ładowaniu wtyczki \"{{pluginName}}\"",
"loaded": "Wtyczka \"{{pluginName}}\" załadowana",
"unload-failed": "Błąd w odłączaniu wtyczki \"{{pluginName}}\"",
"unloaded": "Wtyczka \"{{pluginName}}\" odłączona"
}
}
},
"language": {
"code": "pl",
"local-name": "Polski",
"name": "Polish"
},
"main": {
"console": {
"did-finish-load": {
"dev-tools": "Ukończono ładowanie. Narzędzia deweloperskie otwarte"
},
"i18n": {
"loaded": "i18n załadowane"
},
"second-instance": {
"receive-command": "Otrzymano komendę przez protokół: \"{{command}}\""
},
"theme": {
"css-file-not-found": "Plik CSS \"{{cssFile}}\" nie istnieje, ignoruję"
},
"unresponsive": {
"details": "Błąd! Brak odpowiedzi:\n{{error}}"
},
"when-ready": {
"clearing-cache-after-20s": "Czyszczenie pamięci podręcznej aplikacji"
},
"window": {
"tried-to-render-offscreen": "Okno próbuje się renderować poza ekranem, rozmiar okna={{windowSize}}, rozmiar monitora={{displaySize}}, pozycja={{position}}"
}
},
"dialog": {
"hide-menu-enabled": {
"detail": "Menu jest ukryte, użyj przycisku [Alt] aby je pokazać (lub [Escape], jeśli używasz menu w aplikacji)",
"message": "Ukrywanie menu jest włączone",
"title": "Ukrywanie Menu Włączone"
},
"need-to-restart": {
"buttons": {
"later": "Później",
"restart-now": "Uruchom ponownie teraz"
},
"detail": "Wtyczka \"{{pluginName}}\" potrzebuje ponownego uruchomienia aby była aktywna",
"message": "\"{{pluginName}}\" potrzebuje ponownego uruchomienia",
"title": "Ponowne uruchomienie potrzebne"
},
"unresponsive": {
"buttons": {
"quit": "Wyjdź",
"relaunch": "Ponownie uruchom",
"wait": "Czekać"
},
"detail": "Przepraszamy za niedogodność! Proszę wybierz co zrobić:",
"message": "Aplikacja nie reaguje",
"title": "Okno nie reaguje"
},
"update-available": {
"buttons": {
"disable": "Wyłącz aktualizacje",
"download": "Pobierz",
"ok": "OK"
},
"detail": "Nowa wersja jest dostępna i możesz ją pobrać tutaj {{downloadLink}}",
"message": "Nowa wersja jest dostępna",
"title": "Aktualizacja jest dostępna"
}
},
"menu": {
"about": "O aplikacji",
"navigation": {
"label": "Nawigacja",
"submenu": {
"copy-current-url": "Skopiuj aktualny adres URL",
"go-back": "Wróć",
"go-forward": "Do przodu",
"quit": "Wyjście",
"restart": "Uruchom ponownie aplikację"
}
},
"options": {
"label": "Opcje",
"submenu": {
"advanced-options": {
"label": "Opcje zaawansowane",
"submenu": {
"auto-reset-app-cache": "Wyczyść pamięć podręczną aplikacji przy jej uruchomieniu",
"disable-hardware-acceleration": "Wyłącz przyspieszanie sprzętowe",
"edit-config-json": "Edytuj config.json",
"override-user-agent": "Zastąp klienta użytkownika (User-Agent)",
"restart-on-config-changes": "Uruchom ponownie po zmianie konfiguracji",
"set-proxy": {
"label": "Ustaw proxy",
"prompt": {
"label": "Podaj adres Proxy: (zostaw pusty aby wyłączyć)",
"placeholder": "Przykład: SOCKS5://127.0.0.1:9999",
"title": "Ustaw proxy"
}
},
"toggle-dev-tools": "Przełącz narzędzia deweloperskie"
}
},
"always-on-top": "Zawsze na wierzchu",
"auto-update": "Automatyczne aktualizacje",
"hide-menu": {
"dialog": {
"message": "Menu będzie ukryte po następnym uruchomieniu, użyj przycisku [Alt] aby je pokazać (lub [`], jeśli używasz menu w aplikacji)",
"title": "Ukrywanie menu włączone"
},
"label": "Ukryj menu"
},
"language": {
"dialog": {
"message": "Język będzie zmieniony po ponownym uruchomieniu",
"title": "Język zmieniony"
},
"label": "Język",
"submenu": {
"to-help-translate": "Chcesz pomóc w tłumaczeniu? Kliknij tutaj"
}
},
"resume-on-start": "Wznów ostatni utwór po uruchomieniu aplikacji",
"single-instance-lock": "Zablokuj do jednej instancji aplikacji",
"start-at-login": "Uruchom po zalogowaniu",
"starting-page": {
"label": "Strona startowa",
"unset": "Pusty"
},
"tray": {
"label": "Ikona w zasobniku",
"submenu": {
"disabled": "Wyłączone",
"enabled-and-hide-app": "Włącz i ukryj aplikację",
"enabled-and-show-app": "Włącz i pokaż aplikację",
"play-pause-on-click": "Odtwórz/Wstrzymaj po kliknięciu"
}
},
"visual-tweaks": {
"label": "Poprawki wizualne",
"submenu": {
"like-buttons": {
"default": "Domyślne",
"force-show": "Wymuś pokazywanie",
"hide": "Ukryj",
"label": "Przyciski polubienia"
},
"remove-upgrade-button": "Usuń przycisk subskrypcji premium",
"theme": {
"label": "Motyw",
"submenu": {
"import-css-file": "Importuj własny plik CSS",
"no-theme": "Bez motywu"
}
}
}
}
}
},
"plugins": {
"enabled": "Włączone",
"label": "Wtyczki",
"new": "NOWOŚĆ"
},
"view": {
"label": "Widok",
"submenu": {
"force-reload": "Wymuś ponowne ładowanie",
"reload": "Ponowne ładowanie",
"reset-zoom": "Rozmiar rzeczywisty",
"toggle-fullscreen": "Przełącz pełny ekran",
"zoom-in": "Powiększ",
"zoom-out": "Pomniejsz"
}
}
},
"tray": {
"next": "Następny",
"play-pause": "Odtwórz/Pauza",
"previous": "Poprzedni",
"quit": "Wyjdź",
"restart": "Uruchom ponownie aplikację",
"show": "Pokaż okno",
"tooltip": {
"default": "YouTube Music",
"with-song-info": "{{title}} (autorstwa {{artist}}) - YT Music"
}
}
},
"plugins": {
"adblocker": {
"description": "Blokuj wszystkie reklamy i śledzenie",
"menu": {
"blocker": "Metoda przechwytywania"
},
"name": "Blokowanie reklam"
},
"album-actions": {
"description": "Dodaje przyciski łapek w górę i dół do wszystkich piosenek podczas w albumach lub listach odtwarzania",
"name": "Akcje albumu"
},
"album-color-theme": {
"description": "Stosuje dynamiczny motyw i efekty wizualne w oparciu o paletę kolorów albumu",
"menu": {
"color-mix-ratio": {
"submenu": {
"percent": "{{ratio}}%"
}
}
},
"name": "Motyw kolorów albumu"
},
"ambient-mode": {
"description": "Stosuje efekt świetlny, rzucając delikatne kolory z wideo na tło ekranu",
"menu": {
"blur-amount": {
"label": "Ilość rozmycia",
"submenu": {
"pixels": "{{blurAmount}} pikseli"
}
},
"buffer": {
"label": "Bufor",
"submenu": {
"buffer": "{{buffer}}"
}
},
"opacity": {
"label": "Nieprzezroczystość",
"submenu": {
"percent": "{{opacity}}%"
}
},
"quality": {
"label": "Jakość",
"submenu": {
"pixels": "{{quality}} pikseli"
}
},
"size": {
"label": "Rozmiar",
"submenu": {
"percent": "{{size}}%"
}
},
"smoothness-transition": {
"label": "Płynność przejścia",
"submenu": {
"during": "W czasie {{interpolationTime}} s"
}
},
"use-fullscreen": {
"label": "Podczas pełnego ekranu"
}
},
"name": "Tryb otoczenia"
},
"audio-compressor": {
"description": "Zastosuj kompresję do dźwięku (obniża głośność najgłośniejszych części sygnału i zwiększa głośność najcichszych części)",
"name": "Kompresor dźwięku"
},
"blur-nav-bar": {
"description": "Sprawia, że pasek nawigacji jest przezroczysty i rozmazany",
"name": "Rozmycie paska nawigacji"
},
"bypass-age-restrictions": {
"description": "Pomija weryfikację wieku",
"name": "Omiń ograniczenia wiekowe"
},
"captions-selector": {
"description": "Selektor napisów dla ścieżek audio YouTube Music",
"menu": {
"autoload": "Automatycznie wybierz ostatnio używanych napisów",
"disable-captions": "Domyślnie, brak napisów"
},
"name": "Selektor napisów",
"prompt": {
"selector": {
"label": "Bieżący język napisów: {{language}}",
"none": "Brak",
"title": "Wybierz język napisów"
}
},
"templates": {
"title": "Otwórz selektor napisów"
}
},
"compact-sidebar": {
"description": "Zawsze ustawiaj pasek boczny w trybie kompaktowym",
"name": "Kompaktowy pasek boczny"
},
"crossfade": {
"description": "Przenikanie pomiędzy utworami",
"menu": {
"advanced": "Zaawansowane"
},
"name": "Płynne przejście [Beta]",
"prompt": {
"options": {
"multi-input": {
"fade-in-duration": "Czas wnikania (ms)",
"fade-out-duration": "Czas zanikania (ms)",
"fade-scaling": {
"label": "Skalowanie zanikania",
"linear": "Liniowe",
"logarithmic": "Logarytmiczne"
},
"seconds-before-end": "Przenikanie N sekund przed końcem"
},
"title": "Opcje przenikania"
}
}
},
"disable-autoplay": {
"description": "Wyłącza automatyczne odtwarzanie utworów",
"menu": {
"apply-once": "Tylko przy uruchomieniu aplikacji"
},
"name": "Wyłącz automatyczne odtwarzanie"
},
"discord": {
"backend": {
"already-connected": "Próbowano połączyć się przy aktywnym połączeniu",
"connected": "Połączono z Discordem",
"disconnected": "Odłączono od Discorda"
},
"description": "Pokaż znajomym, czego słuchasz dzięki Rich Presence",
"menu": {
"auto-reconnect": "Automatyczne wznawianie połączenia",
"clear-activity": "Wyczyść aktywność",
"clear-activity-after-timeout": "Wyczyść aktywność po czasie",
"connected": "Połączono",
"disconnected": "Odłączono",
"hide-duration-left": "Ukryj pozostały czas trwania",
"hide-github-button": "Ukryj przycisk do GitHub",
"play-on-youtube-music": "Odtwórz w YouTube Music",
"set-inactivity-timeout": "Ustaw limit czasu bezczynności"
},
"name": "Discord Rich Presence",
"prompt": {
"set-inactivity-timeout": {
"label": "Podaj limit czasu bezczynności w sekundach:",
"title": "Ustaw limit czasu bezczynności"
}
}
},
"downloader": {
"backend": {
"dialog": {
"error": {
"buttons": {
"ok": "OK"
},
"message": "Argh! Przepraszamy, pobieranie nie powiodło się…",
"title": "Błąd podczas pobierania!"
},
"start-download-playlist": {
"buttons": {
"ok": "OK"
},
"detail": "({{playlistSize}} utworów)",
"message": "Pobieranie playlisty {{playlistTitle}}",
"title": "Pobieranie rozpoczęte"
}
},
"feedback": {
"conversion-progress": "Konwertowanie: {{percent}}%",
"converting": "Konwertowanie…",
"done": "Gotowe: {{filePath}}",
"download-info": "Pobieranie {{artist}} - {{title}} {{videoId}}",
"download-progress": "Pobieranie: {{percent}}%",
"downloading": "Pobieranie…",
"downloading-counter": "Pobieranie {{current}}/{{total}} …",
"downloading-playlist": "Pobieranie playlisty \"{{playlistTitle}}\" - {{playlistSize}} utworów ({{playlistId}})",
"error-while-downloading": "Błąd pobierania \"{{author}} - {{title}}\": {{error}}",
"folder-already-exists": "Folder {{playlistFolder}} już istnieje",
"getting-playlist-info": "Pobieram informacje o playliście…",
"loading": "Ładowanie…",
"playlist-has-only-one-song": "Playlista zawiera tylko jeden element, zostanie pobrany bezpośrednio",
"playlist-id-not-found": "Nie znaleziono ID playlisty",
"playlist-is-empty": "Playlista jest pusta",
"playlist-is-mix-or-private": "Podczas pobierania informacji o playliście wystąpił błąd: upewnij się, że nie jest to playlista prywatna ani playlista „Składanki dla Ciebie”.\n\n{{error}}",
"preparing-file": "Przygotowuję plik…",
"saving": "Zapisuję…",
"trying-to-get-playlist-id": "Próbuję uzyskać ID playlisty: {{playlistId}}",
"video-id-not-found": "Nie znaleziono filmu",
"writing-id3": "Zapisywanie tagów ID3…"
}
},
"description": "Pobiera MP3/ źródło audio bezpośrednio z interfejsu",
"menu": {
"choose-download-folder": "Wybierz folder pobierania",
"download-playlist": "Pobierz playlistę",
"presets": "Predefiniowane ustawienia",
"skip-existing": "Pomiń istniejące pliki"
},
"name": "Pobieranie",
"renderer": {
"can-not-update-progress": "Nie można zaktualizować postępu"
},
"templates": {
"button": "Pobierz"
}
},
"exponential-volume": {
"description": "Sprawia, że suwak głośności jest proporcjonalna, dzięki czemu łatwiej jest wybrać niższą głośność.",
"name": "Proporcjonalna głośność"
},
"in-app-menu": {
"description": "Nadaje paskom menu elegancki, ciemny lub albumowy wygląd",
"menu": {
"hide-dom-window-controls": "Ukryj kontrolki okna DOM"
},
"name": "Menu w aplikacji"
},
"lumiastream": {
"description": "Dodaje obsługę Lumia Stream",
"name": "Lumia Stream [Beta]"
},
"lyrics-genius": {
"description": "Dodaje obsługę tekstów dla większości piosenek",
"menu": {
"romanized-lyrics": "Teksty zromanizowane"
},
"name": "Tekst piosenek od Genius",
"renderer": {
"fetched-lyrics": "Tekst dostarczony przez Genius"
}
},
"music-together": {
"description": "Pozwala na udostępnianie listy odtwarzania w taki sposób, że osoby po wejściu w link słuchają tego samego utworu co host (jeżeli słucha jej z owej listy odtwarzania)",
"dialog": {
"enter-host": "Wpisz ID hosta"
},
"internal": {
"save": "Zapisz",
"track-source": "Źródło utworu",
"unknown-user": "Nieznany użytkownik"
},
"menu": {
"click-to-copy-id": "Kopiuj ID hosta",
"close": "Zakończ host",
"connected-users": "Połączeni użytkownicy",
"disconnect": "Rozłącz z hosta",
"empty-user": "Brak połączonych użytkowników",
"host": "Host słuchania razem",
"join": "Połącz z hostem",
"permission": {
"all": "Połączeni użytkownicy mają kontrolę nad listą odtwarzania oraz playerem",
"host-only": "Tylko host może kontrolować listę odtwarzania oraz playera",
"playlist": "Połączeni użytkownicy maja kontrolę tylko nad listą odtwarzania"
},
"set-permission": "Zmień permisję kontroli",
"status": {
"disconnected": "Rozłączony",
"guest": "Połączony jako Użytkownik",
"host": "Połączony jako Host"
}
},
"name": "Słuchanie razem [Beta]",
"toast": {
"add-song-failed": "Wystąpił błąd z dodaniem muzyki",
"closed": "Host słuchania razem został zamknięty pomyślnie",
"disconnected": "Pomyślnie rozłączono z hosta słuchania razem",
"host-failed": "Wystąpił błąd z hostem słuchania razem",
"id-copied": "ID hosta został wklejony do schowka",
"id-copy-failed": "Wystąpił błąd z próbą skopiowania ID do schowka",
"join-failed": "Wystąpił błąd z dołączeniem do hosta",
"joined": "Dołączono pomyślnie do hosta słuchania razem",
"permission-changed": "Permisja hosta została zmieniona na \"{{permission}}\"",
"remove-song-failed": "Wystąpił błąd z usunięciem utworu",
"user-connected": "Do hosta dołączył {{name}}",
"user-disconnected": "{{name}} właśnie wyszedł z hosta"
}
},
"navigation": {
"description": "Strzałki nawigacyjne Dalej/Wstecz zintegrowane bezpośrednio z interfejsem, tak jak w Twojej ulubionej przeglądarce",
"name": "Nawigacja"
},
"no-google-login": {
"description": "Usuń przyciski i linki logowania Google z interfejsu",
"name": "Usuń logowanie do Google"
},
"notifications": {
"description": "Wyświetl powiadomienie, gdy rozpocznie się odtwarzanie utworu (interaktywne powiadomienia są dostępne w systemie Windows)",
"menu": {
"interactive": "Interaktywne powiadomienia",
"interactive-settings": {
"label": "Interaktywne ustawienia",
"submenu": {
"hide-button-text": "Ukryj tekst przycisku",
"refresh-on-play-pause": "Odśwież podczas odtwarzania/pauzy",
"tray-controls": "Otwórz/zamknij po kliknięciu ikony na pasku zadań"
}
},
"priority": "Priorytet powiadomień",
"toast-style": "Styl powiadomień \"Toast\"",
"unpause-notification": "Pokaż powiadomienie po wznowieniu"
},
"name": "Powiadomienia"
},
"picture-in-picture": {
"description": "Umożliwia przełączenie aplikacji w tryb obrazu w obrazie",
"menu": {
"always-on-top": "Zawsze na wierzchu",
"hotkey": {
"label": "Klawisz skrótu",
"prompt": {
"keybind-options": {
"hotkey": "Klawisz skrótu"
},
"label": "Wybierz klawisz skrótu do przełączania trybu obrazu w obrazie",
"title": "Klawisz skrótu obrazu w obrazie"
}
},
"save-window-position": "Zapisz pozycję okna",
"save-window-size": "Zapisz rozmiar okna",
"use-native-pip": "Użyj natywnego PiP dla przeglądarki"
},
"name": "Obraz w obrazie",
"templates": {
"button": "Obraz w obrazie"
}
},
"playback-speed": {
"description": "Słuchaj szybko, słuchaj powoli! Dodaje suwak kontrolujący prędkość utworu",
"name": "Szybkość odtwarzania",
"templates": {
"button": "Szybkość"
}
},
"precise-volume": {
"description": "Precyzyjnie kontroluj głośność za pomocą kółka myszy/klawiszy skrótu, z niestandardowym interfejsem HUD i konfigurowalnymi krokami głośności",
"menu": {
"arrows-shortcuts": "Sterowanie za pomocą klawiszy strzałek",
"custom-volume-steps": "Ustaw niestandardowe kroki głośności",
"global-shortcuts": "Globalne skróty klawiszowe"
},
"name": "Precyzyjna głośność",
"prompt": {
"global-shortcuts": {
"keybind-options": {
"decrease": "Zmniejsz głośność",
"increase": "Zwiększ głośność"
},
"label": "Wybierz globalne skróty klawiaturowe głośności:",
"title": "Globalne skróty klawiszowe głośności"
},
"volume-steps": {
"label": "Wybierz kroki zwiększania/zmniejszania głośności",
"title": "Kroki głośności"
}
}
},
"quality-changer": {
"backend": {
"dialog": {
"quality-changer": {
"detail": "Aktualna jakość: {{quality}}",
"message": "Wybierz jakość wideo:",
"title": "Wybierz jakość wideo"
}
}
},
"description": "Umożliwia zmianę jakości wideo za pomocą przycisku na nakładce wideo",
"name": "Zmieniacz jakości wideo"
},
"scrobbler": {
"menu": {
"listenbrainz": {
"token": "Podaj token użytkownika ListenBrainz"
}
},
"prompt": {
"lastfm": {
"api-key": "klucz API Last.fm"
},
"listenbrainz": {
"token": {
"label": "Podaj swój token użytkownika ListenBrainz:",
"title": "Token ListenBrainz"
}
}
}
},
"shortcuts": {
"description": "Umożliwia ustawienie globalnych skrótów klawiszowych do odtwarzania (odtwarzanie/pauza/następny/poprzedni) + wyłączanie OSD multimediów poprzez zastąpienie klawiszy multimediów, włączając kombinację klawiszy Ctrl/CMD + F w celu wyszukiwania, obsługę Linux MPRIS dla klawiszy multimediów oraz niestandardowe skróty klawiszowe dla zaawansowanych użytkowników",
"menu": {
"override-media-keys": "Zastąp klawisze multimediów",
"set-keybinds": "Ustaw globalne sterowanie utworem"
},
"name": "Skróty klawiszowe (oraz MPRIS)",
"prompt": {
"keybind": {
"keybind-options": {
"next": "Następny",
"play-pause": "Odtwarzanie / Pauza",
"previous": "Poprzedni"
},
"label": "Wybierz globalne skróty klawiszowe do sterowania utworami:",
"title": "Globalne skróty klawiszowe"
}
}
},
"skip-disliked-songs": {
"description": "Pomija nieulubione piosenki (zaznaczone łapką w dół)",
"name": "Pomijanie nieulubionych piosenek"
},
"skip-silences": {
"description": "Automatycznie pomijaj sekcje bez dźwięku w utworach",
"name": "Pomiń ciszę"
},
"sponsorblock": {
"description": "Automatycznie pomija fragmenty niebędące muzyką, takie jak wstęp/zakończenie lub fragmenty teledysków, w których utwór nie jest odtwarzany",
"name": "Pomiń nieistotne fragmenty"
},
"taskbar-mediacontrol": {
"description": "Steruj odtwarzaniem z paska zadań systemu Windows",
"name": "Kontroler odtwarzania z paska zadań"
},
"touchbar": {
"description": "Dodaje widżet do paska dotykowego dla użytkowników systemu macOS",
"name": "Pasek dotykowy"
},
"tuna-obs": {
"description": "Integracja z wtyczką OBS Tuna",
"name": "Tuna OBS"
},
"video-toggle": {
"description": "Dodaje przycisk do przełączania między trybem wideo a piosenki. Może również opcjonalnie usunąć całą kartę wideo",
"menu": {
"align": {
"label": "Wyrównanie",
"submenu": {
"left": "Lewo",
"middle": "Środek",
"right": "Prawo"
}
},
"force-hide": "Wymuś usunięcie zakładki wideo",
"mode": {
"label": "Tryb",
"submenu": {
"custom": "Niestandardowy przełącznik",
"disabled": "Wyłączony",
"native": "Natywny przełącznik"
}
}
},
"name": "Przełącznik wideo",
"templates": {
"button": "Utwór"
}
},
"visualizer": {
"description": "Dodaje wizualizator do odtwarzacza",
"menu": {
"visualizer-type": "Typ wizualizatora"
},
"name": "Wizualizator"
}
}
}

View File

@ -1,649 +0,0 @@
{
"common": {
"console": {
"plugins": {
"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"
}
}
},
"language": {
"code": "pt",
"local-name": "Português",
"name": "Portuguese"
},
"main": {
"console": {
"did-finish-load": {
"dev-tools": "Carregamento finalizado. DevTools aberto"
},
"i18n": {
"loaded": "i18n carregado"
},
"second-instance": {
"receive-command": "Comando recebido através do protocolo: \"{{command}}\""
},
"theme": {
"css-file-not-found": "Arquivo CSS \"{{cssFile}}\" não existe, ignorando"
},
"unresponsive": {
"details": "Erro sem resposta!\n{{error}}"
},
"when-ready": {
"clearing-cache-after-20s": "Limpando o cache do aplicativo"
},
"window": {
"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, 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}} precisa ser reiniciado para ter efeito",
"message": "\"{{pluginName}}\" precisa ser reiniciado",
"title": "É necessário reiniciar"
},
"unresponsive": {
"buttons": {
"quit": "Sair",
"relaunch": "Reiniciar",
"wait": "Espere"
},
"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": "Desabilitar atualizações",
"download": "Baixar",
"ok": "Ok"
},
"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": "Sobre",
"navigation": {
"label": "Navegação",
"submenu": {
"copy-current-url": "Copiar URL atual",
"go-back": "Voltar",
"go-forward": "Avançar",
"quit": "Saída",
"restart": "Reiniciar aplicativo"
}
},
"options": {
"label": "Opções",
"submenu": {
"advanced-options": {
"label": "Opções avançadas",
"submenu": {
"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": "Reinicie as alterações de configurações feitas",
"set-proxy": {
"label": "Definir proxy",
"prompt": {
"label": "Inserir Endereço do Proxy: (deixe em branco para desativar)",
"placeholder": "Exemplo: SOCKS5://127.0.0.1:9999",
"title": "Definir proxy"
}
},
"toggle-dev-tools": "Ativar DevTools"
}
},
"always-on-top": "Sempre no topo",
"auto-update": "Atualização automática",
"hide-menu": {
"dialog": {
"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": "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": "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": "Indefinido"
},
"tray": {
"label": "Bandeja",
"submenu": {
"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": "Tweaks Visuais",
"submenu": {
"like-buttons": {
"default": "Padrão",
"force-show": "Forçar mostrar",
"hide": "Esconder",
"label": "Botões de curtida"
},
"remove-upgrade-button": "Remover botão upgrade",
"theme": {
"label": "Tema",
"submenu": {
"import-css-file": "Importar arquivo CSS personalizado",
"no-theme": "Sem tema"
}
}
}
}
}
},
"plugins": {
"enabled": "Ativado",
"label": "Plugins",
"new": "NOVO"
},
"view": {
"label": "Ver",
"submenu": {
"force-reload": "Forçar Recarregamento",
"reload": "Recarregar",
"reset-zoom": "Tamanho Atual",
"toggle-fullscreen": "Ativar Tela Cheia",
"zoom-in": "Zoom Dentro",
"zoom-out": "Zoom Fora"
}
}
},
"tray": {
"next": "Próximo",
"play-pause": "Play/Pausa",
"previous": "Anterior",
"quit": "Sair",
"restart": "Reiniciar aplicativo",
"show": "Mostrar janela",
"tooltip": {
"default": "YouTube Music",
"with-song-info": "YouTube Music: {{artist}} - {{title}}"
}
}
},
"plugins": {
"adblocker": {
"description": "Bloquear todos os anúncios e rastreamento automaticamente",
"menu": {
"blocker": "Bloqueador"
},
"name": "Bloqueador de anúncios"
},
"album-actions": {
"description": "Adiciona os botões Gostei e Não Gostei para ser aplicado a todas as músicas em uma lista de reprodução ou álbum.",
"name": "Ações no álbum"
},
"album-color-theme": {
"description": "Aplica um tema dinâmico e efeitos visuais com base na paleta de cores do álbum",
"name": "Tema de cores do álbum"
},
"ambient-mode": {
"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",
"submenu": {
"pixels": "{{blurAmount}} píxeis"
}
},
"buffer": {
"label": "Buffer",
"submenu": {
"buffer": "{{buffer}}"
}
},
"opacity": {
"label": "Opacidade",
"submenu": {
"percent": "{{opacity}}%"
}
},
"quality": {
"label": "Qualidade",
"submenu": {
"pixels": "{{quality}} píxeis"
}
},
"size": {
"label": "Tamanho",
"submenu": {
"percent": "{{size}}%"
}
},
"smoothness-transition": {
"label": "Transição suave",
"submenu": {
"during": "Durante {{interpolationTime}}s"
}
},
"use-fullscreen": {
"label": "Tela cheia"
}
},
"name": "Modo Ambiente"
},
"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"
},
"blur-nav-bar": {
"description": "Torna a barra de navegação transparente e desfocada",
"name": "Barra de navegação desfocada"
},
"bypass-age-restrictions": {
"description": "Ignorar a verificação de idade do YouTube",
"name": "Ignorar restrições de idade"
},
"captions-selector": {
"description": "Seletor de legenda para faixas de áudio do YouTube Music",
"menu": {
"autoload": "Selecionar automaticamente a última legenda usada",
"disable-captions": "Sem legendas por padrão"
},
"name": "Seletor de legendas",
"prompt": {
"selector": {
"label": "Idioma da legenda atual: {{language}}",
"none": "Nenhuma",
"title": "Selecione o idioma da legenda"
}
},
"templates": {
"title": "Seletor de legendas aberto"
}
},
"compact-sidebar": {
"description": "Definir sempre a barra lateral no modo compacto",
"name": "Barra lateral compacta"
},
"crossfade": {
"description": "Transição entre músicas",
"menu": {
"advanced": "Avançado"
},
"name": "Transição entre músicas [Beta]",
"prompt": {
"options": {
"multi-input": {
"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 final"
},
"title": "Opções de transição"
}
}
},
"disable-autoplay": {
"description": "Faz a música começar no modo \"pausado\"",
"menu": {
"apply-once": "Aplicar apenas na inicialização"
},
"name": "Desativar reprodução automática"
},
"discord": {
"backend": {
"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 você ouve com Rich Presence",
"menu": {
"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 de link do GitHub",
"play-on-youtube-music": "Reproduzir no YouTube Music",
"set-inactivity-timeout": "Definir tempo limite de inatividade"
},
"name": "Discord Rich Presence",
"prompt": {
"set-inactivity-timeout": {
"label": "Insira o tempo limite de inatividade em segundos:",
"title": "Definir tempo limite de inatividade"
}
}
},
"downloader": {
"backend": {
"dialog": {
"error": {
"buttons": {
"ok": "OK"
},
"message": "Poxa! Desculpe, o download falhou…",
"title": "Erro no download!"
},
"start-download-playlist": {
"buttons": {
"ok": "OK"
},
"detail": "({{playlistSize}} músicas)",
"message": "Baixando lista de reprodução {{playlistTitle}}",
"title": "Download iniciado"
}
},
"feedback": {
"conversion-progress": "Conversão: {{percent}}%",
"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": "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 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": "Escrevendo tags ID3…"
}
},
"description": "Baixa MP3 / fonte de áudio diretamente da interface",
"menu": {
"choose-download-folder": "Escolha a pasta de download",
"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": "Baixar"
}
},
"exponential-volume": {
"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 controles da janela DOM"
},
"name": "Menu no aplicativo"
},
"lumiastream": {
"description": "Adiciona suporte Lumia Stream",
"name": "Lumia Stream [Beta]"
},
"lyrics-genius": {
"description": "Adiciona suporte a letras para a maioria das músicas",
"menu": {
"romanized-lyrics": "Letras romanizadas"
},
"name": "Letras Genius",
"renderer": {
"fetched-lyrics": "Buscar letras no Genius"
}
},
"music-together": {
"description": "Compartilha a playlist com outros. Quando o host tocar uma música, todos poderão ouvir a mesma canção",
"dialog": {
"enter-host": "Digite o ID do Host"
},
"internal": {
"save": "Salvar",
"track-source": "Fonte da faixa",
"unknown-user": "Usuário Desconhecido"
},
"menu": {
"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": "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 controle",
"status": {
"disconnected": "Desconectado",
"guest": "Conectado como Convidado",
"host": "Conectado como Host"
}
},
"name": "Música Juntos [Beta]",
"toast": {
"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 Música Juntos",
"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 Música Juntos",
"joined": "Entrou em Música Juntos",
"permission-changed": "A permissão do Música Juntos foi alterada para \"{{permission}}\"",
"remove-song-failed": "Falha ao remover música",
"user-connected": "{{name}} entrou em Música Juntos",
"user-disconnected": "{{name}} saiu do Música Juntos"
}
},
"navigation": {
"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 e links de login do Google da interface",
"name": "Sem login do Google"
},
"notifications": {
"description": "Exibir uma notificação quando uma música começar a tocar (notificações interativas estão disponíveis no Windows)",
"menu": {
"interactive": "Notificações interativas",
"interactive-settings": {
"label": "Configurações interativas",
"submenu": {
"hide-button-text": "Ocultar texto do botão",
"refresh-on-play-pause": "Atualizar ao reproduzir/pausar",
"tray-controls": "Abrir/Fechar no clique da bandeja"
}
},
"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 o aplicativo para o modo picture-in-picture",
"menu": {
"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 alternar o picture-in-picture",
"title": "Tecla de atalho picture-in-picture"
}
},
"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": {
"button": "Picture-in-picture"
}
},
"playback-speed": {
"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 usando a roda do mouse/teclas de atalho, com um HUD personalizado e etapas de volume personalizáveis",
"menu": {
"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",
"prompt": {
"global-shortcuts": {
"keybind-options": {
"decrease": "Diminuir o volume",
"increase": "Aumentar o volume"
},
"label": "Escolha atalhos de teclado de volume global:",
"title": "Atalhos de teclado de volume global"
},
"volume-steps": {
"label": "Escolha as etapas de aumento/diminuição de volume",
"title": "Etapas de volume"
}
}
},
"quality-changer": {
"backend": {
"dialog": {
"quality-changer": {
"detail": "Qualidade atual: {{quality}}",
"message": "Escolha a qualidade do vídeo:",
"title": "Escolha a qualidade do 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"
},
"shortcuts": {
"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 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",
"previous": "Anterior"
},
"label": "Escolha atalhos de teclado globais para controle de músicas:",
"title": "Atalhos globais do teclado"
}
}
},
"skip-disliked-songs": {
"description": "Pula músicas com 'não gostei'",
"name": "Pular músicas com 'não gostei'"
},
"skip-silences": {
"description": "Pular automaticamente seções de silêncio nas músicas",
"name": "Pular silêncios"
},
"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)"
},
"taskbar-mediacontrol": {
"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 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. Também pode, opcionalmente, remover toda a guia de vídeo",
"menu": {
"align": {
"label": "Alinhamento",
"submenu": {
"left": "Esquerda",
"middle": "Centro",
"right": "Direita"
}
},
"force-hide": "Forçar remoção da guia de vídeo",
"mode": {
"label": "Modo",
"submenu": {
"custom": "Alternar personalizado",
"disabled": "Desabilitado",
"native": "Alternar nativo"
}
}
},
"name": "Botão de Alternar Vídeo",
"templates": {
"button": "Música"
}
},
"visualizer": {
"description": "Adiciona um visualizador ao player",
"menu": {
"visualizer-type": "Tipo de visualizador"
},
"name": "Visualizer"
}
}
}

View File

@ -1,682 +0,0 @@
{
"common": {
"console": {
"plugins": {
"execute-failed": "Ошибка загрузки плагина {{pluginName}}::{{contextName}}",
"executed-at-ms": "Плагин {{pluginName}}::{{contextName}} загружен за {{ms}}мс",
"initialize-failed": "Ошибка инициализации плагина \"{{pluginName}}\"",
"load-all": "Загружаем все плагины",
"load-failed": "Ошибка загрузки плагина \"{{pluginName}}\"",
"loaded": "Плагин \"{{pluginName}}\" загружен",
"unload-failed": "Ошибка выгрузки плагина \"{{pluginName}}\"",
"unloaded": "Плагин \"{{pluginName}}\" выгружен"
}
}
},
"language": {
"code": "ru",
"local-name": "Русский",
"name": "Russian"
},
"main": {
"console": {
"did-finish-load": {
"dev-tools": "Загрузка завершена. DevTools открыт"
},
"i18n": {
"loaded": "i18n загружен"
},
"second-instance": {
"receive-command": "Получена команда по протоколу: \"{{command}}\""
},
"theme": {
"css-file-not-found": "CSS файл \"{{cssFile}}\" не существует, игнорирую"
},
"unresponsive": {
"details": "Приложение не отвечает!\n{{error}}"
},
"when-ready": {
"clearing-cache-after-20s": "Очищаю кеш приложения"
},
"window": {
"tried-to-render-offscreen": "Окно попробовало открыться за пределами экрана, размер окна={{windowSize}}, разрешение экрана={{displaySize}}, местоположение={{position}}"
}
},
"dialog": {
"hide-menu-enabled": {
"detail": "Меню скрыто, 'Alt' чтобы показать его ('Escape' если используете внутреннее меню приложения)",
"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": "OK"
},
"detail": "Новая версия доступна для загрузки на {{downloadLink}}",
"message": "Доступная новая версия",
"title": "Доступно обновление"
}
},
"menu": {
"about": "О приложении",
"navigation": {
"label": "Навигация",
"submenu": {
"copy-current-url": "Скопировать текущий 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": "Переопределить User-Agent",
"restart-on-config-changes": "Перезапускать при изменениях конфига",
"set-proxy": {
"label": "Задать прокси",
"prompt": {
"label": "Введите прокси адрес (оставьте поле пустым для отключения)",
"placeholder": "Пример: SOCKS5://127.0.0.1:9999",
"title": "Задать прокси"
}
},
"toggle-dev-tools": "Включить DevTools"
}
},
"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": "Default",
"force-show": "Всегда показывать",
"hide": "Скрывать",
"label": "Кнопка лайка"
},
"remove-upgrade-button": "Убрать кнопку Youtube Premium",
"theme": {
"label": "Тема",
"submenu": {
"import-css-file": "Импортировать кастомный CSS файл",
"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": "YouTube Music",
"with-song-info": "YouTube Music: {{artist}} - {{title}}"
}
}
},
"plugins": {
"adblocker": {
"description": "Блокируйте всю рекламу и трекинг сразу после установки",
"menu": {
"blocker": "Блокировщик"
},
"name": "Блокировщик рекламы"
},
"album-actions": {
"description": "Добавляет кнопки Undislike, Dislike и Unlike, чтобы применять их на все композиции в плейлисте или альбоме",
"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": "Режим Ambient"
},
"audio-compressor": {
"description": "Применяет компрессию к аудио (уменьшает громкость самых громких частей сигнала и повышает громкость самых тихих частей)",
"name": "Аудио компрессор"
},
"blur-nav-bar": {
"description": "Делает панель навигации прозрачной и размытой",
"name": "Размытие панели навигации"
},
"bypass-age-restrictions": {
"description": "Обход проверки возраста на YouTube",
"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": "Discord Rich Presence",
"prompt": {
"set-inactivity-timeout": {
"label": "Введите таймер неактивности в секундах:",
"title": "Задать таймер неактивности"
}
}
},
"downloader": {
"backend": {
"dialog": {
"error": {
"buttons": {
"ok": "OK"
},
"message": "Ой-ой! Прошу простить, загрузка дала сбой…",
"title": "Error in download!"
},
"start-download-playlist": {
"buttons": {
"ok": "OK"
},
"detail": "({{playlistSize}} песен)",
"message": "Скачиваем плейлист {{playlistTitle}}",
"title": "Загрузка началась"
}
},
"feedback": {
"conversion-progress": "Конвертация: {{percent}}%",
"converting": "Конвертация…",
"done": "Выполнено: {{filePath}}",
"download-info": "Скачиваем {{artist}}-{{title}} {{videoId}}",
"download-progress": "Download: {{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": "Пытаемся получить ID плейлиста: {{playlistId}}",
"video-id-not-found": "Видео не найдено",
"writing-id3": "Пишем ID3 теги…"
}
},
"description": "Скачивать MP3 / исходное аудио напрямую из интерфейса",
"menu": {
"choose-download-folder": "Выберите папку для загрузок",
"download-playlist": "Скачать плейлист",
"presets": "Пресеты",
"skip-existing": "Пропускать уже существующие файлы"
},
"name": "Загрузчик",
"renderer": {
"can-not-update-progress": "Невозможно обновить прогресс"
},
"templates": {
"button": "Скачать"
}
},
"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": "Романизированный текст (any -> en)"
},
"name": "Тексты песен от Genius",
"renderer": {
"fetched-lyrics": "Текст от Genius был получен"
}
},
"music-together": {
"description": "Поделитесь плейлистом с другими. Когда хост играет песню, все остальные слышат ту же песню",
"dialog": {
"enter-host": "Введите ID хоста"
},
"internal": {
"save": "Сохранить",
"track-source": "Источник трека",
"unknown-user": "Неизвестный пользователь"
},
"menu": {
"click-to-copy-id": "Копировать ID хоста",
"close": "Закрыть Music Together",
"connected-users": "Подключенные пользователи",
"disconnect": "Отключиться от Music Together",
"empty-user": "Нет подключенных пользователей",
"host": "Хост Music Together",
"join": "Подключиться к Music Together",
"permission": {
"all": "Позволить гостям управлять плейлистом и плеером",
"host-only": "Только хост может управлять плейлистом и плеером",
"playlist": "Позволить гостям управлять плейлистом"
},
"set-permission": "Изменить разрешения на управление разрешениями",
"status": {
"disconnected": "Отключено",
"guest": "Подключен как гость",
"host": "Подключен как хост"
}
},
"name": "Music Together [Beta]",
"toast": {
"add-song-failed": "Не удалось добавить песню",
"closed": "Music Together закрыт",
"disconnected": "Music Together отключен",
"host-failed": "Не удалось запустить Music Together",
"id-copied": "ID хоста скопирован в буфер обмена",
"id-copy-failed": "Не удалось скопировать айди хоста в буфер обмена",
"join-failed": "Не удалось присоединиться к Music Together",
"joined": "Присоединился к Music Together",
"permission-changed": "Разрешения Music Together изменены на \"{{permission}}\"",
"remove-song-failed": "Не удалось удалить песню",
"user-connected": "{{name}} присоединился к Music Together",
"user-disconnected": "{{name}} отключился от Music Together"
}
},
"navigation": {
"description": "Стрелки навигации \"вперед/назад\" интегрированы в интерфейс, как в вашем любимом браузере",
"name": "Навигация"
},
"no-google-login": {
"description": "Убрать из интерфейса кнопки и ссылки для входа через Google",
"name": "Нет входа в систему Google"
},
"notifications": {
"description": "Показывать уведомления о начале воспроизведения песни (интерактивные уведомления доступны в Windows)",
"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": "Использовать нативный 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": "Добавьте поддержку скробблинга (например, last.fm, Listenbrainz)",
"menu": {
"lastfm": {
"api-settings": "Настройки API Last.fm"
},
"listenbrainz": {
"token": "Введите токен пользователя ListenBrainz"
},
"scrobble-other-media": "Скробблинг других медиа"
},
"name": "Скробблер",
"prompt": {
"lastfm": {
"api-key": "Ключ API Last.fm",
"api-secret": "Секрет API Last.fm"
},
"listenbrainz": {
"token": {
"label": "Введите токен пользователя ListenBrainz:",
"title": "Токен ListenBrainz"
}
}
}
},
"shortcuts": {
"description": "Позволяет задать глобальные горячие клавиши для воспроизведения (play/pause/next/previous) и отключить экранное мультимедийное меню, изменяя мультимедийные клавиши, также включает 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"
},
"taskbar-mediacontrol": {
"description": "Управляйте воспроизведением с панели задач Windows",
"name": "Управление мультимедиа на панели задач"
},
"touchbar": {
"description": "Добавляет виджет тачбара для пользователей macOS",
"name": "Тачбар"
},
"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

@ -1,128 +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": "th",
"local-name": "ภาษาไทย",
"name": "Thai"
},
"main": {
"console": {
"did-finish-load": {
"dev-tools": "การโหลดเสร็จสิ้น DevTools ได้ถูกเปิดแล้ว"
},
"i18n": {
"loaded": "โหลด i18n แล้ว"
},
"second-instance": {
"receive-command": "คำสั่งที่ได้รับผ่านโปรโตคอล: \"{{command}}\""
},
"theme": {
"css-file-not-found": "กำลังเพิกเฉยไฟล์ CSS \"{{cssFile}}\" เนื่องจากไม่มีอยู่"
},
"unresponsive": {
"details": "มีข้อผิดพลาดจากไม่การตอบสนอง!\n{{error}}"
},
"when-ready": {
"clearing-cache-after-20s": "กำลังล้างแคชของแอป"
},
"window": {
"tried-to-render-offscreen": "หน้าต่างพยายามแสดงผลเกินขนาดหน้าจอ windowSize={{windowSize}}, displaySize={{displaySize}}, position={{position}}"
}
},
"dialog": {
"unresponsive": {
"buttons": {
"quit": "เลิก"
}
},
"update-available": {
"buttons": {
"disable": "ปิดใช้งานการอัปเดต",
"download": "ดาวน์โหลด",
"ok": "ตกลง"
},
"detail": "มีเวอร์ชันใหม่ให้ดาวน์โหลดแล้วที่ {{downloadLink}}",
"message": "มีเวอร์ชันใหม่ให้ใช้งานแล้ว",
"title": "อัปเดตพร้อมใช้งาน"
}
},
"menu": {
"about": "เกี่ยวกับ",
"navigation": {
"submenu": {
"copy-current-url": "คัดลอก 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": "แทนที่ User-Agent"
}
}
}
}
}
},
"plugins": {
"downloader": {
"backend": {
"feedback": {
"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": "ไม่พบ ID เพลย์ลิสต์",
"playlist-is-empty": "เพลย์ลิสต์ว่างเปล่า",
"playlist-is-mix-or-private": "เกิดข้อผิดพลาดในการรับข้อมูลเพลย์ลิสต์: ตรวจสอบให้แน่ใจว่าไม่ใช่เพลย์ลิสต์ส่วนตัวหรือเพลย์ลิสต์ \"มิกซ์สำหรับคุณ\"\n\n{{error}}",
"preparing-file": "กำลังเตรียมไฟล์…",
"saving": "กำลังบันทึก…",
"trying-to-get-playlist-id": "กำลังพยายามรับ ID เพลย์ลิสต์: {{playlistId}}",
"video-id-not-found": "ไม่พบวิดีโอ",
"writing-id3": "กำลังเขียนแท็ก ID3…"
}
},
"description": "ดาวน์โหลด MP3 / เสียงต้นฉบับโดยตรงจากอินเทอร์เฟซ",
"menu": {
"choose-download-folder": "เลือกโฟลเดอร์ดาวน์โหลด",
"download-playlist": "ดาวน์โหลดเพลย์ลิสต์",
"skip-existing": "ข้ามไฟล์ที่มีอยู่แล้ว"
},
"name": "ตัวดาวน์โหลด",
"renderer": {
"can-not-update-progress": "ไม่สามารถอัปเดตความคืบหน้าได้"
},
"templates": {
"button": "ดาวน์โหลด"
}
}
}
}

View File

@ -1,682 +0,0 @@
{
"common": {
"console": {
"plugins": {
"execute-failed": "{{pluginName}}::{{contextName}} eklentisi çalıştırılamadı",
"executed-at-ms": "{{pluginName}}::{{contextName}} eklentisi {{ms}}ms'de çalıştırıldı",
"initialize-failed": "\"{{pluginName}}\" eklentisi başlatılamadı",
"load-all": "Tüm eklentiler yükleniyor",
"load-failed": "\"{{pluginName}}\" eklentisi yüklenemedi",
"loaded": "\"{{pluginName}}\" eklentisi yüklendi",
"unload-failed": "\"{{pluginName}}\" eklentisi çıkartılamadı",
"unloaded": "\"{{pluginName}}\" eklentisi çıkartıldı"
}
}
},
"language": {
"code": "tr",
"local-name": "Türkçe",
"name": "Turkish"
},
"main": {
"console": {
"did-finish-load": {
"dev-tools": "Yükleme tamamlandı. Geliştirici araçlarııldı"
},
"i18n": {
"loaded": "i18n yüklendi"
},
"second-instance": {
"receive-command": "Protokol üzerinden alınan komut: \"{{command}}\""
},
"theme": {
"css-file-not-found": "\"{{cssFile}}\" adlı CSS dosyası bulunamadı, yok sayılıyor"
},
"unresponsive": {
"details": "Yanıt verilmedi!\n{{error}}"
},
"when-ready": {
"clearing-cache-after-20s": "Uygulama ön belleği temizleme"
},
"window": {
"tried-to-render-offscreen": "Pencere ekranın dışında oluşturulmaya çalışıldı, windowSize={{windowSize}}, displaySize={{displaySize}}, position={{position}}"
}
},
"dialog": {
"hide-menu-enabled": {
"detail": "Menü gizli, göstermek için 'Alt' tuşunu kullanın (veya Uygulama İçi Menüyü kullanıyorsanız 'Escape' tuşunu kullanın)",
"message": "Menüyü gizle etkinleştirildi",
"title": "Menüyü gizle etkinleştirildi"
},
"need-to-restart": {
"buttons": {
"later": "Daha Sonra",
"restart-now": "Şimdi yeniden başlat"
},
"detail": "\"{{pluginName}}\" eklentisinin çalışabilmesi için yeniden başlatman gerekiyor",
"message": "\"{{pluginName}}\" için yeniden başlatman gerekiyor",
"title": "Uygulamayı yeniden başlatman gerekiyor"
},
"unresponsive": {
"buttons": {
"quit": ıkış",
"relaunch": "Tekrar Başlat",
"wait": "Bekle"
},
"detail": "Rahatsızlık için özür dileriz! Lütfen ne yapacağınızı seçin:",
"message": "Uygulama yanıt vermiyor",
"title": "Pencere yanıt vermiyor"
},
"update-available": {
"buttons": {
"disable": "Güncellemeleri devre dışı bırak",
"download": "İndir",
"ok": "Tamam"
},
"detail": "Yeni bir sürüm mevcut. {{downloadLink}} adresi üzerinden indirebilirsin",
"message": "Yeni bir sürüm mevcut",
"title": "Güncelleme Mevcut"
}
},
"menu": {
"about": "Hakkında",
"navigation": {
"label": "Navigasyon",
"submenu": {
"copy-current-url": "Geçerli Url'yi kopyala",
"go-back": "Geri dön",
"go-forward": "İlerle",
"quit": ıkış",
"restart": "Uygulamayı Yeniden Başlat"
}
},
"options": {
"label": "Seçenekler",
"submenu": {
"advanced-options": {
"label": "Gelişmiş Seçenekler",
"submenu": {
"auto-reset-app-cache": "Uygulama başlatıldığında uygulama önbelleğini sıfırla",
"disable-hardware-acceleration": "Donanım hızlandırmayı devre dışı bırak",
"edit-config-json": "Düzenle config.json",
"override-user-agent": "\"User-Agent \"ı geçersiz kıl",
"restart-on-config-changes": "Yapılandırma değişikliğinde yeniden başlat",
"set-proxy": {
"label": "Proxy ayarla",
"prompt": {
"label": "Proxy Adresini Gir: (devre dışı bırakmak için boş bırakın)",
"placeholder": "Örnek: SOCKS5://127.0.0.1:9999",
"title": "Proxy ayarla"
}
},
"toggle-dev-tools": "DevTools'u Aç / Kapat"
}
},
"always-on-top": "Her zaman üstte",
"auto-update": "Otomatik Güncelleme",
"hide-menu": {
"dialog": {
"message": "Menü bir sonraki açılışta gizlenecektir, göstermek için [Alt] tuşunu kullanın (veya uygulama-içi-menü kullanıyorsanız [`] tuşuna geri basın)",
"title": "Gizli Menü Aktif"
},
"label": "Gizli Menü"
},
"language": {
"dialog": {
"message": "Dil değişikliği yeniden başlattıktan sonra etkinleşecektir",
"title": "Dil değiştirildi"
},
"label": "Dil",
"submenu": {
"to-help-translate": "Çeviriye yardım etmek ister misiniz? Buraya tıklayın"
}
},
"resume-on-start": "Uygulama başlatıldığında son şarkıyı devam ettir",
"single-instance-lock": "Tek Örnek Kilidi",
"start-at-login": "Başlangıçta çalıştır",
"starting-page": {
"label": "Başlangıç sayfası",
"unset": "Ayarlanmadı"
},
"tray": {
"label": "Tepsi",
"submenu": {
"disabled": "Devre Dışı",
"enabled-and-hide-app": "Uygulamayı etkinleştirin gizleyin",
"enabled-and-show-app": "Etkinleştir ve uygulamayı göster",
"play-pause-on-click": "Tıklandığında Oynat/Duraklat"
}
},
"visual-tweaks": {
"label": "Görsel İnce Ayarlar",
"submenu": {
"like-buttons": {
"default": "Varsayılan",
"force-show": "Zorla göster",
"hide": "Gizle",
"label": "Beğenme düğmeleri"
},
"remove-upgrade-button": "Yükseltme düğmesini kaldır",
"theme": {
"label": "Tema",
"submenu": {
"import-css-file": "Özel CSS dosyanı içeri aktar",
"no-theme": "Tema Yok"
}
}
}
}
}
},
"plugins": {
"enabled": "Aktif",
"label": "Eklentiler",
"new": "YENİ"
},
"view": {
"label": "Görüntü",
"submenu": {
"force-reload": "Zorla yeniden başlat",
"reload": "Yeniden Başlat",
"reset-zoom": "Asıl Boyut",
"toggle-fullscreen": "Tam Ekran'a Geçiş",
"zoom-in": "Yakınlaştır",
"zoom-out": "Uzaklaştır"
}
}
},
"tray": {
"next": "Sonraki",
"play-pause": "Oynat/Durdur",
"previous": "Önceki",
"quit": ıkış",
"restart": "Yeniden başlat",
"show": "Pencereyi görüntüle",
"tooltip": {
"default": "YouTube Müzik",
"with-song-info": "YouTube Müzik: {{artist}} - {{title}}"
}
}
},
"plugins": {
"adblocker": {
"description": "Tüm reklamları ve izleyicileri engelle",
"menu": {
"blocker": "Engelleyici"
},
"name": "Reklam Engelleyici"
},
"album-actions": {
"description": "Çalma listesindeki veya albümdeki tüm şarkılara Beğendim ve Beğenmedim düğmeleri ekler",
"name": "Albüm Eylemleri"
},
"album-color-theme": {
"description": "Albümün renk paletine dayalı dinamik bir tema ve efektler uygular",
"menu": {
"color-mix-ratio": {
"label": "Renk karışım oranı",
"submenu": {
"percent": "{{ratio}}%"
}
}
},
"name": "Albüm Renk Teması"
},
"ambient-mode": {
"description": "Videodaki yumuşak renkleri ekranınızın arka planına yansıtarak bir ışık efekti uygular",
"menu": {
"blur-amount": {
"label": "Bulanıklık miktarı",
"submenu": {
"pixels": "{{blurAmount}} piksel"
}
},
"buffer": {
"label": "Önbellek",
"submenu": {
"buffer": "{{buffer}}"
}
},
"opacity": {
"label": "Opaklık Miktarı",
"submenu": {
"percent": "{{opacity}}%"
}
},
"quality": {
"label": "Kalite",
"submenu": {
"pixels": "{{quality}} piksel"
}
},
"size": {
"label": "Boyut",
"submenu": {
"percent": "{{size}}%"
}
},
"smoothness-transition": {
"label": "Yumuşak Geçiş",
"submenu": {
"during": "{{interpolationTime}} saniye boyunca"
}
},
"use-fullscreen": {
"label": "Tam ekran kullanılıyor"
}
},
"name": "Ambiyans Modu"
},
"audio-compressor": {
"description": "Ses sıkıştırma (dalganın en gürültülü bölümlerinin ses düzeyini azaltır ve daha yumuşak bölümlerin ses düzeyini artırır)",
"name": "Ses Sıkıştırma"
},
"blur-nav-bar": {
"description": "Gezinme çubuğunu şeffaf ve bulanık yapar",
"name": "Navigasyon barını bulanıklaştır"
},
"bypass-age-restrictions": {
"description": "YouTube yaş doğrulamasını atla",
"name": "Yaş doğrulamasını atla"
},
"captions-selector": {
"description": "YouTube Music için altyazı seçici",
"menu": {
"autoload": "Son kullanılan altyazıyı otomatik olarak seç",
"disable-captions": "Varsayılan olarak altyazı yok"
},
"name": "Altyazı Seçici",
"prompt": {
"selector": {
"label": "Geçerli altyazı dili: {{language}}",
"none": "Hiçbiri",
"title": "Altyazı dilini seç"
}
},
"templates": {
"title": "Altyazı seçiciyi aç"
}
},
"compact-sidebar": {
"description": "Her zaman kompakt kenar çubugu",
"name": "Kompakt Kenar Çubuğu"
},
"crossfade": {
"description": "Şarkılar arasında Çapraz Geçiş",
"menu": {
"advanced": "Gelişmiş"
},
"name": "Çapraz Geçiş [Beta]",
"prompt": {
"options": {
"multi-input": {
"fade-in-duration": "Güçlenme süresi (ms)",
"fade-out-duration": "Zayıflama süresi (ms)",
"fade-scaling": {
"label": "Zayıflama Ölçeği",
"linear": "Doğrusal",
"logarithmic": "Logaritmik"
},
"seconds-before-end": "Bitişten N saniye önce çapraz geçiş"
},
"title": "Çapraz Geçiş ayarları"
}
}
},
"disable-autoplay": {
"description": "Şarkıların otomatik olarak duraklatılmasını sağlar",
"menu": {
"apply-once": "Yalnızca ilk şarkı için geçerlidir"
},
"name": "Otomatik oynatmayı devre dışı bırak"
},
"discord": {
"backend": {
"already-connected": "Aktif bağlantı olduğu halde bağlantı kurulmaya çalışıldı",
"connected": "Discord'a bağlandı",
"disconnected": "Discord ile bağlantı kesildi"
},
"description": "Rich Presence ile Discord'da ne dinlediğinizi gösterin",
"menu": {
"auto-reconnect": "Otomatik yeniden bağlan",
"clear-activity": "Etkinliği temizle",
"clear-activity-after-timeout": "Zaman aşımından sonra etkinliği temizle",
"connected": "Bağlı",
"disconnected": "Bağlantı kesildi",
"hide-duration-left": "Kalan süreyi gizle",
"hide-github-button": "GitHub bağlantısını gizle",
"play-on-youtube-music": "YouTube Music de oynat",
"set-inactivity-timeout": "Hareketsizlik zaman aşımını ayarla"
},
"name": "Discord Etkinlik Durumu",
"prompt": {
"set-inactivity-timeout": {
"label": "Hareketsizlik zaman aşımını saniye cinsinden girin:",
"title": "Hareketsizlik zaman aşımını ayarla"
}
}
},
"downloader": {
"backend": {
"dialog": {
"error": {
"buttons": {
"ok": "Tamam"
},
"message": "Argh! Özür dilerim, indirme başarısız oldu…",
"title": "İndirme sırasında bir hata meydana geldi!"
},
"start-download-playlist": {
"buttons": {
"ok": "Tamam"
},
"detail": "({{playlistSize}} şarkı)",
"message": "Oynatma listesini indir : {{playlistTitle}}",
"title": "İndirme Başladı"
}
},
"feedback": {
"conversion-progress": "Dönüştürme : {{percent}}%",
"converting": "Dönüştürülüyor…",
"done": "Tamamlandı: {{filePath}}",
"download-info": "{{artist}} - {{title}} [{{videoId}} indiriliyor",
"download-progress": "İndirme : {{percent}}%",
"downloading": "İndiriliyor…",
"downloading-counter": "İndiriliyor {{current}}/{{total}}…",
"downloading-playlist": "\"{{playlistTitle}}\" şarkı listesi indiriliyor - {{playlistSize}} şarkı ({{playlistId}})",
"error-while-downloading": "\"{{author}} - {{title}}\" indirilirken hata oluştu: {{error}}",
"folder-already-exists": "{{playlistFolder}} klasörü zaten mevcut",
"getting-playlist-info": "Oynatma listesi bilgisi alınıyor…",
"loading": "Yükleniyor…",
"playlist-has-only-one-song": "Oynatma listesinde yalnızca bir şarkı var, doğrudan indiriliyor",
"playlist-id-not-found": "Oynatma listesi ID'si bulunamadı",
"playlist-is-empty": "Oynatma listesi boş",
"playlist-is-mix-or-private": "Çalma listesi bilgisi alınırken hata oluştu: özel veya \"Size özel karışık\" bir çalma listesi olmadığından emin olun\n\n{{error}}",
"preparing-file": "Dosya Hazırlanıyor…",
"saving": "Kaydediliyor…",
"trying-to-get-playlist-id": "Çalma listesi ID'si alınmaya çalışılıyor: {{playlistId}}",
"video-id-not-found": "Video bulunamadı",
"writing-id3": "ID3 etiketleri yazılıyor…"
}
},
"description": "MP3 / kaynak sesini doğrudan arayüzden indir",
"menu": {
"choose-download-folder": "İndirme klasörünü seç",
"download-playlist": "Oynatma listesini indir",
"presets": "Hazır Ayarlar",
"skip-existing": "Mevcut dosyaları atla"
},
"name": "İndirici",
"renderer": {
"can-not-update-progress": "İlerleme güncellenemiyor"
},
"templates": {
"button": "İndir"
}
},
"exponential-volume": {
"description": "Ses seviyesi kaydırıcısını üstel hale getirir, böylece daha düşük ses seviyelerini seçmek daha kolay olur.",
"name": "Üstel Ses Seviyesi"
},
"in-app-menu": {
"description": "Menü çubuklarına süslü, koyu veya albüm renginde bir görünüm verir",
"menu": {
"hide-dom-window-controls": "DOM penceresi kontrollerini gizle"
},
"name": "Uygulama İçi Menü"
},
"lumiastream": {
"description": "Lumia Stream desteği ekler",
"name": "Lumia Stream [Beta]"
},
"lyrics-genius": {
"description": "Çoğu şarkı için şarkı sözü desteği ekler",
"menu": {
"romanized-lyrics": "Romanlaştırılmış Şarkı Sözleri"
},
"name": "Genius Şarkı Sözleri",
"renderer": {
"fetched-lyrics": "Şarkı sözleri genius tarafından alındı"
}
},
"music-together": {
"description": "Oynatma listesini başkalarıyla paylaşın. Sunucu sahibi bir şarkı çaldığında, diğer herkes aynı şarkıyı duyacak",
"dialog": {
"enter-host": "Sunucu ID'si Girin"
},
"internal": {
"save": "Kaydet",
"track-source": "Parça Kaynağı",
"unknown-user": "Bilinmeyen Kullanıcı"
},
"menu": {
"click-to-copy-id": "Sunucu ID'sini kopyala",
"close": "Birlikte Müziği Kapat",
"connected-users": "Bağlanan Kullanıcılar",
"disconnect": "Birlikte Müzik Bağlantısını Kesin",
"empty-user": "Bağlı kullanıcı bulunmuyor",
"host": "Birlikte Müzik Sunucusu",
"join": "Birlikte Müziğe Katıl",
"permission": {
"all": "Konukların oynatma listesini ve oynatıcıyı kontrol etmesine izin ver",
"host-only": "Çalma listesini ve oynatıcıyı yalnızca yönetici kontrol edebilir",
"playlist": "Konukların oynatma listesini kontrol etmesine izin ver"
},
"set-permission": "Kontrol İznini Değiştir",
"status": {
"disconnected": "Bağlantı kesildi",
"guest": "Misafir olarak bağlandı",
"host": "Sunucu Sahibi olarak bağlandı"
}
},
"name": "Birlikte Müzik [Beta]",
"toast": {
"add-song-failed": "Şarkı eklenirken bir hata meydana geldi",
"closed": "Birlikte Müzik kapatıldı",
"disconnected": "Birlikte Müzik bağlantı kesildi",
"host-failed": "Birlikte Müzik sunucusu kurulamadı",
"id-copied": "Sunucu ID'si kopyalandı",
"id-copy-failed": "Sunucu ID'si panoya kopyalanamadı",
"join-failed": "Birlikte Müziğe katılırken bir hata meydana geldi",
"joined": "Birlikte Müziğe Katıldı",
"permission-changed": "Birlikte Müzik yetkisi \"{{permission}}\" olarak değiştirildi",
"remove-song-failed": "Şarkı kaldırılırken bir hata meydana geldi",
"user-connected": "{{name}} Birlikte Müziğe Katıldı",
"user-disconnected": "{{name}} Birlikte Müzik'ten ayrıldı"
}
},
"navigation": {
"description": "Favori tarayıcınızdaki gibi doğrudan arayüze entegre edilmiş İleri/Geri gezinme okları",
"name": "Navigasyon"
},
"no-google-login": {
"description": "Google giriş düğmelerini ve bağlantılarını arayüzden kaldır",
"name": "Google Girişini Kaldır"
},
"notifications": {
"description": "Bir şarkı çalmaya başladığında bir bildirim görüntüler (etkileşimli bildirimler Windows'ta mevcuttur)",
"menu": {
"interactive": "İnteraktif Bildirimler",
"interactive-settings": {
"label": "İnteraktif Ayarlar",
"submenu": {
"hide-button-text": "Buton metnini gizle",
"refresh-on-play-pause": "Oynat/Duraklat'ta Yenile",
"tray-controls": "Tepsi tıklamasıyla Aç/Kapat"
}
},
"priority": "Bildirim Önceliği",
"toast-style": "Bildirim Tarzı",
"unpause-notification": "Şarkı tekrar oynatılınca bildirim göster"
},
"name": "Bildirimler"
},
"picture-in-picture": {
"description": "Uygulamayı resim-içinde-resim moduna geçirmeye izin verir",
"menu": {
"always-on-top": "Her zaman üstte",
"hotkey": {
"label": "Kısayol",
"prompt": {
"keybind-options": {
"hotkey": "Kısayol"
},
"label": "Resim-içinde-resim arasında geçiş yapmak için bir kısayol tuşu seçin",
"title": "Resim-içinde-resim Kısayol Tuşu"
}
},
"save-window-position": "Pencere konumunu kaydet",
"save-window-size": "Pencere boyutunu kaydet",
"use-native-pip": "Tarayıcı yerel PiP'sini kullan"
},
"name": "Resim-içinde-resim",
"templates": {
"button": "Resim-içinde-resim"
}
},
"playback-speed": {
"description": "Hızlı dinle, yavaş dinle! Şarkı hızını kontrol eden bir kaydırıcı ekler",
"name": "Oynatma Hızı",
"templates": {
"button": "Hız"
}
},
"precise-volume": {
"description": "Özel bir HUD ve özelleştirilebilir ses seviyesi adımları ile fare tekerleği / kısayol tuşlarını kullanarak ses seviyesini hassas bir şekilde kontrol edin",
"menu": {
"arrows-shortcuts": "Yerel Ok Tuşu Kontrolleri",
"custom-volume-steps": "Özel Ses Seviyesi Adımlarını Ayarlama",
"global-shortcuts": "Genel Kısayol Tuşları"
},
"name": "Hassas Ses Seviyesi",
"prompt": {
"global-shortcuts": {
"keybind-options": {
"decrease": "Ses Seviyesi Azaltma",
"increase": "Ses Seviyesi Yükseltme"
},
"label": "Genel Ses Tuş Atamalarını seçin:",
"title": "Genel Ses Tuş Atamaları"
},
"volume-steps": {
"label": "Ses Artırma/Azaltma Kademelerini Seçin",
"title": "Ses Kademeleri"
}
}
},
"quality-changer": {
"backend": {
"dialog": {
"quality-changer": {
"detail": "Mevcut Kalite: {{quality}}",
"message": "Video Kalitesini Seçin:",
"title": "Video Kalitesini Seçin"
}
}
},
"description": "Video katmanı üzerindeki bir düğme ile video kalitesinin değiştirilmesine izin verir",
"name": "Video Kalitesi Değiştirici"
},
"scrobbler": {
"description": "Listeleme desteği ekler (lastfm, listenbrainz ve benzeri)",
"menu": {
"lastfm": {
"api-settings": "Last.fm API Ayarları"
},
"listenbrainz": {
"token": "ListenBrainz kullanıcı kimliğinizi girin"
},
"scrobble-other-media": "Diğer medya ortamlarında listele"
},
"name": "Listeleyici",
"prompt": {
"lastfm": {
"api-key": "Last.fm API anahtarı",
"api-secret": "Last.fm API gizli anahtar"
},
"listenbrainz": {
"token": {
"label": "ListenBrainz kullanıcı kimliğinizi girin:",
"title": "ListenBrainz kimliği"
}
}
}
},
"shortcuts": {
"description": "Oynatma için global kısayol tuşları (oynat/duraklat/sonraki/önceki) ayarlamaya ve medya tuşlarını geçersiz kılarak medya OSD'sini kapatmaya, arama yapmak için Ctrl/CMD + F tuşlarını açmaya, medya tuşları için Linux MPRIS desteğini açmaya ve ileri düzey kullanıcılar için özel kısayol tuşlarına izin verir",
"menu": {
"override-media-keys": "Medya Tuşlarını Geçersiz Kıl",
"set-keybinds": "Global Şarkı Kontrollerini Ayarla"
},
"name": "Kısayollar (& MPRIS)",
"prompt": {
"keybind": {
"keybind-options": {
"next": "İler",
"play-pause": "Oynat / Durdur",
"previous": "Önceki"
},
"label": "Şarkı Kontrolü için Genel Tuş Atamaları'nı seçin:",
"title": "Genel Tuş Atamaları"
}
}
},
"skip-disliked-songs": {
"description": "Beğenmediğin şarkıları atlar",
"name": "Beğenmediklerini Atla"
},
"skip-silences": {
"description": "Şarkılardaki sessiz bölümleri otomatik olarak atlar",
"name": "Sessizlikleri Atla"
},
"sponsorblock": {
"description": "Giriş/Çıkış gibi müzik olmayan kısımları veya müzik videolarında şarkının çalmadığı kısımları otomatik olarak atlar",
"name": "SponsorBlock"
},
"taskbar-mediacontrol": {
"description": "Windows görev çubuğu üzerinden oynatmayı kontrol edebilmenize olanak sağlar",
"name": "Görev Çubuğu Medya Kontrolü"
},
"touchbar": {
"description": "macOS kullanıcıları için bir TouchBar widget'ı ekler",
"name": "TouchBar"
},
"tuna-obs": {
"description": "OBS eklentisi Tuna ile entegrasyon sağlar",
"name": "Tuna OBS"
},
"video-toggle": {
"description": "Video/Şarkı modu arasında geçiş yapmak için bir düğme ekler. ayrıca isteğe bağlı olarak tüm video sekmesini kaldırabilir",
"menu": {
"align": {
"label": "Hizalama",
"submenu": {
"left": "Sol",
"middle": "Orta",
"right": "Sağ"
}
},
"force-hide": "Video sekmesini kaldırmaya zorla",
"mode": {
"label": "Mod",
"submenu": {
"custom": "Özel Ayar",
"disabled": "Devre dışı",
"native": "Yerel geçiş"
}
}
},
"name": "Video Geçiş",
"templates": {
"button": "Şarkı"
}
},
"visualizer": {
"description": "Oynatıcıya bir görselleştirici ekler",
"menu": {
"visualizer-type": "Görselleştirici Tipi"
},
"name": "Görselleştirici"
}
}
}

View File

@ -1,682 +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": "uk",
"local-name": "Українська",
"name": "Ukrainian"
},
"main": {
"console": {
"did-finish-load": {
"dev-tools": "Завантаження завершено. Відкрито DevTools"
},
"i18n": {
"loaded": "i18n завантажено"
},
"second-instance": {
"receive-command": "Отримано команду за протоколом: \"{{command}}\""
},
"theme": {
"css-file-not-found": "Файл CSS \"{{cssFile}}\" не існує, ігноруємо"
},
"unresponsive": {
"details": "Невідповідна помилка!\n{{error}}"
},
"when-ready": {
"clearing-cache-after-20s": "Очищення кешу програми"
},
"window": {
"tried-to-render-offscreen": "Вікно намагалися відобразитися поза екраном, windowSize={{windowSize}}, displaySize={{displaySize}}, position={{position}}"
}
},
"dialog": {
"hide-menu-enabled": {
"detail": "Меню приховано, використовуйте \"Alt\", щоб показати його (або \"Escape\", якщо ви використовуєте меню в додатку)",
"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": "Копіювати поточну 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": "Змінити User-Agent",
"restart-on-config-changes": "Перезапуск після зміни конфігурації",
"set-proxy": {
"label": "Задати проксі",
"prompt": {
"label": "Введіть адресу проксі-сервера: (залиште порожнім, щоб вимкнути)",
"placeholder": "Приклад: SOCKS5://127.0.0.1:9999",
"title": "Задати проксі"
}
},
"toggle-dev-tools": "Перемкнути DevTools"
}
},
"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": {
"label": "Тема",
"submenu": {
"import-css-file": "Імпортувати власний CSS файл",
"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": "YouTube Music",
"with-song-info": "YouTube Music: {{artist}} - {{title}}"
}
}
},
"plugins": {
"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}} с"
}
},
"use-fullscreen": {
"label": "Повноекранний режим"
}
},
"name": "Режим навколишнього середовища"
},
"audio-compressor": {
"description": "Застосувати стиснення аудіо (зменшити гучність найгучніших фрагментів сигналу та збільшити гучність тихих фрагментів)",
"name": "Аудіокомпресор"
},
"blur-nav-bar": {
"description": "Робить панель навігації прозорою та розмитою",
"name": "Розмиття панелі навігації"
},
"bypass-age-restrictions": {
"description": "Обхід перевірки віку на YouTube",
"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": "Тривалість згасання (ms)",
"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": "Активність Discord",
"prompt": {
"set-inactivity-timeout": {
"label": "Введіть тайм-аут бездіяльності в секундах:",
"title": "Встановлення тайм-ауту бездіяльності"
}
}
},
"downloader": {
"backend": {
"dialog": {
"error": {
"buttons": {
"ok": "Добре"
},
"message": "Йой! При завантаженні виникла помилка…",
"title": "Помилка при завантаженні!"
},
"start-download-playlist": {
"buttons": {
"ok": "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-playlist": "Завантажити плейлист",
"presets": "Попередні налаштування",
"skip-existing": "Пропустити наявні файли"
},
"name": "Завантажувач",
"renderer": {
"can-not-update-progress": "Неможливо оновити прогрес"
},
"templates": {
"button": "Завантажити"
}
},
"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": "Введіть ID хоста"
},
"internal": {
"save": "Зберегти",
"track-source": "Джерело композиції",
"unknown-user": "Невідомий користувач"
},
"menu": {
"click-to-copy-id": "Скопіювати хост ID",
"close": "Вимкнути сумісне прослуховування",
"connected-users": "Підключені користувачі",
"disconnect": "Відключитись від Music Together",
"empty-user": "Немає підключених користувачів",
"host": "Хост Music Together",
"join": "Приєднатися до Music Together",
"permission": {
"all": "Дозволити гостям керувати списком відтворення та плеєром",
"host-only": "Лише хост може керувати списком відтворення та плеєром",
"playlist": "Дозволити гостям керувати списком відтворення"
},
"set-permission": "Змінити дозвіл на керування",
"status": {
"disconnected": "Відключено",
"guest": "Підключено як гість",
"host": "Підключено як хост"
}
},
"name": "Music Together [Бета]",
"toast": {
"add-song-failed": "Не вдалося додати пісню",
"closed": "Music Together закритий",
"disconnected": "Music Together відключено",
"host-failed": "Не вдалося увімкнути Music Together",
"id-copied": "ID хоста скопійовано в буфер обміну",
"id-copy-failed": "Не вдалося скопіювати ID хоста в буфер обміну",
"join-failed": "Не вдалося приєднатися до Music Together",
"joined": "Приєднано до Music Together",
"permission-changed": "Дозвіл Music Together змінено на \"{{permission}}\"",
"remove-song-failed": "Не вдалося видалити пісню",
"user-connected": "{{name}} приєднався до Music Together",
"user-disconnected": "{{name}} вийшов з Music Together"
}
},
"navigation": {
"description": "Стрілки навігації Вперед/Назад безпосередньо інтегровані в інтерфейс, як у вашому браузері, який ви використовуєте",
"name": "Навігація"
},
"no-google-login": {
"description": "Видалити кнопки та посилання для входу через Google з інтерфейсу",
"name": "Без входу в Google"
},
"notifications": {
"description": "Відображати сповіщення, коли пісня починає грати (інтерактивні сповіщення доступні в Windows)",
"menu": {
"interactive": "Інтерактивні сповіщення",
"interactive-settings": {
"label": "Інтерактивні налаштування",
"submenu": {
"hide-button-text": "Сховати текст кнопки",
"refresh-on-play-pause": "Оновлення при відтворенні/паузі",
"tray-controls": "Відкриття/закриття при натисканні на значок в області повідомлень (tray)"
}
},
"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": "Додає підтримку скроблінгу (last.fm, Listenbrainz тощо)",
"menu": {
"lastfm": {
"api-settings": "Налаштування API Last.fm"
},
"listenbrainz": {
"token": "Ввести токен користувача ListenBrainz"
},
"scrobble-other-media": "Скробилити інші медіа"
},
"name": "Скроблер",
"prompt": {
"lastfm": {
"api-key": "Ключ API Last.fm",
"api-secret": "Секрет API Last.fm"
},
"listenbrainz": {
"token": {
"label": "Введіть ваш токен користувача ListenBrainz:",
"title": "Токен ListenBrainz"
}
}
}
},
"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"
},
"taskbar-mediacontrol": {
"description": "Керування відтворенням з панелі завдань Windows",
"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

@ -1,673 +0,0 @@
{
"common": {
"console": {
"plugins": {
"execute-failed": "Lỗi khi bắt đầu phần mở rộng {{pluginName}}::{{contextName}}",
"executed-at-ms": "Phần mở rộng {{pluginName}}::{{contextName}} đã bắt đầu trong {{ms}}ms",
"initialize-failed": "Lỗi khi khởi động phần mở rộng \"{{pluginName}}\"",
"load-all": "Đang tải tất cả phần mở rộng",
"load-failed": "Lỗi khi tải phần mở rộng\"{{pluginName}}\"",
"loaded": "Đã tải phần mở rộng \"{{pluginName}}\"",
"unload-failed": "Lỗi khi hủy tải phần mở rộng \"{{pluginName}}\"",
"unloaded": "Đã hủy tải phần mở rộng \"{{pluginName}}\""
}
}
},
"language": {
"code": "vi",
"local-name": "Tiếng Việt",
"name": "Vietnamese"
},
"main": {
"console": {
"did-finish-load": {
"dev-tools": "Đã tải xong. Đã mở Công cụ dành cho nhà phát triển"
},
"i18n": {
"loaded": "i18n đã được tải"
},
"second-instance": {
"receive-command": "Đã nhận được lệnh qua giao thức: \"{{command}}\""
},
"theme": {
"css-file-not-found": "Tệp tin CSS \"{{cssFile}}\"không tồn tại, đang bỏ qua"
},
"unresponsive": {
"details": "Lỗi không phản hồi!\n{{error}}"
},
"when-ready": {
"clearing-cache-after-20s": "Xóa bộ nhớ đệm ứng dụng"
},
"window": {
"tried-to-render-offscreen": "Cửa sổ đã cố gắng hiển thị ngoài màn hình, windowSize={{windowSize}}, displaySize={{displaySize}}, location={{position}}"
}
},
"dialog": {
"hide-menu-enabled": {
"detail": "Menu đã ẩn, ấn phím 'Alt' để hiện menu (hoặc ấn 'Escape' nếu bạn đang bật In-app Menu)",
"message": "Ẩn Menu đã được bật",
"title": "Ẩn Menu đã được bật"
},
"need-to-restart": {
"buttons": {
"later": "Để sau",
"restart-now": "Khởi động lại ngay"
},
"detail": "Tiện ích mở rộng \"{{pluginName}}\" yêu cầu khởi động lại ứng dụng để áp dụng",
"message": "\"{{pluginName}}\" cần khởi động lại",
"title": "Yêu cầu khởi động lại"
},
"unresponsive": {
"buttons": {
"quit": "Thoát",
"relaunch": "Khởi chạy lại",
"wait": "Đợi"
},
"detail": "Chúng tôi xin lỗi về sự bất tiện này! hãy chọn việc cần làm:",
"message": "Ứng dụng không phản hồi",
"title": "Cửa sổ không phản hồi"
},
"update-available": {
"buttons": {
"disable": "Tắt cập nhật",
"download": "Tải xuống",
"ok": "Đồng ý"
},
"detail": "Đã có phiên bản mới hơn, bạn có thể tải xuống tại {{downloadLink}}",
"message": "Đã có phiên bản mới",
"title": "Cập nhật có sẵn"
}
},
"menu": {
"about": "Giới thiệu",
"navigation": {
"label": "Điều hướng",
"submenu": {
"copy-current-url": "Copy URL hiện tại",
"go-back": "Quay lại",
"go-forward": "Tiến về trước",
"quit": "Thoát",
"restart": "Khởi động lại ứng dụng"
}
},
"options": {
"label": "Tùy chọn",
"submenu": {
"advanced-options": {
"label": "Tùy chọn nâng cao",
"submenu": {
"auto-reset-app-cache": "Làm mới bộ nhớ đệm khi khởi động ứng dụng",
"disable-hardware-acceleration": "Vô hiệu hóa tăng tốc phần cứng",
"edit-config-json": "Chỉnh sửa config.json",
"override-user-agent": "Ghi đè User-Agent",
"restart-on-config-changes": "Khởi động lại khi thay đổi cấu hình",
"set-proxy": {
"label": "Cài đặt proxy",
"prompt": {
"label": "Nhập địa chỉ Proxy: (để trống nếu muốn tắt)",
"placeholder": "Ví dụ: SOCKS5://127.0.0.1:9999",
"title": "Cài proxy"
}
},
"toggle-dev-tools": "Bật/tắt DevTools"
}
},
"always-on-top": "Luôn ở trên cùng",
"auto-update": "Tự động cập nhật",
"hide-menu": {
"dialog": {
"message": "Menu sẽ bị ẩn khi ứng dụng được chạy vào lần tới, dùng phím [Alt] để hiện nó (hoặc phím [`] nếu sử dụng in-app-menu)",
"title": "Ẩn Menu đã được bật"
},
"label": "Ẩn Menu"
},
"language": {
"dialog": {
"message": "Ngôn ngữ sẽ được thay đổi sau khi ứng dụng khởi động lại",
"title": "Ngôn ngữ đã thay đổi"
},
"label": "Ngôn ngữ",
"submenu": {
"to-help-translate": "Bạn muốn giúp dịch? Bấm vào đây"
}
},
"resume-on-start": "Tiếp tục bài hát cuối cùng khi ứng dụng khởi động",
"single-instance-lock": "Khóa một trường hợp",
"start-at-login": "Bắt đầu lúc đăng nhập",
"starting-page": {
"label": "Trang bắt đầu",
"unset": "Bỏ thiết đặt"
},
"tray": {
"label": "Khay",
"submenu": {
"disabled": "Vô hiệu hóa",
"enabled-and-hide-app": "Đã bật và ẩn ứng dụng",
"enabled-and-show-app": "Đã bật và hiển thị ứng dụng",
"play-pause-on-click": "Phát/Tạm dừng khi nhấp chuột"
}
},
"visual-tweaks": {
"label": "Tinh chỉnh hình ảnh",
"submenu": {
"like-buttons": {
"default": "Mặc định",
"force-show": "Tập trung hiển thị",
"hide": "Ẩn",
"label": "Nút thích"
},
"remove-upgrade-button": "Xóa nút nâng cấp",
"theme": {
"label": "Chủ đề",
"submenu": {
"import-css-file": "Nhập tệp CSS tùy chỉnh",
"no-theme": "Không có chủ đề"
}
}
}
}
}
},
"plugins": {
"enabled": "Đã bật",
"label": "Trình bổ sung",
"new": "MỚI"
},
"view": {
"label": "Xem",
"submenu": {
"force-reload": "Buộc tải lại",
"reload": "Tải lại",
"reset-zoom": "Kích thước thực",
"toggle-fullscreen": "Bật chế độ toàn màn hình",
"zoom-in": "Phóng to",
"zoom-out": "Thu nhỏ"
}
}
},
"tray": {
"next": "Tiếp theo",
"play-pause": "Phát/Tạm Dừng",
"previous": "Trước",
"quit": "Thoát",
"restart": "Khởi động lại ứng dụng",
"show": "Hiện cửa sổ",
"tooltip": {
"default": "YouTube Music",
"with-song-info": "YouTube Music: {{artist}} - {{title}}"
}
}
},
"plugins": {
"adblocker": {
"description": "Chặn toàn bộ quảng cáo và trình theo dõi",
"menu": {
"blocker": "Trình chặn"
},
"name": "Chặn quảng cáo"
},
"album-actions": {
"description": "Thêm nút hủy không thích, không thích, thích và không thích để áp dụng cho tất cả danh sách phát hoặc album",
"name": "Tác vụ với album"
},
"album-color-theme": {
"description": "Áp dụng chủ đề động và hiệu ứng hình ảnh dựa trên bảng màu của album",
"name": "Màu nền album"
},
"ambient-mode": {
"description": "Áp dụng hiệu ứng ánh sáng bằng cách truyền các màu nhẹ từ video vào nền màn hình của bạn",
"menu": {
"blur-amount": {
"label": "Lượng mờ",
"submenu": {
"pixels": "{{blurAmount}} điểm ảnh"
}
},
"buffer": {
"label": "Bộ đệm",
"submenu": {
"buffer": "{{buffer}}"
}
},
"opacity": {
"label": "Độ mờ",
"submenu": {
"percent": "{{opacity}}%"
}
},
"quality": {
"label": "Chất lượng",
"submenu": {
"pixels": "{{quality}} điểm ảnh"
}
},
"size": {
"label": "Kích thước",
"submenu": {
"percent": "{{size}}%"
}
},
"smoothness-transition": {
"label": "Độ mượt chuyển cảnh",
"submenu": {
"during": "Trong {{interpolationTime}} s"
}
},
"use-fullscreen": {
"label": "Dùng chế độ toàn màn hình"
}
},
"name": "Chế độ Môi trường xung quanh"
},
"audio-compressor": {
"description": "Áp dụng tính năng nén cho âm thanh (giảm âm lượng của phần to nhất của tín hiệu và tăng âm lượng của phần nhỏ nhất)",
"name": "Bộ nén âm thanh"
},
"blur-nav-bar": {
"description": "Làm mờ và trong suốt thanh điều hướng",
"name": "Thanh điều hướng mờ"
},
"bypass-age-restrictions": {
"description": "Bỏ qua xác minh độ tuổi của YouTube",
"name": "Bỏ qua hạn chế độ tuổi"
},
"captions-selector": {
"description": "Bộ lựa chọn phụ đề cho các bài hát trên Youtube Music",
"menu": {
"autoload": "Tự động chọn phụ đề vừa sử dụng",
"disable-captions": "Không có phụ đề đặt làm mặc định"
},
"name": "Bộ lựa chọn phụ đề",
"prompt": {
"selector": {
"label": "Ngôn ngữ phụ đề hiện tại: {{language}}",
"none": "Không có",
"title": "Chọn ngôn ngữ phụ đề"
}
},
"templates": {
"title": "Mở lựa chọn phụ đề"
}
},
"compact-sidebar": {
"description": "Luôn đặt thanh bên cạnh ở chế độ thu gọn",
"name": "Thanh bên thu gọn"
},
"crossfade": {
"description": "Chuyển tiếp giữa các bài hát",
"menu": {
"advanced": "Nâng cao"
},
"name": "Xen kẽ [thử nghiệm]",
"prompt": {
"options": {
"multi-input": {
"fade-in-duration": "Xuất hiện mờ dần trong khoảng thời gian (ms)",
"fade-out-duration": "Khoảng thời gian hoát ra mờ dần (ms)",
"fade-scaling": {
"label": "Làm mờ theo tỉ lệ",
"linear": "Trực tuyến",
"logarithmic": "Logarit"
},
"seconds-before-end": "Xen kẽ N giây trước khi kết thúc"
},
"title": "Tùy chọn xen kẽ"
}
}
},
"disable-autoplay": {
"description": "Bắt đầu bài hát khi ở chế độ \"tạm dừng\"",
"menu": {
"apply-once": "Áp dụng khi khởi động"
},
"name": "Tắt tự động phát"
},
"discord": {
"backend": {
"already-connected": "Đã cố gắng kết nối với kết nối khả dụng",
"connected": "Đã kết nối với Discord",
"disconnected": "Đã ngắt kết nối với Discord"
},
"description": "Cho bạn bè của bạn thấy những gì bạn nghe với Rich Presence",
"menu": {
"auto-reconnect": "Tự động kết nối lại",
"clear-activity": "Xoá hoạt động",
"clear-activity-after-timeout": "Xóa hoạt động sau khi hết thời gian chờ",
"connected": "Đã kết nối",
"disconnected": "Đã ngắt kết nối",
"hide-duration-left": "Ẩn thời lượng còn lại",
"hide-github-button": "Ẩn nút liên kết GitHub",
"play-on-youtube-music": "Phát trong Youtube Music",
"set-inactivity-timeout": "Đặt thời gian chờ không hoạt động"
},
"name": "Discord Rich Presence",
"prompt": {
"set-inactivity-timeout": {
"label": "Nhập thời gian chờ không hoạt động tính bằng giây:",
"title": "Đặt thời gian chờ không hoạt động"
}
}
},
"downloader": {
"backend": {
"dialog": {
"error": {
"buttons": {
"ok": "Đồng ý"
},
"message": "Argh! Xin lỗi, tải xuống thất bại…",
"title": "Lỗi khi tải xuống!"
},
"start-download-playlist": {
"buttons": {
"ok": "Đồng ý"
},
"detail": "({{playlistSize}} bài hát)",
"message": "Đang tải danh sách phát {{playlistTitle}}",
"title": "Đã bắt đầu tải xuống"
}
},
"feedback": {
"conversion-progress": "Chuyển đổi: {{percent}}%",
"converting": "Đang chuyển đổi…",
"done": "Đã xong: {{filePath}}",
"download-info": "Đang tải {{artist}} - {{title}} [{{videoId}}",
"download-progress": "Đang tải: {{percent}}%",
"downloading": "Đang tải…",
"downloading-counter": "Đang tải {{current}}/{{total}}…",
"downloading-playlist": "Đang tải danh sách phát \"{{playlistTitle}}\" - {{playlistSize}} bài hát ({{playlistId}})",
"error-while-downloading": "Lỗi tải xuống \"{{author}} - {{title}}\": {{error}}",
"folder-already-exists": "Thư mục {{playlistFolder}} đã tồn tại",
"getting-playlist-info": "Đang lấy thông tin danh sách phát…",
"loading": "Đang tải…",
"playlist-has-only-one-song": "Danh sách phát chỉ có một mục, tải trực tiếp",
"playlist-id-not-found": "Không tìm thấy ID danh sách phát",
"playlist-is-empty": "Danh sách phát trống",
"playlist-is-mix-or-private": "Lỗi lấy thông tin danh sách phát: đảm bảo danh sách phát không ở chế độ riêng tư hoặc là danh sách phát \"Dành cho bạn\"\n\n{{error}}",
"preparing-file": "Đang chuẩn bị thư mục…",
"saving": "Đang lưu…",
"trying-to-get-playlist-id": "Đang lấy ID danh sách phát: {{playlistId}}",
"video-id-not-found": "Không tìm thấy video",
"writing-id3": "Đang ghi thẻ ID3…"
}
},
"description": "Tải xuống MP3 / âm thanh nguồn trực tiếp từ giao diện",
"menu": {
"choose-download-folder": "Chọn thư mục tải xuống",
"download-playlist": "Tải danh sách phát",
"presets": "Cài đặt sẵn",
"skip-existing": "Bỏ qua các tập tin hiện có"
},
"name": "Trình tải xuống",
"renderer": {
"can-not-update-progress": "Không thể cập nhật tiến độ"
},
"templates": {
"button": "Tải xuống"
}
},
"exponential-volume": {
"description": "Làm cho thanh trượt âm lượng theo cấp số nhân để dễ dàng chọn âm lượng thấp hơn.",
"name": "Âm lượng theo cấp số nhân"
},
"in-app-menu": {
"description": "Mang lại cho thanh menu một giao diện lạ mắt, tối màu hoặc màu album",
"menu": {
"hide-dom-window-controls": "Ẩn cửa sổ điều khiển DOM"
},
"name": "Menu trong ứng dụng"
},
"lumiastream": {
"description": "Thêm hỗ trợ Lumia Stream",
"name": "Lumia Stream [Thử nghiệm]"
},
"lyrics-genius": {
"description": "Thêm hỗ trợ lời bài hát cho hầu hết các bài hát",
"menu": {
"romanized-lyrics": "Lời bài hát La Mã"
},
"name": "Lời bài hát từ Genius",
"renderer": {
"fetched-lyrics": "Lời bài hát được tìm nạp cho Genius"
}
},
"music-together": {
"description": "Chia sẻ danh sách phát với người khác. Khi máy chủ phát một bài hát, những người khác cũng sẽ nghe bài hát đó",
"dialog": {
"enter-host": "Nhập ID máy chủ"
},
"internal": {
"save": "Lưu",
"track-source": "Nguồn âm thanh",
"unknown-user": "Người dùng không rõ"
},
"menu": {
"click-to-copy-id": "Sao chép ID máy chủ",
"close": "Đóng Music Together",
"connected-users": "Người dùng đã kết nối",
"disconnect": "Ngắt kết nối Music Together",
"empty-user": "Không có người dùng đã kết nối",
"host": "Máy chủ Music Together",
"join": "Tham gia Music Together",
"permission": {
"all": "Cho phép người tham gia kiểm soát danh sách phát và trình phát",
"host-only": "Chỉ người tạo máy chủ có quyền điều khiển danh sách phát và trình phát",
"playlist": "Cho phép người tham gia điều khiển danh sách phát"
},
"set-permission": "Thay đổi quyền điều khiển",
"status": {
"disconnected": "Đã ngắt kết nối",
"guest": "Đã kết nối với tư cách Người tham gia",
"host": "Đã kết nối với tư cách Máy chủ"
}
},
"name": "Music Together [Thử nghiệm]",
"toast": {
"add-song-failed": "Thêm bài hát thất bại",
"closed": "Đã đóng Music Together",
"disconnected": "Đã ngắt kết nối Music Together",
"host-failed": "Không thể tổ chức Music Together",
"id-copied": "Đã sao chép ID máy chủ vào bộ nhớ tạm",
"id-copy-failed": "Sao chepd ID máy chủ vào bộ nhớ tạm không thành công",
"join-failed": "Không thể tham gia Music Together",
"joined": "Đã tham gia Music Together",
"permission-changed": "Quyền của Music Together đã thay đổi thành \"{{permission}}\"",
"remove-song-failed": "Không thể xoá bài hát",
"user-connected": "{{name}} đã tham gia Music Together",
"user-disconnected": "{{name}} đã rời khỏi Music Together"
}
},
"navigation": {
"description": "Mũi tên điều hướng Tiếp theo/Quay lại được tích hợp trực tiếp trong giao diện, giống như trong trình duyệt yêu thích của bạn",
"name": "Điều hướng"
},
"no-google-login": {
"description": "Xóa các nút và liên kết đăng nhập Google khỏi giao diện",
"name": "Không đăng nhập Google"
},
"notifications": {
"description": "Hiển thị thông báo khi bài hát bắt đầu phát (thông báo tương tác có sẵn trên Windows)",
"menu": {
"interactive": "Thông báo tương tác",
"interactive-settings": {
"label": "Cài đặt tương tác",
"submenu": {
"hide-button-text": "Ẩn tên nút",
"refresh-on-play-pause": "Làm mới khi phát/tạm dừng",
"tray-controls": "Mở/Đóng khi nhấp vào khay"
}
},
"priority": "Ưu tiên thông báo",
"toast-style": "Kiểu toast",
"unpause-notification": "Hiển thị thông báo khi bỏ tạm dừng"
},
"name": "Thông báo"
},
"picture-in-picture": {
"description": "Cho phép chuyển ứng dụng sang chế độ ảnh trong ảnh",
"menu": {
"always-on-top": "Luôn ở trên cùng",
"hotkey": {
"label": "Phím nóng",
"prompt": {
"keybind-options": {
"hotkey": "Phím nóng"
},
"label": "Chọn phím nóng để chuyển đổi ảnh trong ảnh",
"title": "Phím nóng ảnh trong ảnh"
}
},
"save-window-position": "Lưu vị trí cửa sổ",
"save-window-size": "Lưu kích thước cửa sổ",
"use-native-pip": "Sử dụng PiP gốc của trình duyệt"
},
"name": "Ảnh trong ảnh",
"templates": {
"button": "Ảnh trong ảnh"
}
},
"playback-speed": {
"description": "Nghe nhanh, nghe chậm! Thêm thanh trượt kiểm soát tốc độ bài hát",
"name": "Tốc độ phát lại",
"templates": {
"button": "Tốc độ"
}
},
"precise-volume": {
"description": "Kiểm soát âm lượng chính xác bằng con lăn chuột/phím nóng, với HUD tùy chỉnh và các bước âm lượng có thể tùy chỉnh",
"menu": {
"arrows-shortcuts": "Điều khiển phím mũi tên cục bộ",
"custom-volume-steps": "Đặt các bước âm lượng tùy chỉnh",
"global-shortcuts": "Phím nóng chung"
},
"name": "Âm lượng chính xác",
"prompt": {
"global-shortcuts": {
"keybind-options": {
"decrease": "Giảm âm lượng",
"increase": "Tăng âm lượng"
},
"label": "Chọn tổ hợp phím âm lượng chung:",
"title": "Liên kết phím âm lượng chung"
},
"volume-steps": {
"label": "Chọn các bước tăng/giảm âm lượng",
"title": "Bước âm lượng"
}
}
},
"quality-changer": {
"backend": {
"dialog": {
"quality-changer": {
"detail": "Chất lượng hiện tại: {{quality}}",
"message": "Chọn chất lượng video:",
"title": "Chọn chất lượng video:"
}
}
},
"description": "Cho phép thay đổi chất lượng video bằng một nút trên lớp phủ video",
"name": "Thay đổi chất lượng video"
},
"scrobbler": {
"description": "Thêm hỗ trợ scrobbling (v.v. Last.fm, Listenbrainz)",
"menu": {
"lastfm": {
"api-settings": "Cài đặt API Last.fm"
},
"listenbrainz": {
"token": "Nhập mã người dùng ListenBrainz"
}
},
"name": "Scrobbler",
"prompt": {
"lastfm": {
"api-key": "Khóa API Last.fm",
"api-secret": "API Last.fm bảo mật"
},
"listenbrainz": {
"token": {
"label": "Nhập mã người dùng ListenBrainz của bạn:",
"title": "Mã ListenBrainz"
}
}
}
},
"shortcuts": {
"description": "Cho phép thiết lập các phím nóng chung để phát lại (phát/tạm dừng/tiếp theo/trước) và tắt OSD media bằng cách ghi đè các phím media, bật Ctrl/CMD + F để tìm kiếm, bật hỗ trợ Linux MPRIS cho các phím media và các phím nóng tùy chỉnh cho người dùng nâng cao",
"menu": {
"override-media-keys": "Ghi đè khóa phương tiện",
"set-keybinds": "Đặt điều khiển bài hát chung"
},
"name": "Phím tắt (& MPRIS)",
"prompt": {
"keybind": {
"keybind-options": {
"next": "Tiếp theo",
"play-pause": "Phát / Tạm dừng",
"previous": "Trước đó"
},
"label": "Chọn tổ hợp phím chung để kiểm soát bài hát:",
"title": "Tổ hợp phím chung"
}
}
},
"skip-disliked-songs": {
"description": "Bỏ qua những bài hát không thích",
"name": "Bỏ qua những bài hát không thích"
},
"skip-silences": {
"description": "Tự động bỏ qua các đoạn im lặng trong bài hát",
"name": "Bỏ qua đoạn im lặng"
},
"sponsorblock": {
"description": "Tự động bỏ qua các phần không phải âm nhạc như phần giới thiệu/kết thúc hoặc các phần của video nhạc mà bài hát không được phát",
"name": "SponsorBlock"
},
"taskbar-mediacontrol": {
"description": "Kiểm soát phát lại từ thanh tác vụ Windows của bạn",
"name": "Kiểm soát phương tiện trên thanh tác vụ"
},
"touchbar": {
"description": "Thêm tiện ích TouchBar cho người dùng macOS",
"name": "TouchBar"
},
"tuna-obs": {
"description": "Tích hợp với plugin Tuna của OBS",
"name": "Tuna OBS"
},
"video-toggle": {
"description": "Thêm nút để chuyển giữa chế độ Video/Bài hát. Cũng có thể tùy ý xóa toàn bộ tab video",
"menu": {
"align": {
"label": "Căn chỉnh",
"submenu": {
"left": "Trái",
"middle": "Giữa",
"right": "Phải"
}
},
"force-hide": "Buộc loại bỏ tab video",
"mode": {
"label": "Chế độ",
"submenu": {
"custom": "Chuyển đổi tùy chỉnh",
"disabled": "Vô hiệu hoá",
"native": "Chuyển đổi gốc"
}
}
},
"name": "Chuyển đổi video",
"templates": {
"button": "Bài hát"
}
},
"visualizer": {
"description": "Thêm trình hiển thị cho trình phát",
"menu": {
"visualizer-type": "Loại trình hiển thị"
},
"name": "Trình hiển thị"
}
}
}

View File

@ -1,682 +0,0 @@
{
"common": {
"console": {
"plugins": {
"execute-failed": "执行插件 {{pluginName}}::{{contextName}} 失败",
"executed-at-ms": "插件 {{pluginName}}::{{contextName}} 已在 {{ms}} 毫秒内执行",
"initialize-failed": "初始化插件 “{{pluginName}}” 失败",
"load-all": "正在加载所有插件",
"load-failed": "加载插件 “{{pluginName}}” 失败",
"loaded": "插件 “{{pluginName}}” 已载入",
"unload-failed": "卸载插件 “{{pluginName}}” 失败",
"unloaded": "插件 “{{pluginName}}” 已卸载"
}
}
},
"language": {
"code": "zh-CN",
"local-name": "简体中文",
"name": "Simplified Chinese"
},
"main": {
"console": {
"did-finish-load": {
"dev-tools": "加载完毕。开发人员工具已启动"
},
"i18n": {
"loaded": "i18n 已载入"
},
"second-instance": {
"receive-command": "已从协议接收到以下指令:“{{command}}”"
},
"theme": {
"css-file-not-found": "CSS 文件 “{{cssFile}}” 不存在,将被忽略"
},
"unresponsive": {
"details": "无响应错误!\n{{error}}"
},
"when-ready": {
"clearing-cache-after-20s": "正在清理应用缓存"
},
"window": {
"tried-to-render-offscreen": "窗口试图于屏幕外绘制, windowSize={{windowSize}}, displaySize={{displaySize}}, position={{position}}"
}
},
"dialog": {
"hide-menu-enabled": {
"detail": "菜单已隐藏,按下 Alt 键来显示(若使用应用内菜单则按 Esc 键)",
"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": "复制当前 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": "覆盖 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 键以显示(若使用应用内菜单则按 ` 键)",
"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": {
"label": "主题",
"submenu": {
"import-css-file": "导入自定义 CSS 文件",
"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": "YouTube Music",
"with-song-info": "YouTube Music: {{artist}} - {{title}}"
}
}
},
"plugins": {
"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}} 秒"
}
},
"use-fullscreen": {
"label": "使用全屏"
}
},
"name": "沉浸模式"
},
"audio-compressor": {
"description": "对音频应用压缩(压低响亮部分,提升柔和部分)",
"name": "音频压缩器"
},
"blur-nav-bar": {
"description": "让导航栏透明及模糊",
"name": "模糊导航栏"
},
"bypass-age-restrictions": {
"description": "绕过 YouTube 年龄验证",
"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": "交叉淡化 [Beta]",
"prompt": {
"options": {
"multi-input": {
"fade-in-duration": "淡入持续时间(毫秒)",
"fade-out-duration": "淡出持续时间(毫秒)",
"fade-scaling": {
"label": "淡化尺度",
"linear": "线性",
"logarithmic": "对数"
},
"seconds-before-end": "歌曲结束前持续交叉淡化时长"
},
"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": "Discord Rich Presence 状态显示",
"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": "未找到播放列表 ID",
"playlist-is-empty": "播放列表是空的",
"playlist-is-mix-or-private": "获取播放列表信息时出错:请确认目标不是私有或“为你推荐”列表\n\n{{error}}",
"preparing-file": "正在准备文件…",
"saving": "正在保存…",
"trying-to-get-playlist-id": "正尝试获取播放列表 ID {{playlistId}}",
"video-id-not-found": "视频未找到",
"writing-id3": "正在写入 ID3 标签…"
}
},
"description": "在界面内直接下载 MP3 / 源音频",
"menu": {
"choose-download-folder": "选择下载文件夹",
"download-playlist": "下载播放列表",
"presets": "预设",
"skip-existing": "跳过已存在的文件"
},
"name": "下载器",
"renderer": {
"can-not-update-progress": "无法更新进度"
},
"templates": {
"button": "下载"
}
},
"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": "输入发起人 ID"
},
"internal": {
"save": "保存",
"track-source": "追踪来源",
"unknown-user": "未知用户"
},
"menu": {
"click-to-copy-id": "复制发起者 ID",
"close": "关闭 Music Together",
"connected-users": "已连接用户",
"disconnect": "断开 Music Together 连接",
"empty-user": "没有已连接的用户",
"host": "Music Together 发起者",
"join": "加入 Music Together",
"permission": {
"all": "允许来宾控制播放列表与播放器",
"host-only": "仅发起人可以控制播放列表与播放器",
"playlist": "允许来宾控制播放列表"
},
"set-permission": "更改控制权限",
"status": {
"disconnected": "已断开连接",
"guest": "已作为来宾连接",
"host": "已作为发起人连接"
}
},
"name": "Music Together [测试]",
"toast": {
"add-song-failed": "添加歌曲失败",
"closed": "Music Together 已关闭",
"disconnected": "Music Together 已断开连接",
"host-failed": "发起 Music Together 失败",
"id-copied": "已将发起者 ID 复制到剪切板",
"id-copy-failed": "复制发起者 ID 到剪贴板时失败",
"join-failed": "加入 Music Together 失败",
"joined": "已加入 Music Together",
"permission-changed": "Music Together 权限已改为 \"{{permission}}\"",
"remove-song-failed": "移除歌曲失败",
"user-connected": "{{name}} 加入了 Music Together",
"user-disconnected": "{{name}} 离开了 Music Together"
}
},
"navigation": {
"description": "如同浏览器般,在应用界面内直接显示前进/后退导航按钮",
"name": "导航"
},
"no-google-login": {
"description": "从界面内移除 Google 登录按钮和链接",
"name": "无 Google 登录"
},
"notifications": {
"description": "歌曲开始时显示通知(交互式通知仅适用于 Windows",
"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": "添加歌曲追踪支持(如 Last.fm 和 Listenbrainz",
"menu": {
"lastfm": {
"api-settings": "Last.fm API 设置"
},
"listenbrainz": {
"token": "输入 ListenBrainz 用户令牌"
},
"scrobble-other-media": "记录其他媒体文件"
},
"name": "歌曲记录器",
"prompt": {
"lastfm": {
"api-key": "Last.fm API 密钥Key",
"api-secret": "Last.fm API 密文Secret"
},
"listenbrainz": {
"token": {
"label": "输入您的v ListenBrainz 用户令牌:",
"title": "ListenBrainz 令牌"
}
}
}
},
"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": "自动跳过非音乐部分,如 MV 的介绍/结语以及歌曲未开始的部分",
"name": "SponsorBlock"
},
"taskbar-mediacontrol": {
"description": "从 Windows 任务栏控制音乐回放",
"name": "任务栏媒体控件"
},
"touchbar": {
"description": "为 macOS 用户启用 TouchBar 支持",
"name": "TouchBar"
},
"tuna-obs": {
"description": "与 OBS 的 Tuna 插件集成",
"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

@ -1,682 +0,0 @@
{
"common": {
"console": {
"plugins": {
"execute-failed": "外掛 {{pluginName}} 無法執行::{{contextName}}",
"executed-at-ms": "外掛 {{pluginName}}::{{contextName}} 用了 {{ms}} 毫秒來執行",
"initialize-failed": "初始化外掛「{{pluginName}}」失敗",
"load-all": "正在載入所有外掛",
"load-failed": "載入外掛「{{pluginName}}」失敗",
"loaded": "外掛「{{pluginName}}」已載入",
"unload-failed": "移除外掛「{{pluginName}}」失敗",
"unloaded": "外掛「{{pluginName}}」已移除"
}
}
},
"language": {
"code": "zh-TW",
"local-name": "正體中文",
"name": "Traditional Chinese"
},
"main": {
"console": {
"did-finish-load": {
"dev-tools": "載入完成。開發人員工具已開啟"
},
"i18n": {
"loaded": "i18n 已載入"
},
"second-instance": {
"receive-command": "使用協定來接收指令:「{{command}}」"
},
"theme": {
"css-file-not-found": "CSS 檔案「{{cssFile}}」不存在,已忽略"
},
"unresponsive": {
"details": "沒有回應錯誤!\n{{error}}"
},
"when-ready": {
"clearing-cache-after-20s": "清理程式的快取資料"
},
"window": {
"tried-to-render-offscreen": "視窗正嘗試在螢幕外繪製,視窗大小 = {{windowSize}},螢幕大小 = {{displaySize}},位置 = {{position}}"
}
},
"dialog": {
"hide-menu-enabled": {
"detail": "選單列已隱藏,使用 Alt 鍵來重新顯示(或是使用 Esc 鍵)",
"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 鍵來重新顯示(或是使用 ` 鍵)",
"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": {
"label": "主題",
"submenu": {
"import-css-file": "匯入自訂 CSS 檔案",
"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": "Youtube Music",
"with-song-info": "YouTube Music: {{artist}} - {{title}}"
}
}
},
"plugins": {
"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}} 秒"
}
},
"use-fullscreen": {
"label": "使用全螢幕"
}
},
"name": "微光效果"
},
"audio-compressor": {
"description": "使用音效壓縮 (大聲部份的音量降低, 柔和部份的音量提高)",
"name": "音效壓縮器"
},
"blur-nav-bar": {
"description": "使導覽列透明及模糊",
"name": "模糊導覽列"
},
"bypass-age-restrictions": {
"description": "繞過Youtube年齡驗證",
"name": "繞過年齡驗證"
},
"captions-selector": {
"description": "音軌標題選擇",
"menu": {
"autoload": "自動選擇最後使用的標題",
"disable-captions": "預設無標題"
},
"name": "標題選擇器",
"prompt": {
"selector": {
"label": "目前標題語言: {{language}}",
"none": "無",
"title": "選擇標題語言"
}
},
"templates": {
"title": "打開標題選擇器"
}
},
"compact-sidebar": {
"description": "永遠讓側邊欄保持收合狀態",
"name": "收合側邊欄"
},
"crossfade": {
"description": "在歌曲之間使用交叉淡化",
"menu": {
"advanced": "進階"
},
"name": "交叉淡化 [Beta]",
"prompt": {
"options": {
"multi-input": {
"fade-in-duration": "淡入時間(毫秒)",
"fade-out-duration": "淡出時間(毫秒)",
"fade-scaling": {
"label": "淡化計算方式",
"linear": "線性",
"logarithmic": "對數"
},
"seconds-before-end": "交叉淡化持續時間 (秒)"
},
"title": "交叉淡化選項"
}
}
},
"disable-autoplay": {
"description": "讓歌曲開始時為暫停模式",
"menu": {
"apply-once": "只在啟動程式時套用"
},
"name": "停用自動播放"
},
"discord": {
"backend": {
"already-connected": "已嘗試可用連接",
"connected": "已連接至Discord",
"disconnected": "與Discord斷開連接"
},
"description": "使用Discord狀態與你的好友分享你正在收聽的音樂",
"menu": {
"auto-reconnect": "自動重新連接",
"clear-activity": "清除狀態",
"clear-activity-after-timeout": "在音樂暫停後清除狀態",
"connected": "已連接",
"disconnected": "已斷開連接",
"hide-duration-left": "隱藏音樂剩餘時間狀態",
"hide-github-button": "隱藏Github頁面按鈕",
"play-on-youtube-music": "顯示Play on YouTube Music按鈕",
"set-inactivity-timeout": "設定閒置狀態時長"
},
"name": "Discord狀態",
"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": "沒有找到播放清單 ID",
"playlist-is-empty": "播放清單是空的",
"playlist-is-mix-or-private": "獲取播放清單資訊時發生錯誤: 請確認非私人播放清單或是\"為你推薦的合輯\"\n\n{{error}}",
"preparing-file": "正在準備檔案…",
"saving": "儲存中…",
"trying-to-get-playlist-id": "正在嘗試獲取播放清單 ID: {{playlistId}}",
"video-id-not-found": "未能找到該影片",
"writing-id3": "正在寫入 ID3 標籤…"
}
},
"description": "在應用程式內下載 MP3原始音樂檔",
"menu": {
"choose-download-folder": "選擇下載位置",
"download-playlist": "下載播放清單",
"presets": "預設格式",
"skip-existing": "跳過已存在的檔案"
},
"name": "歌曲下載",
"renderer": {
"can-not-update-progress": "無法更新進度"
},
"templates": {
"button": "下載"
}
},
"exponential-volume": {
"description": "使音量滑桿指數化,以便更容易選擇較低的音量。",
"name": "指數化音量調整"
},
"in-app-menu": {
"description": "使選單列變更為黑色或隨主題變色",
"menu": {
"hide-dom-window-controls": "隱藏DOM視窗控制"
},
"name": "程式內選單列"
},
"lumiastream": {
"description": "新增對 Lumia Stream 的支援",
"name": "Lumia Stream [Beta]"
},
"lyrics-genius": {
"description": "為更多歌曲新增字幕支援",
"menu": {
"romanized-lyrics": "羅馬拼音字幕"
},
"name": "第三方字幕",
"renderer": {
"fetched-lyrics": "為Genius獲取字幕"
}
},
"music-together": {
"description": "與他人共享播放清單。當發起人播放歌曲時,其他成員也會同步收聽",
"dialog": {
"enter-host": "輸入發起人 ID"
},
"internal": {
"save": "儲存",
"track-source": "追蹤來源",
"unknown-user": "未知使用者"
},
"menu": {
"click-to-copy-id": "複製發起人 ID",
"close": "同步關閉音樂",
"connected-users": "已連接的使用者",
"disconnect": "斷開連接共享音樂",
"empty-user": "無已連接的使用者",
"host": "發起共享音樂",
"join": "加入共享音樂",
"permission": {
"all": "允許加入的使用者控制播放清單及播放控制",
"host-only": "不允許加入的使用者控制播放清單及播放控制",
"playlist": "只允許加入的使用者控制播放清單"
},
"set-permission": "切換共享音樂播放權限",
"status": {
"disconnected": "已斷開連接",
"guest": "以使用者身份加入",
"host": "以發起人身份加入"
}
},
"name": "共享音樂 [Beta]",
"toast": {
"add-song-failed": "歌曲加入失敗",
"closed": "關閉共享音樂",
"disconnected": "共享音樂已斷開連接",
"host-failed": "發起共享音樂失敗",
"id-copied": "已複製發起人 ID",
"id-copy-failed": "複製發起人 ID 失敗",
"join-failed": "加入共享音樂失敗",
"joined": "加入共享音樂",
"permission-changed": "共享音樂播放權限已切換至 \"{{permission}}\"",
"remove-song-failed": "歌曲移除失敗",
"user-connected": "{{name}} 已加入共享音樂",
"user-disconnected": "{{name}} 離開了共享音樂"
}
},
"navigation": {
"description": "將上一頁/下一頁按鈕新增至應用程式上方, 就像你最熟悉的瀏覽器",
"name": "導覽列"
},
"no-google-login": {
"description": "移除Google登入按鈕及連結",
"name": "停用Google登入"
},
"notifications": {
"description": "在歌曲播放時發送一個系統通知 (可互動通知僅限Windows)",
"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)",
"menu": {
"lastfm": {
"api-settings": "Last.fm API 設定"
},
"listenbrainz": {
"token": "輸入 ListenBrainz 使用者憑證"
},
"scrobble-other-media": "紀錄其他媒體文件"
},
"name": "Scrobbler",
"prompt": {
"lastfm": {
"api-key": "Last.fm API 金鑰",
"api-secret": "Last.fm API 密鑰"
},
"listenbrainz": {
"token": {
"label": "輸入您的 ListenBrainz 使用者憑證:",
"title": "ListenBrainz 使用者憑證"
}
}
}
},
"shortcuts": {
"description": "使用全域快捷鍵控制音樂 (播放/暫停/下一首/上一首) + 透過覆寫媒體快捷鍵停用媒體OSD + 允許Ctrl/CMD + F來搜尋 + 支援Linux MPRIS媒體快捷鍵 + 更多自訂快捷鍵給進階使用者",
"menu": {
"override-media-keys": "覆寫媒體快捷鍵",
"set-keybinds": "設定全域歌曲控制"
},
"name": "Shortcuts (& MPRIS)",
"prompt": {
"keybind": {
"keybind-options": {
"next": "下一首",
"play-pause": "播放/暫停",
"previous": "上一首"
},
"label": "選擇全域音樂控制快捷鍵:",
"title": "全域快捷鍵"
}
}
},
"skip-disliked-songs": {
"description": "自動跳過不喜歡的歌曲",
"name": "自動跳過不喜歡的歌曲"
},
"skip-silences": {
"description": "自動跳過音樂無聲片段",
"name": "自動跳過無聲片段"
},
"sponsorblock": {
"description": "自動跳過贊助片段",
"name": "贊助阻擋"
},
"taskbar-mediacontrol": {
"description": "透過工作列應用程式圖式控制媒體播放",
"name": "工作列媒體控制"
},
"touchbar": {
"description": "為macOS使用者新增觸控列支援",
"name": "觸控列 (Touchbar) 支援"
},
"tuna-obs": {
"description": "與 OBS 的 Tuna 外掛連接",
"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

@ -1,14 +0,0 @@
<!-- WARNING:
This file only exists for the build system to work properly.
Any changes done here won't be reflected in the final build.
The actual loading of `renderer.ts` is done in `src/index.ts`
within the `createMainWindow` function.
Archived link for reference:
https://github.com/th-ch/youtube-music/blob/a3104fda4b0d58b076d0c737111636a66e468acc/src/index.ts#L407-L443
-->
<script type="module" src="./renderer.ts"></script>

View File

@ -1,66 +1,49 @@
import path from 'node:path'; import path from 'node:path';
import url from 'node:url';
import fs from 'node:fs';
import { import { BrowserWindow, app, screen, globalShortcut, session, shell, dialog, ipcMain } from 'electron';
BrowserWindow, import enhanceWebRequest, { BetterSession } from '@jellybrick/electron-better-web-request';
app,
screen,
globalShortcut,
session,
shell,
dialog,
ipcMain,
} from 'electron';
import enhanceWebRequest, {
BetterSession,
} from '@jellybrick/electron-better-web-request';
import is from 'electron-is'; import is from 'electron-is';
import unhandled from 'electron-unhandled'; import unhandled from 'electron-unhandled';
import { autoUpdater } from 'electron-updater'; import { autoUpdater } from 'electron-updater';
import electronDebug from 'electron-debug'; import electronDebug from 'electron-debug';
import { parse } from 'node-html-parser';
import { deepmerge } from 'deepmerge-ts';
import { deepEqual } from 'fast-equals';
import { allPlugins, mainPlugins } from 'virtual:plugins'; import config from './config';
import { refreshMenu, setApplicationMenu } from './menu';
import { fileExists, injectCSS, injectCSSAsFile } from './plugins/utils';
import { isTesting } from './utils/testing';
import { setUpTray } from './tray';
import { setupSongInfo } from './providers/song-info';
import { restart, setupAppControls } from './providers/app-controls';
import { APP_PROTOCOL, handleProtocol, setupProtocolHandler } from './providers/protocol-handler';
import { languageResources } from 'virtual:i18n'; import adblocker from './plugins/adblocker/back';
import albumColorTheme from './plugins/album-color-theme/back';
import ambientMode from './plugins/ambient-mode/back';
import blurNavigationBar from './plugins/blur-nav-bar/back';
import captionsSelector from './plugins/captions-selector/back';
import crossfade from './plugins/crossfade/back';
import discord from './plugins/discord/back';
import downloader from './plugins/downloader/back';
import httpApi from './plugins/http-api/back';
import inAppMenu from './plugins/in-app-menu/back';
import lastFm from './plugins/last-fm/back';
import lumiaStream from './plugins/lumiastream/back';
import lyricsGenius from './plugins/lyrics-genius/back';
import navigation from './plugins/navigation/back';
import noGoogleLogin from './plugins/no-google-login/back';
import notifications from './plugins/notifications/back';
import pictureInPicture, { setOptions as pipSetOptions } from './plugins/picture-in-picture/back';
import preciseVolume from './plugins/precise-volume/back';
import qualityChanger from './plugins/quality-changer/back';
import shortcuts from './plugins/shortcuts/back';
import sponsorBlock from './plugins/sponsorblock/back';
import taskbarMediaControl from './plugins/taskbar-mediacontrol/back';
import touchbar from './plugins/touchbar/back';
import tunaObs from './plugins/tuna-obs/back';
import videoToggle from './plugins/video-toggle/back';
import visualizer from './plugins/visualizer/back';
import config from '@/config'; import youtubeMusicCSS from './youtube-music.css';
import { refreshMenu, setApplicationMenu } from '@/menu';
import { fileExists, injectCSS, injectCSSAsFile } from '@/plugins/utils/main';
import { isTesting } from '@/utils/testing';
import { setUpTray } from '@/tray';
import { setupSongInfo } from '@/providers/song-info';
import { restart, setupAppControls } from '@/providers/app-controls';
import {
APP_PROTOCOL,
handleProtocol,
setupProtocolHandler,
} from '@/providers/protocol-handler';
import youtubeMusicCSS from '@/youtube-music.css?inline';
import {
forceLoadMainPlugin,
forceUnloadMainPlugin,
getAllLoadedMainPlugins,
loadAllMainPlugins,
} from '@/loader/main';
import { LoggerPrefix } from '@/utils';
import { loadI18n, setLanguage, t } from '@/i18n';
import type { PluginConfig } from '@/types/plugins';
if (!is.macOS()) {
delete allPlugins['touchbar'];
}
if (!is.windows()) {
delete allPlugins['taskbar-mediacontrol'];
}
// Catch errors and log them // Catch errors and log them
unhandled({ unhandled({
@ -82,10 +65,7 @@ if (!gotTheLock) {
// SharedArrayBuffer: Required for downloader (@ffmpeg/core-mt) // SharedArrayBuffer: Required for downloader (@ffmpeg/core-mt)
// OverlayScrollbar: Required for overlay scrollbars // OverlayScrollbar: Required for overlay scrollbars
app.commandLine.appendSwitch( app.commandLine.appendSwitch('enable-features', 'OverlayScrollbar,SharedArrayBuffer');
'enable-features',
'OverlayScrollbar,SharedArrayBuffer',
);
if (config.get('options.disableHardwareAcceleration')) { if (config.get('options.disableHardwareAcceleration')) {
if (is.dev()) { if (is.dev()) {
console.log('Disabling hardware acceleration'); console.log('Disabling hardware acceleration');
@ -121,116 +101,50 @@ function onClosed() {
mainWindow = null; mainWindow = null;
} }
ipcMain.handle('ytmd:get-main-plugin-names', () => Object.keys(mainPlugins)); const mainPlugins = {
'adblocker': adblocker,
const initHook = (win: BrowserWindow) => { 'album-color-theme': albumColorTheme,
ipcMain.handle( 'ambient-mode': ambientMode,
'ytmd:get-config', 'blur-nav-bar': blurNavigationBar,
(_, id: string) => 'captions-selector': captionsSelector,
deepmerge( 'crossfade': crossfade,
allPlugins[id].config ?? { enabled: false }, 'discord': discord,
config.get(`plugins.${id}`) ?? {}, 'downloader': downloader,
) as PluginConfig, 'http-api': httpApi,
); 'in-app-menu': inAppMenu,
ipcMain.handle('ytmd:set-config', (_, name: string, obj: object) => 'last-fm': lastFm,
config.setPartial(`plugins.${name}`, obj, allPlugins[name].config), 'lumiastream': lumiaStream,
); 'lyrics-genius': lyricsGenius,
'navigation': navigation,
config.watch((newValue, oldValue) => { 'no-google-login': noGoogleLogin,
const newPluginConfigList = (newValue?.plugins ?? {}) as Record< 'notifications': notifications,
string, 'picture-in-picture': pictureInPicture,
unknown 'precise-volume': preciseVolume,
>; 'quality-changer': qualityChanger,
const oldPluginConfigList = (oldValue?.plugins ?? {}) as Record< 'shortcuts': shortcuts,
string, 'sponsorblock': sponsorBlock,
unknown 'taskbar-mediacontrol': undefined as typeof taskbarMediaControl | undefined,
>; 'touchbar': undefined as typeof touchbar | undefined,
'tuna-obs': tunaObs,
Object.entries(newPluginConfigList).forEach(([id, newPluginConfig]) => { 'video-toggle': videoToggle,
const isEqual = deepEqual(oldPluginConfigList[id], newPluginConfig); 'visualizer': visualizer,
if (!isEqual) {
const oldConfig = oldPluginConfigList[id] as PluginConfig;
const config = deepmerge(
allPlugins[id].config ?? { enabled: false },
newPluginConfig ?? {},
) as PluginConfig;
if (config.enabled !== oldConfig?.enabled) {
if (config.enabled) {
win.webContents.send('plugin:enable', id);
ipcMain.emit('plugin:enable', id);
forceLoadMainPlugin(id, win);
} else {
win.webContents.send('plugin:unload', id);
ipcMain.emit('plugin:unload', id);
forceUnloadMainPlugin(id, win);
}
if (allPlugins[id]?.restartNeeded) {
showNeedToRestartDialog(id);
}
}
const mainPlugin = getAllLoadedMainPlugins()[id];
if (mainPlugin) {
if (config.enabled && typeof mainPlugin.backend !== 'function') {
mainPlugin.backend?.onConfigChange?.call(
mainPlugin.backend,
config,
);
}
}
win.webContents.send('config-changed', id, config);
}
});
});
}; };
export const mainPluginNames = Object.keys(mainPlugins);
const showNeedToRestartDialog = (id: string) => { if (is.windows()) {
const plugin = allPlugins[id]; mainPlugins['taskbar-mediacontrol'] = taskbarMediaControl;
delete mainPlugins['touchbar'];
} else if (is.macOS()) {
mainPlugins['touchbar'] = touchbar;
delete mainPlugins['taskbar-mediacontrol'];
} else {
delete mainPlugins['touchbar'];
delete mainPlugins['taskbar-mediacontrol'];
}
const dialogOptions: Electron.MessageBoxOptions = { ipcMain.handle('get-main-plugin-names', () => Object.keys(mainPlugins));
type: 'info',
buttons: [
t('main.dialog.need-to-restart.buttons.restart-now'),
t('main.dialog.need-to-restart.buttons.later'),
],
title: t('main.dialog.need-to-restart.title'),
message: t('main.dialog.need-to-restart.message', {
pluginName: plugin?.name?.() ?? id,
}),
detail: t('main.dialog.need-to-restart.detail', {
pluginName: plugin?.name?.() ?? id,
}),
defaultId: 0,
cancelId: 1,
};
let dialogPromise: Promise<Electron.MessageBoxReturnValue>; async function loadPlugins(win: BrowserWindow) {
if (mainWindow) {
dialogPromise = dialog.showMessageBox(mainWindow, dialogOptions);
} else {
dialogPromise = dialog.showMessageBox(dialogOptions);
}
dialogPromise.then((dialogOutput) => {
switch (dialogOutput.response) {
case 0: {
restart();
break;
}
// Ignore
default: {
break;
}
}
});
};
function initTheme(win: BrowserWindow) {
injectCSS(win.webContents, youtubeMusicCSS); injectCSS(win.webContents, youtubeMusicCSS);
// Load user CSS // Load user CSS
const themes: string[] = config.get('options.themes'); const themes: string[] = config.get('options.themes');
@ -242,10 +156,7 @@ function initTheme(win: BrowserWindow) {
injectCSSAsFile(win.webContents, cssFile); injectCSSAsFile(win.webContents, cssFile);
}, },
() => { () => {
console.warn( console.warn(`CSS file "${cssFile}" does not exist, ignoring`);
LoggerPrefix,
t('main.console.theme.css-file-not-found', { cssFile }),
);
}, },
); );
} }
@ -253,10 +164,24 @@ function initTheme(win: BrowserWindow) {
win.webContents.once('did-finish-load', () => { win.webContents.once('did-finish-load', () => {
if (is.dev()) { if (is.dev()) {
console.debug(LoggerPrefix, t('main.console.did-finish-load.dev-tools')); console.log('did finish load');
win.webContents.openDevTools(); win.webContents.openDevTools();
} }
}); });
for (const [plugin, options] of config.plugins.getEnabled()) {
try {
if (Object.hasOwn(mainPlugins, plugin)) {
console.log('Loaded plugin - ' + plugin);
const handler = mainPlugins[plugin as keyof typeof mainPlugins];
if (handler) {
await handler(win, options as never);
}
}
} catch (e) {
console.error(`Failed to load plugin "${plugin}"`, e);
}
}
} }
async function createMainWindow() { async function createMainWindow() {
@ -265,12 +190,6 @@ async function createMainWindow() {
const windowPosition: Electron.Point = config.get('window-position'); const windowPosition: Electron.Point = config.get('window-position');
const useInlineMenu = config.plugins.isEnabled('in-app-menu'); const useInlineMenu = config.plugins.isEnabled('in-app-menu');
const defaultTitleBarOverlayOptions: Electron.TitleBarOverlay = {
color: '#00000000',
symbolColor: '#ffffff',
height: 32,
};
const win = new BrowserWindow({ const win = new BrowserWindow({
icon, icon,
width: windowSize.width, width: windowSize.width,
@ -278,62 +197,53 @@ async function createMainWindow() {
backgroundColor: '#000', backgroundColor: '#000',
show: false, show: false,
webPreferences: { webPreferences: {
contextIsolation: true, // TODO: re-enable contextIsolation once it can work with FFMpeg.wasm
preload: path.join(__dirname, '..', 'preload', 'preload.js'), // Possible bundling? https://github.com/ffmpegwasm/ffmpeg.wasm/issues/126
contextIsolation: false,
preload: path.join(__dirname, 'preload.js'),
nodeIntegrationInSubFrames: true,
...(isTesting() ...(isTesting()
? undefined ? undefined
: { : {
// Sandbox is only enabled in tests for now // Sandbox is only enabled in tests for now
// See https://www.electronjs.org/docs/latest/tutorial/sandbox#preload-scripts // See https://www.electronjs.org/docs/latest/tutorial/sandbox#preload-scripts
sandbox: false, sandbox: false,
}), }),
}, },
frame: !is.macOS() && !useInlineMenu, frame: !is.macOS() && !useInlineMenu,
titleBarOverlay: defaultTitleBarOverlayOptions, titleBarOverlay: {
color: '#00000000',
symbolColor: '#ffffff',
height: 36,
},
titleBarStyle: useInlineMenu titleBarStyle: useInlineMenu
? 'hidden' ? 'hidden'
: is.macOS() : (is.macOS()
? 'hiddenInset' ? 'hiddenInset'
: 'default', : 'default'),
autoHideMenuBar: config.get('options.hideMenu'), autoHideMenuBar: config.get('options.hideMenu'),
}); });
initHook(win); await loadPlugins(win);
initTheme(win);
await loadAllMainPlugins(win);
if (windowPosition) { if (windowPosition) {
const { x: windowX, y: windowY } = windowPosition; const { x: windowX, y: windowY } = windowPosition;
const winSize = win.getSize(); const winSize = win.getSize();
const display = screen.getDisplayNearestPoint(windowPosition); const displaySize
const scaleFactor = is.windows() ? display.scaleFactor: 1; = screen.getDisplayNearestPoint(windowPosition).bounds;
const scaledWidth = Math.floor(windowSize.width / scaleFactor);
const scaledHeight = Math.floor(windowSize.height / scaleFactor);
const scaledX = windowX;
const scaledY = windowY;
if ( if (
scaledX + scaledWidth < display.bounds.x - 8 || windowX + winSize[0] < displaySize.x - 8
scaledX - scaledWidth > display.bounds.x + display.bounds.width || || windowX - winSize[0] > displaySize.x + displaySize.width
scaledY < display.bounds.y - 8 || || windowY < displaySize.y - 8
scaledY > display.bounds.y + display.bounds.height || windowY > displaySize.y + displaySize.height
) { ) {
// Window is offscreen // Window is offscreen
if (is.dev()) { if (is.dev()) {
console.warn( console.log(
LoggerPrefix, `Window tried to render offscreen, windowSize=${String(winSize)}, displaySize=${String(displaySize)}, position=${String(windowPosition)}`,
t('main.console.window.tried-to-render-offscreen', {
winSize: String(winSize),
displaySize: String(display.bounds),
windowPosition: String(windowPosition),
}),
); );
} }
} else { } else {
win.setSize(scaledWidth, scaledHeight); win.setPosition(windowX, windowY);
win.setPosition(scaledX, scaledY);
} }
} }
@ -350,22 +260,40 @@ async function createMainWindow() {
: config.defaultConfig.url; : config.defaultConfig.url;
win.on('closed', onClosed); win.on('closed', onClosed);
type PiPOptions = typeof config.defaultConfig.plugins['picture-in-picture'];
const setPiPOptions = config.plugins.isEnabled('picture-in-picture')
// eslint-disable-next-line @typescript-eslint/no-var-requires
? (key: string, value: unknown) => pipSetOptions({ [key]: value })
: () => {};
win.on('move', () => { win.on('move', () => {
if (win.isMaximized()) { if (win.isMaximized()) {
return; return;
} }
const [x, y] = win.getPosition(); const position = win.getPosition();
lateSave('window-position', { x, y }); const isPiPEnabled: boolean
= config.plugins.isEnabled('picture-in-picture')
&& config.plugins.getOptions<PiPOptions>('picture-in-picture').isInPiP;
if (!isPiPEnabled) {
lateSave('window-position', { x: position[0], y: position[1] });
} else if (config.plugins.getOptions<PiPOptions>('picture-in-picture').savePosition) {
lateSave('pip-position', position, setPiPOptions);
}
}); });
let winWasMaximized: boolean; let winWasMaximized: boolean;
win.on('resize', () => { win.on('resize', () => {
const [width, height] = win.getSize(); const windowSize = win.getSize();
const isMaximized = win.isMaximized(); const isMaximized = win.isMaximized();
if (winWasMaximized !== isMaximized) { const isPiPEnabled
= config.plugins.isEnabled('picture-in-picture')
&& config.plugins.getOptions<PiPOptions>('picture-in-picture').isInPiP;
if (!isPiPEnabled && winWasMaximized !== isMaximized) {
winWasMaximized = isMaximized; winWasMaximized = isMaximized;
config.set('window-maximized', isMaximized); config.set('window-maximized', isMaximized);
} }
@ -374,19 +302,19 @@ async function createMainWindow() {
return; return;
} }
lateSave('window-size', { if (!isPiPEnabled) {
width, lateSave('window-size', {
height, width: windowSize[0],
}); height: windowSize[1],
});
} else if (config.plugins.getOptions<PiPOptions>('picture-in-picture').saveSize) {
lateSave('pip-size', windowSize, setPiPOptions);
}
}); });
const savedTimeouts: Record<string, NodeJS.Timeout | undefined> = {}; const savedTimeouts: Record<string, NodeJS.Timeout | undefined> = {};
function lateSave( function lateSave(key: string, value: unknown, fn: (key: string, value: unknown) => void = config.set) {
key: string,
value: unknown,
fn: (key: string, value: unknown) => void = config.set,
) {
if (savedTimeouts[key]) { if (savedTimeouts[key]) {
clearTimeout(savedTimeouts[key]); clearTimeout(savedTimeouts[key]);
} }
@ -397,7 +325,7 @@ async function createMainWindow() {
}, 600); }, 600);
} }
app.on('render-process-gone', (_event, _webContents, details) => { app.on('render-process-gone', (event, webContents, details) => {
showUnresponsiveDialog(win, details); showUnresponsiveDialog(win, details);
}); });
@ -409,61 +337,32 @@ async function createMainWindow() {
removeContentSecurityPolicy(); removeContentSecurityPolicy();
win.webContents.on('dom-ready', () => {
if (useInlineMenu && is.windows()) {
win.setTitleBarOverlay({
...defaultTitleBarOverlayOptions,
height: Math.floor(
defaultTitleBarOverlayOptions.height! *
win.webContents.getZoomFactor(),
),
});
}
});
win.webContents.on('will-redirect', (event) => {
const url = new URL(event.url);
// Workarounds for regions where YTM is restricted
if (url.hostname.endsWith('youtube.com') && url.pathname === '/premium') {
event.preventDefault();
win.webContents.loadURL(
'https://accounts.google.com/ServiceLogin?ltmpl=music&service=youtube&continue=https%3A%2F%2Fwww.youtube.com%2Fsignin%3Faction_handle_signin%3Dtrue%26next%3Dhttps%253A%252F%252Fmusic.youtube.com%252F'
);
}
});
win.webContents.loadURL(urlToLoad); win.webContents.loadURL(urlToLoad);
return win; return win;
} }
app.once('browser-window-created', (_event, win) => { app.once('browser-window-created', (event, win) => {
if (config.get('options.overrideUserAgent')) { if (config.get('options.overrideUserAgent')) {
// User agents are from https://developers.whatismybrowser.com/useragents/explore/ // User agents are from https://developers.whatismybrowser.com/useragents/explore/
const originalUserAgent = win.webContents.userAgent; const originalUserAgent = win.webContents.userAgent;
const userAgents = { const userAgents = {
mac: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 12.1; rv:95.0) Gecko/20100101 Firefox/95.0', mac: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 12.1; rv:95.0) Gecko/20100101 Firefox/95.0',
windows: windows: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:95.0) Gecko/20100101 Firefox/95.0',
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:95.0) Gecko/20100101 Firefox/95.0',
linux: 'Mozilla/5.0 (Linux x86_64; rv:95.0) Gecko/20100101 Firefox/95.0', linux: 'Mozilla/5.0 (Linux x86_64; rv:95.0) Gecko/20100101 Firefox/95.0',
}; };
const updatedUserAgent = is.macOS() const updatedUserAgent
? userAgents.mac = is.macOS() ? userAgents.mac
: is.windows() : (is.windows() ? userAgents.windows
? userAgents.windows : userAgents.linux);
: userAgents.linux;
win.webContents.userAgent = updatedUserAgent; win.webContents.userAgent = updatedUserAgent;
app.userAgentFallback = updatedUserAgent; app.userAgentFallback = updatedUserAgent;
win.webContents.session.webRequest.onBeforeSendHeaders((details, cb) => { win.webContents.session.webRequest.onBeforeSendHeaders((details, cb) => {
// This will only happen if login failed, and "retry" was pressed // This will only happen if login failed, and "retry" was pressed
if ( if (win.webContents.getURL().startsWith('https://accounts.google.com') && details.url.startsWith('https://accounts.google.com')) {
win.webContents.getURL().startsWith('https://accounts.google.com') &&
details.url.startsWith('https://accounts.google.com')
) {
details.requestHeaders['User-Agent'] = originalUserAgent; details.requestHeaders['User-Agent'] = originalUserAgent;
} }
@ -474,41 +373,33 @@ app.once('browser-window-created', (_event, win) => {
setupSongInfo(win); setupSongInfo(win);
setupAppControls(); setupAppControls();
win.webContents.on( win.webContents.on('did-fail-load', (
'did-fail-load', _event,
( errorCode,
_event, errorDescription,
validatedURL,
isMainFrame,
frameProcessId,
frameRoutingId,
) => {
const log = JSON.stringify({
error: 'did-fail-load',
errorCode, errorCode,
errorDescription, errorDescription,
validatedURL, validatedURL,
isMainFrame, isMainFrame,
frameProcessId, frameProcessId,
frameRoutingId, frameRoutingId,
) => { }, null, '\t');
const log = JSON.stringify( if (is.dev()) {
{ console.log(log);
error: 'did-fail-load', }
errorCode,
errorDescription,
validatedURL,
isMainFrame,
frameProcessId,
frameRoutingId,
},
null,
'\t',
);
if (is.dev()) {
console.log(log);
}
if (errorCode !== -3) { if (errorCode !== -3) { // -3 is a false positive
// -3 is a false positive win.webContents.send('log', log);
win.webContents.send('log', log); win.webContents.loadFile(path.join(__dirname, 'error.html'));
win.webContents.loadFile(path.join(__dirname, 'error.html')); }
} });
},
);
win.webContents.on('will-prevent-unload', (event) => { win.webContents.on('will-prevent-unload', (event) => {
event.preventDefault(); event.preventDefault();
@ -534,30 +425,12 @@ app.on('activate', async () => {
} }
}); });
const getDefaultLocale = (locale: string) => app.on('ready', async () => {
Object.keys(languageResources).includes(locale) ? locale : null;
app.whenReady().then(async () => {
if (!config.get('options.language')) {
const locale = getDefaultLocale(app.getLocale());
if (locale) {
config.set('options.language', locale);
}
}
await loadI18n().then(async () => {
await setLanguage(config.get('options.language') ?? 'en');
console.log(LoggerPrefix, t('main.console.i18n.loaded'));
});
if (config.get('options.autoResetAppCache')) { if (config.get('options.autoResetAppCache')) {
// Clear cache after 20s // Clear cache after 20s
const clearCacheTimeout = setTimeout(() => { const clearCacheTimeout = setTimeout(() => {
if (is.dev()) { if (is.dev()) {
console.log( console.log('Clearing app cache.');
LoggerPrefix,
t('main.console.when-ready.clearing-cache-after-20s'),
);
} }
session.defaultSession.clearCache(); session.defaultSession.clearCache();
@ -572,29 +445,17 @@ app.whenReady().then(async () => {
const appLocation = process.execPath; const appLocation = process.execPath;
const appData = app.getPath('appData'); const appData = app.getPath('appData');
// Check shortcut validity if not in dev mode / running portable app // Check shortcut validity if not in dev mode / running portable app
if ( if (!is.dev() && !appLocation.startsWith(path.join(appData, '..', 'Local', 'Temp'))) {
!is.dev() && const shortcutPath = path.join(appData, 'Microsoft', 'Windows', 'Start Menu', 'Programs', 'YouTube Music.lnk');
!appLocation.startsWith(path.join(appData, '..', 'Local', 'Temp')) try { // Check if shortcut is registered and valid
) { const shortcutDetails = shell.readShortcutLink(shortcutPath); // Throw error if doesn't exist yet
const shortcutPath = path.join(
appData,
'Microsoft',
'Windows',
'Start Menu',
'Programs',
'YouTube Music.lnk',
);
try {
// Check if shortcut is registered and valid
const shortcutDetails = shell.readShortcutLink(shortcutPath); // Throw error if it doesn't exist yet
if ( if (
shortcutDetails.target !== appLocation || shortcutDetails.target !== appLocation
shortcutDetails.appUserModelId !== appID || shortcutDetails.appUserModelId !== appID
) { ) {
throw 'needUpdate'; throw 'needUpdate';
} }
} catch (error) { } catch (error) { // If not valid -> Register shortcut
// If not valid -> Register shortcut
shell.writeShortcutLink( shell.writeShortcutLink(
shortcutPath, shortcutPath,
error === 'needUpdate' ? 'update' : 'create', error === 'needUpdate' ? 'update' : 'create',
@ -609,51 +470,9 @@ app.whenReady().then(async () => {
} }
} }
ipcMain.on('get-renderer-script', (event) => {
// Inject index.html file as string using insertAdjacentHTML
// In dev mode, get string from process.env.VITE_DEV_SERVER_URL, else use fs.readFileSync
if (is.dev() && process.env.ELECTRON_RENDERER_URL) {
// HACK: to make vite work with electron renderer (supports hot reload)
event.returnValue = [null, `
console.log('${LoggerPrefix}', 'Loading vite from dev server');
(async () => {
await new Promise((resolve) => {
if (document.readyState === 'loading') {
console.log('${LoggerPrefix}', 'Waiting for DOM to load');
document.addEventListener('DOMContentLoaded', () => resolve(), { once: true });
} else {
resolve();
}
});
const viteScript = document.createElement('script');
viteScript.type = 'module';
viteScript.src = '${process.env.ELECTRON_RENDERER_URL}/@vite/client';
const rendererScript = document.createElement('script');
rendererScript.type = 'module';
rendererScript.src = '${process.env.ELECTRON_RENDERER_URL}/renderer.ts';
document.body.appendChild(viteScript);
document.body.appendChild(rendererScript);
})();
0
`];
} else {
const rendererPath = path.join(__dirname, '..', 'renderer');
const indexHTML = parse(
fs.readFileSync(path.join(rendererPath, 'index.html'), 'utf-8'),
);
const scriptSrc = indexHTML.querySelector('script')!;
const scriptPath = path.join(
rendererPath,
scriptSrc.getAttribute('src')!,
);
const scriptString = fs.readFileSync(scriptPath, 'utf-8');
event.returnValue = [url.pathToFileURL(scriptPath).toString(), scriptString + ';0'];
}
});
mainWindow = await createMainWindow(); mainWindow = await createMainWindow();
await setApplicationMenu(mainWindow); setApplicationMenu(mainWindow);
await refreshMenu(mainWindow); refreshMenu(mainWindow);
setUpTray(app, mainWindow); setUpTray(app, mainWindow);
setupProtocolHandler(mainWindow); setupProtocolHandler(mainWindow);
@ -665,10 +484,7 @@ app.whenReady().then(async () => {
const lastIndex = protocolArgv.endsWith('/') ? -1 : undefined; const lastIndex = protocolArgv.endsWith('/') ? -1 : undefined;
const command = protocolArgv.slice(uri.length, lastIndex); const command = protocolArgv.slice(uri.length, lastIndex);
if (is.dev()) { if (is.dev()) {
console.debug( console.debug(`Received command over protocol: "${command}"`);
LoggerPrefix,
t('main.console.second-instance.receive-command', { command }),
);
} }
handleProtocol(command); handleProtocol(command);
@ -701,30 +517,16 @@ app.whenReady().then(async () => {
clearTimeout(updateTimeout); clearTimeout(updateTimeout);
}, 2000); }, 2000);
autoUpdater.on('update-available', () => { autoUpdater.on('update-available', () => {
const downloadLink = const downloadLink
'https://github.com/th-ch/youtube-music/releases/latest'; = 'https://github.com/th-ch/youtube-music/releases/latest';
const dialogOptions: Electron.MessageBoxOptions = { const dialogOptions: Electron.MessageBoxOptions = {
type: 'info', type: 'info',
buttons: [ buttons: ['OK', 'Download', 'Disable updates'],
t('main.dialog.update-available.buttons.ok'), title: 'Application Update',
t('main.dialog.update-available.buttons.download'), message: 'A new version is available',
t('main.dialog.update-available.buttons.disable'), detail: `A new version is available and can be downloaded at ${downloadLink}`,
],
title: t('main.dialog.update-available.title'),
message: t('main.dialog.update-available.message'),
detail: t('main.dialog.update-available.detail', { downloadLink }),
defaultId: 1,
cancelId: 0,
}; };
dialog.showMessageBox(dialogOptions).then((dialogOutput) => {
let dialogPromise: Promise<Electron.MessageBoxReturnValue>;
if (mainWindow) {
dialogPromise = dialog.showMessageBox(mainWindow, dialogOptions);
} else {
dialogPromise = dialog.showMessageBox(dialogOptions);
}
dialogPromise.then((dialogOutput) => {
switch (dialogOutput.response) { switch (dialogOutput.response) {
// Download // Download
case 1: { case 1: {
@ -738,7 +540,7 @@ app.whenReady().then(async () => {
break; break;
} }
case 0: { default: {
break; break;
} }
} }
@ -748,9 +550,8 @@ app.whenReady().then(async () => {
if (config.get('options.hideMenu') && !config.get('options.hideMenuWarned')) { if (config.get('options.hideMenu') && !config.get('options.hideMenuWarned')) {
dialog.showMessageBox(mainWindow, { dialog.showMessageBox(mainWindow, {
type: 'info', type: 'info', title: 'Hide Menu Enabled',
title: t('main.dialog.hide-menu-enabled.title'), message: "Menu is hidden, use 'Alt' to show it (or 'Escape' if using in-app-menu)",
message: t('main.dialog.hide-menu-enabled.message'),
}); });
config.set('options.hideMenuWarned', true); config.set('options.hideMenuWarned', true);
} }
@ -776,45 +577,31 @@ app.whenReady().then(async () => {
} }
}); });
function showUnresponsiveDialog( function showUnresponsiveDialog(win: BrowserWindow, details: Electron.RenderProcessGoneDetails) {
win: BrowserWindow,
details: Electron.RenderProcessGoneDetails,
) {
if (details) { if (details) {
console.error( console.log('Unresponsive Error!\n' + JSON.stringify(details, null, '\t'));
LoggerPrefix,
t('main.console.unresponsive.details', {
error: JSON.stringify(details, null, '\t'),
}),
);
} }
dialog dialog.showMessageBox(win, {
.showMessageBox(win, { type: 'error',
type: 'error', title: 'Window Unresponsive',
title: t('main.dialog.unresponsive.title'), message: 'The Application is Unresponsive',
message: t('main.dialog.unresponsive.message'), detail: 'We are sorry for the inconvenience! please choose what to do:',
detail: t('main.dialog.unresponsive.detail'), buttons: ['Wait', 'Relaunch', 'Quit'],
buttons: [ cancelId: 0,
t('main.dialog.unresponsive.buttons.wait'), }).then((result) => {
t('main.dialog.unresponsive.buttons.relaunch'), switch (result.response) {
t('main.dialog.unresponsive.buttons.quit'), case 1: {
], restart();
cancelId: 0, break;
})
.then((result) => {
switch (result.response) {
case 1: {
restart();
break;
}
case 2: {
app.quit();
break;
}
} }
});
case 2: {
app.quit();
break;
}
}
});
} }
function removeContentSecurityPolicy( function removeContentSecurityPolicy(
@ -837,21 +624,18 @@ function removeContentSecurityPolicy(
}); });
// When multiple listeners are defined, apply them all // When multiple listeners are defined, apply them all
betterSession.webRequest.setResolver( betterSession.webRequest.setResolver('onHeadersReceived', async (listeners) => {
'onHeadersReceived', return listeners.reduce(
async (listeners) => { async (accumulator, listener) => {
return listeners.reduce( const acc = await accumulator;
async (accumulator, listener) => { if (acc.cancel) {
const acc = await accumulator; return acc;
if (acc.cancel) { }
return acc;
}
const result = await listener.apply(); const result = await listener.apply();
return { ...accumulator, ...result }; return { ...accumulator, ...result };
}, },
Promise.resolve({ cancel: false }), Promise.resolve({ cancel: false }),
); );
}, });
);
} }

View File

@ -1,161 +0,0 @@
import { BrowserWindow, ipcMain } from 'electron';
import { deepmerge } from 'deepmerge-ts';
import { allPlugins, mainPlugins } from 'virtual:plugins';
import config from '@/config';
import { LoggerPrefix, startPlugin, stopPlugin } from '@/utils';
import { t } from '@/i18n';
import type { PluginConfig, PluginDef } from '@/types/plugins';
import type { BackendContext } from '@/types/contexts';
const loadedPluginMap: Record<
string,
PluginDef<unknown, unknown, unknown>
> = {};
const createContext = (
id: string,
win: BrowserWindow,
): BackendContext<PluginConfig> => ({
getConfig: () =>
deepmerge(
allPlugins[id].config ?? { enabled: false },
config.get(`plugins.${id}`) ?? {},
) as PluginConfig,
setConfig: (newConfig) => {
config.setPartial(`plugins.${id}`, newConfig, allPlugins[id].config);
},
ipc: {
send: (event: string, ...args: unknown[]) => {
win.webContents.send(event, ...args);
},
handle: (event: string, listener: CallableFunction) => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
ipcMain.handle(event, (_, ...args: unknown[]) => listener(...args));
},
on: (event: string, listener: CallableFunction) => {
ipcMain.on(event, (_, ...args: unknown[]) => {
listener(...args);
});
},
removeHandler: (event: string) => {
ipcMain.removeHandler(event);
},
},
window: win,
});
export const forceUnloadMainPlugin = async (
id: string,
win: BrowserWindow,
): Promise<void> => {
const plugin = loadedPluginMap[id];
if (!plugin) return;
try {
const hasStopped = await stopPlugin(id, plugin, {
ctx: 'backend',
context: createContext(id, win),
});
if (
hasStopped ||
(hasStopped === null &&
typeof plugin.backend !== 'function' &&
plugin.backend)
) {
delete loadedPluginMap[id];
console.log(
LoggerPrefix,
t('common.console.plugins.unloaded', { pluginName: id }),
);
return;
} else {
console.log(
LoggerPrefix,
t('common.console.plugins.unload-failed', { pluginName: id }),
);
return Promise.reject();
}
} catch (err) {
console.error(
LoggerPrefix,
t('common.console.plugins.unload-failed', { pluginName: id }),
);
console.trace(err);
return Promise.reject(err);
}
};
export const forceLoadMainPlugin = async (
id: string,
win: BrowserWindow,
): Promise<void> => {
const plugin = mainPlugins[id];
if (!plugin) return;
try {
const hasStarted = await startPlugin(id, plugin, {
ctx: 'backend',
context: createContext(id, win),
});
if (
hasStarted ||
(hasStarted === null &&
typeof plugin.backend !== 'function' &&
plugin.backend)
) {
loadedPluginMap[id] = plugin;
} else {
console.log(
LoggerPrefix,
t('common.console.plugins.load-failed', { pluginName: id }),
);
return Promise.reject();
}
} catch (err) {
console.error(
LoggerPrefix,
t('common.console.plugins.initialize-failed', { pluginName: id }),
);
console.trace(err);
return Promise.reject(err);
}
};
export const loadAllMainPlugins = async (win: BrowserWindow) => {
console.log(LoggerPrefix, t('common.console.plugins.load-all'));
const pluginConfigs = config.plugins.getPlugins();
const queue: Promise<void>[] = [];
for (const [plugin, pluginDef] of Object.entries(mainPlugins)) {
const config = deepmerge(pluginDef.config, pluginConfigs[plugin] ?? {});
if (config.enabled) {
queue.push(forceLoadMainPlugin(plugin, win));
} else if (loadedPluginMap[plugin]) {
queue.push(forceUnloadMainPlugin(plugin, win));
}
}
await Promise.allSettled(queue);
};
export const unloadAllMainPlugins = async (win: BrowserWindow) => {
for (const id of Object.keys(loadedPluginMap)) {
await forceUnloadMainPlugin(id, win);
}
};
export const getLoadedMainPlugin = (
id: string,
): PluginDef<unknown, unknown, unknown> | undefined => {
return loadedPluginMap[id];
};
export const getAllLoadedMainPlugins = () => {
return loadedPluginMap;
};

View File

@ -1,91 +0,0 @@
import { deepmerge } from 'deepmerge-ts';
import { allPlugins } from 'virtual:plugins';
import config from '@/config';
import { setApplicationMenu } from '@/menu';
import { LoggerPrefix } from '@/utils';
import { t } from '@/i18n';
import type { MenuContext } from '@/types/contexts';
import type { BrowserWindow, MenuItemConstructorOptions } from 'electron';
import type { PluginConfig } from '@/types/plugins';
const menuTemplateMap: Record<string, MenuItemConstructorOptions[]> = {};
const createContext = (
id: string,
win: BrowserWindow,
): MenuContext<PluginConfig> => ({
getConfig: () =>
deepmerge(
allPlugins[id].config ?? { enabled: false },
config.get(`plugins.${id}`) ?? {},
) as PluginConfig,
setConfig: (newConfig) => {
config.setPartial(`plugins.${id}`, newConfig, allPlugins[id].config);
},
window: win,
refresh: async () => {
await setApplicationMenu(win);
if (config.plugins.isEnabled('in-app-menu')) {
win.webContents.send('refresh-in-app-menu');
}
},
});
export const forceLoadMenuPlugin = async (id: string, win: BrowserWindow) => {
try {
const plugin = allPlugins[id];
if (!plugin) return;
const menu = plugin.menu?.(createContext(id, win));
if (menu) {
const result = await menu;
if (result.length > 0) {
menuTemplateMap[id] = result;
} else {
return;
}
} else return;
console.log(
LoggerPrefix,
t('common.console.plugins.loaded', { pluginName: `${id}::menu` }),
);
} catch (err) {
console.error(
LoggerPrefix,
t('common.console.plugins.initialize-failed', {
pluginName: `${id}::menu`,
}),
);
console.trace(err);
}
};
export const loadAllMenuPlugins = async (win: BrowserWindow) => {
const pluginConfigs = config.plugins.getPlugins();
for (const [pluginId, pluginDef] of Object.entries(allPlugins)) {
const config = deepmerge(
pluginDef.config ?? { enabled: false },
pluginConfigs[pluginId] ?? {},
);
if (config.enabled) {
await forceLoadMenuPlugin(pluginId, win);
}
}
};
export const getMenuTemplate = (
id: string,
): MenuItemConstructorOptions[] | undefined => {
return menuTemplateMap[id];
};
export const getAllMenuTemplate = () => {
return menuTemplateMap;
};

View File

@ -1,114 +0,0 @@
import { deepmerge } from 'deepmerge-ts';
import { allPlugins, preloadPlugins } from 'virtual:plugins';
import { LoggerPrefix, startPlugin, stopPlugin } from '@/utils';
import config from '@/config';
import { t } from '@/i18n';
import type { PreloadContext } from '@/types/contexts';
import type { PluginConfig, PluginDef } from '@/types/plugins';
const loadedPluginMap: Record<
string,
PluginDef<unknown, unknown, unknown>
> = {};
const createContext = (id: string): PreloadContext<PluginConfig> => ({
getConfig: () =>
deepmerge(
allPlugins[id].config ?? { enabled: false },
config.get(`plugins.${id}`) ?? {},
) as PluginConfig,
setConfig: (newConfig) => {
config.setPartial(`plugins.${id}`, newConfig, allPlugins[id].config);
},
});
export const forceUnloadPreloadPlugin = async (id: string) => {
if (!loadedPluginMap[id]) return;
const hasStopped = await stopPlugin(id, loadedPluginMap[id], {
ctx: 'preload',
context: createContext(id),
});
if (hasStopped || (hasStopped === null && loadedPluginMap[id].preload)) {
console.log(
LoggerPrefix,
t('common.console.plugins.unloaded', { pluginName: id }),
);
delete loadedPluginMap[id];
} else {
console.error(
LoggerPrefix,
t('common.console.plugins.unload-failed', { pluginName: id }),
);
}
};
export const forceLoadPreloadPlugin = async (id: string) => {
try {
const plugin = preloadPlugins[id];
if (!plugin) return;
const hasStarted = await startPlugin(id, plugin, {
ctx: 'preload',
context: createContext(id),
});
if (
hasStarted ||
(hasStarted === null &&
typeof plugin.preload !== 'function' &&
plugin.preload)
) {
loadedPluginMap[id] = plugin;
}
console.log(
LoggerPrefix,
t('common.console.plugins.loaded', { pluginName: id }),
);
} catch (err) {
console.error(
LoggerPrefix,
t('common.console.plugins.initialize-failed', { pluginName: id }),
);
console.trace(err);
}
};
export const loadAllPreloadPlugins = () => {
const pluginConfigs = config.plugins.getPlugins();
for (const [pluginId, pluginDef] of Object.entries(preloadPlugins)) {
const config = deepmerge(
pluginDef.config ?? { enable: false },
pluginConfigs[pluginId] ?? {},
);
if (config.enabled) {
forceLoadPreloadPlugin(pluginId);
} else {
if (loadedPluginMap[pluginId]) {
forceUnloadPreloadPlugin(pluginId);
}
}
}
};
export const unloadAllPreloadPlugins = async () => {
for (const id of Object.keys(loadedPluginMap)) {
await forceUnloadPreloadPlugin(id);
}
};
export const getLoadedPreloadPlugin = (
id: string,
): PluginDef<unknown, unknown, unknown> | undefined => {
return loadedPluginMap[id];
};
export const getAllLoadedPreloadPlugins = () => {
return loadedPluginMap;
};

View File

@ -1,145 +0,0 @@
import { deepmerge } from 'deepmerge-ts';
import { rendererPlugins } from 'virtual:plugins';
import { LoggerPrefix, startPlugin, stopPlugin } from '@/utils';
import { t } from '@/i18n';
import type { RendererContext } from '@/types/contexts';
import type { PluginConfig, PluginDef } from '@/types/plugins';
const unregisterStyleMap: Record<string, (() => void)[]> = {};
const loadedPluginMap: Record<
string,
PluginDef<unknown, unknown, unknown>
> = {};
export const createContext = <Config extends PluginConfig>(
id: string,
): RendererContext<Config> => ({
getConfig: async () => window.ipcRenderer.invoke('ytmd:get-config', id),
setConfig: async (newConfig) => {
await window.ipcRenderer.invoke('ytmd:set-config', id, newConfig);
},
ipc: {
send: (event: string, ...args: unknown[]) => {
window.ipcRenderer.send(event, ...args);
},
invoke: (event: string, ...args: unknown[]) =>
window.ipcRenderer.invoke(event, ...args),
on: (event: string, listener: CallableFunction) => {
window.ipcRenderer.on(event, (_, ...args: unknown[]) => {
listener(...args);
});
},
removeAllListeners: (event: string) => {
window.ipcRenderer.removeAllListeners(event);
},
},
});
export const forceUnloadRendererPlugin = async (id: string) => {
unregisterStyleMap[id]?.forEach((unregister) => unregister());
delete unregisterStyleMap[id];
delete loadedPluginMap[id];
const plugin = rendererPlugins[id];
if (!plugin) return;
const hasStopped = await stopPlugin(id, plugin, {
ctx: 'renderer',
context: createContext(id),
});
if (plugin?.stylesheets) {
document.querySelector(`style#plugin-${id}`)?.remove();
}
if (hasStopped || (hasStopped === null && plugin?.renderer)) {
console.log(
LoggerPrefix,
t('common.console.plugins.unloaded', { pluginName: id }),
);
} else {
console.error(
LoggerPrefix,
t('common.console.plugins.unload-failed', { pluginName: id }),
);
}
};
export const forceLoadRendererPlugin = async (id: string) => {
const plugin = rendererPlugins[id];
if (!plugin) return;
const hasEvaled = await startPlugin(id, plugin, {
ctx: 'renderer',
context: createContext(id),
});
if (
hasEvaled ||
plugin?.stylesheets ||
(hasEvaled === null &&
typeof plugin?.renderer !== 'function' &&
plugin?.renderer)
) {
loadedPluginMap[id] = plugin;
if (plugin?.stylesheets) {
const styleSheetList = plugin.stylesheets.map((style) => {
const styleSheet = new CSSStyleSheet();
styleSheet.replaceSync(style);
return styleSheet;
});
document.adoptedStyleSheets = [
...document.adoptedStyleSheets,
...styleSheetList,
];
}
console.log(
LoggerPrefix,
t('common.console.plugins.loaded', { pluginName: id }),
);
} else {
console.log(
LoggerPrefix,
t('common.console.plugins.initialize-failed', { pluginName: id }),
);
}
};
export const loadAllRendererPlugins = async () => {
const pluginConfigs = window.mainConfig.plugins.getPlugins();
for (const [pluginId, pluginDef] of Object.entries(rendererPlugins)) {
const config = deepmerge(pluginDef.config, pluginConfigs[pluginId] ?? {});
if (config.enabled) {
await forceLoadRendererPlugin(pluginId);
} else {
if (loadedPluginMap[pluginId]) {
await forceUnloadRendererPlugin(pluginId);
}
}
}
};
export const unloadAllRendererPlugins = async () => {
for (const id of Object.keys(loadedPluginMap)) {
await forceUnloadRendererPlugin(id);
}
};
export const getLoadedRendererPlugin = (
id: string,
): PluginDef<unknown, unknown, unknown> | undefined => {
return loadedPluginMap[id];
};
export const getAllLoadedRendererPlugins = () => {
return loadedPluginMap;
};

View File

@ -1,47 +1,56 @@
import is from 'electron-is'; import is from 'electron-is';
import { import { app, BrowserWindow, clipboard, dialog, Menu } from 'electron';
app,
BrowserWindow,
clipboard,
dialog,
Menu,
MenuItem,
shell,
} from 'electron';
import prompt from 'custom-electron-prompt'; import prompt from 'custom-electron-prompt';
import { satisfies } from 'semver';
import { allPlugins } from 'virtual:plugins';
import { languageResources } from 'virtual:i18n';
import config from './config';
import { restart } from './providers/app-controls'; import { restart } from './providers/app-controls';
import config from './config';
import { startingPages } from './providers/extracted-data'; import { startingPages } from './providers/extracted-data';
import promptOptions from './providers/prompt-options'; import promptOptions from './providers/prompt-options';
import { getAllMenuTemplate, loadAllMenuPlugins } from './loader/menu'; import adblockerMenu from './plugins/adblocker/menu';
import { setLanguage, t } from '@/i18n'; import ambientModeMenu from './plugins/ambient-mode/menu';
import captionsSelectorMenu from './plugins/captions-selector/menu';
import packageJson from '../package.json'; import crossfadeMenu from './plugins/crossfade/menu';
import disableAutoplayMenu from './plugins/disable-autoplay/menu';
import discordMenu from './plugins/discord/menu';
import downloaderMenu from './plugins/downloader/menu';
import inAppMenuTitlebarMenu from './plugins/in-app-menu/menu';
import lyricsGeniusMenu from './plugins/lyrics-genius/menu';
import notificationsMenu from './plugins/notifications/menu';
import pictureInPictureMenu from './plugins/picture-in-picture/menu';
import preciseVolumeMenu from './plugins/precise-volume/menu';
import shortcutsMenu from './plugins/shortcuts/menu';
import videoToggleMenu from './plugins/video-toggle/menu';
import visualizerMenu from './plugins/visualizer/menu';
import { getAvailablePluginNames } from './plugins/utils';
export type MenuTemplate = Electron.MenuItemConstructorOptions[]; export type MenuTemplate = Electron.MenuItemConstructorOptions[];
// True only if in-app-menu was loaded on launch // True only if in-app-menu was loaded on launch
const inAppMenuActive = config.plugins.isEnabled('in-app-menu'); const inAppMenuActive = config.plugins.isEnabled('in-app-menu');
const pluginEnabledMenu = ( const betaPlugins = ['crossfade', 'lumiastream'];
plugin: string,
label = '', const pluginMenus = {
description: string | undefined = undefined, 'adblocker': adblockerMenu,
isNew = false, 'ambient-mode': ambientModeMenu,
hasSubmenu = false, 'disable-autoplay': disableAutoplayMenu,
refreshMenu: (() => void) | undefined = undefined, 'captions-selector': captionsSelectorMenu,
): Electron.MenuItemConstructorOptions => ({ 'crossfade': crossfadeMenu,
'discord': discordMenu,
'downloader': downloaderMenu,
'in-app-menu': inAppMenuTitlebarMenu,
'lyrics-genius': lyricsGeniusMenu,
'notifications': notificationsMenu,
'picture-in-picture': pictureInPictureMenu,
'precise-volume': preciseVolumeMenu,
'shortcuts': shortcutsMenu,
'video-toggle': videoToggleMenu,
'visualizer': visualizerMenu,
};
const pluginEnabledMenu = (plugin: string, label = '', hasSubmenu = false, refreshMenu: (() => void ) | undefined = undefined): Electron.MenuItemConstructorOptions => ({
label: label || plugin, label: label || plugin,
sublabel: isNew ? t('main.menu.plugins.new') : undefined,
toolTip: description,
type: 'checkbox', type: 'checkbox',
checked: config.plugins.isEnabled(plugin), checked: config.plugins.isEnabled(plugin),
click(item: Electron.MenuItem) { click(item: Electron.MenuItem) {
@ -57,117 +66,78 @@ const pluginEnabledMenu = (
}, },
}); });
export const refreshMenu = async (win: BrowserWindow) => { export const refreshMenu = (win: BrowserWindow) => {
await setApplicationMenu(win); setApplicationMenu(win);
if (inAppMenuActive) { if (inAppMenuActive) {
win.webContents.send('refresh-in-app-menu'); win.webContents.send('refreshMenu');
} }
}; };
export const mainMenuTemplate = async ( export const mainMenuTemplate = (win: BrowserWindow): MenuTemplate => {
win: BrowserWindow,
): Promise<MenuTemplate> => {
const innerRefreshMenu = () => refreshMenu(win); const innerRefreshMenu = () => refreshMenu(win);
await loadAllMenuPlugins(win);
const menuResult = Object.entries(getAllMenuTemplate()).map(
([id, template]) => {
const plugin = allPlugins[id];
const pluginLabel = plugin?.name?.() ?? id;
const pluginDescription = plugin?.description?.() ?? undefined;
const isNew = plugin?.addedVersion ? satisfies(packageJson.version, plugin.addedVersion) : false;
if (!config.plugins.isEnabled(id)) {
return [
id,
pluginEnabledMenu(id, pluginLabel, pluginDescription, isNew, true, innerRefreshMenu),
] as const;
}
return [
id,
{
label: pluginLabel,
sublabel: isNew ? t('main.menu.plugins.new') : undefined,
toolTip: pluginDescription,
submenu: [
pluginEnabledMenu(
id,
t('main.menu.plugins.enabled'),
undefined,
false,
true,
innerRefreshMenu,
),
{ type: 'separator' },
...template,
],
} satisfies Electron.MenuItemConstructorOptions,
] as const;
},
);
const availablePlugins = Object.keys(allPlugins);
const pluginMenus = availablePlugins
.sort((a, b) => {
const aPluginLabel = allPlugins[a]?.name?.() ?? a;
const bPluginLabel = allPlugins[b]?.name?.() ?? b;
return aPluginLabel.localeCompare(bPluginLabel);
})
.map((id) => {
const predefinedTemplate = menuResult.find((it) => it[0] === id);
if (predefinedTemplate) return predefinedTemplate[1];
const plugin = allPlugins[id];
const pluginLabel = plugin?.name?.() ?? id;
const pluginDescription = plugin?.description?.() ?? undefined;
const isNew = plugin?.addedVersion ? satisfies(packageJson.version, plugin.addedVersion) : false;
return pluginEnabledMenu(id, pluginLabel, pluginDescription, isNew, true, innerRefreshMenu);
});
const availableLanguages = Object.keys(languageResources);
return [ return [
{ {
label: t('main.menu.plugins.label'), label: 'Plugins',
submenu: pluginMenus, submenu:
getAvailablePluginNames().map((pluginName) => {
let pluginLabel = pluginName;
if (betaPlugins.includes(pluginLabel)) {
pluginLabel += ' [beta]';
}
if (Object.hasOwn(pluginMenus, pluginName)) {
const getPluginMenu = pluginMenus[pluginName as keyof typeof pluginMenus];
if (!config.plugins.isEnabled(pluginName)) {
return pluginEnabledMenu(pluginName, pluginLabel, true, innerRefreshMenu);
}
return {
label: pluginLabel,
submenu: [
pluginEnabledMenu(pluginName, 'Enabled', true, innerRefreshMenu),
{ type: 'separator' },
...getPluginMenu(win, config.plugins.getOptions(pluginName), innerRefreshMenu),
],
} satisfies Electron.MenuItemConstructorOptions;
}
return pluginEnabledMenu(pluginName, pluginLabel);
}),
}, },
{ {
label: t('main.menu.options.label'), label: 'Options',
submenu: [ submenu: [
{ {
label: t('main.menu.options.submenu.auto-update'), label: 'Auto-update',
type: 'checkbox', type: 'checkbox',
checked: config.get('options.autoUpdates'), checked: config.get('options.autoUpdates'),
click(item: MenuItem) { click(item) {
config.setMenuOption('options.autoUpdates', item.checked); config.setMenuOption('options.autoUpdates', item.checked);
}, },
}, },
{ {
label: t('main.menu.options.submenu.resume-on-start'), label: 'Resume last song when app starts',
type: 'checkbox', type: 'checkbox',
checked: config.get('options.resumeOnStart'), checked: config.get('options.resumeOnStart'),
click(item: MenuItem) { click(item) {
config.setMenuOption('options.resumeOnStart', item.checked); config.setMenuOption('options.resumeOnStart', item.checked);
}, },
}, },
{ {
label: t('main.menu.options.submenu.starting-page.label'), label: 'Starting page',
submenu: (() => { submenu: (() => {
const subMenuArray: Electron.MenuItemConstructorOptions[] = const subMenuArray: Electron.MenuItemConstructorOptions[] = Object.keys(startingPages).map((name) => ({
Object.keys(startingPages).map((name) => ({ label: name,
label: name, type: 'radio',
type: 'radio', checked: config.get('options.startingPage') === name,
checked: config.get('options.startingPage') === name, click() {
click() { config.set('options.startingPage', name);
config.set('options.startingPage', name); },
}, }));
}));
subMenuArray.unshift({ subMenuArray.unshift({
label: t('main.menu.options.submenu.starting-page.unset'), label: 'Unset',
type: 'radio', type: 'radio',
checked: config.get('options.startingPage') === '', checked: config.get('options.startingPage') === '',
click() { click() {
@ -178,30 +148,21 @@ export const mainMenuTemplate = async (
})(), })(),
}, },
{ {
label: t('main.menu.options.submenu.visual-tweaks.label'), label: 'Visual Tweaks',
submenu: [ submenu: [
{ {
label: t( label: 'Remove upgrade button',
'main.menu.options.submenu.visual-tweaks.submenu.remove-upgrade-button',
),
type: 'checkbox', type: 'checkbox',
checked: config.get('options.removeUpgradeButton'), checked: config.get('options.removeUpgradeButton'),
click(item: MenuItem) { click(item) {
config.setMenuOption( config.setMenuOption('options.removeUpgradeButton', item.checked);
'options.removeUpgradeButton',
item.checked,
);
}, },
}, },
{ {
label: t( label: 'Like buttons',
'main.menu.options.submenu.visual-tweaks.submenu.like-buttons.label',
),
submenu: [ submenu: [
{ {
label: t( label: 'Default',
'main.menu.options.submenu.visual-tweaks.submenu.like-buttons.default',
),
type: 'radio', type: 'radio',
checked: !config.get('options.likeButtons'), checked: !config.get('options.likeButtons'),
click() { click() {
@ -209,9 +170,7 @@ export const mainMenuTemplate = async (
}, },
}, },
{ {
label: t( label: 'Force show',
'main.menu.options.submenu.visual-tweaks.submenu.like-buttons.force-show',
),
type: 'radio', type: 'radio',
checked: config.get('options.likeButtons') === 'force', checked: config.get('options.likeButtons') === 'force',
click() { click() {
@ -219,9 +178,7 @@ export const mainMenuTemplate = async (
}, },
}, },
{ {
label: t( label: 'Hide',
'main.menu.options.submenu.visual-tweaks.submenu.like-buttons.hide',
),
type: 'radio', type: 'radio',
checked: config.get('options.likeButtons') === 'hide', checked: config.get('options.likeButtons') === 'hide',
click() { click() {
@ -231,14 +188,10 @@ export const mainMenuTemplate = async (
], ],
}, },
{ {
label: t( label: 'Theme',
'main.menu.options.submenu.visual-tweaks.submenu.theme.label',
),
submenu: [ submenu: [
{ {
label: t( label: 'No theme',
'main.menu.options.submenu.visual-tweaks.submenu.theme.submenu.no-theme',
),
type: 'radio', type: 'radio',
checked: config.get('options.themes')?.length === 0, // Todo rename "themes" checked: config.get('options.themes')?.length === 0, // Todo rename "themes"
click() { click() {
@ -247,9 +200,7 @@ export const mainMenuTemplate = async (
}, },
{ type: 'separator' }, { type: 'separator' },
{ {
label: t( label: 'Import custom CSS file',
'main.menu.options.submenu.visual-tweaks.submenu.theme.submenu.import-css-file',
),
type: 'normal', type: 'normal',
async click() { async click() {
const { filePaths } = await dialog.showOpenDialog({ const { filePaths } = await dialog.showOpenDialog({
@ -266,10 +217,10 @@ export const mainMenuTemplate = async (
], ],
}, },
{ {
label: t('main.menu.options.submenu.single-instance-lock'), label: 'Single instance lock',
type: 'checkbox', type: 'checkbox',
checked: true, checked: true,
click(item: MenuItem) { click(item) {
if (!item.checked && app.hasSingleInstanceLock()) { if (!item.checked && app.hasSingleInstanceLock()) {
app.releaseSingleInstanceLock(); app.releaseSingleInstanceLock();
} else if (item.checked && !app.hasSingleInstanceLock()) { } else if (item.checked && !app.hasSingleInstanceLock()) {
@ -278,56 +229,51 @@ export const mainMenuTemplate = async (
}, },
}, },
{ {
label: t('main.menu.options.submenu.always-on-top'), label: 'Always on top',
type: 'checkbox', type: 'checkbox',
checked: config.get('options.alwaysOnTop'), checked: config.get('options.alwaysOnTop'),
click(item: MenuItem) { click(item) {
config.setMenuOption('options.alwaysOnTop', item.checked); config.setMenuOption('options.alwaysOnTop', item.checked);
win.setAlwaysOnTop(item.checked); win.setAlwaysOnTop(item.checked);
}, },
}, },
...((is.windows() || is.linux() ...(is.windows() || is.linux()
? [ ? [
{ {
label: t('main.menu.options.submenu.hide-menu.label'), label: 'Hide menu',
type: 'checkbox', type: 'checkbox',
checked: config.get('options.hideMenu'), checked: config.get('options.hideMenu'),
click(item) { click(item) {
config.setMenuOption('options.hideMenu', item.checked); config.setMenuOption('options.hideMenu', item.checked);
if (item.checked && !config.get('options.hideMenuWarned')) { if (item.checked && !config.get('options.hideMenuWarned')) {
dialog.showMessageBox(win, { dialog.showMessageBox(win, {
type: 'info', type: 'info', title: 'Hide Menu Enabled',
title: t( message: 'Menu will be hidden on next launch, use [Alt] to show it (or backtick [`] if using in-app-menu)',
'main.menu.options.submenu.hide-menu.dialog.title', });
), }
message: t(
'main.menu.options.submenu.hide-menu.dialog.message',
),
});
}
},
}, },
] },
: []) satisfies Electron.MenuItemConstructorOptions[]), ]
...((is.windows() || is.macOS() : []) satisfies Electron.MenuItemConstructorOptions[],
...(is.windows() || is.macOS()
? // Only works on Win/Mac ? // Only works on Win/Mac
// https://www.electronjs.org/docs/api/app#appsetloginitemsettingssettings-macos-windows // https://www.electronjs.org/docs/api/app#appsetloginitemsettingssettings-macos-windows
[ [
{ {
label: t('main.menu.options.submenu.start-at-login'), label: 'Start at login',
type: 'checkbox', type: 'checkbox',
checked: config.get('options.startAtLogin'), checked: config.get('options.startAtLogin'),
click(item) { click(item) {
config.setMenuOption('options.startAtLogin', item.checked); config.setMenuOption('options.startAtLogin', item.checked);
},
}, },
] },
: []) satisfies Electron.MenuItemConstructorOptions[]), ]
: []) satisfies Electron.MenuItemConstructorOptions[],
{ {
label: t('main.menu.options.submenu.tray.label'), label: 'Tray',
submenu: [ submenu: [
{ {
label: t('main.menu.options.submenu.tray.submenu.disabled'), label: 'Disabled',
type: 'radio', type: 'radio',
checked: !config.get('options.tray'), checked: !config.get('options.tray'),
click() { click() {
@ -336,24 +282,18 @@ export const mainMenuTemplate = async (
}, },
}, },
{ {
label: t( label: 'Enabled + app visible',
'main.menu.options.submenu.tray.submenu.enabled-and-show-app',
),
type: 'radio', type: 'radio',
checked: checked: config.get('options.tray') && config.get('options.appVisible'),
config.get('options.tray') && config.get('options.appVisible'),
click() { click() {
config.setMenuOption('options.tray', true); config.setMenuOption('options.tray', true);
config.setMenuOption('options.appVisible', true); config.setMenuOption('options.appVisible', true);
}, },
}, },
{ {
label: t( label: 'Enabled + app hidden',
'main.menu.options.submenu.tray.submenu.enabled-and-hide-app',
),
type: 'radio', type: 'radio',
checked: checked: config.get('options.tray') && !config.get('options.appVisible'),
config.get('options.tray') && !config.get('options.appVisible'),
click() { click() {
config.setMenuOption('options.tray', true); config.setMenuOption('options.tray', true);
config.setMenuOption('options.appVisible', false); config.setMenuOption('options.appVisible', false);
@ -361,143 +301,75 @@ export const mainMenuTemplate = async (
}, },
{ type: 'separator' }, { type: 'separator' },
{ {
label: t( label: 'Play/Pause on click',
'main.menu.options.submenu.tray.submenu.play-pause-on-click',
),
type: 'checkbox', type: 'checkbox',
checked: config.get('options.trayClickPlayPause'), checked: config.get('options.trayClickPlayPause'),
click(item: MenuItem) { click(item) {
config.setMenuOption( config.setMenuOption('options.trayClickPlayPause', item.checked);
'options.trayClickPlayPause',
item.checked,
);
}, },
}, },
], ],
}, },
{
label: t('main.menu.options.submenu.language.label') + ' (Language)',
submenu: [
{
label: t(
'main.menu.options.submenu.language.submenu.to-help-translate',
),
type: 'normal',
click() {
const url = 'https://hosted.weblate.org/engage/youtube-music/';
shell.openExternal(url);
},
} as Electron.MenuItemConstructorOptions,
].concat(
availableLanguages
.map(
(lang): Electron.MenuItemConstructorOptions => ({
label: `${languageResources[lang].translation.language?.name ?? 'Unknown'} (${languageResources[lang].translation.language?.['local-name'] ?? 'Unknown'})`,
type: 'checkbox',
checked: (config.get('options.language') ?? 'en') === lang,
click() {
config.setMenuOption('options.language', lang);
refreshMenu(win);
setLanguage(lang);
dialog.showMessageBox(win, {
title: t(
'main.menu.options.submenu.language.dialog.title',
),
message: t(
'main.menu.options.submenu.language.dialog.message',
),
});
},
}),
)
.sort((a, b) => a.label!.localeCompare(b.label!)),
),
},
{ type: 'separator' }, { type: 'separator' },
{ {
label: t('main.menu.options.submenu.advanced-options.label'), label: 'Advanced options',
submenu: [ submenu: [
{ {
label: t( label: 'Set Proxy',
'main.menu.options.submenu.advanced-options.submenu.set-proxy.label',
),
type: 'normal', type: 'normal',
async click(item: MenuItem) { async click(item) {
await setProxy(item, win); await setProxy(item, win);
}, },
}, },
{ {
label: t( label: 'Override useragent',
'main.menu.options.submenu.advanced-options.submenu.override-user-agent',
),
type: 'checkbox', type: 'checkbox',
checked: config.get('options.overrideUserAgent'), checked: config.get('options.overrideUserAgent'),
click(item: MenuItem) { click(item) {
config.setMenuOption('options.overrideUserAgent', item.checked); config.setMenuOption('options.overrideUserAgent', item.checked);
}, },
}, },
{ {
label: t( label: 'Disable hardware acceleration',
'main.menu.options.submenu.advanced-options.submenu.disable-hardware-acceleration',
),
type: 'checkbox', type: 'checkbox',
checked: config.get('options.disableHardwareAcceleration'), checked: config.get('options.disableHardwareAcceleration'),
click(item: MenuItem) { click(item) {
config.setMenuOption( config.setMenuOption('options.disableHardwareAcceleration', item.checked);
'options.disableHardwareAcceleration',
item.checked,
);
}, },
}, },
{ {
label: t( label: 'Restart on config changes',
'main.menu.options.submenu.advanced-options.submenu.restart-on-config-changes',
),
type: 'checkbox', type: 'checkbox',
checked: config.get('options.restartOnConfigChanges'), checked: config.get('options.restartOnConfigChanges'),
click(item: MenuItem) { click(item) {
config.setMenuOption( config.setMenuOption('options.restartOnConfigChanges', item.checked);
'options.restartOnConfigChanges',
item.checked,
);
}, },
}, },
{ {
label: t( label: 'Reset App cache when app starts',
'main.menu.options.submenu.advanced-options.submenu.auto-reset-app-cache',
),
type: 'checkbox', type: 'checkbox',
checked: config.get('options.autoResetAppCache'), checked: config.get('options.autoResetAppCache'),
click(item: MenuItem) { click(item) {
config.setMenuOption('options.autoResetAppCache', item.checked); config.setMenuOption('options.autoResetAppCache', item.checked);
}, },
}, },
{ type: 'separator' }, { type: 'separator' },
is.macOS() is.macOS()
? { ? {
label: t( label: 'Toggle DevTools',
'main.menu.options.submenu.advanced-options.submenu.toggle-dev-tools', // Cannot use "toggleDevTools" role in macOS
), click() {
// Cannot use "toggleDevTools" role in macOS const { webContents } = win;
click() { if (webContents.isDevToolsOpened()) {
const { webContents } = win; webContents.closeDevTools();
if (webContents.isDevToolsOpened()) { } else {
webContents.closeDevTools(); webContents.openDevTools();
} else { }
webContents.openDevTools();
}
},
}
: {
label: t(
'main.menu.options.submenu.advanced-options.submenu.toggle-dev-tools',
),
role: 'toggleDevTools',
}, },
}
: { role: 'toggleDevTools' },
{ {
label: t( label: 'Edit config.json',
'main.menu.options.submenu.advanced-options.submenu.edit-config-json',
),
click() { click() {
config.edit(); config.edit();
}, },
@ -507,55 +379,23 @@ export const mainMenuTemplate = async (
], ],
}, },
{ {
label: t('main.menu.view.label'), label: 'View',
submenu: [ submenu: [
{ { role: 'reload' },
label: t('main.menu.view.submenu.reload'), { role: 'forceReload' },
role: 'reload',
},
{
label: t('main.menu.view.submenu.force-reload'),
role: 'forceReload',
},
{ type: 'separator' }, { type: 'separator' },
{ { role: 'zoomIn', accelerator: process.platform === 'darwin' ? 'Cmd+I' : 'Ctrl+I' },
label: t('main.menu.view.submenu.zoom-in'), { role: 'zoomOut', accelerator: process.platform === 'darwin' ? 'Cmd+O' : 'Ctrl+O' },
role: 'zoomIn', { role: 'resetZoom' },
accelerator: 'CmdOrCtrl+=',
visible: false,
},
{
label: t('main.menu.view.submenu.zoom-in'),
role: 'zoomIn',
accelerator: 'CmdOrCtrl+Plus',
},
{
label: t('main.menu.view.submenu.zoom-out'),
role: 'zoomOut',
accelerator: 'CmdOrCtrl+-',
},
{
label: t('main.menu.view.submenu.zoom-out'),
role: 'zoomOut',
accelerator: 'CmdOrCtrl+Shift+-',
visible: false,
},
{
label: t('main.menu.view.submenu.reset-zoom'),
role: 'resetZoom',
},
{ type: 'separator' }, { type: 'separator' },
{ { role: 'togglefullscreen' },
label: t('main.menu.view.submenu.toggle-fullscreen'),
role: 'togglefullscreen',
},
], ],
}, },
{ {
label: t('main.menu.navigation.label'), label: 'Navigation',
submenu: [ submenu: [
{ {
label: t('main.menu.navigation.submenu.go-back'), label: 'Go back',
click() { click() {
if (win.webContents.canGoBack()) { if (win.webContents.canGoBack()) {
win.webContents.goBack(); win.webContents.goBack();
@ -563,7 +403,7 @@ export const mainMenuTemplate = async (
}, },
}, },
{ {
label: t('main.menu.navigation.submenu.go-forward'), label: 'Go forward',
click() { click() {
if (win.webContents.canGoForward()) { if (win.webContents.canGoForward()) {
win.webContents.goForward(); win.webContents.goForward();
@ -571,30 +411,29 @@ export const mainMenuTemplate = async (
}, },
}, },
{ {
label: t('main.menu.navigation.submenu.copy-current-url'), label: 'Copy current URL',
click() { click() {
const currentURL = win.webContents.getURL(); const currentURL = win.webContents.getURL();
clipboard.writeText(currentURL); clipboard.writeText(currentURL);
}, },
}, },
{ {
label: t('main.menu.navigation.submenu.restart'), label: 'Restart App',
click: restart, click: restart,
}, },
{ { role: 'quit' },
label: t('main.menu.navigation.submenu.quit'),
role: 'quit',
},
], ],
}, },
{ {
label: t('main.menu.about'), label: 'About',
submenu: [{ role: 'about' }], submenu: [
}, { role: 'about' },
],
}
]; ];
}; };
export const setApplicationMenu = async (win: Electron.BrowserWindow) => { export const setApplicationMenu = (win: Electron.BrowserWindow) => {
const menuTemplate: MenuTemplate = [...(await mainMenuTemplate(win))]; const menuTemplate: MenuTemplate = [...mainMenuTemplate(win)];
if (process.platform === 'darwin') { if (process.platform === 'darwin') {
const { name } = app; const { name } = app;
menuTemplate.unshift({ menuTemplate.unshift({
@ -623,33 +462,23 @@ export const setApplicationMenu = async (win: Electron.BrowserWindow) => {
}; };
async function setProxy(item: Electron.MenuItem, win: BrowserWindow) { async function setProxy(item: Electron.MenuItem, win: BrowserWindow) {
const output = await prompt( const output = await prompt({
{ title: 'Set Proxy',
title: t( label: 'Enter Proxy Address: (leave empty to disable)',
'main.menu.options.submenu.advanced-options.submenu.set-proxy.prompt.title', value: config.get('options.proxy'),
), type: 'input',
label: t( inputAttrs: {
'main.menu.options.submenu.advanced-options.submenu.set-proxy.prompt.label', type: 'url',
), placeholder: "Example: 'socks5://127.0.0.1:9999",
value: config.get('options.proxy'),
type: 'input',
inputAttrs: {
type: 'url',
placeholder: t(
'main.menu.options.submenu.advanced-options.submenu.set-proxy.prompt.placeholder',
),
},
width: 450,
...promptOptions(),
}, },
win, width: 450,
); ...promptOptions(),
}, win);
if (typeof output === 'string') { if (typeof output === 'string') {
config.setMenuOption('options.proxy', output); config.setMenuOption('options.proxy', output);
item.checked = output !== ''; item.checked = output !== '';
} else { } else { // User pressed cancel
// User pressed cancel
item.checked = !item.checked; // Reset checkbox item.checked = !item.checked; // Reset checkbox
} }
} }

7
src/navigation.d.ts vendored
View File

@ -62,10 +62,7 @@ interface Navigation extends EventTarget {
onnavigateerror: ((this: Navigation, ev: Event) => any) | null; onnavigateerror: ((this: Navigation, ev: Event) => any) | null;
oncurrententrychange: ((this: Navigation, ev: Event) => any) | null; oncurrententrychange: ((this: Navigation, ev: Event) => any) | null;
addEventListener<K extends keyof NavigationEventsMap>( addEventListener<K extends keyof NavigationEventsMap>(name: K, listener: (event: NavigationEventsMap[K]) => void);
name: K,
listener: (event: NavigationEventsMap[K]) => void,
);
} }
declare class NavigateEvent extends Event { declare class NavigateEvent extends Event {
@ -87,5 +84,5 @@ type NavigationHistoryBehavior = 'auto' | 'push' | 'replace';
declare const Navigation: { declare const Navigation: {
prototype: Navigation; prototype: Navigation;
new (): Navigation; new(): Navigation;
}; };

View File

@ -0,0 +1,19 @@
import { BrowserWindow } from 'electron';
import { loadAdBlockerEngine } from './blocker';
import { shouldUseBlocklists } from './config';
import type { ConfigType } from '../../config/dynamic';
type AdBlockOptions = ConfigType<'adblocker'>;
export default async (win: BrowserWindow, options: AdBlockOptions) => {
if (shouldUseBlocklists()) {
await loadAdBlockerEngine(
win.webContents.session,
options.cache,
options.additionalBlockLists,
options.disableDefaultLists,
);
}
};

View File

@ -17,12 +17,10 @@ const SOURCES = [
'https://secure.fanboy.co.nz/fanboy-annoyance_ubo.txt', 'https://secure.fanboy.co.nz/fanboy-annoyance_ubo.txt',
]; ];
let blocker: ElectronBlocker | undefined;
export const loadAdBlockerEngine = async ( export const loadAdBlockerEngine = async (
session: Electron.Session | undefined = undefined, session: Electron.Session | undefined = undefined,
cache: boolean = true, cache = true,
additionalBlockLists: string[] = [], additionalBlockLists = [],
disableDefaultLists: boolean | unknown[] = false, disableDefaultLists: boolean | unknown[] = false,
) => { ) => {
// Only use cache if no additional blocklists are passed // Only use cache if no additional blocklists are passed
@ -30,24 +28,24 @@ export const loadAdBlockerEngine = async (
if (!fs.existsSync(cacheDirectory)) { if (!fs.existsSync(cacheDirectory)) {
fs.mkdirSync(cacheDirectory); fs.mkdirSync(cacheDirectory);
} }
const cachingOptions = const cachingOptions
cache && additionalBlockLists.length === 0 = cache && additionalBlockLists.length === 0
? { ? {
path: path.join(cacheDirectory, 'adblocker-engine.bin'), path: path.join(cacheDirectory, 'adblocker-engine.bin'),
read: promises.readFile, read: promises.readFile,
write: promises.writeFile, write: promises.writeFile,
} }
: undefined; : undefined;
const lists = [ const lists = [
...((disableDefaultLists && !Array.isArray(disableDefaultLists)) || ...(
(Array.isArray(disableDefaultLists) && disableDefaultLists.length > 0) (disableDefaultLists && !Array.isArray(disableDefaultLists)) ||
? [] (Array.isArray(disableDefaultLists) && disableDefaultLists.length > 0) ? [] : SOURCES
: SOURCES), ),
...additionalBlockLists, ...additionalBlockLists,
]; ];
try { try {
blocker = await ElectronBlocker.fromLists( const blocker = await ElectronBlocker.fromLists(
(url: string) => net.fetch(url), (url: string) => net.fetch(url),
lists, lists,
{ {
@ -66,11 +64,4 @@ export const loadAdBlockerEngine = async (
} }
}; };
export const unloadAdBlockerEngine = (session: Electron.Session) => { export default { loadAdBlockerEngine };
if (blocker) {
blocker.disableBlockingInSession(session);
}
};
export const isBlockerEnabled = (session: Electron.Session) =>
blocker !== undefined && blocker.isBlockingEnabled(session);

View File

@ -0,0 +1,15 @@
/* eslint-disable @typescript-eslint/await-thenable */
/* renderer */
import { blockers } from './blocker-types';
import { PluginConfig } from '../../config/dynamic';
const config = new PluginConfig('adblocker', { enableFront: true });
export const shouldUseBlocklists = () => config.get('blocker') !== blockers.InPlayer;
export default Object.assign(config, {
shouldUseBlocklists,
blockers,
});

View File

@ -1,133 +0,0 @@
import { contextBridge, webFrame } from 'electron';
import { blockers } from './types';
import { createPlugin } from '@/utils';
import {
isBlockerEnabled,
loadAdBlockerEngine,
unloadAdBlockerEngine,
} from './blocker';
import injectCliqzPreload from './injectors/inject-cliqz-preload';
import { inject, isInjected } from './injectors/inject';
import { t } from '@/i18n';
import type { BrowserWindow } from 'electron';
interface AdblockerConfig {
/**
* Whether to enable the adblocker.
* @default true
*/
enabled: boolean;
/**
* When enabled, the adblocker will cache the blocklists.
* @default true
*/
cache: boolean;
/**
* Which adblocker to use.
* @default blockers.InPlayer
*/
blocker: (typeof blockers)[keyof typeof blockers];
/**
* Additional list of filters to use.
* @example ["https://raw.githubusercontent.com/uBlockOrigin/uAssets/master/filters/filters.txt"]
* @default []
*/
additionalBlockLists: string[];
/**
* Disable the default blocklists.
* @default false
*/
disableDefaultLists: boolean;
}
export default createPlugin({
name: () => t('plugins.adblocker.name'),
description: () => t('plugins.adblocker.description'),
restartNeeded: false,
config: {
enabled: true,
cache: true,
blocker: blockers.InPlayer,
additionalBlockLists: [],
disableDefaultLists: false,
} as AdblockerConfig,
menu: async ({ getConfig, setConfig }) => {
const config = await getConfig();
return [
{
label: t('plugins.adblocker.menu.blocker'),
submenu: Object.values(blockers).map((blocker) => ({
label: blocker,
type: 'radio',
checked: (config.blocker || blockers.WithBlocklists) === blocker,
click() {
setConfig({ blocker });
},
})),
},
];
},
backend: {
mainWindow: null as BrowserWindow | null,
async start({ getConfig, window }) {
const config = await getConfig();
this.mainWindow = window;
if (config.blocker === blockers.WithBlocklists) {
await loadAdBlockerEngine(
window.webContents.session,
config.cache,
config.additionalBlockLists,
config.disableDefaultLists,
);
}
},
stop({ window }) {
if (isBlockerEnabled(window.webContents.session)) {
unloadAdBlockerEngine(window.webContents.session);
}
},
async onConfigChange(newConfig) {
if (this.mainWindow) {
if (
newConfig.blocker === blockers.WithBlocklists &&
!isBlockerEnabled(this.mainWindow.webContents.session)
) {
await loadAdBlockerEngine(
this.mainWindow.webContents.session,
newConfig.cache,
newConfig.additionalBlockLists,
newConfig.disableDefaultLists,
);
}
}
},
},
preload: {
script: 'window.JSON.parse = window._proxyJsonParse; window._proxyJsonParse = undefined; window.Response.prototype.json = window._proxyResponseJson; window._proxyResponseJson = undefined; 0',
async start({ getConfig }) {
const config = await getConfig();
if (config.blocker === blockers.WithBlocklists) {
// Preload adblocker to inject scripts/styles
await injectCliqzPreload();
} else if (config.blocker === blockers.InPlayer && !isInjected()) {
inject(contextBridge);
await webFrame.executeJavaScript(this.script);
}
},
async onConfigChange(newConfig) {
if (newConfig.blocker === blockers.WithBlocklists) {
await injectCliqzPreload();
} else if (newConfig.blocker === blockers.InPlayer && !isInjected()) {
inject(contextBridge);
await webFrame.executeJavaScript(this.script);
}
},
},
});

3
src/plugins/adblocker/inject.d.ts vendored Normal file
View File

@ -0,0 +1,3 @@
const inject: () => void;
export default inject;

View File

@ -7,17 +7,7 @@
Parts of this code is derived from set-constant.js: Parts of this code is derived from set-constant.js:
https://github.com/gorhill/uBlock/blob/5de0ce975753b7565759ac40983d31978d1f84ca/assets/resources/scriptlets.js#L704 https://github.com/gorhill/uBlock/blob/5de0ce975753b7565759ac40983d31978d1f84ca/assets/resources/scriptlets.js#L704
*/ */
module.exports = () => {
let injected = false;
export const isInjected = () => injected;
/**
* @param {Electron.ContextBridge} contextBridge
* @returns {*}
*/
export const inject = (contextBridge) => {
injected = true;
{ {
const pruner = function (o) { const pruner = function (o) {
delete o.playerAds; delete o.playerAds;
@ -32,17 +22,17 @@ export const inject = (contextBridge) => {
return o; return o;
}; };
contextBridge.exposeInMainWorld('_proxyJsonParse', new Proxy(JSON.parse, { JSON.parse = new Proxy(JSON.parse, {
apply() { apply() {
return pruner(Reflect.apply(...arguments)); return pruner(Reflect.apply(...arguments));
}, },
})); });
contextBridge.exposeInMainWorld('_proxyResponseJson', new Proxy(Response.prototype.json, { Response.prototype.json = new Proxy(Response.prototype.json, {
apply() { apply() {
return Reflect.apply(...arguments).then((o) => pruner(o)); return Reflect.apply(...arguments).then((o) => pruner(o));
}, },
})); });
} }
(function () { (function () {
@ -77,7 +67,8 @@ export const inject = (contextBridge) => {
} }
case 'noopFunc': { case 'noopFunc': {
cValue = function () {}; cValue = function () {
};
break; break;
} }
@ -106,7 +97,7 @@ export const inject = (contextBridge) => {
return; return;
} }
if (Math.abs(cValue) > 0x7f_ff) { if (Math.abs(cValue) > 0x7F_FF) {
return; return;
} }
} else { } else {
@ -122,12 +113,12 @@ export const inject = (contextBridge) => {
return true; return true;
} }
aborted = aborted
v !== undefined && = v !== undefined
v !== null && && v !== null
cValue !== undefined && && cValue !== undefined
cValue !== null && && cValue !== null
typeof v !== typeof cValue; && typeof v !== typeof cValue;
return aborted; return aborted;
}; };
@ -275,7 +266,8 @@ export const inject = (contextBridge) => {
} }
case 'noopFunc': { case 'noopFunc': {
cValue = function () {}; cValue = function () {
};
break; break;
} }
@ -304,7 +296,7 @@ export const inject = (contextBridge) => {
return; return;
} }
if (Math.abs(cValue) > 0x7f_ff) { if (Math.abs(cValue) > 0x7F_FF) {
return; return;
} }
} else { } else {
@ -320,12 +312,12 @@ export const inject = (contextBridge) => {
return true; return true;
} }
aborted = aborted
v !== undefined && = v !== undefined
v !== null && && v !== null
cValue !== undefined && && cValue !== undefined
cValue !== null && && cValue !== null
typeof v !== typeof cValue; && typeof v !== typeof cValue;
return aborted; return aborted;
}; };

View File

@ -1,5 +0,0 @@
import type { ContextBridge } from 'electron';
export const inject: (contextBridge: ContextBridge) => void;
export const isInjected: () => boolean;

View File

@ -0,0 +1,21 @@
import config from './config';
import { blockers } from './blocker-types';
import { MenuTemplate } from '../../menu';
export default (): MenuTemplate => {
return [
{
label: 'Blocker',
submenu: Object.values(blockers).map((blocker: string) => ({
label: blocker,
type: 'radio',
checked: (config.get('blocker') || blockers.WithBlocklists) === blocker,
click() {
config.set('blocker', blocker);
},
})),
},
];
};

View File

@ -0,0 +1,15 @@
import config, { shouldUseBlocklists } from './config';
import inject from './inject';
import injectCliqzPreload from './inject-cliqz-preload';
import { blockers } from './blocker-types';
export default async () => {
if (shouldUseBlocklists()) {
// Preload adblocker to inject scripts/styles
await injectCliqzPreload();
// eslint-disable-next-line @typescript-eslint/await-thenable
} else if ((config.get('blocker')) === blockers.InPlayer) {
inject();
}
};

View File

@ -1,193 +0,0 @@
import { t } from '@/i18n';
import { createPlugin } from '@/utils';
import { ElementFromHtml } from '@/plugins/utils/renderer';
import undislikeHTML from './templates/undislike.html?raw';
import dislikeHTML from './templates/dislike.html?raw';
import likeHTML from './templates/like.html?raw';
import unlikeHTML from './templates/unlike.html?raw';
export default createPlugin<
unknown,
unknown,
{
observer?: MutationObserver;
loadObserver?: MutationObserver;
changeObserver?: MutationObserver;
waiting: boolean;
onPageChange(): void;
waitForElem(selector: string): Promise<HTMLElement>;
loadFullList: (event: MouseEvent) => void;
applyToList(id: string, loader: HTMLElement): void;
start(): void;
stop(): void;
}
>({
name: () => t('plugins.album-actions.name'),
description: () => t('plugins.album-actions.description'),
restartNeeded: false,
addedVersion: '3.2.X',
config: {
enabled: false,
},
renderer: {
waiting: false,
start() {
// Waits for pagechange
this.onPageChange();
this.observer = new MutationObserver(() => {
this.onPageChange();
});
this.observer.observe(document.querySelector('#browse-page')!, {
attributes: false,
childList: true,
subtree: true,
});
},
async onPageChange() {
if (this.waiting) {
return;
} else {
this.waiting = true;
}
const continuations = await this.waitForElem('#continuations');
this.waiting = false;
//Gets the for buttons
const buttons: Array<HTMLElement> = [
ElementFromHtml(undislikeHTML),
ElementFromHtml(dislikeHTML),
ElementFromHtml(likeHTML),
ElementFromHtml(unlikeHTML),
];
//Finds the playlist
const playlist =
document.querySelector('ytmusic-shelf-renderer') ??
document.querySelector('ytmusic-playlist-shelf-renderer')!;
// Adds an observer for every button, so it gets updated when one is clicked
this.changeObserver?.disconnect();
this.changeObserver = new MutationObserver(() => {
this.stop();
this.start();
});
const allButtons = playlist.querySelectorAll(
'yt-button-shape.ytmusic-like-button-renderer',
);
for (const btn of allButtons) {
this.changeObserver.observe(btn, {
attributes: true,
childList: false,
subtree: false,
});
}
//Determine if button is needed and colors the percentage
const listsLength = playlist.querySelectorAll(
'#button-shape-dislike > button',
).length;
if (continuations.children.length == 0 && listsLength > 0) {
const counts = [
playlist?.querySelectorAll(
'#button-shape-dislike[aria-pressed=true] > button',
).length,
playlist?.querySelectorAll(
'#button-shape-dislike[aria-pressed=false] > button',
).length,
playlist?.querySelectorAll(
'#button-shape-like[aria-pressed=false] > button',
).length,
playlist?.querySelectorAll(
'#button-shape-like[aria-pressed=true] > button',
).length,
];
let i = 0;
for (const count of counts) {
if (count == 0) {
buttons.splice(i, 1);
i--;
} else {
(buttons[i].children[0].children[0] as HTMLElement).style.setProperty(
'-webkit-mask-size',
`100% ${100 - ((count / listsLength) * 100)}%`,
);
}
i++;
}
}
const menu = document.querySelector('.detail-page-menu');
if (menu && !document.querySelector('.like-menu')) {
for (const button of buttons) {
menu.appendChild(button);
button.addEventListener('click', this.loadFullList);
}
}
},
loadFullList(event: MouseEvent) {
if (event.currentTarget instanceof Element) {
event.stopPropagation();
const id = event.currentTarget.id;
const loader = document.getElementById('continuations')!;
this.loadObserver = new MutationObserver(() => {
this.applyToList(id, loader);
});
this.applyToList(id, loader);
this.loadObserver.observe(loader, {
attributes: true,
childList: true,
subtree: true,
});
loader?.style.setProperty('top', '0');
loader?.style.setProperty('left', '50%');
loader?.style.setProperty('position', 'absolute');
}
},
applyToList(id: string, loader: HTMLElement) {
if (loader.children.length != 0) return;
this.loadObserver?.disconnect();
let playlistButtons: NodeListOf<HTMLElement> | undefined;
const playlist = document.querySelector('ytmusic-shelf-renderer')
? document.querySelector('ytmusic-shelf-renderer')
: document.querySelector('ytmusic-playlist-shelf-renderer');
switch (id) {
case 'allundislike':
playlistButtons = playlist?.querySelectorAll(
'#button-shape-dislike[aria-pressed=true] > button',
);
break;
case 'alldislike':
playlistButtons = playlist?.querySelectorAll(
'#button-shape-dislike[aria-pressed=false] > button',
);
break;
case 'alllike':
playlistButtons = playlist?.querySelectorAll(
'#button-shape-like[aria-pressed=false] > button',
);
break;
case 'allunlike':
playlistButtons = playlist?.querySelectorAll(
'#button-shape-like[aria-pressed=true] > button',
);
break;
default:
}
playlistButtons?.forEach((elem) => elem.click());
},
stop() {
this.observer?.disconnect();
this.changeObserver?.disconnect();
for (const button of document.querySelectorAll('.like-menu')) {
button.remove();
}
},
waitForElem(selector: string) {
return new Promise((resolve) => {
const interval = setInterval(() => {
const elem = document.querySelector<HTMLElement>(selector);
if (!elem) return;
clearInterval(interval);
resolve(elem);
});
});
},
},
});

View File

@ -1,74 +0,0 @@
<button
id="alldislike"
data-type="dislike"
data-filled="false"
class="like-menu yt-spec-button-shape-next yt-spec-button-shape-next--text yt-spec-button-shape-next--mono yt-spec-button-shape-next--size-m yt-spec-button-shape-next--icon-button"
aria-pressed="false"
aria-label="Dislike all"
>
<div
class="yt-spec-button-shape-next__icon"
style="color: var(--ytmusic-setting-item-toggle-active)"
aria-hidden="true"
>
<div
class="yt-spec-button-shape-next__icon"
style="
color: white;
-webkit-mask: linear-gradient(grey, grey);
-webkit-mask-size: 100% 50%;
-webkit-mask-repeat: no-repeat;
z-index: 1;
position: absolute;
"
aria-hidden="true"
>
<div style="width: 24px; height: 24px">
<svg
viewBox="0 0 24 24"
preserveAspectRatio="xMidYMid meet"
focusable="false"
class="style-scope yt-icon"
style="
pointer-events: none;
display: block;
width: 100%;
height: 100%;
"
>
<g class="style-scope yt-icon">
<path
d="M18,4h3v10h-3V4z M5.23,14h4.23l-1.52,4.94C7.62,19.97,8.46,21,9.62,21c0.58,0,1.14-0.24,1.52-0.65L17,14V4H6.57 C5.5,4,4.59,4.67,4.38,5.61l-1.34,6C2.77,12.85,3.82,14,5.23,14z"
class="style-scope yt-icon"
></path>
</g>
</svg>
</div>
</div>
<div style="width: 24px; height: 24px">
<svg
viewBox="0 0 24 24"
preserveAspectRatio="xMidYMid meet"
focusable="false"
class="style-scope yt-icon"
style="pointer-events: none; display: block; width: 100%; height: 100%"
>
<g class="style-scope yt-icon">
<path
d="M18,4h3v10h-3V4z M5.23,14h4.23l-1.52,4.94C7.62,19.97,8.46,21,9.62,21c0.58,0,1.14-0.24,1.52-0.65L17,14V4H6.57 C5.5,4,4.59,4.67,4.38,5.61l-1.34,6C2.77,12.85,3.82,14,5.23,14z"
class="style-scope yt-icon"
></path>
</g>
</svg>
</div>
</div>
<yt-touch-feedback-shape style="border-radius: inherit">
<div
class="yt-spec-touch-feedback-shape yt-spec-touch-feedback-shape--touch-response"
aria-hidden="true"
>
<div class="yt-spec-touch-feedback-shape__stroke"></div>
<div class="yt-spec-touch-feedback-shape__fill"></div>
</div>
</yt-touch-feedback-shape>
</button>

View File

@ -1,74 +0,0 @@
<button
id="alllike"
data-type="like"
data-filled="false"
class="like-menu yt-spec-button-shape-next yt-spec-button-shape-next--text yt-spec-button-shape-next--mono yt-spec-button-shape-next--size-m yt-spec-button-shape-next--icon-button"
aria-pressed="false"
aria-label="Like all"
>
<div
class="yt-spec-button-shape-next__icon"
style="color: var(--ytmusic-setting-item-toggle-active)"
aria-hidden="true"
>
<div
class="yt-spec-button-shape-next__icon"
style="
color: white;
-webkit-mask: linear-gradient(grey, grey);
-webkit-mask-size: 100% 50%;
-webkit-mask-repeat: no-repeat;
z-index: 1;
position: absolute;
"
aria-hidden="true"
>
<div style="width: 24px; height: 24px">
<svg
viewBox="0 0 24 24"
preserveAspectRatio="xMidYMid meet"
focusable="false"
class="style-scope yt-icon"
style="
pointer-events: none;
display: block;
width: 100%;
height: 100%;
"
>
<g class="style-scope yt-icon">
<path
d="M3,11h3v10H3V11z M18.77,11h-4.23l1.52-4.94C16.38,5.03,15.54,4,14.38,4c-0.58,0-1.14,0.24-1.52,0.65L7,11v10h10.43 c1.06,0,1.98-0.67,2.19-1.61l1.34-6C21.23,12.15,20.18,11,18.77,11z"
class="style-scope yt-icon"
></path>
</g>
</svg>
</div>
</div>
<div style="width: 24px; height: 24px">
<svg
viewBox="0 0 24 24"
preserveAspectRatio="xMidYMid meet"
focusable="false"
class="style-scope yt-icon"
style="pointer-events: none; display: block; width: 100%; height: 100%"
>
<g class="style-scope yt-icon">
<path
d="M3,11h3v10H3V11z M18.77,11h-4.23l1.52-4.94C16.38,5.03,15.54,4,14.38,4c-0.58,0-1.14,0.24-1.52,0.65L7,11v10h10.43 c1.06,0,1.98-0.67,2.19-1.61l1.34-6C21.23,12.15,20.18,11,18.77,11z"
class="style-scope yt-icon"
></path>
</g>
</svg>
</div>
</div>
<yt-touch-feedback-shape style="border-radius: inherit">
<div
class="yt-spec-touch-feedback-shape yt-spec-touch-feedback-shape--touch-response"
aria-hidden="true"
>
<div class="yt-spec-touch-feedback-shape__stroke"></div>
<div class="yt-spec-touch-feedback-shape__fill"></div>
</div>
</yt-touch-feedback-shape>
</button>

View File

@ -1,74 +0,0 @@
<button
id="allundislike"
data-type="dislike"
data-filled="true"
class="like-menu yt-spec-button-shape-next yt-spec-button-shape-next--text yt-spec-button-shape-next--mono yt-spec-button-shape-next--size-m yt-spec-button-shape-next--icon-button"
aria-pressed="false"
aria-label="Undislike all"
>
<div
class="yt-spec-button-shape-next__icon"
style="color: var(--ytmusic-setting-item-toggle-active)"
aria-hidden="true"
>
<div
class="yt-spec-button-shape-next__icon"
style="
color: white;
-webkit-mask: linear-gradient(grey, grey);
-webkit-mask-size: 100% 50%;
-webkit-mask-repeat: no-repeat;
z-index: 1;
position: absolute;
"
aria-hidden="true"
>
<div style="width: 24px; height: 24px">
<svg
viewBox="0 0 24 24"
preserveAspectRatio="xMidYMid meet"
focusable="false"
class="style-scope yt-icon"
style="
pointer-events: none;
display: block;
width: 100%;
height: 100%;
"
>
<g class="style-scope yt-icon">
<path
d="M17,4h-1H6.57C5.5,4,4.59,4.67,4.38,5.61l-1.34,6C2.77,12.85,3.82,14,5.23,14h4.23l-1.52,4.94C7.62,19.97,8.46,21,9.62,21 c0.58,0,1.14-0.24,1.52-0.65L17,14h4V4H17z M10.4,19.67C10.21,19.88,9.92,20,9.62,20c-0.26,0-0.5-0.11-0.63-0.3 c-0.07-0.1-0.15-0.26-0.09-0.47l1.52-4.94l0.4-1.29H9.46H5.23c-0.41,0-0.8-0.17-1.03-0.46c-0.12-0.15-0.25-0.4-0.18-0.72l1.34-6 C5.46,5.35,5.97,5,6.57,5H16v8.61L10.4,19.67z M20,13h-3V5h3V13z"
class="style-scope yt-icon"
></path>
</g>
</svg>
</div>
</div>
<div style="width: 24px; height: 24px">
<svg
viewBox="0 0 24 24"
preserveAspectRatio="xMidYMid meet"
focusable="false"
class="style-scope yt-icon"
style="pointer-events: none; display: block; width: 100%; height: 100%"
>
<g class="style-scope yt-icon">
<path
d="M17,4h-1H6.57C5.5,4,4.59,4.67,4.38,5.61l-1.34,6C2.77,12.85,3.82,14,5.23,14h4.23l-1.52,4.94C7.62,19.97,8.46,21,9.62,21 c0.58,0,1.14-0.24,1.52-0.65L17,14h4V4H17z M10.4,19.67C10.21,19.88,9.92,20,9.62,20c-0.26,0-0.5-0.11-0.63-0.3 c-0.07-0.1-0.15-0.26-0.09-0.47l1.52-4.94l0.4-1.29H9.46H5.23c-0.41,0-0.8-0.17-1.03-0.46c-0.12-0.15-0.25-0.4-0.18-0.72l1.34-6 C5.46,5.35,5.97,5,6.57,5H16v8.61L10.4,19.67z M20,13h-3V5h3V13z"
class="style-scope yt-icon"
></path>
</g>
</svg>
</div>
</div>
<yt-touch-feedback-shape style="border-radius: inherit">
<div
class="yt-spec-touch-feedback-shape yt-spec-touch-feedback-shape--touch-response"
aria-hidden="true"
>
<div class="yt-spec-touch-feedback-shape__stroke"></div>
<div class="yt-spec-touch-feedback-shape__fill"></div>
</div>
</yt-touch-feedback-shape>
</button>

View File

@ -1,74 +0,0 @@
<button
id="allunlike"
data-type="like"
data-filled="true"
class="like-menu yt-spec-button-shape-next yt-spec-button-shape-next--text yt-spec-button-shape-next--mono yt-spec-button-shape-next--size-m yt-spec-button-shape-next--icon-button"
aria-pressed="false"
aria-label="Unlike all"
>
<div
class="yt-spec-button-shape-next__icon"
style="color: var(--ytmusic-setting-item-toggle-active)"
aria-hidden="true"
>
<div
class="yt-spec-button-shape-next__icon"
style="
color: white;
-webkit-mask: linear-gradient(grey, grey);
-webkit-mask-size: 100% 50%;
-webkit-mask-repeat: no-repeat;
z-index: 1;
position: absolute;
"
aria-hidden="true"
>
<div style="width: 24px; height: 24px">
<svg
viewBox="0 0 24 24"
preserveAspectRatio="xMidYMid meet"
focusable="false"
class="style-scope yt-icon"
style="
pointer-events: none;
display: block;
width: 100%;
height: 100%;
"
>
<g class="style-scope yt-icon">
<path
d="M18.77,11h-4.23l1.52-4.94C16.38,5.03,15.54,4,14.38,4c-0.58,0-1.14,0.24-1.52,0.65L7,11H3v10h4h1h9.43 c1.06,0,1.98-0.67,2.19-1.61l1.34-6C21.23,12.15,20.18,11,18.77,11z M7,20H4v-8h3V20z M19.98,13.17l-1.34,6 C18.54,19.65,18.03,20,17.43,20H8v-8.61l5.6-6.06C13.79,5.12,14.08,5,14.38,5c0.26,0,0.5,0.11,0.63,0.3 c0.07,0.1,0.15,0.26,0.09,0.47l-1.52,4.94L13.18,12h1.35h4.23c0.41,0,0.8,0.17,1.03,0.46C19.92,12.61,20.05,12.86,19.98,13.17z"
class="style-scope yt-icon"
></path>
</g>
</svg>
</div>
</div>
<div style="width: 24px; height: 24px">
<svg
viewBox="0 0 24 24"
preserveAspectRatio="xMidYMid meet"
focusable="false"
class="style-scope yt-icon"
style="pointer-events: none; display: block; width: 100%; height: 100%"
>
<g class="style-scope yt-icon">
<path
d="M18.77,11h-4.23l1.52-4.94C16.38,5.03,15.54,4,14.38,4c-0.58,0-1.14,0.24-1.52,0.65L7,11H3v10h4h1h9.43 c1.06,0,1.98-0.67,2.19-1.61l1.34-6C21.23,12.15,20.18,11,18.77,11z M7,20H4v-8h3V20z M19.98,13.17l-1.34,6 C18.54,19.65,18.03,20,17.43,20H8v-8.61l5.6-6.06C13.79,5.12,14.08,5,14.38,5c0.26,0,0.5,0.11,0.63,0.3 c0.07,0.1,0.15,0.26,0.09,0.47l-1.52,4.94L13.18,12h1.35h4.23c0.41,0,0.8,0.17,1.03,0.46C19.92,12.61,20.05,12.86,19.98,13.17z"
class="style-scope yt-icon"
></path>
</g>
</svg>
</div>
</div>
<yt-touch-feedback-shape style="border-radius: inherit">
<div
class="yt-spec-touch-feedback-shape yt-spec-touch-feedback-shape--touch-response"
aria-hidden="true"
>
<div class="yt-spec-touch-feedback-shape__stroke"></div>
<div class="yt-spec-touch-feedback-shape__fill"></div>
</div>
</yt-touch-feedback-shape>
</button>

View File

@ -0,0 +1,9 @@
import { BrowserWindow } from 'electron';
import style from './style.css';
import { injectCSS } from '../utils';
export default (win: BrowserWindow) => {
injectCSS(win.webContents, style);
};

View File

@ -0,0 +1,127 @@
import { FastAverageColor } from 'fast-average-color';
import { ConfigType } from '../../config/dynamic';
function hexToHSL(H: string) {
// Convert hex to RGB first
let r = 0;
let g = 0;
let b = 0;
if (H.length == 4) {
r = Number('0x' + H[1] + H[1]);
g = Number('0x' + H[2] + H[2]);
b = Number('0x' + H[3] + H[3]);
} else if (H.length == 7) {
r = Number('0x' + H[1] + H[2]);
g = Number('0x' + H[3] + H[4]);
b = Number('0x' + H[5] + H[6]);
}
// Then to HSL
r /= 255;
g /= 255;
b /= 255;
const cmin = Math.min(r, g, b);
const cmax = Math.max(r, g, b);
const delta = cmax - cmin;
let h: number;
let s: number;
let l: number;
if (delta == 0) {
h = 0;
} else if (cmax == r) {
h = ((g - b) / delta) % 6;
} else if (cmax == g) {
h = ((b - r) / delta) + 2;
} else {
h = ((r - g) / delta) + 4;
}
h = Math.round(h * 60);
if (h < 0) {
h += 360;
}
l = (cmax + cmin) / 2;
s = delta == 0 ? 0 : delta / (1 - Math.abs((2 * l) - 1));
s = +(s * 100).toFixed(1);
l = +(l * 100).toFixed(1);
//return "hsl(" + h + "," + s + "%," + l + "%)";
return [h,s,l];
}
let hue = 0;
let saturation = 0;
let lightness = 0;
function changeElementColor(element: HTMLElement | null, hue: number, saturation: number, lightness: number){
if (element) {
element.style.backgroundColor = `hsl(${hue}, ${saturation}%, ${lightness}%)`;
}
}
export default (_: ConfigType<'album-color-theme'>) => {
// updated elements
const playerPage = document.querySelector<HTMLElement>('#player-page');
const navBarBackground = document.querySelector<HTMLElement>('#nav-bar-background');
const ytmusicPlayerBar = document.querySelector<HTMLElement>('ytmusic-player-bar');
const playerBarBackground = document.querySelector<HTMLElement>('#player-bar-background');
const sidebarBig = document.querySelector<HTMLElement>('#guide-wrapper');
const sidebarSmall = document.querySelector<HTMLElement>('#mini-guide-background');
const ytmusicAppLayout = document.querySelector<HTMLElement>('#layout');
const observer = new MutationObserver((mutationsList) => {
for (const mutation of mutationsList) {
if (mutation.type === 'attributes') {
const isPageOpen = ytmusicAppLayout?.hasAttribute('player-page-open');
if (isPageOpen) {
changeElementColor(sidebarSmall, hue, saturation, lightness - 30);
} else {
if (sidebarSmall) {
sidebarSmall.style.backgroundColor = 'black';
}
}
}
}
});
if (playerPage) {
observer.observe(playerPage, { attributes: true });
}
document.addEventListener('apiLoaded', (apiEvent) => {
const fastAverageColor = new FastAverageColor();
apiEvent.detail.addEventListener('videodatachange', (name: string) => {
if (name === 'dataloaded') {
const playerResponse = apiEvent.detail.getPlayerResponse();
const thumbnail = playerResponse?.videoDetails?.thumbnail?.thumbnails?.at(0);
if (thumbnail) {
fastAverageColor.getColorAsync(thumbnail.url)
.then((albumColor) => {
if (albumColor) {
[hue, saturation, lightness] = hexToHSL(albumColor.hex);
changeElementColor(playerPage, hue, saturation, lightness - 30);
changeElementColor(navBarBackground, hue, saturation, lightness - 15);
changeElementColor(ytmusicPlayerBar, hue, saturation, lightness - 15);
changeElementColor(playerBarBackground, hue, saturation, lightness - 15);
changeElementColor(sidebarBig, hue, saturation, lightness - 15);
if (ytmusicAppLayout?.hasAttribute('player-page-open')) {
changeElementColor(sidebarSmall, hue, saturation, lightness - 30);
}
const ytRightClickList = document.querySelector<HTMLElement>('tp-yt-paper-listbox');
changeElementColor(ytRightClickList, hue, saturation, lightness - 15);
} else {
if (playerPage) {
playerPage.style.backgroundColor = '#000000';
}
}
})
.catch((e) => console.error(e));
}
}
});
});
};

View File

@ -1,191 +0,0 @@
import { FastAverageColor } from 'fast-average-color';
import Color from 'color';
import style from './style.css?inline';
import { createPlugin } from '@/utils';
import { t } from '@/i18n';
const COLOR_KEY = '--ytmusic-album-color';
const DARK_COLOR_KEY = '--ytmusic-album-color-dark';
const RATIO_KEY = '--ytmusic-album-color-ratio';
export default createPlugin<
unknown,
unknown,
{
color?: Color;
darkColor?: Color;
playerPage: HTMLElement | null;
navBarBackground: HTMLElement | null;
ytmusicPlayerBar: HTMLElement | null;
playerBarBackground: HTMLElement | null;
sidebarBig: HTMLElement | null;
sidebarSmall: HTMLElement | null;
ytmusicAppLayout: HTMLElement | null;
getMixedColor(color: string, key: string, alpha?: number, ratioMultiply?: number): string;
updateColor(): void;
},
{
enabled: boolean;
ratio: number;
}
>({
name: () => t('plugins.album-color-theme.name'),
description: () => t('plugins.album-color-theme.description'),
restartNeeded: false,
config: {
enabled: false,
ratio: 0.5,
},
stylesheets: [style],
menu: async ({ getConfig, setConfig }) => {
const ratioList = [0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1];
const config = await getConfig();
return [
{
label: t('plugins.album-color-theme.menu.color-mix-ratio.label'),
submenu: ratioList.map((ratio) => ({
label: t(
'plugins.album-color-theme.menu.color-mix-ratio.submenu.percent',
{
ratio: ratio * 100,
},
),
type: 'radio',
checked: config.ratio === ratio,
click() {
setConfig({ ratio });
},
})),
},
];
},
renderer: {
playerPage: null,
navBarBackground: null,
ytmusicPlayerBar: null,
playerBarBackground: null,
sidebarBig: null,
sidebarSmall: null,
ytmusicAppLayout: null,
async start({ getConfig }) {
this.playerPage = document.querySelector<HTMLElement>('#player-page');
this.navBarBackground = document.querySelector<HTMLElement>(
'#nav-bar-background',
);
this.ytmusicPlayerBar =
document.querySelector<HTMLElement>('ytmusic-player-bar');
this.playerBarBackground = document.querySelector<HTMLElement>(
'#player-bar-background',
);
this.sidebarBig = document.querySelector<HTMLElement>('#guide-wrapper');
this.sidebarSmall = document.querySelector<HTMLElement>(
'#mini-guide-background',
);
this.ytmusicAppLayout = document.querySelector<HTMLElement>('#layout');
const config = await getConfig();
document.documentElement.style.setProperty(RATIO_KEY, `${~~(config.ratio * 100)}%`);
},
onPlayerApiReady(playerApi) {
const fastAverageColor = new FastAverageColor();
document.addEventListener('videodatachange', async (event) => {
if (event.detail.name !== 'dataloaded') return;
const playerResponse = playerApi.getPlayerResponse();
const thumbnail = playerResponse?.videoDetails?.thumbnail?.thumbnails?.at(0);
if (!thumbnail) return;
const albumColor = await fastAverageColor.getColorAsync(thumbnail.url)
.catch((err) => {
console.error(err);
return null;
});
if (albumColor) {
const target = Color(albumColor.hex);
this.darkColor = target.darken(0.3).rgb();
this.color = target.darken(0.15).rgb();
while (this.color.luminosity() > 0.5) {
this.color = this.color?.darken(0.05);
this.darkColor = this.darkColor?.darken(0.05);
}
document.documentElement.style.setProperty(COLOR_KEY, `${~~this.color.red()}, ${~~this.color.green()}, ${~~this.color.blue()}`);
document.documentElement.style.setProperty(DARK_COLOR_KEY, `${~~this.darkColor.red()}, ${~~this.darkColor.green()}, ${~~this.darkColor.blue()}`);
} else {
document.documentElement.style.setProperty(COLOR_KEY, '0, 0, 0');
document.documentElement.style.setProperty(DARK_COLOR_KEY, '0, 0, 0');
}
this.updateColor();
});
},
onConfigChange(config) {
document.documentElement.style.setProperty(RATIO_KEY, `${~~(config.ratio * 100)}%`);
},
getMixedColor(color: string, key: string, alpha = 1, ratioMultiply) {
const keyColor = `rgba(var(${key}), ${alpha})`;
let colorRatio = `var(${RATIO_KEY}, 50%)`;
let originalRatio = `calc(100% - var(${RATIO_KEY}, 50%))`;
if (ratioMultiply) {
colorRatio = `calc(var(${RATIO_KEY}, 50%) * ${ratioMultiply})`;
originalRatio = `calc(100% - calc(var(${RATIO_KEY}, 50%) * ${ratioMultiply}))`;
}
return `color-mix(in srgb, ${color} ${originalRatio}, ${keyColor} ${colorRatio})`;
},
updateColor() {
const variableMap = {
'--ytmusic-color-black1': '#212121',
'--ytmusic-color-black2': '#181818',
'--ytmusic-color-black3': '#030303',
'--ytmusic-color-black4': '#030303',
'--ytmusic-color-blackpure': '#000',
'--dark-theme-background-color': '#212121',
'--yt-spec-base-background': '#0f0f0f',
'--yt-spec-raised-background': '#212121',
'--yt-spec-menu-background': '#282828',
'--yt-spec-static-brand-black': '#212121',
'--yt-spec-static-overlay-background-solid': '#000',
'--yt-spec-static-overlay-background-heavy': 'rgba(0,0,0,0.8)',
'--yt-spec-static-overlay-background-medium': 'rgba(0,0,0,0.6)',
'--yt-spec-static-overlay-background-medium-light': 'rgba(0,0,0,0.3)',
'--yt-spec-static-overlay-background-light': 'rgba(0,0,0,0.1)',
'--yt-spec-general-background-a': '#181818',
'--yt-spec-general-background-b': '#0f0f0f',
'--yt-spec-general-background-c': '#030303',
'--yt-spec-snackbar-background': '#030303',
'--yt-spec-filled-button-text': '#030303',
'--yt-spec-black-1': '#282828',
'--yt-spec-black-2': '#1f1f1f',
'--yt-spec-black-3': '#161616',
'--yt-spec-black-4': '#0d0d0d',
'--yt-spec-black-pure': '#000',
'--yt-spec-black-pure-alpha-5': 'rgba(0,0,0,0.05)',
'--yt-spec-black-pure-alpha-10': 'rgba(0,0,0,0.1)',
'--yt-spec-black-pure-alpha-15': 'rgba(0,0,0,0.15)',
'--yt-spec-black-pure-alpha-30': 'rgba(0,0,0,0.3)',
'--yt-spec-black-pure-alpha-60': 'rgba(0,0,0,0.6)',
'--yt-spec-black-pure-alpha-80': 'rgba(0,0,0,0.8)',
'--yt-spec-black-1-alpha-98': 'rgba(40,40,40,0.98)',
'--yt-spec-black-1-alpha-95': 'rgba(40,40,40,0.95)',
};
Object.entries(variableMap).map(([variable, color]) => {
document.documentElement.style.setProperty(variable, this.getMixedColor(color, COLOR_KEY), 'important');
});
document.body.style.setProperty('background', this.getMixedColor('#030303', COLOR_KEY), 'important');
document.documentElement.style.setProperty('--ytmusic-background', this.getMixedColor('#030303', DARK_COLOR_KEY), 'important');
},
},
});

View File

@ -4,86 +4,26 @@ yt-page-navigation-progress {
} }
#player-page { #player-page {
transition: transform 300ms, transition: transform 300ms,background-color 300ms cubic-bezier(0.2,0,0.6,1) !important;
background-color 300ms cubic-bezier(0.2, 0, 0.6, 1) !important;
} }
#nav-bar-background { #nav-bar-background {
transition: opacity 200ms, transition: opacity 200ms,background-color 300ms cubic-bezier(0.2,0,0.6,1) !important;
background-color 300ms cubic-bezier(0.2, 0, 0.6, 1) !important;
} }
#mini-guide-background { #mini-guide-background {
transition: opacity 200ms, transition: opacity 200ms,background-color 300ms cubic-bezier(0.2,0,0.6,1) !important;
background-color 300ms cubic-bezier(0.2, 0, 0.6, 1) !important;
border-right: 0px !important; border-right: 0px !important;
} }
#guide-wrapper { #guide-wrapper {
transition: opacity 200ms, transition: opacity 200ms,background-color 300ms cubic-bezier(0.2,0,0.6,1) !important;
background-color 300ms cubic-bezier(0.2, 0, 0.6, 1) !important;
} }
#img, #img, #player, .song-media-controls.style-scope.ytmusic-player {
#player,
.song-media-controls.style-scope.ytmusic-player {
border-radius: 2% !important; border-radius: 2% !important;
} }
#items { #items {
border-radius: 10px !important; border-radius: 10px !important;
} }
/* fix blur navigation bar */
ytmusic-app-layout > [slot='player-page'] {
padding-top: 90px;
margin-top: calc(-90px + var(--menu-bar-height, 0px)) !important;
}
/* fix icon color */
.duration.ytmusic-player-queue-item, .byline.ytmusic-player-queue-item {
color: rgba(255, 255, 255, 0.5) !important;
--yt-endpoint-color: rgba(255, 255, 255, 0.5) !important;
--yt-endpoint-hover-color: rgba(255, 255, 255, 0.5) !important;
--yt-endpoint-visited-color: rgba(255, 255, 255, 0.5) !important;
}
.icon.ytmusic-menu-navigation-item-renderer {
color: rgba(255, 255, 255, 0.5) !important;
}
.menu.ytmusic-player-bar {
--iron-icon-fill-color: rgba(255, 255, 255, 0.5) !important;
}
ytmusic-player-bar {
color: rgba(255, 255, 255, 0.5) !important;
}
.time-info.ytmusic-player-bar {
color: rgba(255, 255, 255, 0.5) !important;
}
.volume-slider.ytmusic-player-bar, .expand-volume-slider.ytmusic-player-bar {
--paper-slider-container-color: rgba(255, 255, 255, 0.5) !important;
}
/* fix background image */
ytmusic-fullbleed-thumbnail-renderer img {
mask: linear-gradient(to bottom, #000 0%, #000 50%, transparent 100%);
}
.background-gradient.style-scope,
ytmusic-app-layout[is-bauhaus-sidenav-enabled] #mini-guide-background.ytmusic-app-layout {
background: var(--ytmusic-background) !important;
}
ytmusic-browse-response[has-background]:not([disable-gradient]) .background-gradient.ytmusic-browse-response {
background: unset !important;
}
#background.immersive-background.style-scope.ytmusic-browse-response {
opacity: 0.6;
}

View File

@ -0,0 +1,14 @@
import { BrowserWindow } from 'electron';
import config from './config';
import style from './style.css';
import { injectCSS } from '../utils';
export default (win: BrowserWindow) => {
config.subscribeAll((newConfig) => {
win.webContents.send('ambient-mode:config-change', newConfig);
});
injectCSS(win.webContents, style);
};

View File

@ -0,0 +1,4 @@
import { PluginConfig } from '../../config/dynamic';
const config = new PluginConfig('ambient-mode');
export default config;

View File

@ -0,0 +1,167 @@
import { ipcRenderer } from 'electron';
import { ConfigType } from '../../config/dynamic';
export default (config: ConfigType<'ambient-mode'>) => {
let interpolationTime = config.interpolationTime; // interpolation time (ms)
let buffer = config.buffer; // frame
let qualityRatio = config.quality; // width size (pixel)
let sizeRatio = config.size / 100; // size ratio (percent)
let blur = config.blur; // blur (pixel)
let opacity = config.opacity; // opacity (percent)
let isFullscreen = config.fullscreen; // fullscreen (boolean)
let unregister: (() => void) | null = null;
const injectBlurVideo = (): (() => void) | null => {
const songVideo = document.querySelector<HTMLDivElement>('#song-video');
const video = document.querySelector<HTMLVideoElement>('#song-video .html5-video-container > video');
const wrapper = document.querySelector('#song-video > .player-wrapper');
if (!songVideo) return null;
if (!video) return null;
if (!wrapper) return null;
const blurCanvas = document.createElement('canvas');
blurCanvas.classList.add('html5-blur-canvas');
const context = blurCanvas.getContext('2d', { willReadFrequently: true });
/* effect */
let lastEffectWorkId: number | null = null;
let lastImageData: ImageData | null = null;
const onSync = () => {
if (typeof lastEffectWorkId === 'number') cancelAnimationFrame(lastEffectWorkId);
lastEffectWorkId = requestAnimationFrame(() => {
if (!context) return;
const width = qualityRatio;
let height = Math.max(Math.floor(blurCanvas.height / blurCanvas.width * width), 1);
if (!Number.isFinite(height)) height = width;
context.globalAlpha = 1;
if (lastImageData) {
const frameOffset = (1 / buffer) * (1000 / interpolationTime);
context.globalAlpha = 1 - (frameOffset * 2); // because of alpha value must be < 1
context.putImageData(lastImageData, 0, 0);
context.globalAlpha = frameOffset;
}
context.drawImage(video, 0, 0, width, height);
const nowImageData = context.getImageData(0, 0, width, height);
lastImageData = nowImageData;
lastEffectWorkId = null;
});
};
const applyVideoAttributes = () => {
const rect = video.getBoundingClientRect();
const newWidth = Math.floor(video.width || rect.width);
const newHeight = Math.floor(video.height || rect.height);
if (newWidth === 0 || newHeight === 0) return;
blurCanvas.width = qualityRatio;
blurCanvas.height = Math.floor(newHeight / newWidth * qualityRatio);
blurCanvas.style.width = `${newWidth * sizeRatio}px`;
blurCanvas.style.height = `${newHeight * sizeRatio}px`;
if (isFullscreen) blurCanvas.classList.add('fullscreen');
else blurCanvas.classList.remove('fullscreen');
const leftOffset = newWidth * (sizeRatio - 1) / 2;
const topOffset = newHeight * (sizeRatio - 1) / 2;
blurCanvas.style.setProperty('--left', `${-1 * leftOffset}px`);
blurCanvas.style.setProperty('--top', `${-1 * topOffset}px`);
blurCanvas.style.setProperty('--blur', `${blur}px`);
blurCanvas.style.setProperty('--opacity', `${opacity}`);
};
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.type === 'attributes') {
applyVideoAttributes();
}
});
});
const resizeObserver = new ResizeObserver(() => {
applyVideoAttributes();
});
const onConfigSync = (_: Electron.IpcRendererEvent, newConfig: ConfigType<'ambient-mode'>) => {
if (typeof newConfig.interpolationTime === 'number') interpolationTime = newConfig.interpolationTime;
if (typeof newConfig.buffer === 'number') buffer = newConfig.buffer;
if (typeof newConfig.quality === 'number') qualityRatio = newConfig.quality;
if (typeof newConfig.size === 'number') sizeRatio = newConfig.size / 100;
if (typeof newConfig.blur === 'number') blur = newConfig.blur;
if (typeof newConfig.opacity === 'number') opacity = newConfig.opacity;
if (typeof newConfig.fullscreen === 'boolean') isFullscreen = newConfig.fullscreen;
applyVideoAttributes();
};
ipcRenderer.on('ambient-mode:config-change', onConfigSync);
/* hooking */
let canvasInterval: NodeJS.Timeout | null = null;
canvasInterval = setInterval(onSync, Math.max(1, Math.ceil(1000 / buffer)));
applyVideoAttributes();
observer.observe(songVideo, { attributes: true });
resizeObserver.observe(songVideo);
window.addEventListener('resize', applyVideoAttributes);
const onPause = () => {
if (canvasInterval) clearInterval(canvasInterval);
canvasInterval = null;
};
const onPlay = () => {
if (canvasInterval) clearInterval(canvasInterval);
canvasInterval = setInterval(onSync, Math.max(1, Math.ceil(1000 / buffer)));
};
songVideo.addEventListener('pause', onPause);
songVideo.addEventListener('play', onPlay);
/* injecting */
wrapper.prepend(blurCanvas);
/* cleanup */
return () => {
if (canvasInterval) clearInterval(canvasInterval);
songVideo.removeEventListener('pause', onPause);
songVideo.removeEventListener('play', onPlay);
observer.disconnect();
resizeObserver.disconnect();
ipcRenderer.off('ambient-mode:config-change', onConfigSync);
window.removeEventListener('resize', applyVideoAttributes);
wrapper.removeChild(blurCanvas);
};
};
const playerPage = document.querySelector<HTMLElement>('#player-page');
const ytmusicAppLayout = document.querySelector<HTMLElement>('#layout');
const observer = new MutationObserver((mutationsList) => {
for (const mutation of mutationsList) {
if (mutation.type === 'attributes') {
const isPageOpen = ytmusicAppLayout?.hasAttribute('player-page-open');
if (isPageOpen) {
unregister?.();
unregister = injectBlurVideo() ?? null;
} else {
unregister?.();
unregister = null;
}
}
}
});
if (playerPage) {
observer.observe(playerPage, { attributes: true });
}
};

View File

@ -1,402 +0,0 @@
import style from './style.css?inline';
import { createPlugin } from '@/utils';
import { t } from '@/i18n';
export type AmbientModePluginConfig = {
enabled: boolean;
quality: number;
buffer: number;
interpolationTime: number;
blur: number;
size: number;
opacity: number;
fullscreen: boolean;
};
const defaultConfig: AmbientModePluginConfig = {
enabled: false,
quality: 50,
buffer: 30,
interpolationTime: 1500,
blur: 100,
size: 100,
opacity: 1,
fullscreen: false,
};
export default createPlugin({
name: () => t('plugins.ambient-mode.name'),
description: () => t('plugins.ambient-mode.description'),
restartNeeded: false,
config: defaultConfig,
stylesheets: [style],
menu: async ({ getConfig, setConfig }) => {
const interpolationTimeList = [0, 500, 1000, 1500, 2000, 3000, 4000, 5000];
const qualityList = [10, 25, 50, 100, 200, 500, 1000];
const sizeList = [100, 110, 125, 150, 175, 200, 300];
const bufferList = [1, 5, 10, 20, 30];
const blurAmountList = [0, 5, 10, 25, 50, 100, 150, 200, 500];
const opacityList = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1];
const config = await getConfig();
return [
{
label: t('plugins.ambient-mode.menu.smoothness-transition.label'),
submenu: interpolationTimeList.map((interpolationTime) => ({
label: t(
'plugins.ambient-mode.menu.smoothness-transition.submenu.during',
{
interpolationTime: interpolationTime / 1000,
},
),
type: 'radio',
checked: config.interpolationTime === interpolationTime,
click() {
setConfig({ interpolationTime });
},
})),
},
{
label: t('plugins.ambient-mode.menu.quality.label'),
submenu: qualityList.map((quality) => ({
label: t('plugins.ambient-mode.menu.quality.submenu.pixels', {
quality,
}),
type: 'radio',
checked: config.quality === quality,
click() {
setConfig({ quality });
},
})),
},
{
label: t('plugins.ambient-mode.menu.size.label'),
submenu: sizeList.map((size) => ({
label: t('plugins.ambient-mode.menu.size.submenu.percent', { size }),
type: 'radio',
checked: config.size === size,
click() {
setConfig({ size });
},
})),
},
{
label: t('plugins.ambient-mode.menu.buffer.label'),
submenu: bufferList.map((buffer) => ({
label: t('plugins.ambient-mode.menu.buffer.submenu.buffer', {
buffer,
}),
type: 'radio',
checked: config.buffer === buffer,
click() {
setConfig({ buffer });
},
})),
},
{
label: t('plugins.ambient-mode.menu.opacity.label'),
submenu: opacityList.map((opacity) => ({
label: t('plugins.ambient-mode.menu.opacity.submenu.percent', {
opacity: opacity * 100,
}),
type: 'radio',
checked: config.opacity === opacity,
click() {
setConfig({ opacity });
},
})),
},
{
label: t('plugins.ambient-mode.menu.blur-amount.label'),
submenu: blurAmountList.map((blur) => ({
label: t('plugins.ambient-mode.menu.blur-amount.submenu.pixels', {
blurAmount: blur,
}),
type: 'radio',
checked: config.blur === blur,
click() {
setConfig({ blur });
},
})),
},
{
label: t('plugins.ambient-mode.menu.use-fullscreen.label'),
type: 'checkbox',
checked: config.fullscreen,
click(item) {
setConfig({ fullscreen: item.checked });
},
},
];
},
renderer: {
interpolationTime: defaultConfig.interpolationTime,
buffer: defaultConfig.buffer,
qualityRatio: defaultConfig.quality,
sizeRatio: defaultConfig.size / 100,
blur: defaultConfig.blur,
opacity: defaultConfig.opacity,
isFullscreen: defaultConfig.fullscreen,
unregister: null as (() => void) | null,
update: null as (() => void) | null,
observer: null as MutationObserver | null,
start() {
const injectBlurImage = () => {
const songImage = document.querySelector<HTMLImageElement>(
'#song-image',
);
const image = document.querySelector<HTMLImageElement>(
'#song-image yt-img-shadow > img',
);
if (!songImage) return null;
if (!image) return null;
const blurImage = document.createElement('img');
blurImage.classList.add('html5-blur-image');
blurImage.src = image.src;
const applyImageAttribute = () => {
const rect = image.getBoundingClientRect();
const newWidth = Math.floor(image.width || rect.width);
const newHeight = Math.floor(image.height || rect.height);
if (newWidth === 0 || newHeight === 0) return;
if (this.isFullscreen) blurImage.classList.add('fullscreen');
else blurImage.classList.remove('fullscreen');
const leftOffset = (newWidth * (this.sizeRatio - 1)) / 2;
const topOffset = (newHeight * (this.sizeRatio - 1)) / 2;
blurImage.style.setProperty('--left', `${-1 * leftOffset}px`);
blurImage.style.setProperty('--top', `${-1 * topOffset}px`);
blurImage.style.setProperty('--width', `${newWidth * this.sizeRatio}px`);
blurImage.style.setProperty('--height', `${newHeight * this.sizeRatio}px`);
blurImage.style.setProperty('--blur', `${this.blur}px`);
blurImage.style.setProperty('--opacity', `${this.opacity}`);
};
this.update = applyImageAttribute;
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.type === 'attributes') {
applyImageAttribute();
}
});
});
const resizeObserver = new ResizeObserver(() => {
applyImageAttribute();
});
applyImageAttribute();
observer.observe(songImage, { attributes: true });
resizeObserver.observe(songImage);
window.addEventListener('resize', applyImageAttribute);
/* injecting */
songImage.prepend(blurImage);
/* cleanup */
return () => {
observer.disconnect();
resizeObserver.disconnect();
window.removeEventListener('resize', applyImageAttribute);
if (blurImage.isConnected) blurImage.remove();
};
};
const injectBlurVideo = (): (() => void) | null => {
const songVideo = document.querySelector<HTMLDivElement>('#song-video');
const video = document.querySelector<HTMLVideoElement>(
'#song-video .html5-video-container > video',
);
const wrapper = document.querySelector('#song-video > .player-wrapper');
if (!songVideo) return null;
if (!video) return null;
if (!wrapper) return null;
const blurCanvas = document.createElement('canvas');
blurCanvas.classList.add('html5-blur-canvas');
const context = blurCanvas.getContext('2d', {
willReadFrequently: true,
});
/* effect */
let lastEffectWorkId: number | null = null;
let lastImageData: ImageData | null = null;
const onSync = () => {
if (typeof lastEffectWorkId === 'number')
cancelAnimationFrame(lastEffectWorkId);
lastEffectWorkId = requestAnimationFrame(() => {
if (!context) return;
const width = this.qualityRatio;
let height = Math.max(
Math.floor((blurCanvas.height / blurCanvas.width) * width),
1,
);
if (!Number.isFinite(height)) height = width;
if (!height) return;
context.globalAlpha = 1;
if (lastImageData) {
const frameOffset =
(1 / this.buffer) * (1000 / this.interpolationTime);
context.globalAlpha = 1 - (frameOffset * 2); // because of alpha value must be < 1
context.putImageData(lastImageData, 0, 0);
context.globalAlpha = frameOffset;
}
context.drawImage(video, 0, 0, width, height);
lastImageData = context.getImageData(0, 0, width, height); // current image data
lastEffectWorkId = null;
});
};
const applyVideoAttributes = () => {
const rect = video.getBoundingClientRect();
const newWidth = Math.floor(video.width || rect.width);
const newHeight = Math.floor(video.height || rect.height);
if (newWidth === 0 || newHeight === 0) return;
blurCanvas.width = this.qualityRatio;
blurCanvas.height = Math.floor(
(newHeight / newWidth) * this.qualityRatio,
);
blurCanvas.style.width = `${newWidth * this.sizeRatio}px`;
blurCanvas.style.height = `${newHeight * this.sizeRatio}px`;
if (this.isFullscreen) blurCanvas.classList.add('fullscreen');
else blurCanvas.classList.remove('fullscreen');
const leftOffset = (newWidth * (this.sizeRatio - 1)) / 2;
const topOffset = (newHeight * (this.sizeRatio - 1)) / 2;
blurCanvas.style.setProperty('--left', `${-1 * leftOffset}px`);
blurCanvas.style.setProperty('--top', `${-1 * topOffset}px`);
blurCanvas.style.setProperty('--blur', `${this.blur}px`);
blurCanvas.style.setProperty('--opacity', `${this.opacity}`);
};
this.update = applyVideoAttributes;
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.type === 'attributes') {
applyVideoAttributes();
}
});
});
const resizeObserver = new ResizeObserver(() => {
applyVideoAttributes();
});
/* hooking */
let canvasInterval: NodeJS.Timeout | null = null;
canvasInterval = setInterval(
onSync,
Math.max(1, Math.ceil(1000 / this.buffer)),
);
applyVideoAttributes();
observer.observe(songVideo, { attributes: true });
resizeObserver.observe(songVideo);
window.addEventListener('resize', applyVideoAttributes);
const onPause = () => {
if (canvasInterval) clearInterval(canvasInterval);
canvasInterval = null;
};
const onPlay = () => {
if (canvasInterval) clearInterval(canvasInterval);
canvasInterval = setInterval(
onSync,
Math.max(1, Math.ceil(1000 / this.buffer)),
);
};
songVideo.addEventListener('pause', onPause);
songVideo.addEventListener('play', onPlay);
/* injecting */
wrapper.prepend(blurCanvas);
/* cleanup */
return () => {
if (canvasInterval) clearInterval(canvasInterval);
songVideo.removeEventListener('pause', onPause);
songVideo.removeEventListener('play', onPlay);
observer.disconnect();
resizeObserver.disconnect();
window.removeEventListener('resize', applyVideoAttributes);
if (blurCanvas.isConnected) blurCanvas.remove();
};
};
const isVideoMode = () => {
const songVideo = document.querySelector<HTMLDivElement>('#song-video');
if (!songVideo) return false;
return getComputedStyle(songVideo).display !== 'none';
};
const playerPage = document.querySelector<HTMLElement>('#player-page');
const ytmusicAppLayout = document.querySelector<HTMLElement>('#layout');
const isPageOpen = ytmusicAppLayout?.hasAttribute('player-page-open');
if (isPageOpen) {
this.unregister?.();
this.unregister = (isVideoMode() ? injectBlurVideo() : injectBlurImage()) ?? null;
}
const observer = new MutationObserver((mutationsList) => {
for (const mutation of mutationsList) {
if (mutation.type === 'attributes') {
const isPageOpen =
ytmusicAppLayout?.hasAttribute('player-page-open');
if (isPageOpen) {
this.unregister?.();
this.unregister = (isVideoMode() ? injectBlurVideo() : injectBlurImage()) ?? null;
} else {
this.unregister?.();
this.unregister = null;
}
}
}
});
if (playerPage) {
observer.observe(playerPage, { attributes: true });
}
},
onConfigChange(newConfig) {
this.interpolationTime = newConfig.interpolationTime;
this.buffer = newConfig.buffer;
this.qualityRatio = newConfig.quality;
this.sizeRatio = newConfig.size / 100;
this.blur = newConfig.blur;
this.opacity = newConfig.opacity;
this.isFullscreen = newConfig.fullscreen;
this.update?.();
},
stop() {
this.observer?.disconnect();
this.update = null;
this.unregister?.();
},
},
});

View File

@ -0,0 +1,87 @@
import config from './config';
import { MenuTemplate } from '../../menu';
const interpolationTimeList = [0, 500, 1000, 1500, 2000, 3000, 4000, 5000];
const qualityList = [10, 25, 50, 100, 200, 500, 1000];
const sizeList = [100, 110, 125, 150, 175, 200, 300];
const bufferList = [1, 5, 10, 20, 30];
const blurAmountList = [0, 5, 10, 25, 50, 100, 150, 200, 500];
const opacityList = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1];
export default (): MenuTemplate => [
{
label: 'Smoothness transition',
submenu: interpolationTimeList.map((interpolationTime) => ({
label: `During ${interpolationTime / 1000}s`,
type: 'radio',
checked: config.get('interpolationTime') === interpolationTime,
click() {
config.set('interpolationTime', interpolationTime);
},
})),
},
{
label: 'Quality',
submenu: qualityList.map((quality) => ({
label: `${quality} pixels`,
type: 'radio',
checked: config.get('quality') === quality,
click() {
config.set('quality', quality);
},
})),
},
{
label: 'Size',
submenu: sizeList.map((size) => ({
label: `${size}%`,
type: 'radio',
checked: config.get('size') === size,
click() {
config.set('size', size);
},
})),
},
{
label: 'Buffer',
submenu: bufferList.map((buffer) => ({
label: `${buffer}`,
type: 'radio',
checked: config.get('buffer') === buffer,
click() {
config.set('buffer', buffer);
},
})),
},
{
label: 'Opacity',
submenu: opacityList.map((opacity) => ({
label: `${opacity * 100}%`,
type: 'radio',
checked: config.get('opacity') === opacity,
click() {
config.set('opacity', opacity);
},
})),
},
{
label: 'Blur amount',
submenu: blurAmountList.map((blur) => ({
label: `${blur} pixels`,
type: 'radio',
checked: config.get('blur') === blur,
click() {
config.set('blur', blur);
},
})),
},
{
label: 'Using fullscreen',
type: 'checkbox',
checked: config.get('fullscreen'),
click(item) {
config.set('fullscreen', item.checked);
},
},
];

View File

@ -24,17 +24,3 @@
#song-video .html5-video-container > video { #song-video .html5-video-container > video {
top: 0 !important; top: 0 !important;
} }
#song-image .html5-blur-image {
position: absolute;
left: var(--left, 0px);
top: var(--top, 0px);
width: var(--width, 100%) !important;
height: var(--height, 100%) !important;
filter: blur(var(--blur, 100px));
opacity: var(--opacity, 1);
pointer-events: none;
}

View File

@ -1,26 +0,0 @@
import { createPlugin } from '@/utils';
import { t } from '@/i18n';
export default createPlugin({
name: () => t('plugins.audio-compressor.name'),
description: () => t('plugins.audio-compressor.description'),
renderer() {
document.addEventListener(
'ytmd:audio-can-play',
({ detail: { audioSource, audioContext } }) => {
const compressor = audioContext.createDynamicsCompressor();
compressor.threshold.value = -50;
compressor.ratio.value = 12;
compressor.knee.value = 40;
compressor.attack.value = 0;
compressor.release.value = 0.25;
audioSource.connect(compressor);
compressor.connect(audioContext.destination);
},
{ once: true, passive: true },
);
},
});

View File

@ -0,0 +1,17 @@
export default () =>
document.addEventListener('audioCanPlay', (e) => {
const { audioContext } = e.detail;
const compressor = audioContext.createDynamicsCompressor();
compressor.threshold.value = -50;
compressor.ratio.value = 12;
compressor.knee.value = 40;
compressor.attack.value = 0;
compressor.release.value = 0.25;
e.detail.audioSource.connect(compressor);
compressor.connect(audioContext.destination);
}, {
once: true, // Only create the audio compressor once, not on each video
passive: true,
});

View File

@ -0,0 +1,9 @@
import { BrowserWindow } from 'electron';
import style from './style.css';
import { injectCSS } from '../utils';
export default (win: BrowserWindow) => {
injectCSS(win.webContents, style);
};

View File

@ -1,24 +0,0 @@
import { createPlugin } from '@/utils';
import { t } from '@/i18n';
import style from './style.css?inline';
export default createPlugin({
name: () => t('plugins.blur-nav-bar.name'),
description: () => t('plugins.blur-nav-bar.description'),
restartNeeded: false,
renderer: {
styleSheet: null as CSSStyleSheet | null,
async start() {
this.styleSheet = new CSSStyleSheet();
await this.styleSheet.replace(style);
document.adoptedStyleSheets = [...document.adoptedStyleSheets, this.styleSheet];
},
async stop() {
await this.styleSheet?.replace('');
},
},
});

View File

@ -1,16 +1,8 @@
#nav-bar-background, #nav-bar-background,
#header.ytmusic-item-section-renderer { #header.ytmusic-item-section-renderer,
background: rgba(0, 0, 0, 0.3) !important;
backdrop-filter: blur(8px) !important;
}
ytmusic-tabs { ytmusic-tabs {
top: calc(var(--ytmusic-nav-bar-height) + var(--menu-bar-height, 36px));
backdrop-filter: blur(8px) !important;
}
ytmusic-tabs.stuck {
background: rgba(0, 0, 0, 0.3) !important; background: rgba(0, 0, 0, 0.3) !important;
backdrop-filter: blur(8px) !important;
} }
#nav-bar-divider { #nav-bar-divider {

View File

@ -0,0 +1,4 @@
export default async () => {
// See https://github.com/zerodytrash/Simple-YouTube-Age-Restriction-Bypass#userscript
await import('simple-youtube-age-restriction-bypass');
};

View File

@ -1,13 +0,0 @@
import { inject } from 'simple-youtube-age-restriction-bypass';
import { createPlugin } from '@/utils';
import { t } from '@/i18n';
export default createPlugin({
name: () => t('plugins.bypass-age-restrictions.name'),
description: () => t('plugins.bypass-age-restrictions.description'),
restartNeeded: true,
// See https://github.com/organization/Simple-YouTube-Age-Restriction-Bypass#userscript
renderer: () => inject(),
});

View File

@ -1,3 +1,4 @@
declare module 'simple-youtube-age-restriction-bypass' { declare module 'simple-youtube-age-restriction-bypass' {
export const inject: () => void; const nothing: never;
export default nothing;
} }

View File

@ -1,33 +1,19 @@
import { BrowserWindow, ipcMain } from 'electron';
import prompt from 'custom-electron-prompt'; import prompt from 'custom-electron-prompt';
import promptOptions from '@/providers/prompt-options'; import promptOptions from '../../providers/prompt-options';
import { createBackend } from '@/utils';
import { t } from '@/i18n';
export default createBackend({ export default (win: BrowserWindow) => {
start({ ipc: { handle }, window }) { ipcMain.handle('captionsSelector', async (_, captionLabels: Record<string, string>, currentIndex: string) => await prompt(
handle( {
'captionsSelector', title: 'Choose Caption',
async (captionLabels: Record<string, string>, currentIndex: string) => label: `Current Caption: ${captionLabels[currentIndex] || 'None'}`,
await prompt( type: 'select',
{ value: currentIndex,
title: t('plugins.captions-selector.prompt.selector.title'), selectOptions: captionLabels,
label: t('plugins.captions-selector.prompt.selector.label', { resizable: true,
language: ...promptOptions(),
captionLabels[currentIndex] || },
t('plugins.captions-selector.prompt.selector.none'), win,
}), ));
type: 'select', };
value: currentIndex,
selectOptions: captionLabels,
resizable: true,
...promptOptions(),
},
window,
),
);
},
stop({ ipc: { removeHandler } }) {
removeHandler('captionsSelector');
},
});

View File

@ -0,0 +1,4 @@
import { PluginConfig } from '../../config/dynamic';
const config = new PluginConfig('captions-selector', { enableFront: true });
export default config;

View File

@ -0,0 +1,101 @@
/* eslint-disable @typescript-eslint/await-thenable */
/* renderer */
import { ipcRenderer } from 'electron';
import configProvider from './config';
import CaptionsSettingsButtonHTML from './templates/captions-settings-template.html';
import { ElementFromHtml } from '../utils';
import { YoutubePlayer } from '../../types/youtube-player';
import type { ConfigType } from '../../config/dynamic';
interface LanguageOptions {
displayName: string;
id: string | null;
is_default: boolean;
is_servable: boolean;
is_translateable: boolean;
kind: string;
languageCode: string; // 2 length
languageName: string;
name: string | null;
vss_id: string;
}
let config: ConfigType<'captions-selector'>;
const $ = <Element extends HTMLElement>(selector: string): Element => document.querySelector(selector)!;
const captionsSettingsButton = ElementFromHtml(CaptionsSettingsButtonHTML);
export default async () => {
// RENDERER
config = await configProvider.getAll();
configProvider.subscribeAll((newConfig) => {
config = newConfig;
});
document.addEventListener('apiLoaded', (event) => setup(event.detail), { once: true, passive: true });
};
function setup(api: YoutubePlayer) {
$('.right-controls-buttons').append(captionsSettingsButton);
let captionTrackList = api.getOption<LanguageOptions[]>('captions', 'tracklist') ?? [];
$('video').addEventListener('srcChanged', () => {
if (config.disableCaptions) {
setTimeout(() => api.unloadModule('captions'), 100);
captionsSettingsButton.style.display = 'none';
return;
}
api.loadModule('captions');
setTimeout(() => {
captionTrackList = api.getOption('captions', 'tracklist') ?? [];
if (config.autoload && config.lastCaptionsCode) {
api.setOption('captions', 'track', {
languageCode: config.lastCaptionsCode,
});
}
captionsSettingsButton.style.display = captionTrackList?.length
? 'inline-block'
: 'none';
}, 250);
});
captionsSettingsButton.addEventListener('click', async () => {
if (captionTrackList?.length) {
const currentCaptionTrack = api.getOption<LanguageOptions>('captions', 'track')!;
let currentIndex = currentCaptionTrack
? captionTrackList.indexOf(captionTrackList.find((track) => track.languageCode === currentCaptionTrack.languageCode)!)
: null;
const captionLabels = [
...captionTrackList.map((track) => track.displayName),
'None',
];
currentIndex = await ipcRenderer.invoke('captionsSelector', captionLabels, currentIndex) as number;
if (currentIndex === null) {
return;
}
const newCaptions = captionTrackList[currentIndex];
configProvider.set('lastCaptionsCode', newCaptions?.languageCode);
if (newCaptions) {
api.setOption('captions', 'track', { languageCode: newCaptions.languageCode });
} else {
api.setOption('captions', 'track', {});
}
setTimeout(() => api.playVideo());
}
});
}

View File

@ -1,55 +0,0 @@
import { createPlugin } from '@/utils';
import { YoutubePlayer } from '@/types/youtube-player';
import backend from './back';
import renderer, { CaptionsSelectorConfig, LanguageOptions } from './renderer';
import { t } from '@/i18n';
export default createPlugin<
unknown,
unknown,
{
captionsSettingsButton: HTMLElement;
captionTrackList: LanguageOptions[] | null;
api: YoutubePlayer | null;
config: CaptionsSelectorConfig | null;
setConfig: (config: Partial<CaptionsSelectorConfig>) => void;
videoChangeListener: () => void;
captionsButtonClickListener: () => void;
},
CaptionsSelectorConfig
>({
name: () => t('plugins.captions-selector.name'),
description: () => t('plugins.captions-selector.description'),
config: {
enabled: false,
disableCaptions: false,
autoload: false,
lastCaptionsCode: '',
},
async menu({ getConfig, setConfig }) {
const config = await getConfig();
return [
{
label: t('plugins.captions-selector.menu.autoload'),
type: 'checkbox',
checked: config.autoload as boolean,
click(item) {
setConfig({ autoload: item.checked });
},
},
{
label: t('plugins.captions-selector.menu.disable-captions'),
type: 'checkbox',
checked: config.disableCaptions as boolean,
click(item) {
setConfig({ disableCaptions: item.checked });
},
},
];
},
backend,
renderer,
});

View File

@ -0,0 +1,22 @@
import config from './config';
import { MenuTemplate } from '../../menu';
export default (): MenuTemplate => [
{
label: 'Automatically select last used caption',
type: 'checkbox',
checked: config.get('autoload'),
click(item) {
config.set('autoload', item.checked);
},
},
{
label: 'No captions by default',
type: 'checkbox',
checked: config.get('disableCaptions'),
click(item) {
config.set('disableCaptions', item.checked);
},
},
];

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