mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-07 04:20:12 +02:00
Compare commits
60 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8815246221 | |||
| 5e353649a4 | |||
| 4ca7b4221c | |||
| 6c61ea898f | |||
| 0b786b025f | |||
| e106fb324f | |||
| 3edc6bab04 | |||
| 0113ef2582 | |||
| 493b81875a | |||
| ed8e5e69ba | |||
| d27a656568 | |||
| 582739a091 | |||
| fb930f1197 | |||
| 849aa97e63 | |||
| bc5abe3ec1 | |||
| 46cd783fa3 | |||
| a93173f55a | |||
| bd04168209 | |||
| 76ffdb6627 | |||
| f674260df3 | |||
| e9315886b7 | |||
| 4741dd0d77 | |||
| 7a1c4f5082 | |||
| abf339bb58 | |||
| bb8f67c4c1 | |||
| e416c2a3b6 | |||
| 5af4344168 | |||
| 55e958b5da | |||
| 2a231ed7af | |||
| a26f7feb31 | |||
| 90e267d9c7 | |||
| 1ab09c5488 | |||
| eadbf3ad7b | |||
| 42bcc4190c | |||
| 3c44db377b | |||
| ba64f4c467 | |||
| 596bf3a378 | |||
| 91e6119afa | |||
| b053538f94 | |||
| 0c14427bdb | |||
| 110a1a63f0 | |||
| d57b4b4b68 | |||
| 4191edb88c | |||
| b38930a277 | |||
| ea865f44b1 | |||
| 0768ce80a7 | |||
| c960cc44b7 | |||
| a84276579b | |||
| b30fadd149 | |||
| aa89c5e80e | |||
| 38ed083693 | |||
| 961d1838c0 | |||
| 67deb3e3d8 | |||
| 79384fa4ed | |||
| bb2f8461ed | |||
| 168153b211 | |||
| c5e8472746 | |||
| a9e0689619 | |||
| 69acbc9a28 | |||
| 58484b87f4 |
@@ -17,7 +17,7 @@ jobs:
|
|||||||
- name: Install Node and NPM
|
- name: Install Node and NPM
|
||||||
uses: actions/setup-node@v1
|
uses: actions/setup-node@v1
|
||||||
with:
|
with:
|
||||||
node-version: 16
|
node-version: 18
|
||||||
cache: npm
|
cache: npm
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
name: Publish Windows and macOS (Manual)
|
name: Publish macOS (Manual)
|
||||||
|
|
||||||
on: workflow_dispatch
|
on: workflow_dispatch
|
||||||
|
|
||||||
@@ -17,7 +17,7 @@ jobs:
|
|||||||
- name: Install Node and NPM
|
- name: Install Node and NPM
|
||||||
uses: actions/setup-node@v1
|
uses: actions/setup-node@v1
|
||||||
with:
|
with:
|
||||||
node-version: 16
|
node-version: 18
|
||||||
cache: npm
|
cache: npm
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
@@ -35,5 +35,5 @@ jobs:
|
|||||||
command: |
|
command: |
|
||||||
npm run postinstall
|
npm run postinstall
|
||||||
npm run build
|
npm run build
|
||||||
npm exec electron-builder -- --publish always --win --mac
|
npm exec electron-builder -- --publish always --mac
|
||||||
on_retry_command: npm cache clean --force
|
on_retry_command: npm cache clean --force
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ jobs:
|
|||||||
- name: Install Node and NPM
|
- name: Install Node and NPM
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: 16
|
node-version: 18
|
||||||
cache: npm
|
cache: npm
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
name: Publish Windows (Manual)
|
||||||
|
|
||||||
|
on: workflow_dispatch
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
publish:
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
os: [windows-latest]
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout git repo
|
||||||
|
uses: actions/checkout@v1
|
||||||
|
|
||||||
|
- name: Install Node and NPM
|
||||||
|
uses: actions/setup-node@v1
|
||||||
|
with:
|
||||||
|
node-version: 18
|
||||||
|
cache: npm
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
npm install --legacy-peer-deps
|
||||||
|
|
||||||
|
- name: Publish releases
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
uses: nick-invision/retry@v2.8.2
|
||||||
|
with:
|
||||||
|
timeout_minutes: 30
|
||||||
|
max_attempts: 3
|
||||||
|
retry_on: error
|
||||||
|
command: |
|
||||||
|
npm run postinstall
|
||||||
|
npm run build
|
||||||
|
npm exec electron-builder -- --publish always --win
|
||||||
|
on_retry_command: npm cache clean --force
|
||||||
@@ -70,6 +70,29 @@ docker build -t feishin .
|
|||||||
docker run --name feishin -p 9180:9180 feishin
|
docker run --name feishin -p 9180:9180 feishin
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### Docker Compose
|
||||||
|
To install via Docker Compose use the following snippit. This also works on Portainer.
|
||||||
|
```
|
||||||
|
version: '3'
|
||||||
|
services:
|
||||||
|
feishin:
|
||||||
|
container_name: feishin
|
||||||
|
image: 'ghcr.io/jeffvli/feishin:latest'
|
||||||
|
environment:
|
||||||
|
- SERVER_NAME=jellyfin # pre defined server name
|
||||||
|
- SERVER_LOCK=true # When true AND name/type/url are set, only username/password can be toggled
|
||||||
|
- SERVER_TYPE=jellyfin # navidrome also works
|
||||||
|
- SERVER_URL= # http://address:port
|
||||||
|
- PUID=1000
|
||||||
|
- PGID=1000
|
||||||
|
- UMASK=002
|
||||||
|
- TZ=America/Los_Angeles
|
||||||
|
ports:
|
||||||
|
- 9180:9180
|
||||||
|
restart: unless-stopped
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
### Configuration
|
### Configuration
|
||||||
|
|
||||||
1. Upon startup you will be greeted with a prompt to select the path to your MPV binary. If you do not have MPV installed, you can download it [here](https://mpv.io/installation/) or install it using any package manager supported by your OS. After inputting the path, restart the app.
|
1. Upon startup you will be greeted with a prompt to select the path to your MPV binary. If you do not have MPV installed, you can download it [here](https://mpv.io/installation/) or install it using any package manager supported by your OS. After inputting the path, restart the app.
|
||||||
|
|||||||
Generated
+1623
-1434
File diff suppressed because it is too large
Load Diff
+12
-11
@@ -2,7 +2,7 @@
|
|||||||
"name": "feishin",
|
"name": "feishin",
|
||||||
"productName": "Feishin",
|
"productName": "Feishin",
|
||||||
"description": "Feishin music server",
|
"description": "Feishin music server",
|
||||||
"version": "0.7.0",
|
"version": "0.7.2",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "concurrently \"npm run build:main\" \"npm run build:renderer\" \"npm run build:remote\"",
|
"build": "concurrently \"npm run build:main\" \"npm run build:renderer\" \"npm run build:remote\"",
|
||||||
"build:main": "cross-env NODE_ENV=production TS_NODE_TRANSPILE_ONLY=true webpack --config ./.erb/configs/webpack.config.main.prod.ts",
|
"build:main": "cross-env NODE_ENV=production TS_NODE_TRANSPILE_ONLY=true webpack --config ./.erb/configs/webpack.config.main.prod.ts",
|
||||||
@@ -14,12 +14,12 @@
|
|||||||
"lint": "concurrently \"npm run lint:code\" \"npm run lint:styles\"",
|
"lint": "concurrently \"npm run lint:code\" \"npm run lint:styles\"",
|
||||||
"lint:code": "cross-env NODE_ENV=development eslint . --ext .js,.jsx,.ts,.tsx --fix",
|
"lint:code": "cross-env NODE_ENV=development eslint . --ext .js,.jsx,.ts,.tsx --fix",
|
||||||
"lint:styles": "npx stylelint **/*.tsx --fix",
|
"lint:styles": "npx stylelint **/*.tsx --fix",
|
||||||
"package": "ts-node ./.erb/scripts/clean.js dist && npm run build && electron-builder build --publish never",
|
"package": "node --import tsx ./.erb/scripts/clean.js dist && npm run build && electron-builder build --publish never",
|
||||||
"package:pr": "ts-node ./.erb/scripts/clean.js dist && npm run build && electron-builder build --publish never --win --mac --linux",
|
"package:pr": "node --import tsx ./.erb/scripts/clean.js dist && npm run build && electron-builder build --publish never --win --mac --linux",
|
||||||
"package:dev": "ts-node ./.erb/scripts/clean.js dist && npm run build && electron-builder build --publish never --dir",
|
"package:dev": "node --import tsx ./.erb/scripts/clean.js dist && npm run build && electron-builder build --publish never --dir",
|
||||||
"postinstall": "ts-node .erb/scripts/check-native-dep.js && electron-builder install-app-deps && cross-env NODE_ENV=development TS_NODE_TRANSPILE_ONLY=true webpack --config ./.erb/configs/webpack.config.renderer.dev.dll.ts",
|
"postinstall": "node --import tsx .erb/scripts/check-native-dep.js && electron-builder install-app-deps && cross-env NODE_ENV=development TS_NODE_TRANSPILE_ONLY=true webpack --config ./.erb/configs/webpack.config.renderer.dev.dll.ts",
|
||||||
"start": "ts-node ./.erb/scripts/check-port-in-use.js && npm run start:renderer",
|
"start": "node --import tsx ./.erb/scripts/check-port-in-use.js && npm run start:renderer",
|
||||||
"start:main": "cross-env NODE_ENV=development electron -r ts-node/register/transpile-only ./src/main/main.ts",
|
"start:main": "cross-env NODE_ENV=development NODE_OPTIONS=\"--import tsx\" electron -r ts-node/register/transpile-only ./src/main/main.ts",
|
||||||
"start:preload": "cross-env NODE_ENV=development TS_NODE_TRANSPILE_ONLY=true webpack --config ./.erb/configs/webpack.config.preload.dev.ts",
|
"start:preload": "cross-env NODE_ENV=development TS_NODE_TRANSPILE_ONLY=true webpack --config ./.erb/configs/webpack.config.preload.dev.ts",
|
||||||
"start:remote": "cross-env NODE_ENV=developemnt TS_NODE_TRANSPILE_ONLY=true webpack --config ./.erb/configs/webpack.config.remote.dev.ts",
|
"start:remote": "cross-env NODE_ENV=developemnt TS_NODE_TRANSPILE_ONLY=true webpack --config ./.erb/configs/webpack.config.remote.dev.ts",
|
||||||
"start:renderer": "cross-env NODE_ENV=development TS_NODE_TRANSPILE_ONLY=true webpack serve --config ./.erb/configs/webpack.config.renderer.dev.ts",
|
"start:renderer": "cross-env NODE_ENV=development TS_NODE_TRANSPILE_ONLY=true webpack serve --config ./.erb/configs/webpack.config.renderer.dev.ts",
|
||||||
@@ -199,7 +199,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@electron/rebuild": "^3.2.10",
|
"@electron/rebuild": "^3.6.0",
|
||||||
"@pmmmwh/react-refresh-webpack-plugin": "0.5.5",
|
"@pmmmwh/react-refresh-webpack-plugin": "0.5.5",
|
||||||
"@stylelint/postcss-css-in-js": "^0.38.0",
|
"@stylelint/postcss-css-in-js": "^0.38.0",
|
||||||
"@teamsupercell/typings-for-css-modules-loader": "^2.5.1",
|
"@teamsupercell/typings-for-css-modules-loader": "^2.5.1",
|
||||||
@@ -231,7 +231,7 @@
|
|||||||
"css-loader": "^6.7.1",
|
"css-loader": "^6.7.1",
|
||||||
"css-minimizer-webpack-plugin": "^3.4.1",
|
"css-minimizer-webpack-plugin": "^3.4.1",
|
||||||
"detect-port": "^1.3.0",
|
"detect-port": "^1.3.0",
|
||||||
"electron": "^26.6.10",
|
"electron": "^31.2.0",
|
||||||
"electron-builder": "^24.13.3",
|
"electron-builder": "^24.13.3",
|
||||||
"electron-devtools-installer": "^3.2.0",
|
"electron-devtools-installer": "^3.2.0",
|
||||||
"electron-notarize": "^1.2.1",
|
"electron-notarize": "^1.2.1",
|
||||||
@@ -278,8 +278,9 @@
|
|||||||
"terser-webpack-plugin": "^5.3.1",
|
"terser-webpack-plugin": "^5.3.1",
|
||||||
"ts-jest": "^27.1.4",
|
"ts-jest": "^27.1.4",
|
||||||
"ts-loader": "^9.2.8",
|
"ts-loader": "^9.2.8",
|
||||||
"ts-node": "^10.7.0",
|
"ts-node": "^10.9.2",
|
||||||
"tsconfig-paths-webpack-plugin": "^4.0.0",
|
"tsconfig-paths-webpack-plugin": "^4.0.0",
|
||||||
|
"tsx": "^4.16.2",
|
||||||
"typescript": "^5.2.2",
|
"typescript": "^5.2.2",
|
||||||
"typescript-plugin-styled-components": "^3.0.0",
|
"typescript-plugin-styled-components": "^3.0.0",
|
||||||
"url-loader": "^4.1.1",
|
"url-loader": "^4.1.1",
|
||||||
@@ -357,7 +358,7 @@
|
|||||||
"styled-components": "^6"
|
"styled-components": "^6"
|
||||||
},
|
},
|
||||||
"devEngines": {
|
"devEngines": {
|
||||||
"node": ">=14.x",
|
"node": ">=18.x",
|
||||||
"npm": ">=7.x"
|
"npm": ">=7.x"
|
||||||
},
|
},
|
||||||
"browserslist": [],
|
"browserslist": [],
|
||||||
|
|||||||
Generated
+44
-27
@@ -1,21 +1,21 @@
|
|||||||
{
|
{
|
||||||
"name": "feishin",
|
"name": "feishin",
|
||||||
"version": "0.7.0",
|
"version": "0.7.2",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "feishin",
|
"name": "feishin",
|
||||||
"version": "0.7.0",
|
"version": "0.7.2",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "GPL-3.0",
|
"license": "GPL-3.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cheerio": "^1.0.0-rc.12",
|
"cheerio": "^1.0.0-rc.12",
|
||||||
"mpris-service": "^2.1.2",
|
"mpris-service": "^2.1.2",
|
||||||
"ws": "^8.13.0"
|
"ws": "^8.18.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"electron": "25.8.4"
|
"electron": "31.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@electron/get": {
|
"node_modules/@electron/get": {
|
||||||
@@ -99,10 +99,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
"version": "18.16.19",
|
"version": "20.14.9",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.16.19.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.9.tgz",
|
||||||
"integrity": "sha512-IXl7o+R9iti9eBW4Wg2hx1xQDig183jj7YLn8F7udNceyfkbn1ZxmzZXuak20gR40D7pIkIY1kYGx5VIGbaHKA==",
|
"integrity": "sha512-06OCtnTXtWOZBJlRApleWndH4JsRVs1pDCc8dLSQp+7PpUpX3ePdHyeNSFTeSe7FtKyQkrlPvHwJOW3SLd8Oyg==",
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"undici-types": "~5.26.4"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/responselike": {
|
"node_modules/@types/responselike": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
@@ -453,14 +456,14 @@
|
|||||||
"integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg=="
|
"integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg=="
|
||||||
},
|
},
|
||||||
"node_modules/electron": {
|
"node_modules/electron": {
|
||||||
"version": "25.8.4",
|
"version": "31.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/electron/-/electron-25.8.4.tgz",
|
"resolved": "https://registry.npmjs.org/electron/-/electron-31.1.0.tgz",
|
||||||
"integrity": "sha512-hUYS3RGdaa6E1UWnzeGnsdsBYOggwMMg4WGxNGvAoWtmRrr6J1BsjFW/yRq4WsJHJce2HdzQXtz4OGXV6yUCLg==",
|
"integrity": "sha512-TBOwqLxSxnx6+pH6GMri7R3JPH2AkuGJHfWZS0p1HsmN+Qr1T9b0IRJnnehSd/3NZAmAre4ft9Ljec7zjyKFJA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@electron/get": "^2.0.0",
|
"@electron/get": "^2.0.0",
|
||||||
"@types/node": "^18.11.18",
|
"@types/node": "^20.9.0",
|
||||||
"extract-zip": "^2.0.1"
|
"extract-zip": "^2.0.1"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
@@ -1270,6 +1273,12 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/undici-types": {
|
||||||
|
"version": "5.26.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
|
||||||
|
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"node_modules/universalify": {
|
"node_modules/universalify": {
|
||||||
"version": "0.1.2",
|
"version": "0.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz",
|
||||||
@@ -1286,9 +1295,9 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/ws": {
|
"node_modules/ws": {
|
||||||
"version": "8.13.0",
|
"version": "8.18.0",
|
||||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz",
|
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz",
|
||||||
"integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==",
|
"integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=10.0.0"
|
"node": ">=10.0.0"
|
||||||
},
|
},
|
||||||
@@ -1408,10 +1417,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@types/node": {
|
"@types/node": {
|
||||||
"version": "18.16.19",
|
"version": "20.14.9",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.16.19.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.9.tgz",
|
||||||
"integrity": "sha512-IXl7o+R9iti9eBW4Wg2hx1xQDig183jj7YLn8F7udNceyfkbn1ZxmzZXuak20gR40D7pIkIY1kYGx5VIGbaHKA==",
|
"integrity": "sha512-06OCtnTXtWOZBJlRApleWndH4JsRVs1pDCc8dLSQp+7PpUpX3ePdHyeNSFTeSe7FtKyQkrlPvHwJOW3SLd8Oyg==",
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"undici-types": "~5.26.4"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"@types/responselike": {
|
"@types/responselike": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
@@ -1672,13 +1684,13 @@
|
|||||||
"integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg=="
|
"integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg=="
|
||||||
},
|
},
|
||||||
"electron": {
|
"electron": {
|
||||||
"version": "25.8.4",
|
"version": "31.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/electron/-/electron-25.8.4.tgz",
|
"resolved": "https://registry.npmjs.org/electron/-/electron-31.1.0.tgz",
|
||||||
"integrity": "sha512-hUYS3RGdaa6E1UWnzeGnsdsBYOggwMMg4WGxNGvAoWtmRrr6J1BsjFW/yRq4WsJHJce2HdzQXtz4OGXV6yUCLg==",
|
"integrity": "sha512-TBOwqLxSxnx6+pH6GMri7R3JPH2AkuGJHfWZS0p1HsmN+Qr1T9b0IRJnnehSd/3NZAmAre4ft9Ljec7zjyKFJA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"@electron/get": "^2.0.0",
|
"@electron/get": "^2.0.0",
|
||||||
"@types/node": "^18.11.18",
|
"@types/node": "^20.9.0",
|
||||||
"extract-zip": "^2.0.1"
|
"extract-zip": "^2.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -2278,6 +2290,12 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
|
"undici-types": {
|
||||||
|
"version": "5.26.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
|
||||||
|
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"universalify": {
|
"universalify": {
|
||||||
"version": "0.1.2",
|
"version": "0.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz",
|
||||||
@@ -2291,10 +2309,9 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"ws": {
|
"ws": {
|
||||||
"version": "8.13.0",
|
"version": "8.18.0",
|
||||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz",
|
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz",
|
||||||
"integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==",
|
"integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw=="
|
||||||
"requires": {}
|
|
||||||
},
|
},
|
||||||
"xml2js": {
|
"xml2js": {
|
||||||
"version": "0.4.23",
|
"version": "0.4.23",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "feishin",
|
"name": "feishin",
|
||||||
"version": "0.7.0",
|
"version": "0.7.2",
|
||||||
"description": "",
|
"description": "",
|
||||||
"main": "./dist/main/main.js",
|
"main": "./dist/main/main.js",
|
||||||
"author": {
|
"author": {
|
||||||
@@ -15,10 +15,10 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cheerio": "^1.0.0-rc.12",
|
"cheerio": "^1.0.0-rc.12",
|
||||||
"mpris-service": "^2.1.2",
|
"mpris-service": "^2.1.2",
|
||||||
"ws": "^8.13.0"
|
"ws": "^8.18.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"electron": "25.8.4"
|
"electron": "31.1.0"
|
||||||
},
|
},
|
||||||
"license": "GPL-3.0"
|
"license": "GPL-3.0"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import nbNO from './locales/nb-NO.json';
|
|||||||
import nl from './locales/nl.json';
|
import nl from './locales/nl.json';
|
||||||
import zhHant from './locales/zh-Hant.json';
|
import zhHant from './locales/zh-Hant.json';
|
||||||
import fa from './locales/fa.json';
|
import fa from './locales/fa.json';
|
||||||
|
import ko from './locales/ko.json';
|
||||||
|
|
||||||
const resources = {
|
const resources = {
|
||||||
en: { translation: en },
|
en: { translation: en },
|
||||||
@@ -29,6 +30,7 @@ const resources = {
|
|||||||
fa: { translation: fa },
|
fa: { translation: fa },
|
||||||
fr: { translation: fr },
|
fr: { translation: fr },
|
||||||
ja: { translation: ja },
|
ja: { translation: ja },
|
||||||
|
ko: { translation: ko },
|
||||||
pl: { translation: pl },
|
pl: { translation: pl },
|
||||||
'zh-Hans': { translation: zhHans },
|
'zh-Hans': { translation: zhHans },
|
||||||
'zh-Hant': { translation: zhHant },
|
'zh-Hant': { translation: zhHant },
|
||||||
@@ -68,6 +70,10 @@ export const languages = [
|
|||||||
label: '日本語',
|
label: '日本語',
|
||||||
value: 'ja',
|
value: 'ja',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: '한국어',
|
||||||
|
value: 'ko',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: 'Nederlands',
|
label: 'Nederlands',
|
||||||
value: 'nl',
|
value: 'nl',
|
||||||
|
|||||||
@@ -28,7 +28,8 @@
|
|||||||
"shuffle_off": "náhodně zakázáno",
|
"shuffle_off": "náhodně zakázáno",
|
||||||
"addLast": "přidat poslední",
|
"addLast": "přidat poslední",
|
||||||
"mute": "ztlumit",
|
"mute": "ztlumit",
|
||||||
"skip_forward": "přeskočit dopředu"
|
"skip_forward": "přeskočit dopředu",
|
||||||
|
"playSimilarSongs": "přehrát podobné skladby"
|
||||||
},
|
},
|
||||||
"setting": {
|
"setting": {
|
||||||
"crossfadeStyle_description": "vyberte způsob prolnutí u přehrávače zvuku",
|
"crossfadeStyle_description": "vyberte způsob prolnutí u přehrávače zvuku",
|
||||||
@@ -210,7 +211,11 @@
|
|||||||
"clearCacheSuccess": "mezipaměť úspěšně vymazána",
|
"clearCacheSuccess": "mezipaměť úspěšně vymazána",
|
||||||
"externalLinks": "zobrazit externí odkazy",
|
"externalLinks": "zobrazit externí odkazy",
|
||||||
"startMinimized_description": "spustit aplikaci do systémové lišty",
|
"startMinimized_description": "spustit aplikaci do systémové lišty",
|
||||||
"passwordStore_description": "který způsob ukládání hesel / tajných klíčů použít. změňte tuto možnost, pokud máte problémy s ukládáním hesel."
|
"passwordStore_description": "který způsob ukládání hesel / tajných klíčů použít. změňte tuto možnost, pokud máte problémy s ukládáním hesel.",
|
||||||
|
"homeFeature": "carousel doporučení na domovské stránce",
|
||||||
|
"homeFeature_description": "ovládá, zda se má zobrazovat velký carousel s doporučenými alby na domovské stránce",
|
||||||
|
"imageAspectRatio": "použít nativní poměr stran obalů alb",
|
||||||
|
"imageAspectRatio_description": "pokud je povoleno, budou obaly alb zobrazeny s jejich nativním poměrem stran. u obalů, které nemají poměr 1:1, bude zbývající místo prázdné"
|
||||||
},
|
},
|
||||||
"action": {
|
"action": {
|
||||||
"editPlaylist": "upravit $t(entity.playlist_one)",
|
"editPlaylist": "upravit $t(entity.playlist_one)",
|
||||||
@@ -371,7 +376,8 @@
|
|||||||
"favorite": "$t(common.favorite)",
|
"favorite": "$t(common.favorite)",
|
||||||
"year": "$t(common.year)",
|
"year": "$t(common.year)",
|
||||||
"albumArtist": "$t(entity.albumArtist_one)",
|
"albumArtist": "$t(entity.albumArtist_one)",
|
||||||
"codec": "$t(common.codec)"
|
"codec": "$t(common.codec)",
|
||||||
|
"songCount": "$t(entity.track_other)"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"column": {
|
"column": {
|
||||||
@@ -534,7 +540,8 @@
|
|||||||
"numberSelected": "vybráno {{count}}",
|
"numberSelected": "vybráno {{count}}",
|
||||||
"removeFromQueue": "$t(action.removeFromQueue)",
|
"removeFromQueue": "$t(action.removeFromQueue)",
|
||||||
"showDetails": "získat informace",
|
"showDetails": "získat informace",
|
||||||
"shareItem": "sdílet položku"
|
"shareItem": "sdílet položku",
|
||||||
|
"playSimilarSongs": "$t(player.playSimilarSongs)"
|
||||||
},
|
},
|
||||||
"home": {
|
"home": {
|
||||||
"mostPlayed": "nejpřehrávanější",
|
"mostPlayed": "nejpřehrávanější",
|
||||||
@@ -563,7 +570,7 @@
|
|||||||
},
|
},
|
||||||
"trackList": {
|
"trackList": {
|
||||||
"title": "$t(entity.track_other)",
|
"title": "$t(entity.track_other)",
|
||||||
"artistTracks": "Skladby od umělce {{artist}}",
|
"artistTracks": "skladby od umělce {{artist}}",
|
||||||
"genreTracks": "$t(entity.track_other) s žánrem „{{genre}}“"
|
"genreTracks": "$t(entity.track_other) s žánrem „{{genre}}“"
|
||||||
},
|
},
|
||||||
"globalSearch": {
|
"globalSearch": {
|
||||||
@@ -579,7 +586,7 @@
|
|||||||
},
|
},
|
||||||
"albumList": {
|
"albumList": {
|
||||||
"title": "$t(entity.album_other)",
|
"title": "$t(entity.album_other)",
|
||||||
"artistAlbums": "Alba od umělce {{artist}}",
|
"artistAlbums": "alba od umělce {{artist}}",
|
||||||
"genreAlbums": "$t(entity.album_other) s žánrem „{{genre}}“"
|
"genreAlbums": "$t(entity.album_other) s žánrem „{{genre}}“"
|
||||||
},
|
},
|
||||||
"albumArtistDetail": {
|
"albumArtistDetail": {
|
||||||
@@ -588,7 +595,7 @@
|
|||||||
"about": "O umělci {{artist}}",
|
"about": "O umělci {{artist}}",
|
||||||
"appearsOn": "také v",
|
"appearsOn": "také v",
|
||||||
"topSongs": "nejlepší skladby",
|
"topSongs": "nejlepší skladby",
|
||||||
"topSongsFrom": "Nejlepší skladby od umělce {{title}}",
|
"topSongsFrom": "nejlepší skladby od umělce {{title}}",
|
||||||
"relatedArtists": "podobní $t(entity.artist_other)",
|
"relatedArtists": "podobní $t(entity.artist_other)",
|
||||||
"viewAllTracks": "zobrazit všechny $t(entity.track_other)",
|
"viewAllTracks": "zobrazit všechny $t(entity.track_other)",
|
||||||
"viewAll": "zobrazit vše"
|
"viewAll": "zobrazit vše"
|
||||||
|
|||||||
@@ -281,7 +281,7 @@
|
|||||||
"viewDiscography": "view discography",
|
"viewDiscography": "view discography",
|
||||||
"relatedArtists": "related $t(entity.artist_other)",
|
"relatedArtists": "related $t(entity.artist_other)",
|
||||||
"topSongs": "top songs",
|
"topSongs": "top songs",
|
||||||
"topSongsFrom": "Top songs from {{title}}",
|
"topSongsFrom": "top songs from {{title}}",
|
||||||
"viewAll": "view all",
|
"viewAll": "view all",
|
||||||
"viewAllTracks": "view all $t(entity.track_other)"
|
"viewAllTracks": "view all $t(entity.track_other)"
|
||||||
},
|
},
|
||||||
@@ -293,7 +293,7 @@
|
|||||||
"moreFromGeneric": "more from {{item}}"
|
"moreFromGeneric": "more from {{item}}"
|
||||||
},
|
},
|
||||||
"albumList": {
|
"albumList": {
|
||||||
"artistAlbums": "Albums by {{artist}}",
|
"artistAlbums": "albums by {{artist}}",
|
||||||
"genreAlbums": "\"{{genre}}\" $t(entity.album_other)",
|
"genreAlbums": "\"{{genre}}\" $t(entity.album_other)",
|
||||||
"title": "$t(entity.album_other)"
|
"title": "$t(entity.album_other)"
|
||||||
},
|
},
|
||||||
@@ -322,6 +322,7 @@
|
|||||||
"moveToTop": "$t(action.moveToTop)",
|
"moveToTop": "$t(action.moveToTop)",
|
||||||
"numberSelected": "{{count}} selected",
|
"numberSelected": "{{count}} selected",
|
||||||
"play": "$t(player.play)",
|
"play": "$t(player.play)",
|
||||||
|
"playSimilarSongs": "$t(player.playSimilarSongs)",
|
||||||
"removeFromFavorites": "$t(action.removeFromFavorites)",
|
"removeFromFavorites": "$t(action.removeFromFavorites)",
|
||||||
"removeFromPlaylist": "$t(action.removeFromPlaylist)",
|
"removeFromPlaylist": "$t(action.removeFromPlaylist)",
|
||||||
"removeFromQueue": "$t(action.removeFromQueue)",
|
"removeFromQueue": "$t(action.removeFromQueue)",
|
||||||
@@ -398,7 +399,7 @@
|
|||||||
"tracks": "$t(entity.track_other)"
|
"tracks": "$t(entity.track_other)"
|
||||||
},
|
},
|
||||||
"trackList": {
|
"trackList": {
|
||||||
"artistTracks": "Tracks by {{artist}}",
|
"artistTracks": "tracks by {{artist}}",
|
||||||
"genreTracks": "\"{{genre}}\" $t(entity.track_other)",
|
"genreTracks": "\"{{genre}}\" $t(entity.track_other)",
|
||||||
"title": "$t(entity.track_other)"
|
"title": "$t(entity.track_other)"
|
||||||
}
|
}
|
||||||
@@ -416,6 +417,7 @@
|
|||||||
"playbackFetchNoResults": "no songs found",
|
"playbackFetchNoResults": "no songs found",
|
||||||
"playbackSpeed": "playback speed",
|
"playbackSpeed": "playback speed",
|
||||||
"playRandom": "play random",
|
"playRandom": "play random",
|
||||||
|
"playSimilarSongs": "play similar songs",
|
||||||
"previous": "previous",
|
"previous": "previous",
|
||||||
"queue_clear": "clear queue",
|
"queue_clear": "clear queue",
|
||||||
"queue_moveToBottom": "move selected to top",
|
"queue_moveToBottom": "move selected to top",
|
||||||
@@ -496,6 +498,8 @@
|
|||||||
"globalMediaHotkeys_description": "enable or disable the usage of your system media hotkeys to control playback",
|
"globalMediaHotkeys_description": "enable or disable the usage of your system media hotkeys to control playback",
|
||||||
"homeConfiguration": "home page configuration",
|
"homeConfiguration": "home page configuration",
|
||||||
"homeConfiguration_description": "configure what items are shown, and in what order, on the home page",
|
"homeConfiguration_description": "configure what items are shown, and in what order, on the home page",
|
||||||
|
"homeFeature": "home featured carousel",
|
||||||
|
"homeFeature_description": "controls whether to show the large featured carousel on the home page",
|
||||||
"hotkey_browserBack": "browser back",
|
"hotkey_browserBack": "browser back",
|
||||||
"hotkey_browserForward": "browser forward",
|
"hotkey_browserForward": "browser forward",
|
||||||
"hotkey_favoriteCurrentSong": "favorite $t(common.currentSong)",
|
"hotkey_favoriteCurrentSong": "favorite $t(common.currentSong)",
|
||||||
@@ -529,6 +533,8 @@
|
|||||||
"hotkey_volumeUp": "volume up",
|
"hotkey_volumeUp": "volume up",
|
||||||
"hotkey_zoomIn": "zoom in",
|
"hotkey_zoomIn": "zoom in",
|
||||||
"hotkey_zoomOut": "zoom out",
|
"hotkey_zoomOut": "zoom out",
|
||||||
|
"imageAspectRatio": "use native cover art aspect ratio",
|
||||||
|
"imageAspectRatio_description": "if enabled, cover art will be shown using their native aspect ratio. for art that is not 1:1, the remaining space will be empty",
|
||||||
"language": "language",
|
"language": "language",
|
||||||
"language_description": "sets the language for the application ($t(common.restartRequired))",
|
"language_description": "sets the language for the application ($t(common.restartRequired))",
|
||||||
"lyricFetch": "fetch lyrics from the internet",
|
"lyricFetch": "fetch lyrics from the internet",
|
||||||
@@ -679,6 +685,7 @@
|
|||||||
"releaseDate": "release date",
|
"releaseDate": "release date",
|
||||||
"rowIndex": "row index",
|
"rowIndex": "row index",
|
||||||
"size": "$t(common.size)",
|
"size": "$t(common.size)",
|
||||||
|
"songCount": "$t(entity.track_other)",
|
||||||
"title": "$t(common.title)",
|
"title": "$t(common.title)",
|
||||||
"titleCombined": "$t(common.title) (combined)",
|
"titleCombined": "$t(common.title) (combined)",
|
||||||
"trackNumber": "track number",
|
"trackNumber": "track number",
|
||||||
|
|||||||
+19
-12
@@ -28,7 +28,8 @@
|
|||||||
"addLast": "añadir último",
|
"addLast": "añadir último",
|
||||||
"mute": "silencio",
|
"mute": "silencio",
|
||||||
"skip_forward": "saltar hacia delante",
|
"skip_forward": "saltar hacia delante",
|
||||||
"pause": "pausa"
|
"pause": "pausa",
|
||||||
|
"playSimilarSongs": "Reproducir canciones similares"
|
||||||
},
|
},
|
||||||
"setting": {
|
"setting": {
|
||||||
"crossfadeStyle_description": "selecciona el estilo de crossfade a usar por el reproductor de audio",
|
"crossfadeStyle_description": "selecciona el estilo de crossfade a usar por el reproductor de audio",
|
||||||
@@ -159,7 +160,7 @@
|
|||||||
"audioPlayer": "reproductor de audio",
|
"audioPlayer": "reproductor de audio",
|
||||||
"hotkey_zoomOut": "reducir",
|
"hotkey_zoomOut": "reducir",
|
||||||
"hotkey_unfavoriteCurrentSong": "$t(common.currentSong) no favorito",
|
"hotkey_unfavoriteCurrentSong": "$t(common.currentSong) no favorito",
|
||||||
"hotkey_rate0": "limpiar calificación",
|
"hotkey_rate0": "Limpiar calificación",
|
||||||
"discordApplicationId": "id de aplicación {{discord}}",
|
"discordApplicationId": "id de aplicación {{discord}}",
|
||||||
"applicationHotkeys_description": "configura las teclas de acceso rápido de la aplicación. marca la casilla para establecerlas como teclas de acceso rápido globales (solo escritorio)",
|
"applicationHotkeys_description": "configura las teclas de acceso rápido de la aplicación. marca la casilla para establecerlas como teclas de acceso rápido globales (solo escritorio)",
|
||||||
"floatingQueueArea_description": "muestra un icono flotante en el lado derecho de la pantalla para ver la cola de reproducción",
|
"floatingQueueArea_description": "muestra un icono flotante en el lado derecho de la pantalla para ver la cola de reproducción",
|
||||||
@@ -191,11 +192,11 @@
|
|||||||
"skipPlaylistPage": "saltar página de lista de reproducción",
|
"skipPlaylistPage": "saltar página de lista de reproducción",
|
||||||
"hotkey_browserForward": "avance",
|
"hotkey_browserForward": "avance",
|
||||||
"hotkey_browserBack": "retroceso",
|
"hotkey_browserBack": "retroceso",
|
||||||
"clearCache": "limpiar la caché del navegador",
|
"clearCache": "Limpiar la caché del navegador",
|
||||||
"clearQueryCache": "limpiar la caché de feishin",
|
"clearQueryCache": "Limpiar la caché de Feishin",
|
||||||
"clearQueryCache_description": "una 'limpieza suave' de feishin. esto refrescará las listas de reproducción, metadatos de pistas y restablece las letras guardadas. se mantienen los ajustes, credenciales del servidor y las imágenes en caché",
|
"clearQueryCache_description": "Una 'limpieza suave' de Feishin. Esto refrescará las listas de reproducción, metadatos de pistas y restablecerá las letras guardadas. Se mantienen los ajustes, credenciales del servidor y las imágenes en caché",
|
||||||
"buttonSize": "tamaño del botón de la barra de reproducción",
|
"buttonSize": "tamaño del botón de la barra de reproducción",
|
||||||
"clearCache_description": "una 'limpieza fuerte' de feishin. para limpiar la caché de feishin, vacía la caché del navegador (imágenes guardadas y otros elementos). se mantienen las credenciales y ajustes del servidor",
|
"clearCache_description": "Una 'limpieza fuerte' de Feishin. Para limpiar la caché de Feishin, vacía la caché del navegador (imágenes guardadas y otros elementos). Se mantienen las credenciales y ajustes del servidor",
|
||||||
"buttonSize_description": "el tamaño de los botones de la barra de reproducción",
|
"buttonSize_description": "el tamaño de los botones de la barra de reproducción",
|
||||||
"passwordStore_description": "qué método de almacenamiento de contraseñas/claves secretas utilizar. cambie esta opción si tiene problemas para guardar contraseñas.",
|
"passwordStore_description": "qué método de almacenamiento de contraseñas/claves secretas utilizar. cambie esta opción si tiene problemas para guardar contraseñas.",
|
||||||
"startMinimized_description": "iniciar la aplicación en la bandeja del sistema",
|
"startMinimized_description": "iniciar la aplicación en la bandeja del sistema",
|
||||||
@@ -210,7 +211,11 @@
|
|||||||
"genreBehavior_description": "Determina si al pulsar en un género se abre por defecto la lista de pistas o de álbumes",
|
"genreBehavior_description": "Determina si al pulsar en un género se abre por defecto la lista de pistas o de álbumes",
|
||||||
"homeConfiguration_description": "Configura qué elementos son mostrados y en qué orden en la página de inicio",
|
"homeConfiguration_description": "Configura qué elementos son mostrados y en qué orden en la página de inicio",
|
||||||
"clearCacheSuccess": "Caché limpiada correctamente",
|
"clearCacheSuccess": "Caché limpiada correctamente",
|
||||||
"externalLinks": "Mostrar enlaces externos"
|
"externalLinks": "Mostrar enlaces externos",
|
||||||
|
"homeFeature": "Carrusel destacado de inicio",
|
||||||
|
"homeFeature_description": "Controla si se muestra el gran carrusel destacado en la página de inicio",
|
||||||
|
"imageAspectRatio_description": "Si está habilitado, la portada será mostrada usando su relación de aspecto nativa. Para arte que no es 1:1, el espacio restante estará vacío",
|
||||||
|
"imageAspectRatio": "Usar relación de aspecto nativa de portada"
|
||||||
},
|
},
|
||||||
"action": {
|
"action": {
|
||||||
"editPlaylist": "editar $t(entity.playlist_one)",
|
"editPlaylist": "editar $t(entity.playlist_one)",
|
||||||
@@ -441,7 +446,8 @@
|
|||||||
"numberSelected": "{{count}} seleccionado",
|
"numberSelected": "{{count}} seleccionado",
|
||||||
"removeFromQueue": "$t(action.removeFromQueue)",
|
"removeFromQueue": "$t(action.removeFromQueue)",
|
||||||
"shareItem": "Compartir elemento",
|
"shareItem": "Compartir elemento",
|
||||||
"showDetails": "Obtener información"
|
"showDetails": "Obtener información",
|
||||||
|
"playSimilarSongs": "$t(player.playSimilarSongs)"
|
||||||
},
|
},
|
||||||
"home": {
|
"home": {
|
||||||
"mostPlayed": "más reproducidos",
|
"mostPlayed": "más reproducidos",
|
||||||
@@ -491,7 +497,7 @@
|
|||||||
"trackList": {
|
"trackList": {
|
||||||
"title": "$t(entity.track_other)",
|
"title": "$t(entity.track_other)",
|
||||||
"genreTracks": "\"{{genre}}\" $t(entity.track_other)",
|
"genreTracks": "\"{{genre}}\" $t(entity.track_other)",
|
||||||
"artistTracks": "Pistas de {{artist}}"
|
"artistTracks": "pistas por {{artist}}"
|
||||||
},
|
},
|
||||||
"globalSearch": {
|
"globalSearch": {
|
||||||
"commands": {
|
"commands": {
|
||||||
@@ -507,13 +513,13 @@
|
|||||||
"albumList": {
|
"albumList": {
|
||||||
"title": "$t(entity.album_other)",
|
"title": "$t(entity.album_other)",
|
||||||
"genreAlbums": "\"{{genre}}\" $t(entity.album_other)",
|
"genreAlbums": "\"{{genre}}\" $t(entity.album_other)",
|
||||||
"artistAlbums": "Álbumes de {{artist}}"
|
"artistAlbums": "álbumes de {{artist}}"
|
||||||
},
|
},
|
||||||
"albumArtistDetail": {
|
"albumArtistDetail": {
|
||||||
"viewAllTracks": "ver todo de $t(entity.track_other)",
|
"viewAllTracks": "ver todo de $t(entity.track_other)",
|
||||||
"relatedArtists": "similar a $t(entity.artist_other)",
|
"relatedArtists": "similar a $t(entity.artist_other)",
|
||||||
"topSongs": "mejores canciones",
|
"topSongs": "mejores canciones",
|
||||||
"topSongsFrom": "Las mejores canciones de {{title}}",
|
"topSongsFrom": "las mejores canciones de {{title}}",
|
||||||
"viewAll": "Ver todo",
|
"viewAll": "Ver todo",
|
||||||
"recentReleases": "Lanzamientos recientes",
|
"recentReleases": "Lanzamientos recientes",
|
||||||
"viewDiscography": "Ver discografía",
|
"viewDiscography": "Ver discografía",
|
||||||
@@ -639,7 +645,8 @@
|
|||||||
"genre": "$t(entity.genre_one)",
|
"genre": "$t(entity.genre_one)",
|
||||||
"favorite": "$t(common.favorite)",
|
"favorite": "$t(common.favorite)",
|
||||||
"year": "$t(common.year)",
|
"year": "$t(common.year)",
|
||||||
"codec": "$t(common.codec)"
|
"codec": "$t(common.codec)",
|
||||||
|
"songCount": "$t(entity.track_other)"
|
||||||
},
|
},
|
||||||
"general": {
|
"general": {
|
||||||
"gap": "$t(common.gap)",
|
"gap": "$t(common.gap)",
|
||||||
|
|||||||
@@ -0,0 +1,144 @@
|
|||||||
|
{
|
||||||
|
"common": {
|
||||||
|
"size": "koko",
|
||||||
|
"search": "etsi",
|
||||||
|
"sortOrder": "järjestys",
|
||||||
|
"setting": "asetus",
|
||||||
|
"title": "otsikko",
|
||||||
|
"trackNumber": "raita",
|
||||||
|
"action_one": "toiminto",
|
||||||
|
"action_other": "toiminnot",
|
||||||
|
"add": "lisää",
|
||||||
|
"areYouSure": "oletko varma?",
|
||||||
|
"ascending": "nouseva",
|
||||||
|
"backward": "takaperin",
|
||||||
|
"bitrate": "bittinopeus",
|
||||||
|
"channel_one": "kanava",
|
||||||
|
"channel_other": "kanavat",
|
||||||
|
"collapse": "luhista",
|
||||||
|
"comingSoon": "tulossa pian…",
|
||||||
|
"configure": "konfiguroi",
|
||||||
|
"confirm": "hyväksy",
|
||||||
|
"disable": "poista käytöstä",
|
||||||
|
"disc": "levy",
|
||||||
|
"dismiss": "hylkää",
|
||||||
|
"favorite": "suosikki",
|
||||||
|
"filter_one": "suodatin",
|
||||||
|
"filter_other": "suodatinta",
|
||||||
|
"filters": "suodattimet",
|
||||||
|
"forceRestartRequired": "käynnistä uudelleen ottaaksesi muutokset käyttöön… sulje ilmoitus käynnistääksesi uudelleen",
|
||||||
|
"gap": "väli",
|
||||||
|
"home": "koti",
|
||||||
|
"left": "vasen",
|
||||||
|
"limit": "raja",
|
||||||
|
"manage": "hallitse",
|
||||||
|
"menu": "valikko",
|
||||||
|
"minimize": "minimoi",
|
||||||
|
"modified": "muokattu",
|
||||||
|
"name": "nimi",
|
||||||
|
"no": "ei",
|
||||||
|
"none": "ei mitään",
|
||||||
|
"noResultsFromQuery": "kysely palautti ei tuloksia",
|
||||||
|
"note": "huomautus",
|
||||||
|
"ok": "ok",
|
||||||
|
"owner": "omistaja",
|
||||||
|
"path": "reitti",
|
||||||
|
"preview": "esikatsele",
|
||||||
|
"previousSong": "edellinen $t(entity.track_one)",
|
||||||
|
"resetToDefault": "palauta oletusarvoihin",
|
||||||
|
"restartRequired": "uudelleen käynnistys vaaditaan",
|
||||||
|
"right": "oikea",
|
||||||
|
"save": "tallenna",
|
||||||
|
"saveAndReplace": "tallenna ja korvaa",
|
||||||
|
"saveAs": "tallenna nimellä",
|
||||||
|
"unknown": "tuntematon",
|
||||||
|
"version": "versio",
|
||||||
|
"year": "vuosi",
|
||||||
|
"yes": "kyllä",
|
||||||
|
"close": "sulje",
|
||||||
|
"descending": "laskeva",
|
||||||
|
"biography": "elämänkerta",
|
||||||
|
"cancel": "peruuta",
|
||||||
|
"bpm": "bpm",
|
||||||
|
"decrease": "pienennä",
|
||||||
|
"center": "keskitä",
|
||||||
|
"clear": "tyhjennä",
|
||||||
|
"codec": "koodekki",
|
||||||
|
"create": "luo",
|
||||||
|
"description": "kuvaus",
|
||||||
|
"currentSong": "nykyinen $t(entity.track_one)",
|
||||||
|
"delete": "poista",
|
||||||
|
"duration": "kesto",
|
||||||
|
"edit": "muokkaa",
|
||||||
|
"enable": "ota käyttöön",
|
||||||
|
"expand": "laajenna",
|
||||||
|
"increase": "lisää",
|
||||||
|
"forward": "eteenpäin",
|
||||||
|
"maximize": "maksimoi",
|
||||||
|
"mbid": "MusicBrainz ID",
|
||||||
|
"share": "jaa",
|
||||||
|
"random": "satunnainen",
|
||||||
|
"reload": "lataa uudelleen",
|
||||||
|
"quit": "poistu",
|
||||||
|
"rating": "arvostelu",
|
||||||
|
"refresh": "virkistä",
|
||||||
|
"reset": "nollaa",
|
||||||
|
"playerMustBePaused": "soitin täytyy olla pysäytetty"
|
||||||
|
},
|
||||||
|
"entity": {
|
||||||
|
"album_one": "albumi",
|
||||||
|
"album_other": "albumia",
|
||||||
|
"albumArtist_one": "albumi artisti",
|
||||||
|
"albumArtist_other": "albumi artistia",
|
||||||
|
"artistWithCount_one": "{{count}} artisti",
|
||||||
|
"artistWithCount_other": "{{count}} artistia",
|
||||||
|
"playlist_one": "soittolista",
|
||||||
|
"playlist_other": "soittolistaa",
|
||||||
|
"playlistWithCount_one": "{{count}} soittolista",
|
||||||
|
"playlistWithCount_other": "{{count}} soittolistaa",
|
||||||
|
"albumArtistCount_one": "{{count}} albumi artisti",
|
||||||
|
"albumArtistCount_other": "{{count}} albumi artistia",
|
||||||
|
"albumWithCount_one": "{{count}} albumi",
|
||||||
|
"albumWithCount_other": "{{count}} albumia",
|
||||||
|
"artist_one": "artisti",
|
||||||
|
"artist_other": "artistia",
|
||||||
|
"favorite_one": "suosikki",
|
||||||
|
"favorite_other": "suosikkia",
|
||||||
|
"folder_one": "kansio",
|
||||||
|
"folder_other": "kansiota",
|
||||||
|
"folderWithCount_one": "{{count}} kansio",
|
||||||
|
"folderWithCount_other": "{{count}} kansiota",
|
||||||
|
"genre_one": "genre",
|
||||||
|
"genre_other": "genreä",
|
||||||
|
"genreWithCount_one": "{{count}} genre",
|
||||||
|
"genreWithCount_other": "{{count}} genreä",
|
||||||
|
"smartPlaylist": "älykäs $t(entity.playlist_one)",
|
||||||
|
"track_one": "raita",
|
||||||
|
"track_other": "raitaa",
|
||||||
|
"trackWithCount_one": "{{count}} raita",
|
||||||
|
"trackWithCount_other": "{{count}} raitaa"
|
||||||
|
},
|
||||||
|
"action": {
|
||||||
|
"clearQueue": "tyhjennä jono",
|
||||||
|
"createPlaylist": "luo $t(entity.playlist_one)",
|
||||||
|
"deselectAll": "poista valinta kaikista",
|
||||||
|
"editPlaylist": "muokkaa $t(entity.playlist_one)",
|
||||||
|
"removeFromQueue": "poista jonosta",
|
||||||
|
"viewPlaylists": "katsele $t(entity.playlist_other)",
|
||||||
|
"openIn": {
|
||||||
|
"lastfm": "Avaa Last.fm:ssä",
|
||||||
|
"musicbrainz": "Avaa MusicBrainz:ssä"
|
||||||
|
},
|
||||||
|
"goToPage": "mene sivulle",
|
||||||
|
"moveToBottom": "siirry pohjalle",
|
||||||
|
"moveToTop": "siirry ylös",
|
||||||
|
"addToFavorites": "lisää $t(entity.favorite_other)",
|
||||||
|
"addToPlaylist": "lisää $t(entity.playlist_one)",
|
||||||
|
"refresh": "$t(common.refresh)",
|
||||||
|
"removeFromFavorites": "poista kohteesta $t(entity.favorite_other)",
|
||||||
|
"toggleSmartPlaylistEditor": "kytke $t(entity.smartPlaylist) editori",
|
||||||
|
"deletePlaylist": "poista $t(entity.playlist_one)",
|
||||||
|
"removeFromPlaylist": "poista kohteesta $t(entity.playlist_one)",
|
||||||
|
"setRating": "aseta arvostelu"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"action": {
|
||||||
|
"createPlaylist": "$t(entity.playlist_one) 생성",
|
||||||
|
"addToFavorites": "$t(entity.favorite_other)에 추가",
|
||||||
|
"addToPlaylist": "$t(entity.playlist_one)에 추가",
|
||||||
|
"clearQueue": "대기열 지우기",
|
||||||
|
"deletePlaylist": "$t(entity.playlist_one) 삭제",
|
||||||
|
"deselectAll": "모두 선택 해제",
|
||||||
|
"editPlaylist": "$t(entity.playlist_one) 편집",
|
||||||
|
"goToPage": "페이지 이동",
|
||||||
|
"moveToBottom": "맨 아래로 이동",
|
||||||
|
"moveToTop": "맨 위로 이동"
|
||||||
|
}
|
||||||
|
}
|
||||||
+87
-17
@@ -16,7 +16,11 @@
|
|||||||
"createPlaylist": "utwórz $t(entity.playlist_one)",
|
"createPlaylist": "utwórz $t(entity.playlist_one)",
|
||||||
"deletePlaylist": "usuń $t(entity.playlist_one)",
|
"deletePlaylist": "usuń $t(entity.playlist_one)",
|
||||||
"moveToBottom": "przesuń na dół",
|
"moveToBottom": "przesuń na dół",
|
||||||
"setRating": "oceń"
|
"setRating": "oceń",
|
||||||
|
"openIn": {
|
||||||
|
"lastfm": "Otwórz w Last.fm",
|
||||||
|
"musicbrainz": "Otwórz w MusicBrainz"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"common": {
|
"common": {
|
||||||
"increase": "zwiększ",
|
"increase": "zwiększ",
|
||||||
@@ -99,7 +103,17 @@
|
|||||||
"decrease": "obniż",
|
"decrease": "obniż",
|
||||||
"path": "ścieżka",
|
"path": "ścieżka",
|
||||||
"center": "środkowy",
|
"center": "środkowy",
|
||||||
"note": "notatka"
|
"note": "notatka",
|
||||||
|
"albumPeak": "spadek albumu",
|
||||||
|
"albumGain": "wzrost albumu",
|
||||||
|
"mbid": "ID MusicBrainz",
|
||||||
|
"reload": "przeładuj",
|
||||||
|
"share": "udostępnij",
|
||||||
|
"trackGain": "gain utworu",
|
||||||
|
"trackPeak": "peak utworu",
|
||||||
|
"codec": "kodek",
|
||||||
|
"preview": "podgląd",
|
||||||
|
"close": "zamknij"
|
||||||
},
|
},
|
||||||
"entity": {
|
"entity": {
|
||||||
"genre_one": "gatunek",
|
"genre_one": "gatunek",
|
||||||
@@ -168,7 +182,10 @@
|
|||||||
"mpvRequired": "wymagane MPV",
|
"mpvRequired": "wymagane MPV",
|
||||||
"audioDeviceFetchError": "wystąpił błąd podczas próby znalezienia urządzeń dźwiękowych",
|
"audioDeviceFetchError": "wystąpił błąd podczas próby znalezienia urządzeń dźwiękowych",
|
||||||
"invalidServer": "nieprawidłowy serwer",
|
"invalidServer": "nieprawidłowy serwer",
|
||||||
"loginRateError": "zbyt dużo prób logowania, poczekaj chwilę i spróbuj ponownie"
|
"loginRateError": "zbyt dużo prób logowania, poczekaj chwilę i spróbuj ponownie",
|
||||||
|
"badAlbum": "ta strona jest wyświetlana, ponieważ ten utwór nie jest częścią albumu. najprawdopodobniej ten problem występuje, jeśli utwór znajduje się w nadrzędnym folderze plików z muzyką. jellyfin grupuje utwory tylko wtedy, gdy znajdują się one w folderze.",
|
||||||
|
"networkError": "wystąpił błąd sieciowy",
|
||||||
|
"openError": "nie można otworzyć pliku"
|
||||||
},
|
},
|
||||||
"filter": {
|
"filter": {
|
||||||
"mostPlayed": "najczęściej odtwarzane",
|
"mostPlayed": "najczęściej odtwarzane",
|
||||||
@@ -262,6 +279,14 @@
|
|||||||
},
|
},
|
||||||
"editPlaylist": {
|
"editPlaylist": {
|
||||||
"title": "edytuj $t(entity.playlist_one)"
|
"title": "edytuj $t(entity.playlist_one)"
|
||||||
|
},
|
||||||
|
"shareItem": {
|
||||||
|
"allowDownloading": "zezwól na pobieranie",
|
||||||
|
"description": "opis",
|
||||||
|
"setExpiration": "ustaw czas wygaśnięcia",
|
||||||
|
"success": "link do udostępniania skopiowany do schowka (lub kliknij tutaj, aby otworzyć)",
|
||||||
|
"createFailed": "nie udało się utworzyć linku do udostępniania (czy udostępnianie jest włączone?)",
|
||||||
|
"expireInvalid": "ustawiony czas wygaśnięcia musi być w przyszłości"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"page": {
|
"page": {
|
||||||
@@ -277,7 +302,9 @@
|
|||||||
"unsynchronized": "niezsynchronizowane",
|
"unsynchronized": "niezsynchronizowane",
|
||||||
"lyricAlignment": "wyrównaj tekst",
|
"lyricAlignment": "wyrównaj tekst",
|
||||||
"useImageAspectRatio": "użyj współczynnika proporcji obrazu",
|
"useImageAspectRatio": "użyj współczynnika proporcji obrazu",
|
||||||
"lyricGap": "odstępy tekstu"
|
"lyricGap": "odstępy tekstu",
|
||||||
|
"dynamicImageBlur": "rozmiar rozmycia obrazu",
|
||||||
|
"dynamicIsImage": "włącz obraz w tle"
|
||||||
},
|
},
|
||||||
"upNext": "następny",
|
"upNext": "następny",
|
||||||
"lyrics": "tekst",
|
"lyrics": "tekst",
|
||||||
@@ -311,7 +338,9 @@
|
|||||||
"addFavorite": "$t(action.addToFavorites)",
|
"addFavorite": "$t(action.addToFavorites)",
|
||||||
"play": "$t(player.play)",
|
"play": "$t(player.play)",
|
||||||
"numberSelected": "zaznaczono {{count}}",
|
"numberSelected": "zaznaczono {{count}}",
|
||||||
"removeFromQueue": "$t(action.removeFromQueue)"
|
"removeFromQueue": "$t(action.removeFromQueue)",
|
||||||
|
"shareItem": "udostępnij pozycję",
|
||||||
|
"showDetails": "zobacz informacje"
|
||||||
},
|
},
|
||||||
"albumDetail": {
|
"albumDetail": {
|
||||||
"moreFromArtist": "więcej od $t(entity.artist_one)",
|
"moreFromArtist": "więcej od $t(entity.artist_one)",
|
||||||
@@ -321,10 +350,14 @@
|
|||||||
"title": "$t(entity.albumArtist_other)"
|
"title": "$t(entity.albumArtist_other)"
|
||||||
},
|
},
|
||||||
"genreList": {
|
"genreList": {
|
||||||
"title": "$t(entity.genre_other)"
|
"title": "$t(entity.genre_other)",
|
||||||
|
"showAlbums": "pokaż $t(entity.genre_one) $t(entity.album_other)",
|
||||||
|
"showTracks": "pokaż $t(entity.genre_one) $t(entity.track_other)"
|
||||||
},
|
},
|
||||||
"albumList": {
|
"albumList": {
|
||||||
"title": "$t(entity.album_other)"
|
"title": "$t(entity.album_other)",
|
||||||
|
"artistAlbums": "albumy artysty {{artist}}",
|
||||||
|
"genreAlbums": "\"{{genre}}\" $t(entity.album_other)"
|
||||||
},
|
},
|
||||||
"sidebar": {
|
"sidebar": {
|
||||||
"nowPlaying": "teraz odtwarzane",
|
"nowPlaying": "teraz odtwarzane",
|
||||||
@@ -337,7 +370,8 @@
|
|||||||
"settings": "$t(common.setting_other)",
|
"settings": "$t(common.setting_other)",
|
||||||
"home": "$t(common.home)",
|
"home": "$t(common.home)",
|
||||||
"artists": "$t(entity.artist_other)",
|
"artists": "$t(entity.artist_other)",
|
||||||
"albumArtists": "$t(entity.albumArtist_other)"
|
"albumArtists": "$t(entity.albumArtist_other)",
|
||||||
|
"shared": "udostępnione $t(entity.playlist_other)"
|
||||||
},
|
},
|
||||||
"home": {
|
"home": {
|
||||||
"mostPlayed": "najczęściej odtwarzane",
|
"mostPlayed": "najczęściej odtwarzane",
|
||||||
@@ -353,7 +387,9 @@
|
|||||||
"windowTab": "okno"
|
"windowTab": "okno"
|
||||||
},
|
},
|
||||||
"trackList": {
|
"trackList": {
|
||||||
"title": "$t(entity.track_other)"
|
"title": "$t(entity.track_other)",
|
||||||
|
"artistTracks": "utwory przez {{artist}}",
|
||||||
|
"genreTracks": "\"{{genre}}\" $t(entity.track_other)"
|
||||||
},
|
},
|
||||||
"globalSearch": {
|
"globalSearch": {
|
||||||
"commands": {
|
"commands": {
|
||||||
@@ -365,6 +401,22 @@
|
|||||||
},
|
},
|
||||||
"playlistList": {
|
"playlistList": {
|
||||||
"title": "$t(entity.playlist_other)"
|
"title": "$t(entity.playlist_other)"
|
||||||
|
},
|
||||||
|
"albumArtistDetail": {
|
||||||
|
"topSongs": "popularne utwory",
|
||||||
|
"topSongsFrom": "popularne utwory z {{title}}",
|
||||||
|
"about": "O {{artist}}",
|
||||||
|
"recentReleases": "ostatnie wydania",
|
||||||
|
"viewAll": "zobacz wszystko",
|
||||||
|
"viewDiscography": "przeglądaj dyskografię",
|
||||||
|
"relatedArtists": "powiązane z $t(entity.artist_other)",
|
||||||
|
"appearsOn": "pojawia się na",
|
||||||
|
"viewAllTracks": "zobacz wszystko $t(entity.track_other)"
|
||||||
|
},
|
||||||
|
"itemDetail": {
|
||||||
|
"copyPath": "kopiuj ścieżkę do schowka",
|
||||||
|
"copiedPath": "ścieżka została skopiowana pomyślnie",
|
||||||
|
"openFile": "pokaż utwór w menedżerze plików"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"player": {
|
"player": {
|
||||||
@@ -413,7 +465,7 @@
|
|||||||
"hotkey_zoomIn": "przybliż",
|
"hotkey_zoomIn": "przybliż",
|
||||||
"hotkey_browserForward": "przeglądarka w przód",
|
"hotkey_browserForward": "przeglądarka w przód",
|
||||||
"audioExclusiveMode_description": "włącz wyłączny tryb wyjścia. W tym trybie, system zwykle jest zablokowany i może odtwarzać tylko poprzez mpv",
|
"audioExclusiveMode_description": "włącz wyłączny tryb wyjścia. W tym trybie, system zwykle jest zablokowany i może odtwarzać tylko poprzez mpv",
|
||||||
"discordUpdateInterval": "{{discord}} interwał aktualizacji obszernej obecności",
|
"discordUpdateInterval": "{{discord}} interwał aktualizacji rich presence",
|
||||||
"fontType_optionBuiltIn": "wbudowana czcionka",
|
"fontType_optionBuiltIn": "wbudowana czcionka",
|
||||||
"hotkey_playbackPlayPause": "odtwarzaj / wstrzymaj",
|
"hotkey_playbackPlayPause": "odtwarzaj / wstrzymaj",
|
||||||
"hotkey_rate1": "oceń na 1 gwiazdkę",
|
"hotkey_rate1": "oceń na 1 gwiazdkę",
|
||||||
@@ -449,7 +501,7 @@
|
|||||||
"crossfadeDuration_description": "ustaw czas trwania efektu przenikania",
|
"crossfadeDuration_description": "ustaw czas trwania efektu przenikania",
|
||||||
"language": "język",
|
"language": "język",
|
||||||
"hotkey_toggleShuffle": "przełącz kolejność losową",
|
"hotkey_toggleShuffle": "przełącz kolejność losową",
|
||||||
"discordRichPresence_description": "włącz status odtwarzania w {{discord}} obszernie obecny. Klucze obrazów to {{icon}}, {{playing}} i {{paused}}. ",
|
"discordRichPresence_description": "włącz status odtwarzania w {{discord}} rich presence. Dzięki temu będą wyświetlane informacje takie jak: {{icon}}, {{playing}} i {{paused}}. ",
|
||||||
"audioDevice": "urządzenia dźwiękowe",
|
"audioDevice": "urządzenia dźwiękowe",
|
||||||
"hotkey_rate2": "oceń na 2 gwiazdki",
|
"hotkey_rate2": "oceń na 2 gwiazdki",
|
||||||
"exitToTray": "zamknij do zasobnika",
|
"exitToTray": "zamknij do zasobnika",
|
||||||
@@ -475,7 +527,7 @@
|
|||||||
"hotkey_zoomOut": "oddal",
|
"hotkey_zoomOut": "oddal",
|
||||||
"hotkey_unfavoriteCurrentSong": "usuń $t(common.currentSong) z ulubionych",
|
"hotkey_unfavoriteCurrentSong": "usuń $t(common.currentSong) z ulubionych",
|
||||||
"hotkey_rate0": "wyczyść oceny",
|
"hotkey_rate0": "wyczyść oceny",
|
||||||
"discordApplicationId": "id aplikacji {{discord}}",
|
"discordApplicationId": "ID aplikacji {{discord}}",
|
||||||
"applicationHotkeys_description": "ustaw skróty klawiszowe aplikacji. przełącz pole wyboru aby ustawić skrót globalny (tylko komputery)",
|
"applicationHotkeys_description": "ustaw skróty klawiszowe aplikacji. przełącz pole wyboru aby ustawić skrót globalny (tylko komputery)",
|
||||||
"floatingQueueArea_description": "wyświetl ikonę najechania kursorem po prawej stronie ekranu, aby wyświetlić kolejkę odtwarzania",
|
"floatingQueueArea_description": "wyświetl ikonę najechania kursorem po prawej stronie ekranu, aby wyświetlić kolejkę odtwarzania",
|
||||||
"hotkey_volumeMute": "wycisz",
|
"hotkey_volumeMute": "wycisz",
|
||||||
@@ -521,7 +573,7 @@
|
|||||||
"sampleRate_description": "wybierz wyjściową częstotliwość próbkowania, która ma być używana, jeśli wybrana częstotliwość próbkowania różni się od częstotliwości bieżącego utworu. wartość mniejsza niż 8000 spowoduje użycie częstotliwości domyślnej",
|
"sampleRate_description": "wybierz wyjściową częstotliwość próbkowania, która ma być używana, jeśli wybrana częstotliwość próbkowania różni się od częstotliwości bieżącego utworu. wartość mniejsza niż 8000 spowoduje użycie częstotliwości domyślnej",
|
||||||
"replayGainMode_optionNone": "$t(common.none)",
|
"replayGainMode_optionNone": "$t(common.none)",
|
||||||
"replayGainClipping": "wzmocnienie {{ReplayGain}}",
|
"replayGainClipping": "wzmocnienie {{ReplayGain}}",
|
||||||
"scrobble_description": "odtwarzanie scrobble na serwerze multimediów",
|
"scrobble_description": "przekazywanie informacji o odtwarzaniu (scrobbling) do twojego serwera multimediów",
|
||||||
"sidePlayQueueStyle": "boczny styl kolejki odtwarzania",
|
"sidePlayQueueStyle": "boczny styl kolejki odtwarzania",
|
||||||
"remoteUsername_description": "ustaw nazwę użytkownika dla serwera zdalnej kontroli. Jeśli nazwa użytkownika i hasło są puste, autoryzacja będzie wyłączona",
|
"remoteUsername_description": "ustaw nazwę użytkownika dla serwera zdalnej kontroli. Jeśli nazwa użytkownika i hasło są puste, autoryzacja będzie wyłączona",
|
||||||
"replayGainMode_optionAlbum": "$t(entity.album_one)",
|
"replayGainMode_optionAlbum": "$t(entity.album_one)",
|
||||||
@@ -564,7 +616,21 @@
|
|||||||
"clearCache_description": "\"twarde wyczyszczenie\" feishin. oprócz wyczyszczenia pamięci podręcznej feishin, opróżnij pamięć podręczną przeglądarki (zapisane obrazy i inne zasoby). dane i ustawienia serwera zostaną zachowane",
|
"clearCache_description": "\"twarde wyczyszczenie\" feishin. oprócz wyczyszczenia pamięci podręcznej feishin, opróżnij pamięć podręczną przeglądarki (zapisane obrazy i inne zasoby). dane i ustawienia serwera zostaną zachowane",
|
||||||
"clearQueryCache_description": "\"miękkie wyczyszczenie\" feishin. spowoduje to odświeżenie list odtwarzania, metadanych utworów i zresetowanie zapisanych tekstów. ustawienia, dane uwierzytelniające serwera i obrazy w pamięci podręcznej zostaną zachowane",
|
"clearQueryCache_description": "\"miękkie wyczyszczenie\" feishin. spowoduje to odświeżenie list odtwarzania, metadanych utworów i zresetowanie zapisanych tekstów. ustawienia, dane uwierzytelniające serwera i obrazy w pamięci podręcznej zostaną zachowane",
|
||||||
"buttonSize_description": "rozmiar przycisków paska odtwarzacza",
|
"buttonSize_description": "rozmiar przycisków paska odtwarzacza",
|
||||||
"clearCache": "wyczyść pamięć podręczną przeglądarki"
|
"clearCache": "wyczyść pamięć podręczną przeglądarki",
|
||||||
|
"playerAlbumArtResolution": "rozdzielczość okładki albumu odtwarzacza",
|
||||||
|
"externalLinks": "pokaż zewnętrzne linki",
|
||||||
|
"genreBehavior_description": "określa, czy kliknięcie gatunku domyślnie otwiera listę utworów czy albumów",
|
||||||
|
"mpvExtraParameters_help": "po jednym na linię",
|
||||||
|
"passwordStore": "hasła",
|
||||||
|
"passwordStore_description": "jakie hasło ma być używane. zmień to, jeśli masz problemy z przechowywaniem haseł.",
|
||||||
|
"playerAlbumArtResolution_description": "rozdzielczość podglądu okładki albumu w dużym odtwarzaczu. większa sprawia, że wygląda bardziej wyraziście, ale może spowolnić ładowanie. domyślnie 0, czyli auto",
|
||||||
|
"startMinimized": "uruchom zminimalizowany",
|
||||||
|
"startMinimized_description": "uruchom aplikację w zasobniku systemowym",
|
||||||
|
"clearCacheSuccess": "pamięć podręczna została wyczyszczona pomyślnie",
|
||||||
|
"genreBehavior": "domyślne zachowanie strony gatunek",
|
||||||
|
"externalLinks_description": "umożliwia wyświetlanie linków zewnętrznych (Last.fm, MusicBrainz) na stronach artystów/albumów",
|
||||||
|
"homeConfiguration": "konfiguracja strony głównej",
|
||||||
|
"homeConfiguration_description": "konfiguracja elementów wyświetlanych na stronie głównej i ich kolejności"
|
||||||
},
|
},
|
||||||
"table": {
|
"table": {
|
||||||
"config": {
|
"config": {
|
||||||
@@ -578,7 +644,9 @@
|
|||||||
"gap": "$t(common.gap)",
|
"gap": "$t(common.gap)",
|
||||||
"tableColumns": "kolumny tabeli",
|
"tableColumns": "kolumny tabeli",
|
||||||
"autoFitColumns": "automatyczne dopasowanie kolumn",
|
"autoFitColumns": "automatyczne dopasowanie kolumn",
|
||||||
"size": "$t(common.size)"
|
"size": "$t(common.size)",
|
||||||
|
"itemSize": "rozmiar elementu (px)",
|
||||||
|
"itemGap": "odstęp między elementami (px)"
|
||||||
},
|
},
|
||||||
"label": {
|
"label": {
|
||||||
"releaseDate": "data premiery",
|
"releaseDate": "data premiery",
|
||||||
@@ -606,7 +674,8 @@
|
|||||||
"discNumber": "numer płyty",
|
"discNumber": "numer płyty",
|
||||||
"favorite": "$t(common.favorite)",
|
"favorite": "$t(common.favorite)",
|
||||||
"year": "$t(common.year)",
|
"year": "$t(common.year)",
|
||||||
"albumArtist": "$t(entity.albumArtist_one)"
|
"albumArtist": "$t(entity.albumArtist_one)",
|
||||||
|
"codec": "$t(common.codec)"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"column": {
|
"column": {
|
||||||
@@ -632,7 +701,8 @@
|
|||||||
"path": "ścieżka",
|
"path": "ścieżka",
|
||||||
"discNumber": "płyta",
|
"discNumber": "płyta",
|
||||||
"channels": "$t(common.channel_other)",
|
"channels": "$t(common.channel_other)",
|
||||||
"size": "$t(common.size)"
|
"size": "$t(common.size)",
|
||||||
|
"codec": "$t(common.codec)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -212,7 +212,23 @@
|
|||||||
"songCount": "contador de músicas",
|
"songCount": "contador de músicas",
|
||||||
"toYear": "até o ano",
|
"toYear": "até o ano",
|
||||||
"random": "aleatório",
|
"random": "aleatório",
|
||||||
"search": "buscar"
|
"search": "buscar",
|
||||||
|
"lastPlayed": "última tocada",
|
||||||
|
"isCompilation": "é compilação",
|
||||||
|
"trackNumber": "faixa",
|
||||||
|
"communityRating": "Nota da comunidade",
|
||||||
|
"isPublic": "é público",
|
||||||
|
"playCount": "contador de execuções",
|
||||||
|
"recentlyUpdated": "atualizado recentemente",
|
||||||
|
"dateAdded": "data de adição",
|
||||||
|
"isRecentlyPlayed": "foi tocado recentemente",
|
||||||
|
"albumArtist": "$t(entity.albumArtist_one)",
|
||||||
|
"recentlyAdded": "adicionado recentemente",
|
||||||
|
"releaseDate": "data de lançamento",
|
||||||
|
"recentlyPlayed": "tocado recentemente",
|
||||||
|
"criticRating": "Nota da crítica",
|
||||||
|
"isFavorited": "é favoritado",
|
||||||
|
"releaseYear": "ano de lançamento"
|
||||||
},
|
},
|
||||||
"player": {
|
"player": {
|
||||||
"playbackFetchNoResults": "nenhuma música encontrada",
|
"playbackFetchNoResults": "nenhuma música encontrada",
|
||||||
@@ -257,7 +273,13 @@
|
|||||||
"folderWithCount_other": "{{count}} pastas",
|
"folderWithCount_other": "{{count}} pastas",
|
||||||
"genreWithCount_one": "{{count}} gênero",
|
"genreWithCount_one": "{{count}} gênero",
|
||||||
"genreWithCount_many": "{{count}} gêneros",
|
"genreWithCount_many": "{{count}} gêneros",
|
||||||
"genreWithCount_other": "{{count}} gêneros"
|
"genreWithCount_other": "{{count}} gêneros",
|
||||||
|
"trackWithCount_one": "faixa",
|
||||||
|
"trackWithCount_many": "faixas",
|
||||||
|
"trackWithCount_other": "faixas",
|
||||||
|
"track_one": "faixa",
|
||||||
|
"track_many": "faixas",
|
||||||
|
"track_other": "faixas"
|
||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
"remotePortWarning": "reinicie o servidor para aplicar a nova porta",
|
"remotePortWarning": "reinicie o servidor para aplicar a nova porta",
|
||||||
|
|||||||
+269
-60
@@ -15,14 +15,18 @@
|
|||||||
"deselectAll": "снять выделение",
|
"deselectAll": "снять выделение",
|
||||||
"moveToBottom": "вниз",
|
"moveToBottom": "вниз",
|
||||||
"setRating": "оценить",
|
"setRating": "оценить",
|
||||||
"toggleSmartPlaylistEditor": "вкл/выкл $t(entity.smartPlaylist) редактор",
|
"toggleSmartPlaylistEditor": "вкл./откл. редактор $t(entity.smartPlaylist)",
|
||||||
"removeFromFavorites": "удалить из $t(entity.favorite_other)"
|
"removeFromFavorites": "удалить из $t(entity.favorite_other)",
|
||||||
|
"openIn": {
|
||||||
|
"lastfm": "открыть на Last.fm",
|
||||||
|
"musicbrainz": "открыть на MusicBrainz"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"common": {
|
"common": {
|
||||||
"backward": "назад",
|
"backward": "назад",
|
||||||
"increase": "увеличить",
|
"increase": "увеличить",
|
||||||
"rating": "рейтинг",
|
"rating": "рейтинг",
|
||||||
"bpm": "ударов в мин.",
|
"bpm": "уд./мин.",
|
||||||
"refresh": "обновить",
|
"refresh": "обновить",
|
||||||
"unknown": "неизвестно",
|
"unknown": "неизвестно",
|
||||||
"areYouSure": "вы уверены?",
|
"areYouSure": "вы уверены?",
|
||||||
@@ -34,19 +38,19 @@
|
|||||||
"currentSong": "текущий $t(entity.track_one)",
|
"currentSong": "текущий $t(entity.track_one)",
|
||||||
"collapse": "закрыть",
|
"collapse": "закрыть",
|
||||||
"trackNumber": "трек",
|
"trackNumber": "трек",
|
||||||
"descending": "убывающий",
|
"descending": "убывание",
|
||||||
"add": "добавить",
|
"add": "добавить",
|
||||||
"gap": "промежуток",
|
"gap": "промежуток",
|
||||||
"ascending": "возрастающий",
|
"ascending": "возрастанию",
|
||||||
"dismiss": "отклонить",
|
"dismiss": "отклонить",
|
||||||
"year": "год",
|
"year": "год",
|
||||||
"manage": "управлять",
|
"manage": "управлять",
|
||||||
"limit": "лимит",
|
"limit": "ограничение",
|
||||||
"minimize": "минимизировать",
|
"minimize": "минимизировать",
|
||||||
"modified": "изменено",
|
"modified": "изменено",
|
||||||
"duration": "продолжительность",
|
"duration": "длительность",
|
||||||
"name": "имя",
|
"name": "имя",
|
||||||
"maximize": "максимизировать",
|
"maximize": "развернуть",
|
||||||
"decrease": "уменьшить",
|
"decrease": "уменьшить",
|
||||||
"ok": "ок",
|
"ok": "ок",
|
||||||
"description": "описание",
|
"description": "описание",
|
||||||
@@ -60,7 +64,7 @@
|
|||||||
"forward": "вперёд",
|
"forward": "вперёд",
|
||||||
"delete": "удалить",
|
"delete": "удалить",
|
||||||
"cancel": "отменить",
|
"cancel": "отменить",
|
||||||
"forceRestartRequired": "перезапустите приложение, чтобы применить изменения... закройте уведомление, чтобы перезапустить приложение",
|
"forceRestartRequired": "перезапустите приложение, чтобы применить изменения... закройте уведомление для перезапуска",
|
||||||
"setting": "настройка",
|
"setting": "настройка",
|
||||||
"version": "версия",
|
"version": "версия",
|
||||||
"title": "название",
|
"title": "название",
|
||||||
@@ -76,14 +80,14 @@
|
|||||||
"action_many": "действий",
|
"action_many": "действий",
|
||||||
"playerMustBePaused": "воспроизведение должно быть остановлено",
|
"playerMustBePaused": "воспроизведение должно быть остановлено",
|
||||||
"confirm": "подтвердить",
|
"confirm": "подтвердить",
|
||||||
"resetToDefault": "сбросить к настройкам по умолчанию",
|
"resetToDefault": "по умолчанию",
|
||||||
"home": "Главная страница",
|
"home": "главная страница",
|
||||||
"comingSoon": "скоро будет…",
|
"comingSoon": "скоро будет…",
|
||||||
"reset": "сбросить",
|
"reset": "сбросить",
|
||||||
"channel_one": "канал",
|
"channel_one": "канал",
|
||||||
"channel_few": "канала",
|
"channel_few": "канала",
|
||||||
"channel_many": "каналов",
|
"channel_many": "каналов",
|
||||||
"disable": "выключить",
|
"disable": "отключить",
|
||||||
"sortOrder": "порядок",
|
"sortOrder": "порядок",
|
||||||
"menu": "меню",
|
"menu": "меню",
|
||||||
"restartRequired": "необходим перезапуск приложения",
|
"restartRequired": "необходим перезапуск приложения",
|
||||||
@@ -91,7 +95,7 @@
|
|||||||
"noResultsFromQuery": "нет результатов",
|
"noResultsFromQuery": "нет результатов",
|
||||||
"quit": "выйти",
|
"quit": "выйти",
|
||||||
"expand": "расширить",
|
"expand": "расширить",
|
||||||
"search": "Поиск",
|
"search": "поиск",
|
||||||
"saveAs": "сохранить как",
|
"saveAs": "сохранить как",
|
||||||
"disc": "диск",
|
"disc": "диск",
|
||||||
"yes": "да",
|
"yes": "да",
|
||||||
@@ -99,7 +103,13 @@
|
|||||||
"size": "размер",
|
"size": "размер",
|
||||||
"biography": "биография",
|
"biography": "биография",
|
||||||
"note": "заметка",
|
"note": "заметка",
|
||||||
"none": "нет"
|
"none": "нет",
|
||||||
|
"mbid": "MusicBrainz ID",
|
||||||
|
"reload": "перезагрузить",
|
||||||
|
"preview": "просмотр",
|
||||||
|
"codec": "кодек",
|
||||||
|
"share": "поделиться",
|
||||||
|
"close": "закрыть"
|
||||||
},
|
},
|
||||||
"entity": {
|
"entity": {
|
||||||
"album_one": "альбом",
|
"album_one": "альбом",
|
||||||
@@ -161,7 +171,9 @@
|
|||||||
"gap": "$t(common.gap)",
|
"gap": "$t(common.gap)",
|
||||||
"tableColumns": "столбцы таблицы",
|
"tableColumns": "столбцы таблицы",
|
||||||
"autoFitColumns": "автоматически расставить столбцы",
|
"autoFitColumns": "автоматически расставить столбцы",
|
||||||
"size": "$t(common.size)"
|
"size": "$t(common.size)",
|
||||||
|
"itemSize": "рамер элементов (px)",
|
||||||
|
"itemGap": "отступ между элементами (px)"
|
||||||
},
|
},
|
||||||
"label": {
|
"label": {
|
||||||
"releaseDate": "дата выхода",
|
"releaseDate": "дата выхода",
|
||||||
@@ -189,7 +201,9 @@
|
|||||||
"discNumber": "номер диска",
|
"discNumber": "номер диска",
|
||||||
"favorite": "$t(common.favorite)",
|
"favorite": "$t(common.favorite)",
|
||||||
"year": "$t(common.year)",
|
"year": "$t(common.year)",
|
||||||
"albumArtist": "$t(entity.albumArtist_one)"
|
"albumArtist": "$t(entity.albumArtist_one)",
|
||||||
|
"codec": "$t(common.codec)",
|
||||||
|
"songCount": "$t(entity.track_other)"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"column": {
|
"column": {
|
||||||
@@ -205,29 +219,43 @@
|
|||||||
"genre": "$t(entity.genre_one)",
|
"genre": "$t(entity.genre_one)",
|
||||||
"path": "путь",
|
"path": "путь",
|
||||||
"discNumber": "диск",
|
"discNumber": "диск",
|
||||||
"size": "$t(common.size)"
|
"size": "$t(common.size)",
|
||||||
|
"dateAdded": "дата добавления",
|
||||||
|
"album": "альбом",
|
||||||
|
"albumArtist": "исполнитель альбома",
|
||||||
|
"biography": "биография",
|
||||||
|
"codec": "$t(common.codec)",
|
||||||
|
"comment": "комментарий",
|
||||||
|
"albumCount": "$t(entity.album_other)",
|
||||||
|
"artist": "$t(entity.artist_one)",
|
||||||
|
"bitrate": "битрейт",
|
||||||
|
"channels": "$t(common.channel_other)",
|
||||||
|
"bpm": "bpm"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
"remotePortWarning": "перезапустить сервер для применения нового порта",
|
"remotePortWarning": "перезапустить сервер для применения нового порта",
|
||||||
"systemFontError": "произошла ошибка при попытке получить системные шрифты",
|
"systemFontError": "произошла ошибка при попытке получить системные шрифты",
|
||||||
"playbackError": "произошла ошибка при попытке проиграть медиа",
|
"playbackError": "произошла ошибка при попытке проиграть медиа",
|
||||||
"endpointNotImplementedError": "запрос {{endpoint}} is not implemented for {{serverType}}",
|
"endpointNotImplementedError": "запрос {{endpoint}} не реализован для {{serverType}}",
|
||||||
"remotePortError": "произошла ошибка при попытке установить порт удаленного сервера",
|
"remotePortError": "произошла ошибка при попытке установить порт удаленного сервера",
|
||||||
"serverRequired": "необходим сервер",
|
"serverRequired": "необходим сервер",
|
||||||
"authenticationFailed": "аутентификация завершилась с ошибкой",
|
"authenticationFailed": "авторизация завершилась с ошибкой",
|
||||||
"apiRouteError": "невозможно выполнить запрос",
|
"apiRouteError": "невозможно выполнить запрос",
|
||||||
"genericError": "произошла ошибка",
|
"genericError": "произошла ошибка",
|
||||||
"credentialsRequired": "необходимы учётные данные",
|
"credentialsRequired": "необходимы учётные данные",
|
||||||
"sessionExpiredError": "ваш сеанс истек",
|
"sessionExpiredError": "ваш сеанс истёк",
|
||||||
"remoteEnableError": "ошибка произошла при попытке $t(common.enable) удаленного сервера",
|
"remoteEnableError": "ошибка произошла при попытке $t(common.enable) удалённый сервер",
|
||||||
"localFontAccessDenied": "не получилось получить доступ к шрифтам",
|
"localFontAccessDenied": "не получилось получить доступ к шрифтам",
|
||||||
"serverNotSelectedError": "не выбран сервер",
|
"serverNotSelectedError": "не выбран сервер",
|
||||||
"remoteDisableError": "ошибка произошла при попытке $t(common.disable) удаленного сервера",
|
"remoteDisableError": "ошибка произошла при попытке $t(common.disable) удалённый сервер",
|
||||||
"mpvRequired": "Необходим MPV",
|
"mpvRequired": "необходим MPV",
|
||||||
"audioDeviceFetchError": "произошла ошибка с аудиоустройством",
|
"audioDeviceFetchError": "произошла ошибка с аудиоустройством",
|
||||||
"invalidServer": "недействительный сервер",
|
"invalidServer": "недействительный сервер",
|
||||||
"loginRateError": "слишком много попыток входа, пожалуйста, попробуйте еще раз через несколько секунд"
|
"loginRateError": "слишком много попыток входа, пожалуйста, попробуйте ещё раз через несколько секунд",
|
||||||
|
"openError": "не удалось открыть файл",
|
||||||
|
"badAlbum": "вы видите эту страницу из-за того, что эта песня не входит в альбом. скорее всего, вы видите эту ошибку, так как песня находится в корневой директории папки с музыкой. jellyfin группирует треки только по папкам.",
|
||||||
|
"networkError": "возникла ошибка сети"
|
||||||
},
|
},
|
||||||
"filter": {
|
"filter": {
|
||||||
"isCompilation": "сборник",
|
"isCompilation": "сборник",
|
||||||
@@ -238,14 +266,14 @@
|
|||||||
"favorited": "любимый",
|
"favorited": "любимый",
|
||||||
"albumArtist": "$t(entity.albumArtist_one)",
|
"albumArtist": "$t(entity.albumArtist_one)",
|
||||||
"isFavorited": "любимый",
|
"isFavorited": "любимый",
|
||||||
"bpm": "ударов в мин.",
|
"bpm": "уд./мин.",
|
||||||
"disc": "диск",
|
"disc": "диск",
|
||||||
"biography": "биография",
|
"biography": "биография",
|
||||||
"artist": "$t(entity.artist_one)",
|
"artist": "$t(entity.artist_one)",
|
||||||
"duration": "продолжительность",
|
"duration": "длительность",
|
||||||
"fromYear": "из года",
|
"fromYear": "из года",
|
||||||
"criticRating": "рейтинг критиков",
|
"criticRating": "рейтинг критиков",
|
||||||
"mostPlayed": "наибольшое кол-во воспроизведений",
|
"mostPlayed": "самое воспроизводимое",
|
||||||
"comment": "комментировать",
|
"comment": "комментировать",
|
||||||
"playCount": "кол-во воспроизведений",
|
"playCount": "кол-во воспроизведений",
|
||||||
"recentlyUpdated": "недавно обновлено",
|
"recentlyUpdated": "недавно обновлено",
|
||||||
@@ -254,17 +282,17 @@
|
|||||||
"owner": "$t(common.owner)",
|
"owner": "$t(common.owner)",
|
||||||
"title": "название",
|
"title": "название",
|
||||||
"rating": "рейтинг",
|
"rating": "рейтинг",
|
||||||
"search": "Поиск",
|
"search": "поиск",
|
||||||
"genre": "$t(entity.genre_one)",
|
"genre": "$t(entity.genre_one)",
|
||||||
"recentlyAdded": "недавно добавлено",
|
"recentlyAdded": "недавно добавлено",
|
||||||
"note": "заметка",
|
"note": "заметка",
|
||||||
"name": "название",
|
"name": "название",
|
||||||
"releaseDate": "дата выхода",
|
"releaseDate": "дата выхода",
|
||||||
"albumCount": "$t(entity.album_other) кол-во",
|
"albumCount": "кол-во $t(entity.album_other)",
|
||||||
"path": "путь",
|
"path": "путь",
|
||||||
"isRecentlyPlayed": "недавно проигрывалась",
|
"isRecentlyPlayed": "недавно проигрывалась",
|
||||||
"releaseYear": "год выхода",
|
"releaseYear": "год выхода",
|
||||||
"id": "#",
|
"id": "№",
|
||||||
"songCount": "кол-во песен",
|
"songCount": "кол-во песен",
|
||||||
"isPublic": "публичный",
|
"isPublic": "публичный",
|
||||||
"random": "случайный",
|
"random": "случайный",
|
||||||
@@ -277,16 +305,16 @@
|
|||||||
"repeat_all": "повтор всех",
|
"repeat_all": "повтор всех",
|
||||||
"stop": "остановить",
|
"stop": "остановить",
|
||||||
"repeat": "повтор",
|
"repeat": "повтор",
|
||||||
"queue_remove": "удалить выделенные",
|
"queue_remove": "удалить выбранное",
|
||||||
"playRandom": "случайные песни",
|
"playRandom": "случайные песни",
|
||||||
"skip": "пропустить",
|
"skip": "пропустить",
|
||||||
"previous": "предыдущий",
|
"previous": "предыдущий",
|
||||||
"toggleFullscreenPlayer": "включить полноэкранный режим",
|
"toggleFullscreenPlayer": "включить полноэкранный режим",
|
||||||
"skip_back": "назад",
|
"skip_back": "назад",
|
||||||
"favorite": "любимый",
|
"favorite": "любимый",
|
||||||
"next": "следующее",
|
"next": "следующий",
|
||||||
"shuffle": "перемешать",
|
"shuffle": "перемешать",
|
||||||
"playbackFetchNoResults": "нет песен",
|
"playbackFetchNoResults": "песни не найдены",
|
||||||
"playbackFetchInProgress": "загрузка песен..",
|
"playbackFetchInProgress": "загрузка песен..",
|
||||||
"addNext": "добавить следующий",
|
"addNext": "добавить следующий",
|
||||||
"playbackSpeed": "скорость воспроизведения",
|
"playbackSpeed": "скорость воспроизведения",
|
||||||
@@ -297,8 +325,8 @@
|
|||||||
"queue_clear": "очистить очередь",
|
"queue_clear": "очистить очередь",
|
||||||
"muted": "звук отключён",
|
"muted": "звук отключён",
|
||||||
"unfavorite": "убрать из любимых",
|
"unfavorite": "убрать из любимых",
|
||||||
"queue_moveToTop": "переместить выделение вниз",
|
"queue_moveToTop": "переместить выделенное вниз",
|
||||||
"queue_moveToBottom": "переместить выделение вверх",
|
"queue_moveToBottom": "переместить выделенное вверх",
|
||||||
"shuffle_off": "перемешивание выключено",
|
"shuffle_off": "перемешивание выключено",
|
||||||
"addLast": "добавить последний",
|
"addLast": "добавить последний",
|
||||||
"mute": "отключить звук",
|
"mute": "отключить звук",
|
||||||
@@ -330,7 +358,9 @@
|
|||||||
"unsynchronized": "несинхронизировано",
|
"unsynchronized": "несинхронизировано",
|
||||||
"lyricAlignment": "выравнивание слов песни",
|
"lyricAlignment": "выравнивание слов песни",
|
||||||
"useImageAspectRatio": "использовать соотношение сторон изображения",
|
"useImageAspectRatio": "использовать соотношение сторон изображения",
|
||||||
"lyricGap": "пробел между словами"
|
"lyricGap": "пробел между словами",
|
||||||
|
"dynamicIsImage": "включить фоновое изображение",
|
||||||
|
"dynamicImageBlur": "сила размытия изображения"
|
||||||
},
|
},
|
||||||
"upNext": "следующее",
|
"upNext": "следующее",
|
||||||
"lyrics": "слова песни",
|
"lyrics": "слова песни",
|
||||||
@@ -364,7 +394,9 @@
|
|||||||
"addFavorite": "$t(action.addToFavorites)",
|
"addFavorite": "$t(action.addToFavorites)",
|
||||||
"play": "$t(player.play)",
|
"play": "$t(player.play)",
|
||||||
"numberSelected": "{{count}} выбрано",
|
"numberSelected": "{{count}} выбрано",
|
||||||
"removeFromQueue": "$t(action.removeFromQueue)"
|
"removeFromQueue": "$t(action.removeFromQueue)",
|
||||||
|
"showDetails": "получить информацию",
|
||||||
|
"shareItem": "поделиться"
|
||||||
},
|
},
|
||||||
"home": {
|
"home": {
|
||||||
"mostPlayed": "наибольшее кол-во воспроизведений",
|
"mostPlayed": "наибольшее кол-во воспроизведений",
|
||||||
@@ -374,7 +406,7 @@
|
|||||||
"recentlyPlayed": "недавно прослушано"
|
"recentlyPlayed": "недавно прослушано"
|
||||||
},
|
},
|
||||||
"albumDetail": {
|
"albumDetail": {
|
||||||
"moreFromArtist": "больше из жанра $t(entity.genre_one)",
|
"moreFromArtist": "больше от $t(entity.artist_one)",
|
||||||
"moreFromGeneric": "больше из {{item}}"
|
"moreFromGeneric": "больше из {{item}}"
|
||||||
},
|
},
|
||||||
"setting": {
|
"setting": {
|
||||||
@@ -387,10 +419,13 @@
|
|||||||
"title": "$t(entity.albumArtist_other)"
|
"title": "$t(entity.albumArtist_other)"
|
||||||
},
|
},
|
||||||
"genreList": {
|
"genreList": {
|
||||||
"title": "$t(entity.genre_other)"
|
"title": "$t(entity.genre_other)",
|
||||||
|
"showAlbums": "показать $t(entity.genre_one) $t(entity.album_other)",
|
||||||
|
"showTracks": "показать $t(entity.genre_one) $t(entity.track_other)"
|
||||||
},
|
},
|
||||||
"trackList": {
|
"trackList": {
|
||||||
"title": "$t(entity.track_other)"
|
"title": "$t(entity.track_other)",
|
||||||
|
"genreTracks": "\"{{genre}}\" $t(entity.track_other)"
|
||||||
},
|
},
|
||||||
"globalSearch": {
|
"globalSearch": {
|
||||||
"commands": {
|
"commands": {
|
||||||
@@ -404,14 +439,32 @@
|
|||||||
"title": "$t(entity.playlist_other)"
|
"title": "$t(entity.playlist_other)"
|
||||||
},
|
},
|
||||||
"albumList": {
|
"albumList": {
|
||||||
"title": "$t(entity.album_other)"
|
"title": "$t(entity.album_other)",
|
||||||
|
"artistAlbums": "альбомы {{artist}}",
|
||||||
|
"genreAlbums": "\"{{genre}}\" $t(entity.album_other)"
|
||||||
|
},
|
||||||
|
"albumArtistDetail": {
|
||||||
|
"topSongs": "популярные треки",
|
||||||
|
"viewAll": "посмотреть всё",
|
||||||
|
"appearsOn": "появляется в",
|
||||||
|
"viewDiscography": "посмотреть дискографию",
|
||||||
|
"relatedArtists": "похож на $t(entity.artist_other)",
|
||||||
|
"viewAllTracks": "посмотреть все $t(entity.track_other)",
|
||||||
|
"recentReleases": "недавние релизы",
|
||||||
|
"about": "О {{artist}}",
|
||||||
|
"topSongsFrom": "популярные треки из {{title}}"
|
||||||
|
},
|
||||||
|
"itemDetail": {
|
||||||
|
"copyPath": "скопировать путь в буфер обмена",
|
||||||
|
"openFile": "открыть трек в менеджере файлов",
|
||||||
|
"copiedPath": "путь успешно скопирован"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"form": {
|
"form": {
|
||||||
"deletePlaylist": {
|
"deletePlaylist": {
|
||||||
"title": "удалить $t(entity.playlist_one)",
|
"title": "удалить $t(entity.playlist_one)",
|
||||||
"success": "$t(entity.playlist_one) успешно удалён",
|
"success": "$t(entity.playlist_one) успешно удалён",
|
||||||
"input_confirm": "напишите название $t(entity.playlist_one), чтобы подтвердить действие"
|
"input_confirm": "напишите название $t(entity.playlist_one)а для подтверждения"
|
||||||
},
|
},
|
||||||
"createPlaylist": {
|
"createPlaylist": {
|
||||||
"input_description": "$t(common.description)",
|
"input_description": "$t(common.description)",
|
||||||
@@ -426,16 +479,16 @@
|
|||||||
"input_username": "пользователь",
|
"input_username": "пользователь",
|
||||||
"input_url": "url",
|
"input_url": "url",
|
||||||
"input_password": "пароль",
|
"input_password": "пароль",
|
||||||
"input_legacyAuthentication": "включить старую аутентификацию",
|
"input_legacyAuthentication": "включить старую авторизацию",
|
||||||
"input_name": "название сервера",
|
"input_name": "название сервера",
|
||||||
"success": "сервер добавлен успешно",
|
"success": "сервер добавлен успешно",
|
||||||
"input_savePassword": "сохранить пароль",
|
"input_savePassword": "сохранить пароль",
|
||||||
"ignoreSsl": "ignore ssl $t(common.restartRequired)",
|
"ignoreSsl": "игнорирование ssl ($t(common.restartRequired))",
|
||||||
"ignoreCors": "$t(common.restartRequired)",
|
"ignoreCors": "игнорирование корсетов ($t(common.restartRequired))",
|
||||||
"error_savePassword": "произошла ошибка во время попытки сохранения пароля"
|
"error_savePassword": "произошла ошибка во время попытки сохранения пароля"
|
||||||
},
|
},
|
||||||
"addToPlaylist": {
|
"addToPlaylist": {
|
||||||
"success": "добавлено(а) {{message}} $t(entity.track_other) в {{numOfPlaylists}} $t(entity.playlist_other)",
|
"success": "добавлено: $t(entity.trackWithCount, {\"count\": {{message}} }) в $t(entity.playlistWithCount, {\"count\": {{numOfPlaylists}} })",
|
||||||
"title": "добавить в $t(entity.playlist_one)",
|
"title": "добавить в $t(entity.playlist_one)",
|
||||||
"input_skipDuplicates": "пропустить дубликаты",
|
"input_skipDuplicates": "пропустить дубликаты",
|
||||||
"input_playlists": "$t(entity.playlist_other)"
|
"input_playlists": "$t(entity.playlist_other)"
|
||||||
@@ -455,35 +508,191 @@
|
|||||||
},
|
},
|
||||||
"editPlaylist": {
|
"editPlaylist": {
|
||||||
"title": "редактировать $t(entity.playlist_one)"
|
"title": "редактировать $t(entity.playlist_one)"
|
||||||
|
},
|
||||||
|
"shareItem": {
|
||||||
|
"success": "ссылка скопирована в буфер обмена (нажмите здесь, чтобы открыть)",
|
||||||
|
"expireInvalid": "время истечения срока действия должно быть в будущем",
|
||||||
|
"createFailed": "не удалось создать ссылку для общего доступа (общий доступ включён?)",
|
||||||
|
"allowDownloading": "разрешить скачивание",
|
||||||
|
"setExpiration": "установить срок действия",
|
||||||
|
"description": "описание"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"setting": {
|
"setting": {
|
||||||
"accentColor": "цвет акцента",
|
"accentColor": "цвет акцента",
|
||||||
"accentColor_description": "устанавливает цвет акцента для приложения",
|
"accentColor_description": "устанавливает цвет акцента для приложения",
|
||||||
"applicationHotkeys": "горячие клавиши приложения",
|
"applicationHotkeys": "горячие клавиши приложения",
|
||||||
"crossfadeStyle_description": "Выберите вид эффекта crossfade для аудиоплеера",
|
"crossfadeStyle_description": "выберите вид эффекта crossfade для аудиоплеера",
|
||||||
"enableRemote_description": "Включает сервер удалённого управления для управления воспроизведением с помощью других устройств",
|
"enableRemote_description": "включает сервер удалённого управления для управления воспроизведением с помощью других устройств",
|
||||||
"fontType_optionSystem": "Системный шрифт",
|
"fontType_optionSystem": "системный",
|
||||||
"mpvExecutablePath_description": "Укажите папку, в которой находится исполняющий файл аудиоплеера MPV",
|
"mpvExecutablePath_description": "укажите папку, в которой находится исполняющий файл аудиоплеера MPV. если оставить пустым, будет использоваться путь по умолчанию",
|
||||||
"crossfadeStyle": "Вид эффекта crossfade",
|
"crossfadeStyle": "Вид эффекта crossfade",
|
||||||
"fontType_optionBuiltIn": "Встроенный в приложение",
|
"fontType_optionBuiltIn": "встроенный",
|
||||||
"disableLibraryUpdateOnStartup": "Отключить проверку новых версий при запуске приложения",
|
"disableLibraryUpdateOnStartup": "Отключить проверку новых версий при запуске приложения",
|
||||||
"minimizeToTray_description": "Сворачивать приложение в панель уведомлений",
|
"minimizeToTray_description": "Сворачивать приложение в панель уведомлений",
|
||||||
"audioPlayer_description": "Укажите - какой аудиоплеер использовать для воспроизведения",
|
"audioPlayer_description": "Укажите - какой аудиоплеер использовать для воспроизведения",
|
||||||
"disableAutomaticUpdates": "Отключить проверку обновлений",
|
"disableAutomaticUpdates": "отключить проверку обновлений",
|
||||||
"exitToTray_description": "При закрытии приложения - оно останется в панели уведомлений",
|
"exitToTray_description": "При закрытии приложения - оно останется в панели уведомлений",
|
||||||
"fontType_optionCustom": "Пользовательский шрифт",
|
"fontType_optionCustom": "пользовательский",
|
||||||
"remotePassword": "Пароль к серверу удалённого управления",
|
"remotePassword": "пароль к серверу удалённого управления",
|
||||||
"font": "Шрифт",
|
"font": "Шрифт",
|
||||||
"crossfadeDuration_description": "Укажите длительность эффекта crossfade",
|
"crossfadeDuration_description": "Укажите длительность эффекта crossfade",
|
||||||
"mpvExecutablePath": "Папка с аудиоплеером MPV",
|
"mpvExecutablePath": "папка с аудиоплеером MPV",
|
||||||
"exitToTray": "Сворачивать в панель уведомлений при закрытии",
|
"exitToTray": "сворачивать в панель уведомлений при закрытии",
|
||||||
"enableRemote": "Включить сервер удалённого управления",
|
"enableRemote": "включить сервер удалённого управления",
|
||||||
"fontType": "Источник шрифта",
|
"fontType": "тип шрифта",
|
||||||
"crossfadeDuration": "Длительность эффекта crossfade",
|
"crossfadeDuration": "Длительность эффекта crossfade",
|
||||||
"audioPlayer": "Аудиоплеер",
|
"audioPlayer": "Аудиоплеер",
|
||||||
"minimizeToTray": "Сворачивать в панель уведомлений",
|
"minimizeToTray": "сворачивать в панель уведомлений",
|
||||||
"font_description": "Выберите - какой шрифт использовать в приложении",
|
"font_description": "Выберите - какой шрифт использовать в приложении",
|
||||||
"remoteUsername": "Имя пользователя для доступа к серверу удалённого управления"
|
"remoteUsername": "имя пользователя для доступа к серверу удалённого управления",
|
||||||
|
"buttonSize_description": "размер кнопок в панели управления воспроизведением",
|
||||||
|
"clearCache": "очистить кэш браузера",
|
||||||
|
"clearQueryCache": "очистить кэш feishin",
|
||||||
|
"audioDevice": "устройство воспроизведения",
|
||||||
|
"audioDevice_description": "выберите устройство воспроизведения (только в режиме аудиоплеера web)",
|
||||||
|
"buttonSize": "размер кнопок панели управления воспроизведением",
|
||||||
|
"hotkey_volumeDown": "уменьшить громкость",
|
||||||
|
"playButtonBehavior_optionAddLast": "$t(player.addLast)",
|
||||||
|
"theme_description": "устанавливает тему, которая будет использоваться приложением",
|
||||||
|
"passwordStore": "хранилище паролей/секретов",
|
||||||
|
"sidebarPlaylistList": "список плейлистов в боковой панели",
|
||||||
|
"windowBarStyle_description": "выберите стиль заголовка окна",
|
||||||
|
"followLyric": "следовать тексту трека",
|
||||||
|
"volumeWheelStep": "шаг регулировки громкости колёсиком мыши",
|
||||||
|
"windowBarStyle": "стиль заголовка окна",
|
||||||
|
"hotkey_zoomOut": "уменьшить масштаб",
|
||||||
|
"playbackStyle_optionCrossFade": "затухание",
|
||||||
|
"replayGainMode": "режим {{ReplayGain}}",
|
||||||
|
"replayGainMode_optionAlbum": "$t(entity.album_one)",
|
||||||
|
"replayGainMode_optionNone": "$t(common.none)",
|
||||||
|
"replayGainMode_optionTrack": "$t(entity.track_one)",
|
||||||
|
"clearQueryCache_description": "\"мягкая очистка\" feishin. при выполнении обновляются плейлисты, метаданные треков, но сохранённые тексты треков сбрасываются. настройки, учётные данные и кэшированные изображения сохраняются",
|
||||||
|
"hotkey_favoriteCurrentSong": "добавить $t(common.currentSong) в избранное",
|
||||||
|
"genreBehavior": "поведения страницы жанров",
|
||||||
|
"globalMediaHotkeys": "глобальные мультимедийные горячие клавиши",
|
||||||
|
"hotkey_browserForward": "кнопка браузера \"вперёд\"",
|
||||||
|
"hotkey_favoritePreviousSong": "добавить $t(common.previousSong) в избранное",
|
||||||
|
"hotkey_globalSearch": "глобальный поиск",
|
||||||
|
"hotkey_playbackNext": "следующий трек",
|
||||||
|
"hotkey_playbackPause": "пауза",
|
||||||
|
"hotkey_playbackPlay": "прослушать",
|
||||||
|
"hotkey_playbackPlayPause": "прослушать / пауза",
|
||||||
|
"hotkey_playbackPrevious": "предыдущий трек",
|
||||||
|
"hotkey_playbackStop": "остановить",
|
||||||
|
"hotkey_rate0": "очистить оценку",
|
||||||
|
"hotkey_rate1": "оценить в 1 звезду",
|
||||||
|
"hotkey_rate2": "оценить в 2 звезды",
|
||||||
|
"hotkey_rate3": "оценить в 3 звезды",
|
||||||
|
"hotkey_rate4": "оценить в 4 звезды",
|
||||||
|
"hotkey_rate5": "оценить в 5 звёзд",
|
||||||
|
"hotkey_skipForward": "перемотать вперёд",
|
||||||
|
"hotkey_toggleCurrentSongFavorite": "добавить/удалить $t(common.currentSong) в избранное",
|
||||||
|
"hotkey_toggleFullScreenPlayer": "включение/выключение полноэкранного плеера",
|
||||||
|
"hotkey_togglePreviousSongFavorite": "добавить/удалить $t(common.previousSong) в избранное",
|
||||||
|
"hotkey_toggleRepeat": "переключить режим повтора",
|
||||||
|
"hotkey_toggleShuffle": "переключить перемешивание",
|
||||||
|
"hotkey_unfavoriteCurrentSong": "удалить $t(common.currentSong) из избранного",
|
||||||
|
"hotkey_unfavoritePreviousSong": "удалить $t(common.previousSong) из избранного",
|
||||||
|
"hotkey_volumeUp": "увеличить громкость",
|
||||||
|
"hotkey_zoomIn": "увеличить масштаб",
|
||||||
|
"language": "язык",
|
||||||
|
"lyricFetchProvider_description": "выберите источники для получения текстов песен. порядок источников соответствует очередности их запросов",
|
||||||
|
"minimumScrobblePercentage_description": "минимальный процент прослушивания трека, прежде чем он будет засчитан как прослушанный",
|
||||||
|
"minimumScrobbleSeconds_description": "минимальное время прослушивания трека в секундах, прежде чем он будет засчитан как прослушанный",
|
||||||
|
"playbackStyle_optionNormal": "нормальный",
|
||||||
|
"mpvExtraParameters": "параметры mpv",
|
||||||
|
"mpvExtraParameters_help": "по одному на строчку",
|
||||||
|
"playbackStyle_description": "выберите стиль воспроизведения, который будет использоваться аудиоплеером",
|
||||||
|
"playButtonBehavior_description": "устанавливает поведение кнопки воспроизведения при добавлении треков в очередь",
|
||||||
|
"playButtonBehavior": "поведение кнопки воспроизведения",
|
||||||
|
"playButtonBehavior_optionAddNext": "$t(player.addNext)",
|
||||||
|
"playButtonBehavior_optionPlay": "$t(player.play)",
|
||||||
|
"playerAlbumArtResolution_description": "разрешение большой версии обложки альбома в проигрывателе. при большем разрешении она выглядит более четкой, но может замедлить загрузку. по умолчанию равно 0 - устанавливает разрешение автоматически",
|
||||||
|
"remotePort": "порт сервера удалённого управления",
|
||||||
|
"remotePort_description": "устанавливает порт для сервера удалённого управления",
|
||||||
|
"replayGainClipping": "{{ReplayGain}} клиппинг",
|
||||||
|
"replayGainFallback": "{{ReplayGain}} по умолчанию",
|
||||||
|
"sampleRate_description": "выберите выходную частоту дискретизации, которая будет использоваться, если выбранная частота дискретизации отличается от частоты дискретизации текущего трека. при значении меньше 8000 будет использоваться частота по умолчанию",
|
||||||
|
"savePlayQueue_description": "сохранять очередь воспроизведения при закрытии приложения и восстанавливать при запуске приложения",
|
||||||
|
"showSkipButton_description": "показывать или скрывать кнопки перемотки на панели управления воспроизведением",
|
||||||
|
"sidebarConfiguration": "конфигурация боковой панели",
|
||||||
|
"sidebarConfiguration_description": "выбрать элементы и порядок их отображения на боковой панели",
|
||||||
|
"sidebarCollapsedNavigation": "кнопки навигации в боковой панели (в свёрнутом режиме)",
|
||||||
|
"showSkipButtons": "показывать кнопки перемотки",
|
||||||
|
"showSkipButtons_description": "показывать или скрывать кнопки перемотки на панели управления воспроизведением",
|
||||||
|
"sidebarPlaylistList_description": "показать или скрыть список плейлистов на боковой панели",
|
||||||
|
"skipDuration": "время перемотки",
|
||||||
|
"skipDuration_description": "задает время перемотки при использовании кнопок перемотки на панели проигрывателя",
|
||||||
|
"useSystemTheme": "использовать тему системы",
|
||||||
|
"themeLight": "тема (светлая)",
|
||||||
|
"themeLight_description": "устанавливает светлую тему приложения",
|
||||||
|
"useSystemTheme_description": "использует тему, заданную в системе (светлую/тёмную)",
|
||||||
|
"zoom": "процент масштабирования",
|
||||||
|
"zoom_description": "устанавливает процент масштабирования приложения",
|
||||||
|
"floatingQueueArea": "показать область наведения для всплывающей очереди",
|
||||||
|
"genreBehavior_description": "определяет, что отобразится при открытии на жанр — список треков или альбомов",
|
||||||
|
"globalMediaHotkeys_description": "включить или отключить использование системных мультимедийных горячих клавиш для управления воспроизведением",
|
||||||
|
"homeConfiguration_description": "позволяет настроить видимость и порядок элементов на домашней странице",
|
||||||
|
"hotkey_toggleQueue": "показать/скрыть очередь воспроизведения",
|
||||||
|
"imageAspectRatio": "использовать оригинальное соотношение сторон обложки",
|
||||||
|
"imageAspectRatio_description": "если эта опция включена, обложки будут отображаться в соответствии с их собственным соотношением сторон. для обложек не 1:1 оставшееся пространство будет пустым",
|
||||||
|
"minimumScrobblePercentage": "минимальное время для скробблинга (в процентах)",
|
||||||
|
"playbackStyle": "стиль воспроизведения",
|
||||||
|
"playerAlbumArtResolution": "разрешение обложки альбома",
|
||||||
|
"remotePassword_description": "задает пароль для сервера удалённого управления. По умолчанию эти учетные данные передаются небезопасным способом, поэтому следует использовать уникальный пароль, который вам не важен",
|
||||||
|
"replayGainClipping_description": "Предотвращение клиппинга, вызванного {{ReplayGain}}, путём автоматического снижения усиления",
|
||||||
|
"replayGainFallback_description": "усиление в db для применения, если у файла нет тегов {{ReplayGain}}",
|
||||||
|
"replayGainMode_description": "регулировать усиление громкости в соответствии со значениями {{ReplayGain}}, хранящимися в метаданных файла",
|
||||||
|
"savePlayQueue": "сохранять очередь воспроизведения",
|
||||||
|
"showSkipButton": "показывать кнопки перемотки",
|
||||||
|
"theme": "тема",
|
||||||
|
"themeDark": "тема (тёмная)",
|
||||||
|
"externalLinks": "показывать ссылки на внешние ресурсы",
|
||||||
|
"gaplessAudio": "бесшовное воспроизведение",
|
||||||
|
"gaplessAudio_optionWeak": "слабое (рекомендуется)",
|
||||||
|
"gaplessAudio_description": "устанавливает настройку mpv для бесшовного воспроизведение",
|
||||||
|
"hotkey_browserBack": "кнопка браузера \"назад\"",
|
||||||
|
"hotkey_localSearch": "поиск на странице",
|
||||||
|
"hotkey_skipBackward": "перемотать назад",
|
||||||
|
"startMinimized": "запуск в свёрнутом режиме",
|
||||||
|
"themeDark_description": "устанавливает тёмную тему приложения",
|
||||||
|
"hotkey_volumeMute": "отключить звук",
|
||||||
|
"clearCache_description": "\"жесткая очистка\" feishin. кроме очистки кэша feishin, также очищает кэш браузера (сохранённые картинки и другие ресурсы). учётные данные и настройки сохраняются",
|
||||||
|
"clearCacheSuccess": "кэш успешно удалён",
|
||||||
|
"customFontPath": "путь к пользовательскому шрифту",
|
||||||
|
"customFontPath_description": "укажите путь к пользовательскому шрифту, который будет использоваться в приложении",
|
||||||
|
"externalLinks_description": "включает отображение внешних ссылок (Last.fm, MusicBrainz) на страницах альбомов и артистов",
|
||||||
|
"floatingQueueArea_description": "включить отображение иконки наведения на правой части экрана, чтобы показать очередь воспроизведения",
|
||||||
|
"followLyric_description": "прокручивать текст трека до текущей позиции воспроизведения",
|
||||||
|
"language_description": "устанавливает язык приложения ($t(common.restartRequired))",
|
||||||
|
"lyricFetch_description": "получать тексты треков из различных интернет-источников",
|
||||||
|
"lyricFetchProvider": "источник текстов треков",
|
||||||
|
"minimumScrobbleSeconds": "минимальное время для скробблинга (в секундах)",
|
||||||
|
"replayGainPreamp": "предусиление {{ReplayGain}} (дБ)",
|
||||||
|
"sidebarCollapsedNavigation_description": "показать или скрыть кнопки навигации в свёрнутой боковой панели",
|
||||||
|
"homeConfiguration": "конфигурация домашней страницы",
|
||||||
|
"remoteUsername_description": "задает имя пользователя для сервера удалённого управления. если имя пользователя и пароль пусты, аутентификация будет отключена",
|
||||||
|
"scrobble": "скробблинг",
|
||||||
|
"replayGainPreamp_description": "настройка усиления предусилителя, применяемого к значениям {{ReplayGain}}",
|
||||||
|
"passwordStore_description": "какое хранилище паролей/секретов использовать. измените это значение, если у вас есть проблемы с хранением паролей.",
|
||||||
|
"lyricFetch": "получать тексты треков из интернета",
|
||||||
|
"sampleRate": "частота дискретизации",
|
||||||
|
"scrobble_description": "скробблинг треков на вашем медиасервере",
|
||||||
|
"startMinimized_description": "запуск приложения в области уведомлений",
|
||||||
|
"volumeWheelStep_description": "количество громкости, изменяемое при прокрутке колёсика мыши над ползунком громкости",
|
||||||
|
"discordRichPresence": "состояние профиля {{discord}}",
|
||||||
|
"discordApplicationId": "{{discord}} application id",
|
||||||
|
"discordApplicationId_description": "application id приложения {{discord}} которое будет отображаться в статусе профиля (по умолчанию {{defaultId}})",
|
||||||
|
"discordIdleStatus": "показывать состояние простоя",
|
||||||
|
"discordIdleStatus_description": "если включено, то обновляет статус, когда пользователь бездействует",
|
||||||
|
"discordUpdateInterval": "интервал обновления статуса профиля {{discord}}",
|
||||||
|
"discordUpdateInterval_description": "время в секундах между каждым обновлением (минимум 15 секунд)",
|
||||||
|
"lyricOffset_description": "Смещение появления текста треков на указанное количество миллисекунд",
|
||||||
|
"skipPlaylistPage": "пропустить страницу плейлиста",
|
||||||
|
"applicationHotkeys_description": "настройка горячих клавиш приложения. включите чекбокс, чтобы сделать горячую клавишу глобальной (только для ПК)",
|
||||||
|
"fontType_description": "встроенный позволяет выбрать один из шрифтов, предоставляемых Feishin. системный позволяет выбрать любой шрифт, предоставляемый вашей операционной системой. пользовательский позволяет выбрать свой собственный шрифт",
|
||||||
|
"discordRichPresence_description": "включить статус воспроизведения в статус профиля в {{discord}}. Ключи изображений: {{icon}}, {{playing}} и {{paused}} ",
|
||||||
|
"lyricOffset": "синхронизация текста треков (мс)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -156,7 +156,8 @@
|
|||||||
"mute": "静音",
|
"mute": "静音",
|
||||||
"skip_forward": "向前跳过",
|
"skip_forward": "向前跳过",
|
||||||
"playbackSpeed": "播放速度",
|
"playbackSpeed": "播放速度",
|
||||||
"pause": "暂停"
|
"pause": "暂停",
|
||||||
|
"playSimilarSongs": "播放类似的曲目"
|
||||||
},
|
},
|
||||||
"setting": {
|
"setting": {
|
||||||
"crossfadeStyle_description": "选择用于音频播放器的淡入淡出风格",
|
"crossfadeStyle_description": "选择用于音频播放器的淡入淡出风格",
|
||||||
@@ -338,7 +339,11 @@
|
|||||||
"genreBehavior_description": "确定单击流派是否默认在曲目或专辑列表中打开",
|
"genreBehavior_description": "确定单击流派是否默认在曲目或专辑列表中打开",
|
||||||
"homeConfiguration": "主页配置",
|
"homeConfiguration": "主页配置",
|
||||||
"homeConfiguration_description": "配置主页上显示的项目以及显示顺序",
|
"homeConfiguration_description": "配置主页上显示的项目以及显示顺序",
|
||||||
"passwordStore": "密码/秘密存储"
|
"passwordStore": "密码/秘密存储",
|
||||||
|
"homeFeature_description": "控制是否在主页上显示大型特色轮播",
|
||||||
|
"homeFeature": "首页 精选 轮播",
|
||||||
|
"imageAspectRatio": "使用原生封面艺术纵横比",
|
||||||
|
"imageAspectRatio_description": "如果启用,封面艺术将使用其原生纵横比显示。对于不是1:1的艺术,剩余的空间将是空的"
|
||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
"remotePortWarning": "重启服务器使新端口生效",
|
"remotePortWarning": "重启服务器使新端口生效",
|
||||||
@@ -498,7 +503,8 @@
|
|||||||
"addLast": "$t(player.addLast)",
|
"addLast": "$t(player.addLast)",
|
||||||
"addFavorite": "$t(action.addToFavorites)",
|
"addFavorite": "$t(action.addToFavorites)",
|
||||||
"showDetails": "获取信息",
|
"showDetails": "获取信息",
|
||||||
"shareItem": "分享项目"
|
"shareItem": "分享项目",
|
||||||
|
"playSimilarSongs": "$t(player.playSimilarSongs)"
|
||||||
},
|
},
|
||||||
"trackList": {
|
"trackList": {
|
||||||
"title": "$t(entity.track_other)",
|
"title": "$t(entity.track_other)",
|
||||||
@@ -639,7 +645,8 @@
|
|||||||
"year": "$t(common.year)",
|
"year": "$t(common.year)",
|
||||||
"albumArtist": "$t(entity.albumArtist_one)",
|
"albumArtist": "$t(entity.albumArtist_one)",
|
||||||
"titleCombined": "$t(common.title)(合并)",
|
"titleCombined": "$t(common.title)(合并)",
|
||||||
"codec": "$t(common.codec)"
|
"codec": "$t(common.codec)",
|
||||||
|
"songCount": "$t(entity.track_other)"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"column": {
|
"column": {
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import console from 'console';
|
import console from 'console';
|
||||||
|
import { rm } from 'fs/promises';
|
||||||
|
import { pid } from 'node:process';
|
||||||
import { app, ipcMain } from 'electron';
|
import { app, ipcMain } from 'electron';
|
||||||
import uniq from 'lodash/uniq';
|
import uniq from 'lodash/uniq';
|
||||||
import MpvAPI from 'node-mpv';
|
import MpvAPI from 'node-mpv';
|
||||||
@@ -18,6 +20,7 @@ declare module 'node-mpv';
|
|||||||
// }
|
// }
|
||||||
|
|
||||||
let mpvInstance: MpvAPI | null = null;
|
let mpvInstance: MpvAPI | null = null;
|
||||||
|
const socketPath = isWindows() ? `\\\\.\\pipe\\mpvserver-${pid}` : `/tmp/node-mpv-${pid}.sock`;
|
||||||
|
|
||||||
const NodeMpvErrorCode = {
|
const NodeMpvErrorCode = {
|
||||||
0: 'Unable to load file or stream',
|
0: 'Unable to load file or stream',
|
||||||
@@ -62,7 +65,6 @@ const mpvLog = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
const MPV_BINARY_PATH = store.get('mpv_path') as string | undefined;
|
const MPV_BINARY_PATH = store.get('mpv_path') as string | undefined;
|
||||||
const isDevelopment = process.env.NODE_ENV === 'development' || process.env.DEBUG_PROD === 'true';
|
|
||||||
|
|
||||||
const prefetchPlaylistParams = [
|
const prefetchPlaylistParams = [
|
||||||
'--prefetch-playlist=no',
|
'--prefetch-playlist=no',
|
||||||
@@ -89,14 +91,12 @@ const createMpv = async (data: {
|
|||||||
|
|
||||||
const params = uniq([...DEFAULT_MPV_PARAMETERS(extraParameters), ...(extraParameters || [])]);
|
const params = uniq([...DEFAULT_MPV_PARAMETERS(extraParameters), ...(extraParameters || [])]);
|
||||||
|
|
||||||
const extra = isDevelopment ? '-dev' : '';
|
|
||||||
|
|
||||||
const mpv = new MpvAPI(
|
const mpv = new MpvAPI(
|
||||||
{
|
{
|
||||||
audio_only: true,
|
audio_only: true,
|
||||||
auto_restart: false,
|
auto_restart: false,
|
||||||
binary: binaryPath || MPV_BINARY_PATH || undefined,
|
binary: binaryPath || MPV_BINARY_PATH || undefined,
|
||||||
socket: isWindows() ? `\\\\.\\pipe\\mpvserver${extra}` : `/tmp/node-mpv${extra}.sock`,
|
socket: socketPath,
|
||||||
time_update: 1,
|
time_update: 1,
|
||||||
},
|
},
|
||||||
params,
|
params,
|
||||||
@@ -149,6 +149,16 @@ export const getMpvInstance = () => {
|
|||||||
return mpvInstance;
|
return mpvInstance;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const quit = async () => {
|
||||||
|
const instance = getMpvInstance();
|
||||||
|
if (instance) {
|
||||||
|
await instance.quit();
|
||||||
|
if (!isWindows()) {
|
||||||
|
await rm(socketPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const setAudioPlayerFallback = (isError: boolean) => {
|
const setAudioPlayerFallback = (isError: boolean) => {
|
||||||
getMainWindow()?.webContents.send('renderer-player-fallback', isError);
|
getMainWindow()?.webContents.send('renderer-player-fallback', isError);
|
||||||
};
|
};
|
||||||
@@ -216,7 +226,7 @@ ipcMain.handle(
|
|||||||
ipcMain.on('player-quit', async () => {
|
ipcMain.on('player-quit', async () => {
|
||||||
try {
|
try {
|
||||||
await getMpvInstance()?.stop();
|
await getMpvInstance()?.stop();
|
||||||
await getMpvInstance()?.quit();
|
await quit();
|
||||||
} catch (err: NodeMpvError | any) {
|
} catch (err: NodeMpvError | any) {
|
||||||
mpvLog({ action: 'Failed to quit mpv' }, err);
|
mpvLog({ action: 'Failed to quit mpv' }, err);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -412,19 +422,34 @@ ipcMain.handle('player-get-time', async (): Promise<number | undefined> => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
app.on('before-quit', async () => {
|
enum MpvState {
|
||||||
try {
|
STARTED,
|
||||||
await getMpvInstance()?.stop();
|
IN_PROGRESS,
|
||||||
await getMpvInstance()?.quit();
|
DONE,
|
||||||
} catch (err: NodeMpvError | any) {
|
}
|
||||||
mpvLog({ action: `Failed to cleanly before-quit` }, err);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
app.on('window-all-closed', async () => {
|
let mpvState = MpvState.STARTED;
|
||||||
try {
|
|
||||||
await getMpvInstance()?.quit();
|
app.on('before-quit', async (event) => {
|
||||||
} catch (err: NodeMpvError | any) {
|
switch (mpvState) {
|
||||||
mpvLog({ action: `Failed to cleanly exit` }, err);
|
case MpvState.DONE:
|
||||||
|
return;
|
||||||
|
case MpvState.IN_PROGRESS:
|
||||||
|
event.preventDefault();
|
||||||
|
break;
|
||||||
|
case MpvState.STARTED: {
|
||||||
|
try {
|
||||||
|
mpvState = MpvState.IN_PROGRESS;
|
||||||
|
event.preventDefault();
|
||||||
|
await getMpvInstance()?.stop();
|
||||||
|
await quit();
|
||||||
|
} catch (err: NodeMpvError | any) {
|
||||||
|
mpvLog({ action: `Failed to cleanly before-quit` }, err);
|
||||||
|
} finally {
|
||||||
|
mpvState = MpvState.DONE;
|
||||||
|
app.quit();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -8,9 +8,10 @@ import { app, ipcMain } from 'electron';
|
|||||||
import { Server as WsServer, WebSocketServer, WebSocket } from 'ws';
|
import { Server as WsServer, WebSocketServer, WebSocket } from 'ws';
|
||||||
import manifest from './manifest.json';
|
import manifest from './manifest.json';
|
||||||
import { ClientEvent, ServerEvent } from '../../../../remote/types';
|
import { ClientEvent, ServerEvent } from '../../../../remote/types';
|
||||||
import { PlayerRepeat, SongUpdate } from '../../../../renderer/types';
|
import { PlayerRepeat, PlayerStatus, SongState } from '../../../../renderer/types';
|
||||||
import { getMainWindow } from '../../../main';
|
import { getMainWindow } from '../../../main';
|
||||||
import { isLinux } from '../../../utils';
|
import { isLinux } from '../../../utils';
|
||||||
|
import type { QueueSong } from '/@/renderer/api/types';
|
||||||
|
|
||||||
let mprisPlayer: any | undefined;
|
let mprisPlayer: any | undefined;
|
||||||
|
|
||||||
@@ -100,9 +101,7 @@ enum Encoding {
|
|||||||
const GZIP_REGEX = /\bgzip\b/;
|
const GZIP_REGEX = /\bgzip\b/;
|
||||||
const ZLIB_REGEX = /bdeflate\b/;
|
const ZLIB_REGEX = /bdeflate\b/;
|
||||||
|
|
||||||
let currentSong: SongUpdate = {
|
const currentState: SongState = {};
|
||||||
currentTime: 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
const getEncoding = (encoding: string | string[]): Encoding => {
|
const getEncoding = (encoding: string | string[]): Encoding => {
|
||||||
const encodingArray = Array.isArray(encoding) ? encoding : [encoding];
|
const encodingArray = Array.isArray(encoding) ? encoding : [encoding];
|
||||||
@@ -388,7 +387,7 @@ const enableServer = (config: RemoteConfig): Promise<void> => {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'proxy': {
|
case 'proxy': {
|
||||||
const toFetch = currentSong.song?.imageUrl?.replaceAll(
|
const toFetch = currentState.song?.imageUrl?.replaceAll(
|
||||||
/&(size|width|height=\d+)/g,
|
/&(size|width|height=\d+)/g,
|
||||||
'',
|
'',
|
||||||
);
|
);
|
||||||
@@ -438,9 +437,9 @@ const enableServer = (config: RemoteConfig): Promise<void> => {
|
|||||||
volume = 0;
|
volume = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
currentSong.volume = volume;
|
currentState.volume = volume;
|
||||||
|
|
||||||
broadcast({ data: { volume }, event: 'song' });
|
broadcast({ data: volume, event: 'volume' });
|
||||||
getMainWindow()?.webContents.send('request-volume', {
|
getMainWindow()?.webContents.send('request-volume', {
|
||||||
volume,
|
volume,
|
||||||
});
|
});
|
||||||
@@ -452,22 +451,22 @@ const enableServer = (config: RemoteConfig): Promise<void> => {
|
|||||||
}
|
}
|
||||||
case 'favorite': {
|
case 'favorite': {
|
||||||
const { favorite, id } = json;
|
const { favorite, id } = json;
|
||||||
if (id && id === currentSong.song?.id) {
|
if (id && id === currentState.song?.id) {
|
||||||
getMainWindow()?.webContents.send('request-favorite', {
|
getMainWindow()?.webContents.send('request-favorite', {
|
||||||
favorite,
|
favorite,
|
||||||
id,
|
id,
|
||||||
serverId: currentSong.song.serverId,
|
serverId: currentState.song.serverId,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'rating': {
|
case 'rating': {
|
||||||
const { rating, id } = json;
|
const { rating, id } = json;
|
||||||
if (id && id === currentSong.song?.id) {
|
if (id && id === currentState.song?.id) {
|
||||||
getMainWindow()?.webContents.send('request-rating', {
|
getMainWindow()?.webContents.send('request-rating', {
|
||||||
id,
|
id,
|
||||||
rating,
|
rating,
|
||||||
serverId: currentSong.song.serverId,
|
serverId: currentState.song.serverId,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -482,7 +481,7 @@ const enableServer = (config: RemoteConfig): Promise<void> => {
|
|||||||
ws.alive = true;
|
ws.alive = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
ws.send(JSON.stringify({ data: currentSong, event: 'song' }));
|
ws.send(JSON.stringify({ data: currentState, event: 'state' }));
|
||||||
});
|
});
|
||||||
|
|
||||||
const heartBeat = setInterval(() => {
|
const heartBeat = setInterval(() => {
|
||||||
@@ -564,13 +563,13 @@ ipcMain.on('remote-username', (_event, username: string) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.on('update-favorite', (_event, favorite: boolean, serverId: string, ids: string[]) => {
|
ipcMain.on('update-favorite', (_event, favorite: boolean, serverId: string, ids: string[]) => {
|
||||||
if (currentSong.song?.serverId !== serverId) return;
|
if (currentState.song?.serverId !== serverId) return;
|
||||||
|
|
||||||
const id = currentSong.song.id;
|
const id = currentState.song.id;
|
||||||
|
|
||||||
for (const songId of ids) {
|
for (const songId of ids) {
|
||||||
if (songId === id) {
|
if (songId === id) {
|
||||||
currentSong.song.userFavorite = favorite;
|
currentState.song.userFavorite = favorite;
|
||||||
broadcast({ data: { favorite, id: songId }, event: 'favorite' });
|
broadcast({ data: { favorite, id: songId }, event: 'favorite' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -578,13 +577,13 @@ ipcMain.on('update-favorite', (_event, favorite: boolean, serverId: string, ids:
|
|||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.on('update-rating', (_event, rating: number, serverId: string, ids: string[]) => {
|
ipcMain.on('update-rating', (_event, rating: number, serverId: string, ids: string[]) => {
|
||||||
if (currentSong.song?.serverId !== serverId) return;
|
if (currentState.song?.serverId !== serverId) return;
|
||||||
|
|
||||||
const id = currentSong.song.id;
|
const id = currentState.song.id;
|
||||||
|
|
||||||
for (const songId of ids) {
|
for (const songId of ids) {
|
||||||
if (songId === id) {
|
if (songId === id) {
|
||||||
currentSong.song.userRating = rating;
|
currentState.song.userRating = rating;
|
||||||
broadcast({ data: { id: songId, rating }, event: 'rating' });
|
broadcast({ data: { id: songId, rating }, event: 'rating' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -592,42 +591,32 @@ ipcMain.on('update-rating', (_event, rating: number, serverId: string, ids: stri
|
|||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.on('update-repeat', (_event, repeat: PlayerRepeat) => {
|
ipcMain.on('update-repeat', (_event, repeat: PlayerRepeat) => {
|
||||||
currentSong.repeat = repeat;
|
currentState.repeat = repeat;
|
||||||
broadcast({ data: { repeat }, event: 'song' });
|
broadcast({ data: repeat, event: 'repeat' });
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.on('update-shuffle', (_event, shuffle: boolean) => {
|
ipcMain.on('update-shuffle', (_event, shuffle: boolean) => {
|
||||||
currentSong.shuffle = shuffle;
|
currentState.shuffle = shuffle;
|
||||||
broadcast({ data: { shuffle }, event: 'song' });
|
broadcast({ data: shuffle, event: 'shuffle' });
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.on('update-song', (_event, data: SongUpdate) => {
|
ipcMain.on('update-playback', (_event, status: PlayerStatus) => {
|
||||||
const { song, ...rest } = data;
|
currentState.status = status;
|
||||||
const songChanged = song?.id !== currentSong.song?.id;
|
broadcast({ data: status, event: 'playback' });
|
||||||
|
});
|
||||||
|
|
||||||
if (!song?.id) {
|
ipcMain.on('update-song', (_event, song: QueueSong | undefined) => {
|
||||||
currentSong = {
|
const songChanged = song?.id !== currentState.song?.id;
|
||||||
...currentSong,
|
currentState.song = song;
|
||||||
...data,
|
|
||||||
song: undefined,
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
currentSong = {
|
|
||||||
...currentSong,
|
|
||||||
...data,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (songChanged) {
|
if (songChanged) {
|
||||||
broadcast({ data: { ...rest, song: song || null }, event: 'song' });
|
broadcast({ data: song || null, event: 'song' });
|
||||||
} else {
|
|
||||||
broadcast({ data: rest, event: 'song' });
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.on('update-volume', (_event, volume: number) => {
|
ipcMain.on('update-volume', (_event, volume: number) => {
|
||||||
currentSong.volume = volume;
|
currentState.volume = volume;
|
||||||
broadcast({ data: { volume }, event: 'song' });
|
broadcast({ data: volume, event: 'volume' });
|
||||||
});
|
});
|
||||||
|
|
||||||
if (mprisPlayer) {
|
if (mprisPlayer) {
|
||||||
@@ -639,13 +628,13 @@ if (mprisPlayer) {
|
|||||||
? PlayerRepeat.ONE
|
? PlayerRepeat.ONE
|
||||||
: PlayerRepeat.NONE;
|
: PlayerRepeat.NONE;
|
||||||
|
|
||||||
currentSong.repeat = repeat;
|
currentState.repeat = repeat;
|
||||||
broadcast({ data: { repeat }, event: 'song' });
|
broadcast({ data: repeat, event: 'repeat' });
|
||||||
});
|
});
|
||||||
|
|
||||||
mprisPlayer.on('shuffle', (shuffle: boolean) => {
|
mprisPlayer.on('shuffle', (shuffle: boolean) => {
|
||||||
currentSong.shuffle = shuffle;
|
currentState.shuffle = shuffle;
|
||||||
broadcast({ data: { shuffle }, event: 'song' });
|
broadcast({ data: shuffle, event: 'shuffle' });
|
||||||
});
|
});
|
||||||
|
|
||||||
mprisPlayer.on('volume', (vol: number) => {
|
mprisPlayer.on('volume', (vol: number) => {
|
||||||
@@ -656,7 +645,7 @@ if (mprisPlayer) {
|
|||||||
} else if (volume < 0) {
|
} else if (volume < 0) {
|
||||||
volume = 0;
|
volume = 0;
|
||||||
}
|
}
|
||||||
currentSong.volume = volume;
|
currentState.volume = volume;
|
||||||
broadcast({ data: { volume }, event: 'song' });
|
broadcast({ data: volume, event: 'volume' });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { ipcMain } from 'electron';
|
import { ipcMain } from 'electron';
|
||||||
import Player from 'mpris-service';
|
import Player from 'mpris-service';
|
||||||
import { PlayerRepeat, PlayerStatus, SongUpdate } from '../../../renderer/types';
|
import { PlayerRepeat, PlayerStatus } from '../../../renderer/types';
|
||||||
import { getMainWindow } from '../../main';
|
import { getMainWindow } from '../../main';
|
||||||
|
import { QueueSong } from '/@/renderer/api/types';
|
||||||
|
|
||||||
const mprisPlayer = Player({
|
const mprisPlayer = Player({
|
||||||
identity: 'Feishin',
|
identity: 'Feishin',
|
||||||
@@ -117,6 +118,10 @@ ipcMain.on('update-volume', (_event, volume) => {
|
|||||||
mprisPlayer.volume = Number(volume) / 100;
|
mprisPlayer.volume = Number(volume) / 100;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ipcMain.on('update-playback', (_event, status: PlayerStatus) => {
|
||||||
|
mprisPlayer.playbackStatus = status === PlayerStatus.PLAYING ? 'Playing' : 'Paused';
|
||||||
|
});
|
||||||
|
|
||||||
const REPEAT_TO_MPRIS: Record<PlayerRepeat, string> = {
|
const REPEAT_TO_MPRIS: Record<PlayerRepeat, string> = {
|
||||||
[PlayerRepeat.ALL]: 'Playlist',
|
[PlayerRepeat.ALL]: 'Playlist',
|
||||||
[PlayerRepeat.ONE]: 'Track',
|
[PlayerRepeat.ONE]: 'Track',
|
||||||
@@ -131,21 +136,9 @@ ipcMain.on('update-shuffle', (_event, shuffle: boolean) => {
|
|||||||
mprisPlayer.shuffle = shuffle;
|
mprisPlayer.shuffle = shuffle;
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.on('update-song', (_event, args: SongUpdate) => {
|
ipcMain.on('update-song', (_event, song: QueueSong | undefined) => {
|
||||||
const { song, status, repeat, shuffle } = args || {};
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
mprisPlayer.playbackStatus = status === PlayerStatus.PLAYING ? 'Playing' : 'Paused';
|
if (!song?.id) {
|
||||||
|
|
||||||
if (repeat) {
|
|
||||||
mprisPlayer.loopStatus = REPEAT_TO_MPRIS[repeat];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (shuffle) {
|
|
||||||
mprisPlayer.shuffle = shuffle;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!song) {
|
|
||||||
mprisPlayer.metadata = {};
|
mprisPlayer.metadata = {};
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
+24
-19
@@ -67,8 +67,8 @@ if (store.get('ignore_ssl')) {
|
|||||||
|
|
||||||
// From https://github.com/tutao/tutanota/commit/92c6ed27625fcf367f0fbcc755d83d7ff8fde94b
|
// From https://github.com/tutao/tutanota/commit/92c6ed27625fcf367f0fbcc755d83d7ff8fde94b
|
||||||
if (isLinux() && !process.argv.some((a) => a.startsWith('--password-store='))) {
|
if (isLinux() && !process.argv.some((a) => a.startsWith('--password-store='))) {
|
||||||
const paswordStore = store.get('password_store', 'gnome-libsecret') as string;
|
const passwordStore = store.get('password_store', 'gnome-libsecret') as string;
|
||||||
app.commandLine.appendSwitch('password-store', paswordStore);
|
app.commandLine.appendSwitch('password-store', passwordStore);
|
||||||
}
|
}
|
||||||
|
|
||||||
let mainWindow: BrowserWindow | null = null;
|
let mainWindow: BrowserWindow | null = null;
|
||||||
@@ -199,7 +199,7 @@ const createTray = () => {
|
|||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
tray.on('double-click', () => {
|
tray.on('click', () => {
|
||||||
mainWindow?.show();
|
mainWindow?.show();
|
||||||
createWinThumbarButtons();
|
createWinThumbarButtons();
|
||||||
});
|
});
|
||||||
@@ -210,7 +210,7 @@ const createTray = () => {
|
|||||||
|
|
||||||
const createWindow = async (first = true) => {
|
const createWindow = async (first = true) => {
|
||||||
if (isDevelopment) {
|
if (isDevelopment) {
|
||||||
await installExtensions();
|
await installExtensions().catch(console.log);
|
||||||
}
|
}
|
||||||
|
|
||||||
const nativeFrame = store.get('window_window_bar_style') === 'linux';
|
const nativeFrame = store.get('window_window_bar_style') === 'linux';
|
||||||
@@ -364,20 +364,6 @@ const createWindow = async (first = true) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('open-item', async (_event, path: string) => {
|
|
||||||
return new Promise<void>((resolve, reject) => {
|
|
||||||
access(path, constants.F_OK, (error) => {
|
|
||||||
if (error) {
|
|
||||||
reject(error);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
shell.showItemInFolder(path);
|
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const globalMediaKeysEnabled = store.get('global_media_hotkeys', true) as boolean;
|
const globalMediaKeysEnabled = store.get('global_media_hotkeys', true) as boolean;
|
||||||
|
|
||||||
if (globalMediaKeysEnabled) {
|
if (globalMediaKeysEnabled) {
|
||||||
@@ -487,7 +473,7 @@ const createWindow = async (first = true) => {
|
|||||||
const menuBuilder = new MenuBuilder(mainWindow);
|
const menuBuilder = new MenuBuilder(mainWindow);
|
||||||
menuBuilder.buildMenu();
|
menuBuilder.buildMenu();
|
||||||
|
|
||||||
// Open urls in the user's browser
|
// Open URLs in the user's browser
|
||||||
mainWindow.webContents.setWindowOpenHandler((edata) => {
|
mainWindow.webContents.setWindowOpenHandler((edata) => {
|
||||||
shell.openExternal(edata.url);
|
shell.openExternal(edata.url);
|
||||||
return { action: 'deny' };
|
return { action: 'deny' };
|
||||||
@@ -629,6 +615,8 @@ if (!singleInstance) {
|
|||||||
if (mainWindow) {
|
if (mainWindow) {
|
||||||
if (mainWindow.isMinimized()) {
|
if (mainWindow.isMinimized()) {
|
||||||
mainWindow.restore();
|
mainWindow.restore();
|
||||||
|
} else if (!mainWindow.isVisible()) {
|
||||||
|
mainWindow.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
mainWindow.focus();
|
mainWindow.focus();
|
||||||
@@ -668,3 +656,20 @@ if (!singleInstance) {
|
|||||||
})
|
})
|
||||||
.catch(console.log);
|
.catch(console.log);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Register 'open-item' handler globally, ensuring it is only registered once
|
||||||
|
if (!ipcMain.eventNames().includes('open-item')) {
|
||||||
|
ipcMain.handle('open-item', async (_event, path: string) => {
|
||||||
|
return new Promise<void>((resolve, reject) => {
|
||||||
|
access(path, constants.F_OK, (error) => {
|
||||||
|
if (error) {
|
||||||
|
reject(error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
shell.showItemInFolder(path);
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { IpcRendererEvent, ipcRenderer } from 'electron';
|
import { IpcRendererEvent, ipcRenderer } from 'electron';
|
||||||
import { SongUpdate } from '/@/renderer/types';
|
import { QueueSong } from '/@/renderer/api/types';
|
||||||
|
import { PlayerStatus } from '/@/renderer/types';
|
||||||
|
|
||||||
const requestFavorite = (
|
const requestFavorite = (
|
||||||
cb: (
|
cb: (
|
||||||
@@ -46,6 +47,10 @@ const updatePassword = (password: string) => {
|
|||||||
ipcRenderer.send('remote-password', password);
|
ipcRenderer.send('remote-password', password);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const updatePlayback = (playback: PlayerStatus) => {
|
||||||
|
ipcRenderer.send('update-playback', playback);
|
||||||
|
};
|
||||||
|
|
||||||
const updateSetting = (
|
const updateSetting = (
|
||||||
enabled: boolean,
|
enabled: boolean,
|
||||||
port: number,
|
port: number,
|
||||||
@@ -67,7 +72,7 @@ const updateShuffle = (shuffle: boolean) => {
|
|||||||
ipcRenderer.send('update-shuffle', shuffle);
|
ipcRenderer.send('update-shuffle', shuffle);
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateSong = (args: SongUpdate) => {
|
const updateSong = (args: QueueSong | undefined) => {
|
||||||
ipcRenderer.send('update-song', args);
|
ipcRenderer.send('update-song', args);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -89,6 +94,7 @@ export const remote = {
|
|||||||
setRemotePort,
|
setRemotePort,
|
||||||
updateFavorite,
|
updateFavorite,
|
||||||
updatePassword,
|
updatePassword,
|
||||||
|
updatePlayback,
|
||||||
updateRating,
|
updateRating,
|
||||||
updateRepeat,
|
updateRepeat,
|
||||||
updateSetting,
|
updateSetting,
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import {
|
|||||||
import { PlayerRepeat, PlayerStatus } from '/@/renderer/types';
|
import { PlayerRepeat, PlayerStatus } from '/@/renderer/types';
|
||||||
import { WrapperSlider } from '/@/remote/components/wrapped-slider';
|
import { WrapperSlider } from '/@/remote/components/wrapped-slider';
|
||||||
import { Tooltip } from '/@/renderer/components/tooltip';
|
import { Tooltip } from '/@/renderer/components/tooltip';
|
||||||
import { Rating } from '/@/renderer/components';
|
import { Rating } from '/@/renderer/components/rating';
|
||||||
|
|
||||||
export const RemoteContainer = () => {
|
export const RemoteContainer = () => {
|
||||||
const { repeat, shuffle, song, status, volume } = useInfo();
|
const { repeat, shuffle, song, status, volume } = useInfo();
|
||||||
@@ -38,7 +38,7 @@ export const RemoteContainer = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{song && (
|
{id && (
|
||||||
<>
|
<>
|
||||||
<Title order={1}>{song.name}</Title>
|
<Title order={1}>{song.name}</Title>
|
||||||
<Group align="flex-end">
|
<Group align="flex-end">
|
||||||
@@ -61,7 +61,7 @@ export const RemoteContainer = () => {
|
|||||||
spacing={0}
|
spacing={0}
|
||||||
>
|
>
|
||||||
<RemoteButton
|
<RemoteButton
|
||||||
disabled={!song}
|
disabled={!id}
|
||||||
tooltip="Previous track"
|
tooltip="Previous track"
|
||||||
variant="default"
|
variant="default"
|
||||||
onClick={() => send({ event: 'previous' })}
|
onClick={() => send({ event: 'previous' })}
|
||||||
@@ -69,8 +69,8 @@ export const RemoteContainer = () => {
|
|||||||
<RiSkipBackFill size={25} />
|
<RiSkipBackFill size={25} />
|
||||||
</RemoteButton>
|
</RemoteButton>
|
||||||
<RemoteButton
|
<RemoteButton
|
||||||
disabled={!song}
|
disabled={!id}
|
||||||
tooltip={song && status === PlayerStatus.PLAYING ? 'Pause' : 'Play'}
|
tooltip={id && status === PlayerStatus.PLAYING ? 'Pause' : 'Play'}
|
||||||
variant="default"
|
variant="default"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (status === PlayerStatus.PLAYING) {
|
if (status === PlayerStatus.PLAYING) {
|
||||||
@@ -80,14 +80,14 @@ export const RemoteContainer = () => {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{song && status === PlayerStatus.PLAYING ? (
|
{id && status === PlayerStatus.PLAYING ? (
|
||||||
<RiPauseFill size={25} />
|
<RiPauseFill size={25} />
|
||||||
) : (
|
) : (
|
||||||
<RiPlayFill size={25} />
|
<RiPlayFill size={25} />
|
||||||
)}
|
)}
|
||||||
</RemoteButton>
|
</RemoteButton>
|
||||||
<RemoteButton
|
<RemoteButton
|
||||||
disabled={!song}
|
disabled={!id}
|
||||||
tooltip="Next track"
|
tooltip="Next track"
|
||||||
variant="default"
|
variant="default"
|
||||||
onClick={() => send({ event: 'next' })}
|
onClick={() => send({ event: 'next' })}
|
||||||
@@ -127,7 +127,7 @@ export const RemoteContainer = () => {
|
|||||||
</RemoteButton>
|
</RemoteButton>
|
||||||
<RemoteButton
|
<RemoteButton
|
||||||
$active={song?.userFavorite}
|
$active={song?.userFavorite}
|
||||||
disabled={!song}
|
disabled={!id}
|
||||||
tooltip={song?.userFavorite ? 'Unfavorite' : 'Favorite'}
|
tooltip={song?.userFavorite ? 'Unfavorite' : 'Favorite'}
|
||||||
variant="default"
|
variant="default"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import merge from 'lodash/merge';
|
|||||||
import { create } from 'zustand';
|
import { create } from 'zustand';
|
||||||
import { devtools, persist } from 'zustand/middleware';
|
import { devtools, persist } from 'zustand/middleware';
|
||||||
import { immer } from 'zustand/middleware/immer';
|
import { immer } from 'zustand/middleware/immer';
|
||||||
import { ClientEvent, ServerEvent, SongUpdateSocket } from '/@/remote/types';
|
import type { ClientEvent, ServerEvent, SongUpdateSocket } from '/@/remote/types';
|
||||||
|
|
||||||
interface StatefulWebSocket extends WebSocket {
|
interface StatefulWebSocket extends WebSocket {
|
||||||
natural: boolean;
|
natural: boolean;
|
||||||
@@ -133,6 +133,12 @@ export const useRemoteStore = create<SettingsSlice>()(
|
|||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case 'playback': {
|
||||||
|
set((state) => {
|
||||||
|
state.info.status = data;
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
case 'proxy': {
|
case 'proxy': {
|
||||||
set((state) => {
|
set((state) => {
|
||||||
if (state.info.song) {
|
if (state.info.song) {
|
||||||
@@ -149,9 +155,34 @@ export const useRemoteStore = create<SettingsSlice>()(
|
|||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case 'repeat': {
|
||||||
|
set((state) => {
|
||||||
|
state.info.repeat = data;
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'shuffle': {
|
||||||
|
set((state) => {
|
||||||
|
state.info.shuffle = data;
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
case 'song': {
|
case 'song': {
|
||||||
set((nested) => {
|
set((state) => {
|
||||||
nested.info = { ...nested.info, ...data };
|
console.log(data);
|
||||||
|
state.info.song = data;
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'state': {
|
||||||
|
set((state) => {
|
||||||
|
state.info = data;
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'volume': {
|
||||||
|
set((state) => {
|
||||||
|
state.info.volume = data;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -212,11 +243,9 @@ export const useRemoteStore = create<SettingsSlice>()(
|
|||||||
{ name: 'store_settings' },
|
{ name: 'store_settings' },
|
||||||
),
|
),
|
||||||
{
|
{
|
||||||
merge: (persistedState, currentState) => {
|
merge: (persistedState, currentState) => merge(currentState, persistedState),
|
||||||
return merge(currentState, persistedState);
|
|
||||||
},
|
|
||||||
name: 'store_settings',
|
name: 'store_settings',
|
||||||
version: 6,
|
version: 7,
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
+39
-4
@@ -1,7 +1,7 @@
|
|||||||
import type { QueueSong } from '/@/renderer/api/types';
|
import type { QueueSong } from '/@/renderer/api/types';
|
||||||
import type { SongUpdate } from '/@/renderer/types';
|
import type { PlayerRepeat, PlayerStatus, SongState } from '/@/renderer/types';
|
||||||
|
|
||||||
export interface SongUpdateSocket extends Omit<SongUpdate, 'song'> {
|
export interface SongUpdateSocket extends Omit<SongState, 'song'> {
|
||||||
song?: QueueSong | null;
|
song?: QueueSong | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -15,6 +15,11 @@ export interface ServerFavorite {
|
|||||||
event: 'favorite';
|
event: 'favorite';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ServerPlayStatus {
|
||||||
|
data: PlayerStatus;
|
||||||
|
event: 'playback';
|
||||||
|
}
|
||||||
|
|
||||||
export interface ServerProxy {
|
export interface ServerProxy {
|
||||||
data: string;
|
data: string;
|
||||||
event: 'proxy';
|
event: 'proxy';
|
||||||
@@ -25,12 +30,42 @@ export interface ServerRating {
|
|||||||
event: 'rating';
|
event: 'rating';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ServerRepeat {
|
||||||
|
data: PlayerRepeat;
|
||||||
|
event: 'repeat';
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ServerShuffle {
|
||||||
|
data: boolean;
|
||||||
|
event: 'shuffle';
|
||||||
|
}
|
||||||
|
|
||||||
export interface ServerSong {
|
export interface ServerSong {
|
||||||
data: SongUpdateSocket;
|
data: QueueSong | null;
|
||||||
event: 'song';
|
event: 'song';
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ServerEvent = ServerError | ServerFavorite | ServerRating | ServerSong | ServerProxy;
|
export interface ServerState {
|
||||||
|
data: SongState;
|
||||||
|
event: 'state';
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ServerVolume {
|
||||||
|
data: number;
|
||||||
|
event: 'volume';
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ServerEvent =
|
||||||
|
| ServerError
|
||||||
|
| ServerFavorite
|
||||||
|
| ServerPlayStatus
|
||||||
|
| ServerRating
|
||||||
|
| ServerRepeat
|
||||||
|
| ServerShuffle
|
||||||
|
| ServerSong
|
||||||
|
| ServerState
|
||||||
|
| ServerProxy
|
||||||
|
| ServerVolume;
|
||||||
|
|
||||||
export interface ClientSimpleEvent {
|
export interface ClientSimpleEvent {
|
||||||
event: 'next' | 'pause' | 'play' | 'previous' | 'proxy' | 'repeat' | 'shuffle';
|
event: 'next' | 'pause' | 'play' | 'previous' | 'proxy' | 'repeat' | 'shuffle';
|
||||||
|
|||||||
@@ -6,8 +6,9 @@ import qs from 'qs';
|
|||||||
import { ServerListItem } from '/@/renderer/api/types';
|
import { ServerListItem } from '/@/renderer/api/types';
|
||||||
import omitBy from 'lodash/omitBy';
|
import omitBy from 'lodash/omitBy';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { authenticationFailure } from '/@/renderer/api/utils';
|
import { authenticationFailure, getClientType } from '/@/renderer/api/utils';
|
||||||
import i18n from '/@/i18n/i18n';
|
import i18n from '/@/i18n/i18n';
|
||||||
|
import packageJson from '../../../../package.json';
|
||||||
|
|
||||||
const c = initContract();
|
const c = initContract();
|
||||||
|
|
||||||
@@ -24,9 +25,6 @@ export const contract = c.router({
|
|||||||
},
|
},
|
||||||
authenticate: {
|
authenticate: {
|
||||||
body: jfType._parameters.authenticate,
|
body: jfType._parameters.authenticate,
|
||||||
headers: z.object({
|
|
||||||
'X-Emby-Authorization': z.string(),
|
|
||||||
}),
|
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
path: 'users/authenticatebyname',
|
path: 'users/authenticatebyname',
|
||||||
responses: {
|
responses: {
|
||||||
@@ -333,6 +331,12 @@ const parsePath = (fullPath: string) => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const createAuthHeader = (): string => {
|
||||||
|
return `MediaBrowser Client="Feishin", Device="${getClientType()}", DeviceId="${
|
||||||
|
useAuthStore.getState().deviceId
|
||||||
|
}", Version="${packageJson.version}"`;
|
||||||
|
};
|
||||||
|
|
||||||
export const jfApiClient = (args: {
|
export const jfApiClient = (args: {
|
||||||
server: ServerListItem | null;
|
server: ServerListItem | null;
|
||||||
signal?: AbortSignal;
|
signal?: AbortSignal;
|
||||||
@@ -359,7 +363,9 @@ export const jfApiClient = (args: {
|
|||||||
data: body,
|
data: body,
|
||||||
headers: {
|
headers: {
|
||||||
...headers,
|
...headers,
|
||||||
...(token && { 'X-MediaBrowser-Token': token }),
|
...(token
|
||||||
|
? { Authorization: createAuthHeader().concat(`, Token="${token}"`) }
|
||||||
|
: { Authorization: createAuthHeader() }),
|
||||||
},
|
},
|
||||||
method: method as Method,
|
method: method as Method,
|
||||||
params,
|
params,
|
||||||
|
|||||||
@@ -57,10 +57,8 @@ import {
|
|||||||
import { jfApiClient } from '/@/renderer/api/jellyfin/jellyfin-api';
|
import { jfApiClient } from '/@/renderer/api/jellyfin/jellyfin-api';
|
||||||
import { jfNormalize } from './jellyfin-normalize';
|
import { jfNormalize } from './jellyfin-normalize';
|
||||||
import { jfType } from '/@/renderer/api/jellyfin/jellyfin-types';
|
import { jfType } from '/@/renderer/api/jellyfin/jellyfin-types';
|
||||||
import packageJson from '../../../../package.json';
|
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { JFSongListSort, JFSortOrder } from '/@/renderer/api/jellyfin.types';
|
import { JFSongListSort, JFSortOrder } from '/@/renderer/api/jellyfin.types';
|
||||||
import isElectron from 'is-electron';
|
|
||||||
import { ServerFeature } from '/@/renderer/api/features-types';
|
import { ServerFeature } from '/@/renderer/api/features-types';
|
||||||
import { VersionInfo, getFeatures } from '/@/renderer/api/utils';
|
import { VersionInfo, getFeatures } from '/@/renderer/api/utils';
|
||||||
import chunk from 'lodash/chunk';
|
import chunk from 'lodash/chunk';
|
||||||
@@ -69,31 +67,6 @@ const formatCommaDelimitedString = (value: string[]) => {
|
|||||||
return value.join(',');
|
return value.join(',');
|
||||||
};
|
};
|
||||||
|
|
||||||
function getHostname(): string {
|
|
||||||
if (isElectron()) {
|
|
||||||
return 'Desktop Client';
|
|
||||||
}
|
|
||||||
const agent = navigator.userAgent;
|
|
||||||
switch (true) {
|
|
||||||
case agent.toLowerCase().indexOf('edge') > -1:
|
|
||||||
return 'Microsoft Edge';
|
|
||||||
case agent.toLowerCase().indexOf('edg/') > -1:
|
|
||||||
return 'Edge Chromium'; // Match also / to avoid matching for the older Edge
|
|
||||||
case agent.toLowerCase().indexOf('opr') > -1:
|
|
||||||
return 'Opera';
|
|
||||||
case agent.toLowerCase().indexOf('chrome') > -1:
|
|
||||||
return 'Chrome';
|
|
||||||
case agent.toLowerCase().indexOf('trident') > -1:
|
|
||||||
return 'Internet Explorer';
|
|
||||||
case agent.toLowerCase().indexOf('firefox') > -1:
|
|
||||||
return 'Firefox';
|
|
||||||
case agent.toLowerCase().indexOf('safari') > -1:
|
|
||||||
return 'Safari';
|
|
||||||
default:
|
|
||||||
return 'PC';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const authenticate = async (
|
const authenticate = async (
|
||||||
url: string,
|
url: string,
|
||||||
body: {
|
body: {
|
||||||
@@ -108,11 +81,6 @@ const authenticate = async (
|
|||||||
Pw: body.password,
|
Pw: body.password,
|
||||||
Username: body.username,
|
Username: body.username,
|
||||||
},
|
},
|
||||||
headers: {
|
|
||||||
'x-emby-authorization': `MediaBrowser Client="Feishin", Device="${getHostname()}", DeviceId="Feishin-${getHostname()}-${encodeURIComponent(
|
|
||||||
body.username,
|
|
||||||
)}", Version="${packageJson.version}"`,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res.status !== 200) {
|
if (res.status !== 200) {
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ const getStreamUrl = (args: {
|
|||||||
`?userId=${server?.userId}` +
|
`?userId=${server?.userId}` +
|
||||||
`&deviceId=${deviceId}` +
|
`&deviceId=${deviceId}` +
|
||||||
'&audioCodec=aac' +
|
'&audioCodec=aac' +
|
||||||
`&api_key=${server?.credential}` +
|
`&apiKey=${server?.credential}` +
|
||||||
`&playSessionId=${deviceId}` +
|
`&playSessionId=${deviceId}` +
|
||||||
'&container=opus,mp3,aac,m4a,m4b,flac,wav,ogg' +
|
'&container=opus,mp3,aac,m4a,m4b,flac,wav,ogg' +
|
||||||
'&transcodingContainer=ts' +
|
'&transcodingContainer=ts' +
|
||||||
|
|||||||
@@ -193,7 +193,16 @@ const getAlbumArtistList = async (args: AlbumArtistListArgs): Promise<AlbumArtis
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
items: res.body.data.map((albumArtist) =>
|
items: res.body.data.map((albumArtist) =>
|
||||||
ndNormalize.albumArtist(albumArtist, apiClientProps.server),
|
// Navidrome native API will return only external URL small/medium/large
|
||||||
|
// image URL. Set large image to undefined to force `albumArtist` to use
|
||||||
|
// /rest/getCoverArt.view?id=ar-...
|
||||||
|
ndNormalize.albumArtist(
|
||||||
|
{
|
||||||
|
...albumArtist,
|
||||||
|
largeImageUrl: undefined,
|
||||||
|
},
|
||||||
|
apiClientProps.server,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
startIndex: query.startIndex,
|
startIndex: query.startIndex,
|
||||||
totalRecordCount: Number(res.body.headers.get('x-total-count') || 0),
|
totalRecordCount: Number(res.body.headers.get('x-total-count') || 0),
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { AxiosHeaders } from 'axios';
|
import { AxiosHeaders } from 'axios';
|
||||||
|
import isElectron from 'is-electron';
|
||||||
import semverCoerce from 'semver/functions/coerce';
|
import semverCoerce from 'semver/functions/coerce';
|
||||||
import semverGte from 'semver/functions/gte';
|
import semverGte from 'semver/functions/gte';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
@@ -99,4 +100,29 @@ export const getFeatures = (
|
|||||||
return features;
|
return features;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getClientType = (): string => {
|
||||||
|
if (isElectron()) {
|
||||||
|
return 'Desktop Client';
|
||||||
|
}
|
||||||
|
const agent = navigator.userAgent;
|
||||||
|
switch (true) {
|
||||||
|
case agent.toLowerCase().indexOf('edge') > -1:
|
||||||
|
return 'Microsoft Edge';
|
||||||
|
case agent.toLowerCase().indexOf('edg/') > -1:
|
||||||
|
return 'Edge Chromium'; // Match also / to avoid matching for the older Edge
|
||||||
|
case agent.toLowerCase().indexOf('opr') > -1:
|
||||||
|
return 'Opera';
|
||||||
|
case agent.toLowerCase().indexOf('chrome') > -1:
|
||||||
|
return 'Chrome';
|
||||||
|
case agent.toLowerCase().indexOf('trident') > -1:
|
||||||
|
return 'Internet Explorer';
|
||||||
|
case agent.toLowerCase().indexOf('firefox') > -1:
|
||||||
|
return 'Firefox';
|
||||||
|
case agent.toLowerCase().indexOf('safari') > -1:
|
||||||
|
return 'Safari';
|
||||||
|
default:
|
||||||
|
return 'PC';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export const SEPARATOR_STRING = ' · ';
|
export const SEPARATOR_STRING = ' · ';
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ export const App = () => {
|
|||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const accent = useSettingsStore((store) => store.general.accent);
|
const accent = useSettingsStore((store) => store.general.accent);
|
||||||
const language = useSettingsStore((store) => store.general.language);
|
const language = useSettingsStore((store) => store.general.language);
|
||||||
|
const nativeImageAspect = useSettingsStore((store) => store.general.nativeAspectRatio);
|
||||||
const { builtIn, custom, system, type } = useSettingsStore((state) => state.font);
|
const { builtIn, custom, system, type } = useSettingsStore((state) => state.font);
|
||||||
const { type: playbackType } = usePlaybackSettings();
|
const { type: playbackType } = usePlaybackSettings();
|
||||||
const { bindings } = useHotkeySettings();
|
const { bindings } = useHotkeySettings();
|
||||||
@@ -90,6 +91,11 @@ export const App = () => {
|
|||||||
root.style.setProperty('--primary-color', accent);
|
root.style.setProperty('--primary-color', accent);
|
||||||
}, [accent]);
|
}, [accent]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const root = document.documentElement;
|
||||||
|
root.style.setProperty('--image-fit', nativeImageAspect ? 'contain' : 'cover');
|
||||||
|
}, [nativeImageAspect]);
|
||||||
|
|
||||||
const providerValue = useMemo(() => {
|
const providerValue = useMemo(() => {
|
||||||
return { handlePlayQueueAdd };
|
return { handlePlayQueueAdd };
|
||||||
}, [handlePlayQueueAdd]);
|
}, [handlePlayQueueAdd]);
|
||||||
|
|||||||
@@ -40,13 +40,13 @@ type WebAudio = {
|
|||||||
gain: GainNode;
|
gain: GainNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Credits: http://stackoverflow.com/questions/12150729/ddg
|
// Credits: https://gist.github.com/novwhisky/8a1a0168b94f3b6abfaa?permalink_comment_id=1551393#gistcomment-1551393
|
||||||
// This is used so that the player will always have an <audio> element. This means that
|
// This is used so that the player will always have an <audio> element. This means that
|
||||||
// player1Source and player2Source are connected BEFORE the user presses play for
|
// player1Source and player2Source are connected BEFORE the user presses play for
|
||||||
// the first time. This workaround is important for Safari, which seems to require the
|
// the first time. This workaround is important for Safari, which seems to require the
|
||||||
// source to be connected PRIOR to resuming audio context
|
// source to be connected PRIOR to resuming audio context
|
||||||
const EMPTY_SOURCE =
|
const EMPTY_SOURCE =
|
||||||
'data:audio/wav;base64,UklGRjIAAABXQVZFZm10IBIAAAABAAEAQB8AAEAfAAABAAgAAABmYWN0BAAAAAAAAABkYXRhAAAAAA==';
|
'data:audio/mp3;base64,SUQzBAAAAAAAI1RTU0UAAAAPAAADTGF2ZjU2LjM2LjEwMAAAAAAAAAAAAAAA//OEAAAAAAAAAAAAAAAAAAAAAAAASW5mbwAAAA8AAAAEAAABIADAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV6urq6urq6urq6urq6urq6urq6urq6urq6v////////////////////////////////8AAAAATGF2YzU2LjQxAAAAAAAAAAAAAAAAJAAAAAAAAAAAASDs90hvAAAAAAAAAAAAAAAAAAAA//MUZAAAAAGkAAAAAAAAA0gAAAAATEFN//MUZAMAAAGkAAAAAAAAA0gAAAAARTMu//MUZAYAAAGkAAAAAAAAA0gAAAAAOTku//MUZAkAAAGkAAAAAAAAA0gAAAAANVVV';
|
||||||
|
|
||||||
export const AudioPlayer = forwardRef(
|
export const AudioPlayer = forwardRef(
|
||||||
(
|
(
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import formatDuration from 'format-duration';
|
||||||
import { generatePath } from 'react-router';
|
import { generatePath } from 'react-router';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
@@ -6,6 +7,7 @@ import { Album, AlbumArtist, Artist, Playlist, Song } from '/@/renderer/api/type
|
|||||||
import { Text } from '/@/renderer/components/text';
|
import { Text } from '/@/renderer/components/text';
|
||||||
import { AppRoute } from '/@/renderer/router/routes';
|
import { AppRoute } from '/@/renderer/router/routes';
|
||||||
import { CardRow } from '/@/renderer/types';
|
import { CardRow } from '/@/renderer/types';
|
||||||
|
import { formatDateAbsolute, formatDateRelative, formatRating } from '/@/renderer/utils/format';
|
||||||
|
|
||||||
const Row = styled.div<{ $secondary?: boolean }>`
|
const Row = styled.div<{ $secondary?: boolean }>`
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -69,7 +71,10 @@ export const CardRows = ({ data, rows }: CardRowsProps) => {
|
|||||||
)}
|
)}
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
{row.arrayProperty && item[row.arrayProperty]}
|
{row.arrayProperty &&
|
||||||
|
(row.format
|
||||||
|
? row.format(item)
|
||||||
|
: item[row.arrayProperty])}
|
||||||
</Text>
|
</Text>
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
))}
|
))}
|
||||||
@@ -88,7 +93,8 @@ export const CardRows = ({ data, rows }: CardRowsProps) => {
|
|||||||
overflow="hidden"
|
overflow="hidden"
|
||||||
size={index > 0 ? 'sm' : 'md'}
|
size={index > 0 ? 'sm' : 'md'}
|
||||||
>
|
>
|
||||||
{row.arrayProperty && item[row.arrayProperty]}
|
{row.arrayProperty &&
|
||||||
|
(row.format ? row.format(item) : item[row.arrayProperty])}
|
||||||
</Text>
|
</Text>
|
||||||
))}
|
))}
|
||||||
</Row>
|
</Row>
|
||||||
@@ -114,7 +120,7 @@ export const CardRows = ({ data, rows }: CardRowsProps) => {
|
|||||||
)}
|
)}
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
{data && data[row.property]}
|
{data && (row.format ? row.format(data) : data[row.property])}
|
||||||
</Text>
|
</Text>
|
||||||
) : (
|
) : (
|
||||||
<Text
|
<Text
|
||||||
@@ -123,7 +129,7 @@ export const CardRows = ({ data, rows }: CardRowsProps) => {
|
|||||||
overflow="hidden"
|
overflow="hidden"
|
||||||
size={index > 0 ? 'sm' : 'md'}
|
size={index > 0 ? 'sm' : 'md'}
|
||||||
>
|
>
|
||||||
{data && data[row.property]}
|
{data && (row.format ? row.format(data) : data[row.property])}
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
</Row>
|
</Row>
|
||||||
@@ -151,12 +157,15 @@ export const ALBUM_CARD_ROWS: { [key: string]: CardRow<Album> } = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
createdAt: {
|
createdAt: {
|
||||||
|
format: (song) => formatDateAbsolute(song.createdAt),
|
||||||
property: 'createdAt',
|
property: 'createdAt',
|
||||||
},
|
},
|
||||||
duration: {
|
duration: {
|
||||||
|
format: (album) => (album.duration === null ? null : formatDuration(album.duration)),
|
||||||
property: 'duration',
|
property: 'duration',
|
||||||
},
|
},
|
||||||
lastPlayedAt: {
|
lastPlayedAt: {
|
||||||
|
format: (album) => formatDateRelative(album.lastPlayedAt),
|
||||||
property: 'lastPlayedAt',
|
property: 'lastPlayedAt',
|
||||||
},
|
},
|
||||||
name: {
|
name: {
|
||||||
@@ -170,6 +179,7 @@ export const ALBUM_CARD_ROWS: { [key: string]: CardRow<Album> } = {
|
|||||||
property: 'playCount',
|
property: 'playCount',
|
||||||
},
|
},
|
||||||
rating: {
|
rating: {
|
||||||
|
format: (album) => formatRating(album),
|
||||||
property: 'userRating',
|
property: 'userRating',
|
||||||
},
|
},
|
||||||
releaseDate: {
|
releaseDate: {
|
||||||
@@ -208,12 +218,15 @@ export const SONG_CARD_ROWS: { [key: string]: CardRow<Song> } = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
createdAt: {
|
createdAt: {
|
||||||
|
format: (song) => formatDateAbsolute(song.createdAt),
|
||||||
property: 'createdAt',
|
property: 'createdAt',
|
||||||
},
|
},
|
||||||
duration: {
|
duration: {
|
||||||
|
format: (song) => (song.duration === null ? null : formatDuration(song.duration)),
|
||||||
property: 'duration',
|
property: 'duration',
|
||||||
},
|
},
|
||||||
lastPlayedAt: {
|
lastPlayedAt: {
|
||||||
|
format: (song) => formatDateRelative(song.lastPlayedAt),
|
||||||
property: 'lastPlayedAt',
|
property: 'lastPlayedAt',
|
||||||
},
|
},
|
||||||
name: {
|
name: {
|
||||||
@@ -227,6 +240,7 @@ export const SONG_CARD_ROWS: { [key: string]: CardRow<Song> } = {
|
|||||||
property: 'playCount',
|
property: 'playCount',
|
||||||
},
|
},
|
||||||
rating: {
|
rating: {
|
||||||
|
format: (song) => formatRating(song),
|
||||||
property: 'userRating',
|
property: 'userRating',
|
||||||
},
|
},
|
||||||
releaseDate: {
|
releaseDate: {
|
||||||
@@ -242,12 +256,14 @@ export const ALBUMARTIST_CARD_ROWS: { [key: string]: CardRow<AlbumArtist> } = {
|
|||||||
property: 'albumCount',
|
property: 'albumCount',
|
||||||
},
|
},
|
||||||
duration: {
|
duration: {
|
||||||
|
format: (artist) => (artist.duration === null ? null : formatDuration(artist.duration)),
|
||||||
property: 'duration',
|
property: 'duration',
|
||||||
},
|
},
|
||||||
genres: {
|
genres: {
|
||||||
property: 'genres',
|
property: 'genres',
|
||||||
},
|
},
|
||||||
lastPlayedAt: {
|
lastPlayedAt: {
|
||||||
|
format: (artist) => formatDateRelative(artist.lastPlayedAt),
|
||||||
property: 'lastPlayedAt',
|
property: 'lastPlayedAt',
|
||||||
},
|
},
|
||||||
name: {
|
name: {
|
||||||
@@ -261,6 +277,7 @@ export const ALBUMARTIST_CARD_ROWS: { [key: string]: CardRow<AlbumArtist> } = {
|
|||||||
property: 'playCount',
|
property: 'playCount',
|
||||||
},
|
},
|
||||||
rating: {
|
rating: {
|
||||||
|
format: (artist) => formatRating(artist),
|
||||||
property: 'userRating',
|
property: 'userRating',
|
||||||
},
|
},
|
||||||
songCount: {
|
songCount: {
|
||||||
@@ -270,6 +287,8 @@ export const ALBUMARTIST_CARD_ROWS: { [key: string]: CardRow<AlbumArtist> } = {
|
|||||||
|
|
||||||
export const PLAYLIST_CARD_ROWS: { [key: string]: CardRow<Playlist> } = {
|
export const PLAYLIST_CARD_ROWS: { [key: string]: CardRow<Playlist> } = {
|
||||||
duration: {
|
duration: {
|
||||||
|
format: (playlist) =>
|
||||||
|
playlist.duration === null ? null : formatDuration(playlist.duration),
|
||||||
property: 'duration',
|
property: 'duration',
|
||||||
},
|
},
|
||||||
name: {
|
name: {
|
||||||
@@ -295,7 +314,4 @@ export const PLAYLIST_CARD_ROWS: { [key: string]: CardRow<Playlist> } = {
|
|||||||
songCount: {
|
songCount: {
|
||||||
property: 'songCount',
|
property: 'songCount',
|
||||||
},
|
},
|
||||||
updatedAt: {
|
|
||||||
property: 'songCount',
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ const Image = styled(SimpleImg)`
|
|||||||
|
|
||||||
img {
|
img {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
object-fit: cover;
|
object-fit: var(--image-fit);
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ const BackgroundImage = styled.img`
|
|||||||
height: 150%;
|
height: 150%;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
filter: blur(24px);
|
filter: blur(24px);
|
||||||
object-fit: cover;
|
object-fit: var(--image-fit);
|
||||||
object-position: 0 30%;
|
object-position: 0 30%;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|||||||
@@ -122,7 +122,7 @@ const Image = styled(SimpleImg)`
|
|||||||
|
|
||||||
img {
|
img {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
object-fit: cover;
|
object-fit: var(--image-fit);
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ const Image = styled(SimpleImg)`
|
|||||||
|
|
||||||
img {
|
img {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
object-fit: cover;
|
object-fit: var(--image-fit);
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|||||||
@@ -13,13 +13,15 @@ import InfiniteLoader from 'react-window-infinite-loader';
|
|||||||
import { VirtualGridWrapper } from '/@/renderer/components/virtual-grid/virtual-grid-wrapper';
|
import { VirtualGridWrapper } from '/@/renderer/components/virtual-grid/virtual-grid-wrapper';
|
||||||
import type { CardRoute, CardRow, PlayQueueAddOptions } from '/@/renderer/types';
|
import type { CardRoute, CardRow, PlayQueueAddOptions } from '/@/renderer/types';
|
||||||
import { ListDisplayType } from '/@/renderer/types';
|
import { ListDisplayType } from '/@/renderer/types';
|
||||||
import { LibraryItem } from '/@/renderer/api/types';
|
import { AnyLibraryItem, Genre, LibraryItem } from '/@/renderer/api/types';
|
||||||
|
|
||||||
|
type LibraryItemOrGenre = AnyLibraryItem | Genre;
|
||||||
|
|
||||||
export type VirtualInfiniteGridRef = {
|
export type VirtualInfiniteGridRef = {
|
||||||
resetLoadMoreItemsCache: () => void;
|
resetLoadMoreItemsCache: () => void;
|
||||||
scrollTo: (index: number) => void;
|
scrollTo: (index: number) => void;
|
||||||
setItemData: (data: any[]) => void;
|
setItemData: (data: LibraryItemOrGenre[]) => void;
|
||||||
updateItemData: (rule: (item: any) => any) => void;
|
updateItemData: (rule: (item: LibraryItemOrGenre) => LibraryItemOrGenre) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
interface VirtualGridProps
|
interface VirtualGridProps
|
||||||
@@ -27,7 +29,7 @@ interface VirtualGridProps
|
|||||||
cardRows: CardRow<any>[];
|
cardRows: CardRow<any>[];
|
||||||
display?: ListDisplayType;
|
display?: ListDisplayType;
|
||||||
fetchFn: (options: { columnCount: number; skip: number; take: number }) => Promise<any>;
|
fetchFn: (options: { columnCount: number; skip: number; take: number }) => Promise<any>;
|
||||||
fetchInitialData?: () => any;
|
fetchInitialData?: () => LibraryItemOrGenre[];
|
||||||
handleFavorite?: (options: {
|
handleFavorite?: (options: {
|
||||||
id: string[];
|
id: string[];
|
||||||
isFavorite: boolean;
|
isFavorite: boolean;
|
||||||
@@ -70,7 +72,10 @@ export const VirtualInfiniteGrid = forwardRef(
|
|||||||
const listRef = useRef<any>(null);
|
const listRef = useRef<any>(null);
|
||||||
const loader = useRef<InfiniteLoader>(null);
|
const loader = useRef<InfiniteLoader>(null);
|
||||||
|
|
||||||
const [itemData, setItemData] = useState<any[]>(fetchInitialData?.() || []);
|
// itemData can be a sparse array. Treat the intermediate elements as being undefined
|
||||||
|
const [itemData, setItemData] = useState<Array<LibraryItemOrGenre | undefined>>(
|
||||||
|
fetchInitialData?.() || [],
|
||||||
|
);
|
||||||
|
|
||||||
const { itemHeight, rowCount, columnCount } = useMemo(() => {
|
const { itemHeight, rowCount, columnCount } = useMemo(() => {
|
||||||
const itemsPerRow = width ? Math.floor(width / (itemSize + itemGap * 2)) : 5;
|
const itemsPerRow = width ? Math.floor(width / (itemSize + itemGap * 2)) : 5;
|
||||||
@@ -109,7 +114,7 @@ export const VirtualInfiniteGrid = forwardRef(
|
|||||||
});
|
});
|
||||||
|
|
||||||
setItemData((itemData) => {
|
setItemData((itemData) => {
|
||||||
const newData: any[] = [...itemData];
|
const newData = [...itemData];
|
||||||
|
|
||||||
let itemIndex = 0;
|
let itemIndex = 0;
|
||||||
for (let rowIndex = start; rowIndex < itemCount; rowIndex += 1) {
|
for (let rowIndex = start; rowIndex < itemCount; rowIndex += 1) {
|
||||||
@@ -135,11 +140,11 @@ export const VirtualInfiniteGrid = forwardRef(
|
|||||||
scrollTo: (index: number) => {
|
scrollTo: (index: number) => {
|
||||||
listRef?.current?.scrollToItem(index);
|
listRef?.current?.scrollToItem(index);
|
||||||
},
|
},
|
||||||
setItemData: (data: any[]) => {
|
setItemData: (data: LibraryItemOrGenre[]) => {
|
||||||
setItemData(data);
|
setItemData(data);
|
||||||
},
|
},
|
||||||
updateItemData: (rule) => {
|
updateItemData: (rule) => {
|
||||||
setItemData((data) => data.map(rule));
|
setItemData((data) => data.map((item) => item && rule(item)));
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ const MetadataWrapper = styled.div`
|
|||||||
|
|
||||||
const StyledImage = styled(SimpleImg)`
|
const StyledImage = styled(SimpleImg)`
|
||||||
img {
|
img {
|
||||||
object-fit: cover;
|
object-fit: var(--image-fit);
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|||||||
@@ -15,8 +15,6 @@ import type { AgGridReactProps } from '@ag-grid-community/react';
|
|||||||
import { AgGridReact } from '@ag-grid-community/react';
|
import { AgGridReact } from '@ag-grid-community/react';
|
||||||
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
|
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
|
||||||
import { useClickOutside, useMergedRef } from '@mantine/hooks';
|
import { useClickOutside, useMergedRef } from '@mantine/hooks';
|
||||||
import dayjs from 'dayjs';
|
|
||||||
import relativeTime from 'dayjs/plugin/relativeTime';
|
|
||||||
import formatDuration from 'format-duration';
|
import formatDuration from 'format-duration';
|
||||||
import { AnimatePresence } from 'framer-motion';
|
import { AnimatePresence } from 'framer-motion';
|
||||||
import { generatePath } from 'react-router';
|
import { generatePath } from 'react-router';
|
||||||
@@ -43,7 +41,7 @@ import { useFixedTableHeader } from '/@/renderer/components/virtual-table/hooks/
|
|||||||
import { NoteCell } from '/@/renderer/components/virtual-table/cells/note-cell';
|
import { NoteCell } from '/@/renderer/components/virtual-table/cells/note-cell';
|
||||||
import { RowIndexCell } from '/@/renderer/components/virtual-table/cells/row-index-cell';
|
import { RowIndexCell } from '/@/renderer/components/virtual-table/cells/row-index-cell';
|
||||||
import i18n from '/@/i18n/i18n';
|
import i18n from '/@/i18n/i18n';
|
||||||
import { formatSizeString } from '/@/renderer/utils/format-size-string';
|
import { formatDateAbsolute, formatDateRelative, formatSizeString } from '/@/renderer/utils/format';
|
||||||
|
|
||||||
export * from './table-config-dropdown';
|
export * from './table-config-dropdown';
|
||||||
export * from './table-pagination';
|
export * from './table-pagination';
|
||||||
@@ -64,8 +62,6 @@ const DummyHeader = styled.div<{ height?: number }>`
|
|||||||
height: ${({ height }) => height || 36}px;
|
height: ${({ height }) => height || 36}px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
dayjs.extend(relativeTime);
|
|
||||||
|
|
||||||
const tableColumns: { [key: string]: ColDef } = {
|
const tableColumns: { [key: string]: ColDef } = {
|
||||||
actions: {
|
actions: {
|
||||||
cellClass: 'ag-cell-favorite',
|
cellClass: 'ag-cell-favorite',
|
||||||
@@ -182,8 +178,7 @@ const tableColumns: { [key: string]: ColDef } = {
|
|||||||
GenericTableHeader(params, { position: 'center' }),
|
GenericTableHeader(params, { position: 'center' }),
|
||||||
headerName: i18n.t('table.column.dateAdded'),
|
headerName: i18n.t('table.column.dateAdded'),
|
||||||
suppressSizeToFit: true,
|
suppressSizeToFit: true,
|
||||||
valueFormatter: (params: ValueFormatterParams) =>
|
valueFormatter: (params: ValueFormatterParams) => formatDateAbsolute(params.value),
|
||||||
params.value ? dayjs(params.value).format('MMM D, YYYY') : '',
|
|
||||||
valueGetter: (params: ValueGetterParams) =>
|
valueGetter: (params: ValueGetterParams) =>
|
||||||
params.data ? params.data.createdAt : undefined,
|
params.data ? params.data.createdAt : undefined,
|
||||||
width: 130,
|
width: 130,
|
||||||
@@ -225,8 +220,7 @@ const tableColumns: { [key: string]: ColDef } = {
|
|||||||
headerComponent: (params: IHeaderParams) =>
|
headerComponent: (params: IHeaderParams) =>
|
||||||
GenericTableHeader(params, { position: 'center' }),
|
GenericTableHeader(params, { position: 'center' }),
|
||||||
headerName: i18n.t('table.column.lastPlayed'),
|
headerName: i18n.t('table.column.lastPlayed'),
|
||||||
valueFormatter: (params: ValueFormatterParams) =>
|
valueFormatter: (params: ValueFormatterParams) => formatDateRelative(params.value),
|
||||||
params.value ? dayjs(params.value).fromNow() : '',
|
|
||||||
valueGetter: (params: ValueGetterParams) =>
|
valueGetter: (params: ValueGetterParams) =>
|
||||||
params.data ? params.data.lastPlayedAt : undefined,
|
params.data ? params.data.lastPlayedAt : undefined,
|
||||||
width: 130,
|
width: 130,
|
||||||
@@ -258,8 +252,7 @@ const tableColumns: { [key: string]: ColDef } = {
|
|||||||
GenericTableHeader(params, { position: 'center' }),
|
GenericTableHeader(params, { position: 'center' }),
|
||||||
headerName: i18n.t('table.column.releaseDate'),
|
headerName: i18n.t('table.column.releaseDate'),
|
||||||
suppressSizeToFit: true,
|
suppressSizeToFit: true,
|
||||||
valueFormatter: (params: ValueFormatterParams) =>
|
valueFormatter: (params: ValueFormatterParams) => formatDateAbsolute(params.value),
|
||||||
params.value ? dayjs(params.value).format('MMM D, YYYY') : '',
|
|
||||||
valueGetter: (params: ValueGetterParams) =>
|
valueGetter: (params: ValueGetterParams) =>
|
||||||
params.data ? params.data.releaseDate : undefined,
|
params.data ? params.data.releaseDate : undefined,
|
||||||
width: 130,
|
width: 130,
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ export const AlbumArtistDetailContent = ({ background }: AlbumArtistDetailConten
|
|||||||
_custom: {
|
_custom: {
|
||||||
jellyfin: {
|
jellyfin: {
|
||||||
...(server?.type === ServerType.JELLYFIN
|
...(server?.type === ServerType.JELLYFIN
|
||||||
? { ArtistIds: albumArtistId }
|
? { AlbumArtistIds: albumArtistId }
|
||||||
: undefined),
|
: undefined),
|
||||||
},
|
},
|
||||||
navidrome: {
|
navidrome: {
|
||||||
|
|||||||
@@ -15,7 +15,8 @@ export const QUEUE_CONTEXT_MENU_ITEMS: SetContextMenuItems = [
|
|||||||
export const SONG_CONTEXT_MENU_ITEMS: SetContextMenuItems = [
|
export const SONG_CONTEXT_MENU_ITEMS: SetContextMenuItems = [
|
||||||
{ id: 'play' },
|
{ id: 'play' },
|
||||||
{ id: 'playLast' },
|
{ id: 'playLast' },
|
||||||
{ divider: true, id: 'playNext' },
|
{ id: 'playNext' },
|
||||||
|
{ divider: true, id: 'playSimilarSongs' },
|
||||||
{ divider: true, id: 'addToPlaylist' },
|
{ divider: true, id: 'addToPlaylist' },
|
||||||
{ id: 'addToFavorites' },
|
{ id: 'addToFavorites' },
|
||||||
{ divider: true, id: 'removeFromFavorites' },
|
{ divider: true, id: 'removeFromFavorites' },
|
||||||
@@ -34,7 +35,8 @@ export const SONG_ALBUM_PAGE: SetContextMenuItems = [
|
|||||||
export const PLAYLIST_SONG_CONTEXT_MENU_ITEMS: SetContextMenuItems = [
|
export const PLAYLIST_SONG_CONTEXT_MENU_ITEMS: SetContextMenuItems = [
|
||||||
{ id: 'play' },
|
{ id: 'play' },
|
||||||
{ id: 'playLast' },
|
{ id: 'playLast' },
|
||||||
{ divider: true, id: 'playNext' },
|
{ id: 'playNext' },
|
||||||
|
{ divider: true, id: 'playSimilarSongs' },
|
||||||
{ id: 'addToPlaylist' },
|
{ id: 'addToPlaylist' },
|
||||||
{ divider: true, id: 'removeFromPlaylist' },
|
{ divider: true, id: 'removeFromPlaylist' },
|
||||||
{ id: 'addToFavorites' },
|
{ id: 'addToFavorites' },
|
||||||
@@ -46,7 +48,8 @@ export const PLAYLIST_SONG_CONTEXT_MENU_ITEMS: SetContextMenuItems = [
|
|||||||
export const SMART_PLAYLIST_SONG_CONTEXT_MENU_ITEMS: SetContextMenuItems = [
|
export const SMART_PLAYLIST_SONG_CONTEXT_MENU_ITEMS: SetContextMenuItems = [
|
||||||
{ id: 'play' },
|
{ id: 'play' },
|
||||||
{ id: 'playLast' },
|
{ id: 'playLast' },
|
||||||
{ divider: true, id: 'playNext' },
|
{ id: 'playNext' },
|
||||||
|
{ divider: true, id: 'playSimilarSongs' },
|
||||||
{ divider: true, id: 'addToPlaylist' },
|
{ divider: true, id: 'addToPlaylist' },
|
||||||
{ id: 'addToFavorites' },
|
{ id: 'addToFavorites' },
|
||||||
{ divider: true, id: 'removeFromFavorites' },
|
{ divider: true, id: 'removeFromFavorites' },
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import {
|
|||||||
RiCloseCircleLine,
|
RiCloseCircleLine,
|
||||||
RiShareForwardFill,
|
RiShareForwardFill,
|
||||||
RiInformationFill,
|
RiInformationFill,
|
||||||
|
RiRadio2Fill,
|
||||||
} from 'react-icons/ri';
|
} from 'react-icons/ri';
|
||||||
import { AnyLibraryItems, LibraryItem, ServerType, AnyLibraryItem } from '/@/renderer/api/types';
|
import { AnyLibraryItems, LibraryItem, ServerType, AnyLibraryItem } from '/@/renderer/api/types';
|
||||||
import {
|
import {
|
||||||
@@ -50,6 +51,7 @@ import { useDeletePlaylist } from '/@/renderer/features/playlists';
|
|||||||
import { useRemoveFromPlaylist } from '/@/renderer/features/playlists/mutations/remove-from-playlist-mutation';
|
import { useRemoveFromPlaylist } from '/@/renderer/features/playlists/mutations/remove-from-playlist-mutation';
|
||||||
import { useCreateFavorite, useDeleteFavorite, useSetRating } from '/@/renderer/features/shared';
|
import { useCreateFavorite, useDeleteFavorite, useSetRating } from '/@/renderer/features/shared';
|
||||||
import {
|
import {
|
||||||
|
getServerById,
|
||||||
useAuthStore,
|
useAuthStore,
|
||||||
useCurrentServer,
|
useCurrentServer,
|
||||||
usePlayerStore,
|
usePlayerStore,
|
||||||
@@ -58,6 +60,8 @@ import {
|
|||||||
import { usePlaybackType } from '/@/renderer/store/settings.store';
|
import { usePlaybackType } from '/@/renderer/store/settings.store';
|
||||||
import { Play, PlaybackType } from '/@/renderer/types';
|
import { Play, PlaybackType } from '/@/renderer/types';
|
||||||
import { ItemDetailsModal } from '/@/renderer/features/item-details/components/item-details-modal';
|
import { ItemDetailsModal } from '/@/renderer/features/item-details/components/item-details-modal';
|
||||||
|
import { updateSong } from '/@/renderer/features/player/update-remote-song';
|
||||||
|
import { controller } from '/@/renderer/api/controller';
|
||||||
|
|
||||||
type ContextMenuContextProps = {
|
type ContextMenuContextProps = {
|
||||||
closeContextMenu: () => void;
|
closeContextMenu: () => void;
|
||||||
@@ -86,7 +90,6 @@ const JELLYFIN_IGNORED_MENU_ITEMS: ContextMenuItemType[] = ['setRating', 'shareI
|
|||||||
// const SUBSONIC_IGNORED_MENU_ITEMS: ContextMenuItemType[] = [];
|
// const SUBSONIC_IGNORED_MENU_ITEMS: ContextMenuItemType[] = [];
|
||||||
|
|
||||||
const mpvPlayer = isElectron() ? window.electron.mpvPlayer : null;
|
const mpvPlayer = isElectron() ? window.electron.mpvPlayer : null;
|
||||||
const remote = isElectron() ? window.electron.remote : null;
|
|
||||||
|
|
||||||
export interface ContextMenuProviderProps {
|
export interface ContextMenuProviderProps {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
@@ -640,7 +643,7 @@ export const ContextMenuProvider = ({ children }: ContextMenuProviderProps) => {
|
|||||||
ctx.tableApi?.redrawRows();
|
ctx.tableApi?.redrawRows();
|
||||||
|
|
||||||
if (isCurrentSongRemoved) {
|
if (isCurrentSongRemoved) {
|
||||||
remote?.updateSong({ song: playerData.current.song });
|
updateSong(playerData.current.song);
|
||||||
}
|
}
|
||||||
}, [ctx.dataNodes, ctx.tableApi, playbackType, removeFromQueue]);
|
}, [ctx.dataNodes, ctx.tableApi, playbackType, removeFromQueue]);
|
||||||
|
|
||||||
@@ -658,6 +661,18 @@ export const ContextMenuProvider = ({ children }: ContextMenuProviderProps) => {
|
|||||||
});
|
});
|
||||||
}, [ctx.data, t]);
|
}, [ctx.data, t]);
|
||||||
|
|
||||||
|
const handleSimilar = useCallback(async () => {
|
||||||
|
const item = ctx.data[0];
|
||||||
|
const songs = await controller.getSimilarSongs({
|
||||||
|
apiClientProps: {
|
||||||
|
server: getServerById(item.serverId),
|
||||||
|
signal: undefined,
|
||||||
|
},
|
||||||
|
query: { albumArtistIds: item.albumArtistIds, songId: item.id },
|
||||||
|
});
|
||||||
|
handlePlayQueueAdd?.({ byData: [ctx.data[0], ...songs], playType: Play.NOW });
|
||||||
|
}, [ctx, handlePlayQueueAdd]);
|
||||||
|
|
||||||
const contextMenuItems: Record<ContextMenuItemType, ContextMenuItem> = useMemo(() => {
|
const contextMenuItems: Record<ContextMenuItemType, ContextMenuItem> = useMemo(() => {
|
||||||
return {
|
return {
|
||||||
addToFavorites: {
|
addToFavorites: {
|
||||||
@@ -719,6 +734,12 @@ export const ContextMenuProvider = ({ children }: ContextMenuProviderProps) => {
|
|||||||
leftIcon: <RiAddCircleFill size="1.1rem" />,
|
leftIcon: <RiAddCircleFill size="1.1rem" />,
|
||||||
onClick: () => handlePlay(Play.NEXT),
|
onClick: () => handlePlay(Play.NEXT),
|
||||||
},
|
},
|
||||||
|
playSimilarSongs: {
|
||||||
|
id: 'playSimilarSongs',
|
||||||
|
label: t('page.contextMenu.playSimilarSongs', { postProcess: 'sentenceCase' }),
|
||||||
|
leftIcon: <RiRadio2Fill size="1.1rem" />,
|
||||||
|
onClick: handleSimilar,
|
||||||
|
},
|
||||||
removeFromFavorites: {
|
removeFromFavorites: {
|
||||||
id: 'removeFromFavorites',
|
id: 'removeFromFavorites',
|
||||||
label: t('page.contextMenu.removeFromFavorites', { postProcess: 'sentenceCase' }),
|
label: t('page.contextMenu.removeFromFavorites', { postProcess: 'sentenceCase' }),
|
||||||
@@ -838,6 +859,7 @@ export const ContextMenuProvider = ({ children }: ContextMenuProviderProps) => {
|
|||||||
handleUpdateRating,
|
handleUpdateRating,
|
||||||
handleShareItem,
|
handleShareItem,
|
||||||
server,
|
server,
|
||||||
|
handleSimilar,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const mergedRef = useMergedRef(ref, clickOutsideRef);
|
const mergedRef = useMergedRef(ref, clickOutsideRef);
|
||||||
|
|||||||
@@ -35,7 +35,8 @@ export type ContextMenuItemType =
|
|||||||
| 'moveToTopOfQueue'
|
| 'moveToTopOfQueue'
|
||||||
| 'removeFromQueue'
|
| 'removeFromQueue'
|
||||||
| 'deselectAll'
|
| 'deselectAll'
|
||||||
| 'showDetails';
|
| 'showDetails'
|
||||||
|
| 'playSimilarSongs';
|
||||||
|
|
||||||
export type SetContextMenuItems = {
|
export type SetContextMenuItems = {
|
||||||
children?: boolean;
|
children?: boolean;
|
||||||
|
|||||||
@@ -33,11 +33,12 @@ const HomeRoute = () => {
|
|||||||
const server = useCurrentServer();
|
const server = useCurrentServer();
|
||||||
const itemsPerPage = 15;
|
const itemsPerPage = 15;
|
||||||
const { windowBarStyle } = useWindowSettings();
|
const { windowBarStyle } = useWindowSettings();
|
||||||
const { homeItems } = useGeneralSettings();
|
const { homeFeature, homeItems } = useGeneralSettings();
|
||||||
|
|
||||||
const feature = useAlbumList({
|
const feature = useAlbumList({
|
||||||
options: {
|
options: {
|
||||||
cacheTime: 1000 * 60,
|
cacheTime: 1000 * 60,
|
||||||
|
enabled: homeFeature,
|
||||||
staleTime: 1000 * 60,
|
staleTime: 1000 * 60,
|
||||||
},
|
},
|
||||||
query: {
|
query: {
|
||||||
@@ -249,7 +250,7 @@ const HomeRoute = () => {
|
|||||||
px="2rem"
|
px="2rem"
|
||||||
spacing="lg"
|
spacing="lg"
|
||||||
>
|
>
|
||||||
<FeatureCarousel data={featureItemsWithImage} />
|
{homeFeature && <FeatureCarousel data={featureItemsWithImage} />}
|
||||||
{sortedCarousel.map((carousel) => (
|
{sortedCarousel.map((carousel) => (
|
||||||
<MemoizedSwiperGridCarousel
|
<MemoizedSwiperGridCarousel
|
||||||
key={`carousel-${carousel.uniqueId}`}
|
key={`carousel-${carousel.uniqueId}`}
|
||||||
|
|||||||
@@ -1,13 +1,11 @@
|
|||||||
import { Group, Table } from '@mantine/core';
|
import { Group, Table } from '@mantine/core';
|
||||||
import dayjs from 'dayjs';
|
|
||||||
import { RiCheckFill, RiCloseFill } from 'react-icons/ri';
|
import { RiCheckFill, RiCloseFill } from 'react-icons/ri';
|
||||||
import { TFunction, useTranslation } from 'react-i18next';
|
import { TFunction, useTranslation } from 'react-i18next';
|
||||||
import { ReactNode } from 'react';
|
import { ReactNode } from 'react';
|
||||||
import { Album, AlbumArtist, AnyLibraryItem, LibraryItem, Song } from '/@/renderer/api/types';
|
import { Album, AlbumArtist, AnyLibraryItem, LibraryItem, Song } from '/@/renderer/api/types';
|
||||||
import { formatDurationString } from '/@/renderer/utils';
|
import { formatDurationString, formatSizeString } from '/@/renderer/utils';
|
||||||
import { formatSizeString } from '/@/renderer/utils/format-size-string';
|
|
||||||
import { replaceURLWithHTMLLinks } from '/@/renderer/utils/linkify';
|
import { replaceURLWithHTMLLinks } from '/@/renderer/utils/linkify';
|
||||||
import { Rating, Spoiler, Text } from '/@/renderer/components';
|
import { Spoiler, Text } from '/@/renderer/components';
|
||||||
import { sanitize } from '/@/renderer/utils/sanitize';
|
import { sanitize } from '/@/renderer/utils/sanitize';
|
||||||
import { SongPath } from '/@/renderer/features/item-details/components/song-path';
|
import { SongPath } from '/@/renderer/features/item-details/components/song-path';
|
||||||
import { generatePath } from 'react-router';
|
import { generatePath } from 'react-router';
|
||||||
@@ -15,6 +13,7 @@ import { Link } from 'react-router-dom';
|
|||||||
import { AppRoute } from '/@/renderer/router/routes';
|
import { AppRoute } from '/@/renderer/router/routes';
|
||||||
import { Separator } from '/@/renderer/components/separator';
|
import { Separator } from '/@/renderer/components/separator';
|
||||||
import { useGenreRoute } from '/@/renderer/hooks/use-genre-route';
|
import { useGenreRoute } from '/@/renderer/hooks/use-genre-route';
|
||||||
|
import { formatDateRelative, formatRating } from '/@/renderer/utils/format';
|
||||||
|
|
||||||
export type ItemDetailsModalProps = {
|
export type ItemDetailsModalProps = {
|
||||||
item: Album | AlbumArtist | Song;
|
item: Album | AlbumArtist | Song;
|
||||||
@@ -82,8 +81,6 @@ const formatArtists = (isAlbumArtist: boolean) => (item: Album | Song) =>
|
|||||||
const formatComment = (item: Album | Song) =>
|
const formatComment = (item: Album | Song) =>
|
||||||
item.comment ? <Spoiler maxHeight={50}>{replaceURLWithHTMLLinks(item.comment)}</Spoiler> : null;
|
item.comment ? <Spoiler maxHeight={50}>{replaceURLWithHTMLLinks(item.comment)}</Spoiler> : null;
|
||||||
|
|
||||||
const formatDate = (key: string | null) => (key ? dayjs(key).fromNow() : '');
|
|
||||||
|
|
||||||
const FormatGenre = (item: Album | AlbumArtist | Song) => {
|
const FormatGenre = (item: Album | AlbumArtist | Song) => {
|
||||||
const genreRoute = useGenreRoute();
|
const genreRoute = useGenreRoute();
|
||||||
|
|
||||||
@@ -104,14 +101,6 @@ const FormatGenre = (item: Album | AlbumArtist | Song) => {
|
|||||||
));
|
));
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatRating = (item: Album | AlbumArtist | Song) =>
|
|
||||||
item.userRating !== null ? (
|
|
||||||
<Rating
|
|
||||||
readOnly
|
|
||||||
value={item.userRating}
|
|
||||||
/>
|
|
||||||
) : null;
|
|
||||||
|
|
||||||
const BoolField = (key: boolean) =>
|
const BoolField = (key: boolean) =>
|
||||||
key ? <RiCheckFill size="1.1rem" /> : <RiCloseFill size="1.1rem" />;
|
key ? <RiCheckFill size="1.1rem" /> : <RiCloseFill size="1.1rem" />;
|
||||||
|
|
||||||
@@ -139,11 +128,11 @@ const AlbumPropertyMapping: ItemDetailRow<Album>[] = [
|
|||||||
{ key: 'playCount', label: 'filter.playCount' },
|
{ key: 'playCount', label: 'filter.playCount' },
|
||||||
{
|
{
|
||||||
label: 'filter.lastPlayed',
|
label: 'filter.lastPlayed',
|
||||||
render: (song) => formatDate(song.lastPlayedAt),
|
render: (song) => formatDateRelative(song.lastPlayedAt),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'common.modified',
|
label: 'common.modified',
|
||||||
render: (song) => formatDate(song.updatedAt),
|
render: (song) => formatDateRelative(song.updatedAt),
|
||||||
},
|
},
|
||||||
{ label: 'filter.comment', render: formatComment },
|
{ label: 'filter.comment', render: formatComment },
|
||||||
{
|
{
|
||||||
@@ -178,7 +167,7 @@ const AlbumArtistPropertyMapping: ItemDetailRow<AlbumArtist>[] = [
|
|||||||
{ key: 'playCount', label: 'filter.playCount' },
|
{ key: 'playCount', label: 'filter.playCount' },
|
||||||
{
|
{
|
||||||
label: 'filter.lastPlayed',
|
label: 'filter.lastPlayed',
|
||||||
render: (song) => formatDate(song.lastPlayedAt),
|
render: (song) => formatDateRelative(song.lastPlayedAt),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'common.mbid',
|
label: 'common.mbid',
|
||||||
@@ -256,11 +245,11 @@ const SongPropertyMapping: ItemDetailRow<Song>[] = [
|
|||||||
{ key: 'playCount', label: 'filter.playCount' },
|
{ key: 'playCount', label: 'filter.playCount' },
|
||||||
{
|
{
|
||||||
label: 'filter.lastPlayed',
|
label: 'filter.lastPlayed',
|
||||||
render: (song) => formatDate(song.lastPlayedAt),
|
render: (song) => formatDateRelative(song.lastPlayedAt),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'common.modified',
|
label: 'common.modified',
|
||||||
render: (song) => formatDate(song.updatedAt),
|
render: (song) => formatDateRelative(song.updatedAt),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'common.albumGain',
|
label: 'common.albumGain',
|
||||||
|
|||||||
@@ -26,6 +26,10 @@ const StyledText = styled(TextTitle)<TitleProps & { $alignment: string; $fontSiz
|
|||||||
&.unsynchronized {
|
&.unsynchronized {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.synchronized {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const LyricLine = ({ text, alignment, fontSize, ...props }: LyricLineProps) => {
|
export const LyricLine = ({ text, alignment, fontSize, ...props }: LyricLineProps) => {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
useCurrentTime,
|
useCurrentTime,
|
||||||
useLyricsSettings,
|
useLyricsSettings,
|
||||||
usePlaybackType,
|
usePlaybackType,
|
||||||
|
usePlayerData,
|
||||||
useSeeked,
|
useSeeked,
|
||||||
} from '/@/renderer/store';
|
} from '/@/renderer/store';
|
||||||
import { PlaybackType, PlayerStatus } from '/@/renderer/types';
|
import { PlaybackType, PlayerStatus } from '/@/renderer/types';
|
||||||
@@ -12,6 +13,7 @@ import isElectron from 'is-electron';
|
|||||||
import { PlayersRef } from '/@/renderer/features/player/ref/players-ref';
|
import { PlayersRef } from '/@/renderer/features/player/ref/players-ref';
|
||||||
import { FullLyricsMetadata, SynchronizedLyricsArray } from '/@/renderer/api/types';
|
import { FullLyricsMetadata, SynchronizedLyricsArray } from '/@/renderer/api/types';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
import { useCenterControls } from '/@/renderer/features/player/hooks/use-center-controls';
|
||||||
|
|
||||||
const mpvPlayer = isElectron() ? window.electron.mpvPlayer : null;
|
const mpvPlayer = isElectron() ? window.electron.mpvPlayer : null;
|
||||||
|
|
||||||
@@ -60,8 +62,10 @@ export const SynchronizedLyrics = ({
|
|||||||
const playersRef = PlayersRef;
|
const playersRef = PlayersRef;
|
||||||
const status = useCurrentStatus();
|
const status = useCurrentStatus();
|
||||||
const playbackType = usePlaybackType();
|
const playbackType = usePlaybackType();
|
||||||
|
const playerData = usePlayerData();
|
||||||
const now = useCurrentTime();
|
const now = useCurrentTime();
|
||||||
const settings = useLyricsSettings();
|
const settings = useLyricsSettings();
|
||||||
|
const centerControls = useCenterControls({ playersRef });
|
||||||
|
|
||||||
const seeked = useSeeked();
|
const seeked = useSeeked();
|
||||||
|
|
||||||
@@ -107,16 +111,18 @@ export const SynchronizedLyrics = ({
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
const player = (
|
const player =
|
||||||
playersRef.current.player1 ?? playersRef.current.player2
|
playerData.current.player === 1
|
||||||
).getInternalPlayer();
|
? playersRef.current.player1
|
||||||
|
: playersRef.current.player2;
|
||||||
|
const underlying = player?.getInternalPlayer();
|
||||||
|
|
||||||
// If it is null, this probably means we added a new song while the lyrics tab is open
|
// If it is null, this probably means we added a new song while the lyrics tab is open
|
||||||
// and the queue was previously empty
|
// and the queue was previously empty
|
||||||
if (!player) return 0;
|
if (!underlying) return 0;
|
||||||
|
|
||||||
return player.currentTime;
|
return underlying.currentTime;
|
||||||
}, [playbackType, playersRef]);
|
}, [playbackType, playersRef, playerData]);
|
||||||
|
|
||||||
const setCurrentLyric = useCallback(
|
const setCurrentLyric = useCallback(
|
||||||
(timeInMs: number, epoch?: number, targetIndex?: number) => {
|
(timeInMs: number, epoch?: number, targetIndex?: number) => {
|
||||||
@@ -331,7 +337,7 @@ export const SynchronizedLyrics = ({
|
|||||||
text={`"${name} by ${artist}"`}
|
text={`"${name} by ${artist}"`}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{lyrics.map(([, text], idx) => (
|
{lyrics.map(([time, text], idx) => (
|
||||||
<LyricLine
|
<LyricLine
|
||||||
key={idx}
|
key={idx}
|
||||||
alignment={settings.alignment}
|
alignment={settings.alignment}
|
||||||
@@ -339,6 +345,7 @@ export const SynchronizedLyrics = ({
|
|||||||
fontSize={settings.fontSize}
|
fontSize={settings.fontSize}
|
||||||
id={`lyric-${idx}`}
|
id={`lyric-${idx}`}
|
||||||
text={text}
|
text={text}
|
||||||
|
onClick={() => centerControls.handleSeekSlider(time / 1000)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</SynchronizedLyricsContainer>
|
</SynchronizedLyricsContainer>
|
||||||
|
|||||||
@@ -14,13 +14,13 @@ import {
|
|||||||
} from 'react-icons/ri';
|
} from 'react-icons/ri';
|
||||||
import { Song } from '/@/renderer/api/types';
|
import { Song } from '/@/renderer/api/types';
|
||||||
import { usePlayerControls, useQueueControls } from '/@/renderer/store';
|
import { usePlayerControls, useQueueControls } from '/@/renderer/store';
|
||||||
import { PlaybackType, PlayerStatus, TableType } from '/@/renderer/types';
|
import { PlaybackType, TableType } from '/@/renderer/types';
|
||||||
import { usePlaybackType } from '/@/renderer/store/settings.store';
|
import { usePlaybackType } from '/@/renderer/store/settings.store';
|
||||||
import { usePlayerStore, useSetCurrentTime } from '../../../store/player.store';
|
import { usePlayerStore, useSetCurrentTime } from '../../../store/player.store';
|
||||||
import { TableConfigDropdown } from '/@/renderer/components/virtual-table';
|
import { TableConfigDropdown } from '/@/renderer/components/virtual-table';
|
||||||
|
import { updateSong } from '/@/renderer/features/player/update-remote-song';
|
||||||
|
|
||||||
const mpvPlayer = isElectron() ? window.electron.mpvPlayer : null;
|
const mpvPlayer = isElectron() ? window.electron.mpvPlayer : null;
|
||||||
const remote = isElectron() ? window.electron.remote : null;
|
|
||||||
|
|
||||||
interface PlayQueueListOptionsProps {
|
interface PlayQueueListOptionsProps {
|
||||||
tableRef: MutableRefObject<{ grid: AgGridReactType<Song> } | null>;
|
tableRef: MutableRefObject<{ grid: AgGridReactType<Song> } | null>;
|
||||||
@@ -79,7 +79,7 @@ export const PlayQueueListControls = ({ type, tableRef }: PlayQueueListOptionsPr
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isCurrentSongRemoved) {
|
if (isCurrentSongRemoved) {
|
||||||
remote?.updateSong({ song: playerData.current.song });
|
updateSong(playerData.current.song);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -91,7 +91,7 @@ export const PlayQueueListControls = ({ type, tableRef }: PlayQueueListOptionsPr
|
|||||||
mpvPlayer!.pause();
|
mpvPlayer!.pause();
|
||||||
}
|
}
|
||||||
|
|
||||||
remote?.updateSong({ song: undefined, status: PlayerStatus.PAUSED });
|
updateSong(undefined);
|
||||||
|
|
||||||
setCurrentTime(0);
|
setCurrentTime(0);
|
||||||
pause();
|
pause();
|
||||||
|
|||||||
@@ -30,16 +30,16 @@ import debounce from 'lodash/debounce';
|
|||||||
import { ErrorBoundary } from 'react-error-boundary';
|
import { ErrorBoundary } from 'react-error-boundary';
|
||||||
import { getColumnDefs, VirtualTable } from '/@/renderer/components/virtual-table';
|
import { getColumnDefs, VirtualTable } from '/@/renderer/components/virtual-table';
|
||||||
import { ErrorFallback } from '/@/renderer/features/action-required';
|
import { ErrorFallback } from '/@/renderer/features/action-required';
|
||||||
import { PlaybackType, PlayerStatus, TableType } from '/@/renderer/types';
|
import { PlaybackType, TableType } from '/@/renderer/types';
|
||||||
import { LibraryItem, QueueSong } from '/@/renderer/api/types';
|
import { LibraryItem, QueueSong } from '/@/renderer/api/types';
|
||||||
import { useHandleTableContextMenu } from '/@/renderer/features/context-menu';
|
import { useHandleTableContextMenu } from '/@/renderer/features/context-menu';
|
||||||
import { QUEUE_CONTEXT_MENU_ITEMS } from '/@/renderer/features/context-menu/context-menu-items';
|
import { QUEUE_CONTEXT_MENU_ITEMS } from '/@/renderer/features/context-menu/context-menu-items';
|
||||||
import { VirtualGridAutoSizerContainer } from '/@/renderer/components/virtual-grid';
|
import { VirtualGridAutoSizerContainer } from '/@/renderer/components/virtual-grid';
|
||||||
import { useAppFocus } from '/@/renderer/hooks';
|
import { useAppFocus } from '/@/renderer/hooks';
|
||||||
import { PlayersRef } from '/@/renderer/features/player/ref/players-ref';
|
import { PlayersRef } from '/@/renderer/features/player/ref/players-ref';
|
||||||
|
import { updateSong } from '/@/renderer/features/player/update-remote-song';
|
||||||
|
|
||||||
const mpvPlayer = isElectron() ? window.electron.mpvPlayer : null;
|
const mpvPlayer = isElectron() ? window.electron.mpvPlayer : null;
|
||||||
const remote = isElectron() ? window.electron.remote : null;
|
|
||||||
|
|
||||||
type QueueProps = {
|
type QueueProps = {
|
||||||
type: TableType;
|
type: TableType;
|
||||||
@@ -82,11 +82,7 @@ export const PlayQueue = forwardRef(({ type }: QueueProps, ref: Ref<any>) => {
|
|||||||
|
|
||||||
const handleDoubleClick = (e: CellDoubleClickedEvent) => {
|
const handleDoubleClick = (e: CellDoubleClickedEvent) => {
|
||||||
const playerData = setCurrentTrack(e.data.uniqueId);
|
const playerData = setCurrentTrack(e.data.uniqueId);
|
||||||
remote?.updateSong({
|
updateSong(playerData.current.song);
|
||||||
currentTime: 0,
|
|
||||||
song: playerData.current.song,
|
|
||||||
status: PlayerStatus.PLAYING,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (playbackType === PlaybackType.LOCAL) {
|
if (playbackType === PlaybackType.LOCAL) {
|
||||||
mpvPlayer!.volume(volume);
|
mpvPlayer!.volume(volume);
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ const Image = styled(motion.div)`
|
|||||||
const PlayerbarImage = styled.img`
|
const PlayerbarImage = styled.img`
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
object-fit: cover;
|
object-fit: var(--image-fit);
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const LineItem = styled.div<{ $secondary?: boolean }>`
|
const LineItem = styled.div<{ $secondary?: boolean }>`
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import isElectron from 'is-electron';
|
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import { usePlaybackType, useSettingsStore } from '/@/renderer/store/settings.store';
|
import { usePlaybackType, useSettingsStore } from '/@/renderer/store/settings.store';
|
||||||
import { PlaybackType } from '/@/renderer/types';
|
import { PlaybackType } from '/@/renderer/types';
|
||||||
@@ -17,6 +16,7 @@ import { CenterControls } from './center-controls';
|
|||||||
import { LeftControls } from './left-controls';
|
import { LeftControls } from './left-controls';
|
||||||
import { RightControls } from './right-controls';
|
import { RightControls } from './right-controls';
|
||||||
import { PlayersRef } from '/@/renderer/features/player/ref/players-ref';
|
import { PlayersRef } from '/@/renderer/features/player/ref/players-ref';
|
||||||
|
import { updateSong } from '/@/renderer/features/player/update-remote-song';
|
||||||
|
|
||||||
const PlayerbarContainer = styled.div`
|
const PlayerbarContainer = styled.div`
|
||||||
width: 100vw;
|
width: 100vw;
|
||||||
@@ -59,8 +59,6 @@ const CenterGridItem = styled.div`
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const remote = isElectron() ? window.electron.remote : null;
|
|
||||||
|
|
||||||
export const Playerbar = () => {
|
export const Playerbar = () => {
|
||||||
const playersRef = PlayersRef;
|
const playersRef = PlayersRef;
|
||||||
const settings = useSettingsStore((state) => state.playback);
|
const settings = useSettingsStore((state) => state.playback);
|
||||||
@@ -75,13 +73,7 @@ export const Playerbar = () => {
|
|||||||
|
|
||||||
const autoNextFn = useCallback(() => {
|
const autoNextFn = useCallback(() => {
|
||||||
const playerData = autoNext();
|
const playerData = autoNext();
|
||||||
|
updateSong(playerData.current.song);
|
||||||
if (remote) {
|
|
||||||
remote.updateSong({
|
|
||||||
currentTime: 0,
|
|
||||||
song: playerData.current.song,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [autoNext]);
|
}, [autoNext]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -14,9 +14,9 @@ import {
|
|||||||
import { usePlaybackType } from '/@/renderer/store/settings.store';
|
import { usePlaybackType } from '/@/renderer/store/settings.store';
|
||||||
import { useScrobble } from '/@/renderer/features/player/hooks/use-scrobble';
|
import { useScrobble } from '/@/renderer/features/player/hooks/use-scrobble';
|
||||||
import debounce from 'lodash/debounce';
|
import debounce from 'lodash/debounce';
|
||||||
import { QueueSong } from '/@/renderer/api/types';
|
|
||||||
import { toast } from '/@/renderer/components';
|
import { toast } from '/@/renderer/components';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { updateSong } from '/@/renderer/features/player/update-remote-song';
|
||||||
|
|
||||||
const mpvPlayer = isElectron() ? window.electron.mpvPlayer : null;
|
const mpvPlayer = isElectron() ? window.electron.mpvPlayer : null;
|
||||||
const mpvPlayerListener = isElectron() ? window.electron.mpvPlayerListener : null;
|
const mpvPlayerListener = isElectron() ? window.electron.mpvPlayerListener : null;
|
||||||
@@ -24,7 +24,7 @@ const ipc = isElectron() ? window.electron.ipc : null;
|
|||||||
const utils = isElectron() ? window.electron.utils : null;
|
const utils = isElectron() ? window.electron.utils : null;
|
||||||
const mpris = isElectron() && utils?.isLinux() ? window.electron.mpris : null;
|
const mpris = isElectron() && utils?.isLinux() ? window.electron.mpris : null;
|
||||||
const remote = isElectron() ? window.electron.remote : null;
|
const remote = isElectron() ? window.electron.remote : null;
|
||||||
const mediaSession = !isElectron() || !utils?.isLinux() ? navigator.mediaSession : null;
|
const mediaSession = navigator.mediaSession;
|
||||||
|
|
||||||
export const useCenterControls = (args: { playersRef: any }) => {
|
export const useCenterControls = (args: { playersRef: any }) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -46,6 +46,23 @@ export const useCenterControls = (args: { playersRef: any }) => {
|
|||||||
|
|
||||||
const { handleScrobbleFromSongRestart, handleScrobbleFromSeek } = useScrobble();
|
const { handleScrobbleFromSongRestart, handleScrobbleFromSeek } = useScrobble();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (mediaSession) {
|
||||||
|
mediaSession.playbackState =
|
||||||
|
playerStatus === PlayerStatus.PLAYING ? 'playing' : 'paused';
|
||||||
|
}
|
||||||
|
|
||||||
|
remote?.updatePlayback(playerStatus);
|
||||||
|
}, [playerStatus]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
remote?.updateRepeat(repeatStatus);
|
||||||
|
}, [repeatStatus]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
remote?.updateShuffle(shuffleStatus !== PlayerShuffle.NONE);
|
||||||
|
}, [shuffleStatus]);
|
||||||
|
|
||||||
const resetPlayers = useCallback(() => {
|
const resetPlayers = useCallback(() => {
|
||||||
if (player1Ref.getInternalPlayer()) {
|
if (player1Ref.getInternalPlayer()) {
|
||||||
player1Ref.getInternalPlayer().currentTime = 0;
|
player1Ref.getInternalPlayer().currentTime = 0;
|
||||||
@@ -76,61 +93,7 @@ export const useCenterControls = (args: { playersRef: any }) => {
|
|||||||
|
|
||||||
const isMpvPlayer = isElectron() && playbackType === PlaybackType.LOCAL;
|
const isMpvPlayer = isElectron() && playbackType === PlaybackType.LOCAL;
|
||||||
|
|
||||||
const mprisUpdateSong = (args?: {
|
|
||||||
currentTime?: number;
|
|
||||||
song?: QueueSong;
|
|
||||||
status?: PlayerStatus;
|
|
||||||
}) => {
|
|
||||||
const { song, currentTime, status } = args || {};
|
|
||||||
|
|
||||||
const time = currentTime || usePlayerStore.getState().current.time;
|
|
||||||
const playStatus = status || usePlayerStore.getState().current.status;
|
|
||||||
const track = song || usePlayerStore.getState().current.song;
|
|
||||||
|
|
||||||
remote?.updateSong({
|
|
||||||
currentTime: time,
|
|
||||||
repeat: usePlayerStore.getState().repeat,
|
|
||||||
shuffle: usePlayerStore.getState().shuffle !== PlayerShuffle.NONE,
|
|
||||||
song: track,
|
|
||||||
status: playStatus,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (mediaSession) {
|
|
||||||
mediaSession.playbackState = playStatus === PlayerStatus.PLAYING ? 'playing' : 'paused';
|
|
||||||
|
|
||||||
let metadata: MediaMetadata;
|
|
||||||
|
|
||||||
if (track) {
|
|
||||||
let artwork: MediaImage[];
|
|
||||||
|
|
||||||
if (track.imageUrl) {
|
|
||||||
const image300 = track.imageUrl
|
|
||||||
?.replace(/&size=\d+/, '&size=300')
|
|
||||||
.replace(/\?width=\d+/, '?width=300')
|
|
||||||
.replace(/&height=\d+/, '&height=300');
|
|
||||||
|
|
||||||
artwork = [{ sizes: '300x300', src: image300, type: 'image/png' }];
|
|
||||||
} else {
|
|
||||||
artwork = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
metadata = new MediaMetadata({
|
|
||||||
album: track.album ?? '',
|
|
||||||
artist: track.artistName,
|
|
||||||
artwork,
|
|
||||||
title: track.name,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
metadata = new MediaMetadata();
|
|
||||||
}
|
|
||||||
|
|
||||||
mediaSession.metadata = metadata;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handlePlay = useCallback(() => {
|
const handlePlay = useCallback(() => {
|
||||||
mprisUpdateSong({ status: PlayerStatus.PLAYING });
|
|
||||||
|
|
||||||
if (isMpvPlayer) {
|
if (isMpvPlayer) {
|
||||||
mpvPlayer?.volume(usePlayerStore.getState().volume);
|
mpvPlayer?.volume(usePlayerStore.getState().volume);
|
||||||
mpvPlayer!.play();
|
mpvPlayer!.play();
|
||||||
@@ -145,8 +108,6 @@ export const useCenterControls = (args: { playersRef: any }) => {
|
|||||||
}, [currentPlayerRef, isMpvPlayer, play]);
|
}, [currentPlayerRef, isMpvPlayer, play]);
|
||||||
|
|
||||||
const handlePause = useCallback(() => {
|
const handlePause = useCallback(() => {
|
||||||
mprisUpdateSong({ status: PlayerStatus.PAUSED });
|
|
||||||
|
|
||||||
if (isMpvPlayer) {
|
if (isMpvPlayer) {
|
||||||
mpvPlayer!.pause();
|
mpvPlayer!.pause();
|
||||||
}
|
}
|
||||||
@@ -155,8 +116,6 @@ export const useCenterControls = (args: { playersRef: any }) => {
|
|||||||
}, [isMpvPlayer, pause]);
|
}, [isMpvPlayer, pause]);
|
||||||
|
|
||||||
const handleStop = useCallback(() => {
|
const handleStop = useCallback(() => {
|
||||||
mprisUpdateSong({ status: PlayerStatus.PAUSED });
|
|
||||||
|
|
||||||
if (isMpvPlayer) {
|
if (isMpvPlayer) {
|
||||||
mpvPlayer!.pause();
|
mpvPlayer!.pause();
|
||||||
mpvPlayer!.seekTo(0);
|
mpvPlayer!.seekTo(0);
|
||||||
@@ -212,13 +171,13 @@ export const useCenterControls = (args: { playersRef: any }) => {
|
|||||||
const handleRepeatAll = {
|
const handleRepeatAll = {
|
||||||
local: () => {
|
local: () => {
|
||||||
const playerData = autoNext();
|
const playerData = autoNext();
|
||||||
mprisUpdateSong({ song: playerData.current.song, status: PlayerStatus.PLAYING });
|
updateSong(playerData.current.song);
|
||||||
mpvPlayer!.autoNext(playerData);
|
mpvPlayer!.autoNext(playerData);
|
||||||
play();
|
play();
|
||||||
},
|
},
|
||||||
web: () => {
|
web: () => {
|
||||||
const playerData = autoNext();
|
const playerData = autoNext();
|
||||||
mprisUpdateSong({ song: playerData.current.song, status: PlayerStatus.PLAYING });
|
updateSong(playerData.current.song);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -226,15 +185,12 @@ export const useCenterControls = (args: { playersRef: any }) => {
|
|||||||
local: () => {
|
local: () => {
|
||||||
if (isLastTrack) {
|
if (isLastTrack) {
|
||||||
const playerData = setCurrentIndex(0);
|
const playerData = setCurrentIndex(0);
|
||||||
mprisUpdateSong({ song: playerData.current.song, status: PlayerStatus.PAUSED });
|
updateSong(playerData.current.song);
|
||||||
mpvPlayer!.setQueue(playerData, true);
|
mpvPlayer!.setQueue(playerData, true);
|
||||||
pause();
|
pause();
|
||||||
} else {
|
} else {
|
||||||
const playerData = autoNext();
|
const playerData = autoNext();
|
||||||
mprisUpdateSong({
|
updateSong(playerData.current.song);
|
||||||
song: playerData.current.song,
|
|
||||||
status: PlayerStatus.PLAYING,
|
|
||||||
});
|
|
||||||
mpvPlayer!.autoNext(playerData);
|
mpvPlayer!.autoNext(playerData);
|
||||||
play();
|
play();
|
||||||
}
|
}
|
||||||
@@ -242,14 +198,10 @@ export const useCenterControls = (args: { playersRef: any }) => {
|
|||||||
web: () => {
|
web: () => {
|
||||||
if (isLastTrack) {
|
if (isLastTrack) {
|
||||||
resetPlayers();
|
resetPlayers();
|
||||||
mprisUpdateSong({ status: PlayerStatus.PAUSED });
|
|
||||||
pause();
|
pause();
|
||||||
} else {
|
} else {
|
||||||
const playerData = autoNext();
|
const playerData = autoNext();
|
||||||
mprisUpdateSong({
|
updateSong(playerData.current.song);
|
||||||
song: playerData.current.song,
|
|
||||||
status: PlayerStatus.PLAYING,
|
|
||||||
});
|
|
||||||
resetPlayers();
|
resetPlayers();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -258,20 +210,15 @@ export const useCenterControls = (args: { playersRef: any }) => {
|
|||||||
const handleRepeatOne = {
|
const handleRepeatOne = {
|
||||||
local: () => {
|
local: () => {
|
||||||
const playerData = autoNext();
|
const playerData = autoNext();
|
||||||
mprisUpdateSong({ song: playerData.current.song, status: PlayerStatus.PLAYING });
|
updateSong(playerData.current.song);
|
||||||
mpvPlayer!.autoNext(playerData);
|
mpvPlayer!.autoNext(playerData);
|
||||||
play();
|
play();
|
||||||
},
|
},
|
||||||
web: () => {
|
web: () => {
|
||||||
if (isLastTrack) {
|
if (isLastTrack) {
|
||||||
mprisUpdateSong({ status: PlayerStatus.PAUSED });
|
|
||||||
resetPlayers();
|
resetPlayers();
|
||||||
} else {
|
} else {
|
||||||
const playerData = autoNext();
|
autoNext();
|
||||||
mprisUpdateSong({
|
|
||||||
song: playerData.current.song,
|
|
||||||
status: PlayerStatus.PLAYING,
|
|
||||||
});
|
|
||||||
resetPlayers();
|
resetPlayers();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -309,12 +256,12 @@ export const useCenterControls = (args: { playersRef: any }) => {
|
|||||||
const handleRepeatAll = {
|
const handleRepeatAll = {
|
||||||
local: () => {
|
local: () => {
|
||||||
const playerData = next();
|
const playerData = next();
|
||||||
mprisUpdateSong({ song: playerData.current.song });
|
updateSong(playerData.current.song);
|
||||||
mpvPlayer!.setQueue(playerData);
|
mpvPlayer!.setQueue(playerData);
|
||||||
},
|
},
|
||||||
web: () => {
|
web: () => {
|
||||||
const playerData = next();
|
const playerData = next();
|
||||||
mprisUpdateSong({ song: playerData.current.song });
|
updateSong(playerData.current.song);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -322,27 +269,24 @@ export const useCenterControls = (args: { playersRef: any }) => {
|
|||||||
local: () => {
|
local: () => {
|
||||||
if (isLastTrack) {
|
if (isLastTrack) {
|
||||||
const playerData = setCurrentIndex(0);
|
const playerData = setCurrentIndex(0);
|
||||||
mprisUpdateSong({ song: playerData.current.song, status: PlayerStatus.PAUSED });
|
updateSong(playerData.current.song);
|
||||||
mpvPlayer!.setQueue(playerData, true);
|
mpvPlayer!.setQueue(playerData, true);
|
||||||
pause();
|
pause();
|
||||||
} else {
|
} else {
|
||||||
const playerData = next();
|
const playerData = next();
|
||||||
mprisUpdateSong({ song: playerData.current.song });
|
updateSong(playerData.current.song);
|
||||||
mpvPlayer!.setQueue(playerData);
|
mpvPlayer!.setQueue(playerData);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
web: () => {
|
web: () => {
|
||||||
if (isLastTrack) {
|
if (isLastTrack) {
|
||||||
const playerData = setCurrentIndex(0);
|
const playerData = setCurrentIndex(0);
|
||||||
mprisUpdateSong({
|
updateSong(playerData.current.song);
|
||||||
song: playerData.current.song,
|
|
||||||
status: PlayerStatus.PAUSED,
|
|
||||||
});
|
|
||||||
resetPlayers();
|
resetPlayers();
|
||||||
pause();
|
pause();
|
||||||
} else {
|
} else {
|
||||||
const playerData = next();
|
const playerData = next();
|
||||||
mprisUpdateSong({ song: playerData.current.song });
|
updateSong(playerData.current.song);
|
||||||
resetPlayers();
|
resetPlayers();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -352,14 +296,14 @@ export const useCenterControls = (args: { playersRef: any }) => {
|
|||||||
local: () => {
|
local: () => {
|
||||||
if (!isLastTrack) {
|
if (!isLastTrack) {
|
||||||
const playerData = next();
|
const playerData = next();
|
||||||
mprisUpdateSong({ song: playerData.current.song });
|
updateSong(playerData.current.song);
|
||||||
mpvPlayer!.setQueue(playerData);
|
mpvPlayer!.setQueue(playerData);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
web: () => {
|
web: () => {
|
||||||
if (!isLastTrack) {
|
if (!isLastTrack) {
|
||||||
const playerData = next();
|
const playerData = next();
|
||||||
mprisUpdateSong({ song: playerData.current.song });
|
updateSong(playerData.current.song);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -413,22 +357,22 @@ export const useCenterControls = (args: { playersRef: any }) => {
|
|||||||
local: () => {
|
local: () => {
|
||||||
if (!isFirstTrack) {
|
if (!isFirstTrack) {
|
||||||
const playerData = previous();
|
const playerData = previous();
|
||||||
mprisUpdateSong({ song: playerData.current.song });
|
updateSong(playerData.current.song);
|
||||||
mpvPlayer!.setQueue(playerData);
|
mpvPlayer!.setQueue(playerData);
|
||||||
} else {
|
} else {
|
||||||
const playerData = setCurrentIndex(queue.length - 1);
|
const playerData = setCurrentIndex(queue.length - 1);
|
||||||
mprisUpdateSong({ song: playerData.current.song });
|
updateSong(playerData.current.song);
|
||||||
mpvPlayer!.setQueue(playerData);
|
mpvPlayer!.setQueue(playerData);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
web: () => {
|
web: () => {
|
||||||
if (isFirstTrack) {
|
if (isFirstTrack) {
|
||||||
const playerData = setCurrentIndex(queue.length - 1);
|
const playerData = setCurrentIndex(queue.length - 1);
|
||||||
mprisUpdateSong({ song: playerData.current.song });
|
updateSong(playerData.current.song);
|
||||||
resetPlayers();
|
resetPlayers();
|
||||||
} else {
|
} else {
|
||||||
const playerData = previous();
|
const playerData = previous();
|
||||||
mprisUpdateSong({ song: playerData.current.song });
|
updateSong(playerData.current.song);
|
||||||
resetPlayers();
|
resetPlayers();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -438,26 +382,22 @@ export const useCenterControls = (args: { playersRef: any }) => {
|
|||||||
local: () => {
|
local: () => {
|
||||||
if (isFirstTrack) {
|
if (isFirstTrack) {
|
||||||
const playerData = setCurrentIndex(0);
|
const playerData = setCurrentIndex(0);
|
||||||
mprisUpdateSong({ song: playerData.current.song, status: PlayerStatus.PAUSED });
|
updateSong(playerData.current.song);
|
||||||
mpvPlayer!.setQueue(playerData, true);
|
mpvPlayer!.setQueue(playerData, true);
|
||||||
pause();
|
pause();
|
||||||
} else {
|
} else {
|
||||||
const playerData = previous();
|
const playerData = previous();
|
||||||
mprisUpdateSong({
|
updateSong(playerData.current.song);
|
||||||
currentTime: usePlayerStore.getState().current.time,
|
|
||||||
song: playerData.current.song,
|
|
||||||
});
|
|
||||||
mpvPlayer!.setQueue(playerData);
|
mpvPlayer!.setQueue(playerData);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
web: () => {
|
web: () => {
|
||||||
if (isFirstTrack) {
|
if (isFirstTrack) {
|
||||||
resetPlayers();
|
resetPlayers();
|
||||||
mprisUpdateSong({ status: PlayerStatus.PAUSED });
|
|
||||||
pause();
|
pause();
|
||||||
} else {
|
} else {
|
||||||
const playerData = previous();
|
const playerData = previous();
|
||||||
mprisUpdateSong({ song: playerData.current.song });
|
updateSong(playerData.current.song);
|
||||||
resetPlayers();
|
resetPlayers();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -466,12 +406,12 @@ export const useCenterControls = (args: { playersRef: any }) => {
|
|||||||
const handleRepeatOne = {
|
const handleRepeatOne = {
|
||||||
local: () => {
|
local: () => {
|
||||||
const playerData = previous();
|
const playerData = previous();
|
||||||
mprisUpdateSong({ song: playerData.current.song });
|
updateSong(playerData.current.song);
|
||||||
mpvPlayer!.setQueue(playerData);
|
mpvPlayer!.setQueue(playerData);
|
||||||
},
|
},
|
||||||
web: () => {
|
web: () => {
|
||||||
const playerData = previous();
|
const playerData = previous();
|
||||||
mprisUpdateSong({ song: playerData.current.song });
|
updateSong(playerData.current.song);
|
||||||
resetPlayers();
|
resetPlayers();
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,13 +2,7 @@ import { useCallback, useRef } from 'react';
|
|||||||
import { useQueryClient } from '@tanstack/react-query';
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
import { useCurrentServer, usePlayerControls, usePlayerStore } from '/@/renderer/store';
|
import { useCurrentServer, usePlayerControls, usePlayerStore } from '/@/renderer/store';
|
||||||
import { usePlaybackType } from '/@/renderer/store/settings.store';
|
import { usePlaybackType } from '/@/renderer/store/settings.store';
|
||||||
import {
|
import { PlayQueueAddOptions, Play, PlaybackType } from '/@/renderer/types';
|
||||||
PlayQueueAddOptions,
|
|
||||||
Play,
|
|
||||||
PlaybackType,
|
|
||||||
PlayerStatus,
|
|
||||||
PlayerShuffle,
|
|
||||||
} from '/@/renderer/types';
|
|
||||||
import { toast } from '/@/renderer/components/toast/index';
|
import { toast } from '/@/renderer/components/toast/index';
|
||||||
import isElectron from 'is-electron';
|
import isElectron from 'is-electron';
|
||||||
import { nanoid } from 'nanoid/non-secure';
|
import { nanoid } from 'nanoid/non-secure';
|
||||||
@@ -29,6 +23,8 @@ import {
|
|||||||
} from '/@/renderer/features/player/utils';
|
} from '/@/renderer/features/player/utils';
|
||||||
import { queryKeys } from '/@/renderer/api/query-keys';
|
import { queryKeys } from '/@/renderer/api/query-keys';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { PlayersRef } from '/@/renderer/features/player/ref/players-ref';
|
||||||
|
import { updateSong } from '/@/renderer/features/player/update-remote-song';
|
||||||
|
|
||||||
const getRootQueryKey = (itemType: LibraryItem, serverId: string) => {
|
const getRootQueryKey = (itemType: LibraryItem, serverId: string) => {
|
||||||
let queryKey;
|
let queryKey;
|
||||||
@@ -58,7 +54,6 @@ const getRootQueryKey = (itemType: LibraryItem, serverId: string) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const mpvPlayer = isElectron() ? window.electron.mpvPlayer : null;
|
const mpvPlayer = isElectron() ? window.electron.mpvPlayer : null;
|
||||||
const remote = isElectron() ? window.electron.remote : null;
|
|
||||||
|
|
||||||
const addToQueue = usePlayerStore.getState().actions.addToQueue;
|
const addToQueue = usePlayerStore.getState().actions.addToQueue;
|
||||||
|
|
||||||
@@ -170,6 +165,8 @@ export const useHandlePlayQueueAdd = () => {
|
|||||||
const hadSong = usePlayerStore.getState().queue.default.length > 0;
|
const hadSong = usePlayerStore.getState().queue.default.length > 0;
|
||||||
const playerData = addToQueue({ initialIndex: initialSongIndex, playType, songs });
|
const playerData = addToQueue({ initialIndex: initialSongIndex, playType, songs });
|
||||||
|
|
||||||
|
updateSong(playerData.current.song);
|
||||||
|
|
||||||
if (playbackType === PlaybackType.LOCAL) {
|
if (playbackType === PlaybackType.LOCAL) {
|
||||||
mpvPlayer!.volume(usePlayerStore.getState().volume);
|
mpvPlayer!.volume(usePlayerStore.getState().volume);
|
||||||
|
|
||||||
@@ -179,6 +176,15 @@ export const useHandlePlayQueueAdd = () => {
|
|||||||
} else {
|
} else {
|
||||||
mpvPlayer!.setQueueNext(playerData);
|
mpvPlayer!.setQueueNext(playerData);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
const player =
|
||||||
|
playerData.current.player === 1
|
||||||
|
? PlayersRef.current?.player1
|
||||||
|
: PlayersRef.current?.player2;
|
||||||
|
const underlying = player?.getInternalPlayer();
|
||||||
|
if (underlying && playType === Play.NOW) {
|
||||||
|
underlying.currentTime = 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// We should only play if the queue was empty, or we are doing play NOW
|
// We should only play if the queue was empty, or we are doing play NOW
|
||||||
@@ -187,14 +193,6 @@ export const useHandlePlayQueueAdd = () => {
|
|||||||
play();
|
play();
|
||||||
}
|
}
|
||||||
|
|
||||||
remote?.updateSong({
|
|
||||||
currentTime: usePlayerStore.getState().current.time,
|
|
||||||
repeat: usePlayerStore.getState().repeat,
|
|
||||||
shuffle: usePlayerStore.getState().shuffle !== PlayerShuffle.NONE,
|
|
||||||
song: playerData.current.song,
|
|
||||||
status: PlayerStatus.PLAYING,
|
|
||||||
});
|
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
[play, playbackType, queryClient, server, t],
|
[play, playbackType, queryClient, server, t],
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import isElectron from 'is-electron';
|
||||||
|
import { QueueSong } from '/@/renderer/api/types';
|
||||||
|
|
||||||
|
const remote = isElectron() ? window.electron.remote : null;
|
||||||
|
const mediaSession = navigator.mediaSession;
|
||||||
|
|
||||||
|
export const updateSong = (song: QueueSong | undefined) => {
|
||||||
|
if (mediaSession) {
|
||||||
|
let metadata: MediaMetadata;
|
||||||
|
|
||||||
|
if (song?.id) {
|
||||||
|
let artwork: MediaImage[];
|
||||||
|
|
||||||
|
if (song.imageUrl) {
|
||||||
|
const image300 = song.imageUrl
|
||||||
|
?.replace(/&size=\d+/, '&size=300')
|
||||||
|
.replace(/\?width=\d+/, '?width=300')
|
||||||
|
.replace(/&height=\d+/, '&height=300');
|
||||||
|
|
||||||
|
artwork = [{ sizes: '300x300', src: image300, type: 'image/png' }];
|
||||||
|
} else {
|
||||||
|
artwork = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
metadata = new MediaMetadata({
|
||||||
|
album: song.album ?? '',
|
||||||
|
artist: song.artistName,
|
||||||
|
artwork,
|
||||||
|
title: song.name,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
metadata = new MediaMetadata();
|
||||||
|
}
|
||||||
|
|
||||||
|
mediaSession.metadata = metadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
remote?.updateSong(song);
|
||||||
|
};
|
||||||
@@ -46,7 +46,7 @@ const MetadataWrapper = styled.div`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
const StyledImage = styled.img`
|
const StyledImage = styled.img`
|
||||||
object-fit: cover;
|
object-fit: var(--image-fit);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|||||||
@@ -90,6 +90,28 @@ export const ControlSettings = () => {
|
|||||||
isHidden: false,
|
isHidden: false,
|
||||||
title: t('setting.playerAlbumArtResolution', { postProcess: 'sentenceCase' }),
|
title: t('setting.playerAlbumArtResolution', { postProcess: 'sentenceCase' }),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
control: (
|
||||||
|
<Switch
|
||||||
|
aria-label="Toggle using native aspect ratio"
|
||||||
|
defaultChecked={settings.nativeAspectRatio}
|
||||||
|
onChange={(e) =>
|
||||||
|
setSettings({
|
||||||
|
general: {
|
||||||
|
...settings,
|
||||||
|
nativeAspectRatio: e.currentTarget.checked,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
description: t('setting.imageAspectRatio', {
|
||||||
|
context: 'description',
|
||||||
|
postProcess: 'sentenceCase',
|
||||||
|
}),
|
||||||
|
isHidden: false,
|
||||||
|
title: t('setting.imageAspectRatio', { postProcess: 'sentenceCase' }),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
control: (
|
control: (
|
||||||
<Switch
|
<Switch
|
||||||
@@ -377,6 +399,28 @@ export const ControlSettings = () => {
|
|||||||
isHidden: false,
|
isHidden: false,
|
||||||
title: t('setting.genreBehavior', { postProcess: 'sentenceCase' }),
|
title: t('setting.genreBehavior', { postProcess: 'sentenceCase' }),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
control: (
|
||||||
|
<Switch
|
||||||
|
aria-label={t('setting.homeFeature', { postProcess: 'sentenceCase' })}
|
||||||
|
defaultChecked={settings.homeFeature}
|
||||||
|
onChange={(e) =>
|
||||||
|
setSettings({
|
||||||
|
general: {
|
||||||
|
...settings,
|
||||||
|
homeFeature: e.currentTarget.checked,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
description: t('setting.homeFeature', {
|
||||||
|
context: 'description',
|
||||||
|
postProcess: 'sentenceCase',
|
||||||
|
}),
|
||||||
|
isHidden: false,
|
||||||
|
title: t('setting.homeFeature', { postProcess: 'sentenceCase' }),
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
return <SettingsSection options={controlOptions} />;
|
return <SettingsSection options={controlOptions} />;
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ export const CacheSettings = () => {
|
|||||||
{t(`common.areYouSure`, { postProcess: 'sentenceCase' })}
|
{t(`common.areYouSure`, { postProcess: 'sentenceCase' })}
|
||||||
</ConfirmModal>
|
</ConfirmModal>
|
||||||
),
|
),
|
||||||
title: t(`setting.${key}`),
|
title: t(`setting.${key}`, { postProcess: 'sentenceCase' }),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -67,8 +67,9 @@ export const CacheSettings = () => {
|
|||||||
),
|
),
|
||||||
description: t('setting.clearQueryCache', {
|
description: t('setting.clearQueryCache', {
|
||||||
context: 'description',
|
context: 'description',
|
||||||
|
postProcess: 'sentenceCase',
|
||||||
}),
|
}),
|
||||||
title: t('setting.clearQueryCache'),
|
title: t('setting.clearQueryCache', { postProcess: 'sentenceCase' }),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
control: (
|
control: (
|
||||||
@@ -83,9 +84,10 @@ export const CacheSettings = () => {
|
|||||||
),
|
),
|
||||||
description: t('setting.clearCache', {
|
description: t('setting.clearCache', {
|
||||||
context: 'description',
|
context: 'description',
|
||||||
|
postProcess: 'sentenceCase',
|
||||||
}),
|
}),
|
||||||
isHidden: !browser,
|
isHidden: !browser,
|
||||||
title: t('setting.clearCache'),
|
title: t('setting.clearCache', { postProcess: 'sentenceCase' }),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ export const WindowSettings = () => {
|
|||||||
control: (
|
control: (
|
||||||
<Switch
|
<Switch
|
||||||
aria-label="Toggle minimize to tray"
|
aria-label="Toggle minimize to tray"
|
||||||
defaultChecked={settings.exitToTray}
|
defaultChecked={settings.minimizeToTray}
|
||||||
disabled={!isElectron()}
|
disabled={!isElectron()}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
if (!e) return;
|
if (!e) return;
|
||||||
|
|||||||
@@ -121,7 +121,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.image {
|
.image {
|
||||||
object-fit: cover;
|
object-fit: var(--image-fit);
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ const ImageContainer = styled(motion.div)<{ height: string }>`
|
|||||||
const SidebarImage = styled.img`
|
const SidebarImage = styled.img`
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
object-fit: cover;
|
object-fit: var(--image-fit);
|
||||||
background: var(--placeholder-bg);
|
background: var(--placeholder-bg);
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|||||||
@@ -1054,7 +1054,7 @@ export const usePlayerData = () =>
|
|||||||
usePlayerStore(
|
usePlayerStore(
|
||||||
(state) => state.actions.getPlayerData(),
|
(state) => state.actions.getPlayerData(),
|
||||||
(a, b) => {
|
(a, b) => {
|
||||||
return a.current.nextIndex === b.current.nextIndex;
|
return a.current.song?.uniqueId === b.current.song?.uniqueId;
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -198,8 +198,10 @@ export interface SettingsState {
|
|||||||
externalLinks: boolean;
|
externalLinks: boolean;
|
||||||
followSystemTheme: boolean;
|
followSystemTheme: boolean;
|
||||||
genreTarget: GenreTarget;
|
genreTarget: GenreTarget;
|
||||||
|
homeFeature: boolean;
|
||||||
homeItems: SortableItem<HomeItem>[];
|
homeItems: SortableItem<HomeItem>[];
|
||||||
language: string;
|
language: string;
|
||||||
|
nativeAspectRatio: boolean;
|
||||||
passwordStore?: string;
|
passwordStore?: string;
|
||||||
playButtonBehavior: Play;
|
playButtonBehavior: Play;
|
||||||
resume: boolean;
|
resume: boolean;
|
||||||
@@ -319,8 +321,10 @@ const initialState: SettingsState = {
|
|||||||
externalLinks: true,
|
externalLinks: true,
|
||||||
followSystemTheme: false,
|
followSystemTheme: false,
|
||||||
genreTarget: GenreTarget.TRACK,
|
genreTarget: GenreTarget.TRACK,
|
||||||
|
homeFeature: true,
|
||||||
homeItems,
|
homeItems,
|
||||||
language: 'en',
|
language: 'en',
|
||||||
|
nativeAspectRatio: false,
|
||||||
passwordStore: undefined,
|
passwordStore: undefined,
|
||||||
playButtonBehavior: Play.NOW,
|
playButtonBehavior: Play.NOW,
|
||||||
resume: false,
|
resume: false,
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { ReactNode } from 'react';
|
||||||
import { ServerFeatures } from '/@/renderer/api/features-types';
|
import { ServerFeatures } from '/@/renderer/api/features-types';
|
||||||
import {
|
import {
|
||||||
Album,
|
Album,
|
||||||
@@ -37,6 +38,7 @@ export type TableType =
|
|||||||
|
|
||||||
export type CardRow<T> = {
|
export type CardRow<T> = {
|
||||||
arrayProperty?: string;
|
arrayProperty?: string;
|
||||||
|
format?: (value: T) => ReactNode;
|
||||||
property: keyof T;
|
property: keyof T;
|
||||||
route?: CardRoute;
|
route?: CardRoute;
|
||||||
};
|
};
|
||||||
@@ -209,8 +211,7 @@ export type GridCardData = {
|
|||||||
route: CardRoute;
|
route: CardRoute;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SongUpdate = {
|
export type SongState = {
|
||||||
currentTime?: number;
|
|
||||||
repeat?: PlayerRepeat;
|
repeat?: PlayerRepeat;
|
||||||
shuffle?: boolean;
|
shuffle?: boolean;
|
||||||
song?: QueueSong;
|
song?: QueueSong;
|
||||||
|
|||||||
@@ -1,24 +0,0 @@
|
|||||||
import formatDuration from 'format-duration';
|
|
||||||
|
|
||||||
export const formatDurationString = (duration: number) => {
|
|
||||||
const rawDuration = formatDuration(duration).split(':');
|
|
||||||
|
|
||||||
let string;
|
|
||||||
|
|
||||||
switch (rawDuration.length) {
|
|
||||||
case 1:
|
|
||||||
string = `${rawDuration[0]} sec`;
|
|
||||||
break;
|
|
||||||
case 2:
|
|
||||||
string = `${rawDuration[0]} min ${rawDuration[1]} sec`;
|
|
||||||
break;
|
|
||||||
case 3:
|
|
||||||
string = `${rawDuration[0]} hr ${rawDuration[1]} min ${rawDuration[2]} sec`;
|
|
||||||
break;
|
|
||||||
case 4:
|
|
||||||
string = `${rawDuration[0]} day ${rawDuration[1]} hr ${rawDuration[2]} min ${rawDuration[3]} sec`;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return string;
|
|
||||||
};
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
const SIZES = ['B', 'KiB', 'MiB', 'GiB', 'TiB'];
|
|
||||||
|
|
||||||
export const formatSizeString = (size?: number): string => {
|
|
||||||
let count = 0;
|
|
||||||
let finalSize = size ?? 0;
|
|
||||||
while (finalSize > 1024) {
|
|
||||||
finalSize /= 1024;
|
|
||||||
count += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return `${finalSize.toFixed(2)} ${SIZES[count]}`;
|
|
||||||
};
|
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import dayjs from 'dayjs';
|
||||||
|
import relativeTime from 'dayjs/plugin/relativeTime';
|
||||||
|
import formatDuration from 'format-duration';
|
||||||
|
import type { Album, AlbumArtist, Song } from '/@/renderer/api/types';
|
||||||
|
import { Rating } from '/@/renderer/components/rating';
|
||||||
|
|
||||||
|
dayjs.extend(relativeTime);
|
||||||
|
|
||||||
|
export const formatDateAbsolute = (key: string | null) =>
|
||||||
|
key ? dayjs(key).format('MMM D, YYYY') : '';
|
||||||
|
|
||||||
|
export const formatDateRelative = (key: string | null) => (key ? dayjs(key).fromNow() : '');
|
||||||
|
|
||||||
|
export const formatDurationString = (duration: number) => {
|
||||||
|
const rawDuration = formatDuration(duration).split(':');
|
||||||
|
|
||||||
|
let string;
|
||||||
|
|
||||||
|
switch (rawDuration.length) {
|
||||||
|
case 1:
|
||||||
|
string = `${rawDuration[0]} sec`;
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
string = `${rawDuration[0]} min ${rawDuration[1]} sec`;
|
||||||
|
break;
|
||||||
|
case 3:
|
||||||
|
string = `${rawDuration[0]} hr ${rawDuration[1]} min ${rawDuration[2]} sec`;
|
||||||
|
break;
|
||||||
|
case 4:
|
||||||
|
string = `${rawDuration[0]} day ${rawDuration[1]} hr ${rawDuration[2]} min ${rawDuration[3]} sec`;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const formatRating = (item: Album | AlbumArtist | Song) =>
|
||||||
|
item.userRating !== null ? (
|
||||||
|
<Rating
|
||||||
|
readOnly
|
||||||
|
value={item.userRating}
|
||||||
|
/>
|
||||||
|
) : null;
|
||||||
|
|
||||||
|
const SIZES = ['B', 'KiB', 'MiB', 'GiB', 'TiB'];
|
||||||
|
|
||||||
|
export const formatSizeString = (size?: number): string => {
|
||||||
|
let count = 0;
|
||||||
|
let finalSize = size ?? 0;
|
||||||
|
while (finalSize > 1024) {
|
||||||
|
finalSize /= 1024;
|
||||||
|
count += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${finalSize.toFixed(2)} ${SIZES[count]}`;
|
||||||
|
};
|
||||||
@@ -5,6 +5,6 @@ export * from './constrain-sidebar-width';
|
|||||||
export * from './title-case';
|
export * from './title-case';
|
||||||
export * from './get-header-color';
|
export * from './get-header-color';
|
||||||
export * from './parse-search-params';
|
export * from './parse-search-params';
|
||||||
export * from './format-duration-string';
|
|
||||||
export * from './rgb-to-rgba';
|
export * from './rgb-to-rgba';
|
||||||
export * from './sentence-case';
|
export * from './sentence-case';
|
||||||
|
export * from './format';
|
||||||
|
|||||||
Reference in New Issue
Block a user