mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-07 12:30:12 +02:00
Compare commits
143 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c94029012f | |||
| 2d9176cd21 | |||
| e28dad3f84 | |||
| 60d3eec8f7 | |||
| 62f9d064d9 | |||
| 196b9be65b | |||
| 587ce68018 | |||
| 1ec6176b77 | |||
| a5f28e49eb | |||
| 0b7d4bfb6a | |||
| 2492456b93 | |||
| 1c22c9506e | |||
| e00aeb2b67 | |||
| b219c900ca | |||
| 5eacb4e3cb | |||
| a86d44a29e | |||
| b7a0b7f997 | |||
| cd2d531c54 | |||
| 19c8980784 | |||
| a2e5f86eac | |||
| d8c93cadce | |||
| 35f87c8552 | |||
| 4f7b0983ec | |||
| 055d9ac5c1 | |||
| 039d008223 | |||
| 2b8db9cfc1 | |||
| caa9448200 | |||
| 176a95a946 | |||
| 6f5dd4881a | |||
| ce6aaa709f | |||
| 217a4d65fd | |||
| b88671161a | |||
| dde48335cd | |||
| 8611f08f54 | |||
| cd18e683bf | |||
| 286441c1b1 | |||
| 5456c2c2b8 | |||
| 5cd4fc227e | |||
| 737d672918 | |||
| a6ac4c8f67 | |||
| c9217827ab | |||
| 0ff8fad071 | |||
| f3cb15eae2 | |||
| 5b34b287e2 | |||
| dc461a253f | |||
| 958416af4c | |||
| 1dd8eec4a5 | |||
| b263db5483 | |||
| 528f60c5f3 | |||
| 007b0166ab | |||
| d3fb2374ff | |||
| 676c091d28 | |||
| 58b7572a8b | |||
| fc77c32a0e | |||
| b5bdea1845 | |||
| 8eb591bd08 | |||
| 88be98f703 | |||
| df6b6d514d | |||
| b6d902e425 | |||
| d922d8b034 | |||
| f4db8fdb84 | |||
| 81ca6937bc | |||
| c382e01f64 | |||
| fb80b66310 | |||
| 63e3b97bca | |||
| fb584b35a9 | |||
| bdc372636b | |||
| 2c5671cf38 | |||
| bd12fbecac | |||
| c1d88ada91 | |||
| d6a3e1d90b | |||
| 789c7f3d81 | |||
| f3c785d0fa | |||
| 062c1c2b61 | |||
| eb078d62cd | |||
| c429ac9223 | |||
| bd26967ff2 | |||
| 620b810191 | |||
| 64866c59bd | |||
| 0afbe4c0a2 | |||
| 6782cd0dcc | |||
| 8f585a5be9 | |||
| ac0c396712 | |||
| b989a66991 | |||
| 2814b623e7 | |||
| 7d29a692ef | |||
| 3f9eb446f7 | |||
| d8f7b49ab6 | |||
| 35e70a3eff | |||
| ef9c16e940 | |||
| 0b39c35132 | |||
| 9f5b4e5410 | |||
| dbf840b185 | |||
| e0f0524eb9 | |||
| 8598313d12 | |||
| c84dd648ea | |||
| 01885c1a9b | |||
| 5121f57171 | |||
| 3d7ee10328 | |||
| 4db47b4d37 | |||
| 786a693526 | |||
| 1faef6a1a7 | |||
| 1598642389 | |||
| 8c4a7f4f91 | |||
| 5878f89339 | |||
| 4acbb1820d | |||
| 01f5745629 | |||
| 73dd781a88 | |||
| d777be6251 | |||
| 6689e84f67 | |||
| ad533a1d9c | |||
| b691891e62 | |||
| 53fa265af9 | |||
| 55f6a382d4 | |||
| 9c30acdd56 | |||
| b29d3e7f78 | |||
| c1330d92b2 | |||
| bea55d48a8 | |||
| ae41fe99bb | |||
| e3751229b6 | |||
| 87c9963354 | |||
| b7fb7c7f94 | |||
| b8ceb174b3 | |||
| 48f085b0ac | |||
| dfc0639f95 | |||
| 80ffd1a925 | |||
| c87bb65023 | |||
| 5ae21bd224 | |||
| 12d293a74c | |||
| 62f4bb0d7b | |||
| 9f11061433 | |||
| aba64b10d0 | |||
| c20e30e387 | |||
| c4b4300845 | |||
| f1e5ed41bc | |||
| 9b79502022 | |||
| 636c227a83 | |||
| e8a94a0b1c | |||
| fa93dfd771 | |||
| 8629994eb6 | |||
| c54423a667 | |||
| d28fc9f630 | |||
| bd2b39fdfb |
@@ -0,0 +1,33 @@
|
||||
name: Feature request
|
||||
description: Request a feature to be added to Feishin
|
||||
title: '[Feature]: '
|
||||
labels: ['enhancement']
|
||||
body:
|
||||
- type: checkboxes
|
||||
id: check-duplicate
|
||||
attributes:
|
||||
label: I have already checked through the existing feature requests and found no duplicates
|
||||
options:
|
||||
- label: 'Yes'
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: server-specific
|
||||
attributes:
|
||||
label: Is this a server-specific feature?
|
||||
options:
|
||||
- Not server-specific
|
||||
- OpenSubsonic
|
||||
- Jellyfin
|
||||
- Navidrome
|
||||
default: 0
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: solution
|
||||
attributes:
|
||||
label: What do you want to be added?
|
||||
placeholder: I would like to see [...]
|
||||
validations:
|
||||
required: true
|
||||
@@ -0,0 +1,74 @@
|
||||
name: Bug report
|
||||
description: You're having technical issues.
|
||||
title: '[Bug]: '
|
||||
labels: ['bug']
|
||||
body:
|
||||
- type: checkboxes
|
||||
id: check-duplicate
|
||||
attributes:
|
||||
label: I have already checked through the existing bug reports and found no duplicates
|
||||
options:
|
||||
- label: 'Yes'
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: version
|
||||
attributes:
|
||||
label: App Version
|
||||
description: What version of the app are you running?
|
||||
placeholder: ex. 1.0.0
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: server-version
|
||||
attributes:
|
||||
label: Music Server and Version
|
||||
description: What music server are you using?
|
||||
placeholder: ex. Navidrome v0.55.0, LMS v3.67.0, Jellyfin v10.10.7, etc.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: environments
|
||||
attributes:
|
||||
label: What local environments are you seeing the problem on?
|
||||
multiple: true
|
||||
options:
|
||||
- Desktop Windows
|
||||
- Desktop macOS
|
||||
- Desktop Linux
|
||||
- Web Firefox
|
||||
- Web Chrome
|
||||
- Web Safari
|
||||
- Web Microsoft Edge
|
||||
- Other (please specify in the next field)
|
||||
|
||||
- type: textarea
|
||||
id: what-happened
|
||||
attributes:
|
||||
label: What happened?
|
||||
description: Also tell us, what did you expect to happen?
|
||||
placeholder: Include screenshots and error logs if possible. The browser devtools can be opened using CTRL + SHIFT + I (Windows/Linux) or CMD + SHIFT + I (macOS).
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: reproduction
|
||||
attributes:
|
||||
label: Steps to reproduce
|
||||
description: How can we reproduce this issue? Are there any specific settings that are enabled that could be the cause?
|
||||
placeholder: |
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: logs
|
||||
attributes:
|
||||
label: Relevant log output
|
||||
description: Please copy and paste any relevant log output. This will be automatically formatted into code.
|
||||
render: shell
|
||||
@@ -1,63 +0,0 @@
|
||||
name: Bug report
|
||||
description: You're having technical issues. 🐞
|
||||
labels: ['bug']
|
||||
body:
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Expected Behavior
|
||||
description: What should have happened?
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Current Behavior
|
||||
description: What went wrong? Add screenshots to help explain your problem. (Open the browser dev tools in the menu or using CTRL + SHIFT + I)
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Steps to Reproduce
|
||||
placeholder: |
|
||||
<!-- Add relevant code and/or a live example -->
|
||||
<!-- Add stack traces -->
|
||||
1.
|
||||
2.
|
||||
3.
|
||||
4.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Possible Solution
|
||||
description: Suggest a reason for the bug or how to fix it.
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Context
|
||||
description: How has this issue affected you? What are you trying to accomplish?
|
||||
validations:
|
||||
required: false
|
||||
- type: input
|
||||
attributes:
|
||||
label: Application version
|
||||
placeholder: (e.g. v0.1.0)
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
attributes:
|
||||
label: Operating System and version
|
||||
placeholder: (e.g. Windows 11 desktop, Webapp in Firefox)
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
attributes:
|
||||
label: Server and Version
|
||||
placeholder: (e.g. Navidrome v0.48.0)
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
attributes:
|
||||
label: Node Version (if developing locally)
|
||||
validations:
|
||||
required: false
|
||||
@@ -1,5 +1,11 @@
|
||||
blank_issues_enabled: true
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Question
|
||||
- name: Questions or help
|
||||
url: https://github.com/jeffvli/feishin/discussions
|
||||
about: Please ask and answer questions here.
|
||||
about: Ask questions or get help in the discussions section
|
||||
- name: Discord Community
|
||||
url: https://discord.gg/FVKpcMDy5f
|
||||
about: The discord/matrix servers are bridged so you can join whichever you prefer
|
||||
- name: Matrix Community
|
||||
url: https://matrix.to/#/#sonixd:matrix.org
|
||||
about: The discord/matrix servers are bridged so you can join whichever you prefer
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
name: Feature request - NOT ACCEPTING NEW FEATURE REQUESTS
|
||||
description: Feature requests are currently closed. The application is actively being rewritten https://github.com/audioling/audioling.
|
||||
labels: ['enhancement']
|
||||
body:
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: What do you want to be added?
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Additional context
|
||||
validations:
|
||||
required: false
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Is this a server-specific feature? (e.g. Jellyfin only)
|
||||
options:
|
||||
- label: 'Yes'
|
||||
required: false
|
||||
@@ -3,6 +3,7 @@ name: Publish Docker to GHCR
|
||||
permissions: write-all
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
tags:
|
||||
- 'v*.*.*'
|
||||
@@ -49,6 +50,6 @@ jobs:
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
platforms: |
|
||||
linux/amd64
|
||||
linux/arm/v7
|
||||
linux/arm64/v8
|
||||
linux/amd64
|
||||
linux/arm/v7
|
||||
linux/arm64/v8
|
||||
|
||||
@@ -24,11 +24,9 @@ jobs:
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
tags: |
|
||||
type=raw,value=latest,enable={{is_default_branch}}
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
- name: Setup Docker buildx
|
||||
@@ -41,6 +39,6 @@ jobs:
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
platforms: |
|
||||
linux/amd64
|
||||
linux/arm/v7
|
||||
linux/arm64/v8
|
||||
linux/amd64
|
||||
linux/arm/v7
|
||||
linux/arm64/v8
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
name: 'Close stale issues and PRs'
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: '30 1 * * *'
|
||||
permissions:
|
||||
contents: read
|
||||
jobs:
|
||||
stale:
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: dessant/lock-threads@v5
|
||||
with:
|
||||
process-only: 'issues, prs'
|
||||
issue-inactive-days: 120
|
||||
pr-inactive-days: 120
|
||||
log-output: true
|
||||
add-issue-labels: 'frozen-due-to-age'
|
||||
add-pr-labels: 'frozen-due-to-age'
|
||||
- uses: actions/stale@v9
|
||||
with:
|
||||
operations-per-run: 999
|
||||
days-before-issue-stale: 180
|
||||
days-before-pr-stale: 180
|
||||
days-before-issue-close: 30
|
||||
days-before-pr-close: 30
|
||||
stale-issue-message: >
|
||||
This issue has been automatically marked as stale because it has not had recent activity. The resources of the Feishin team are limited, and so we are asking for your help.
|
||||
|
||||
If this is a **bug** and you can still reproduce this error on the <code>development</code> branch, please reply with all of the information you have about it in order to keep the issue open.
|
||||
|
||||
This issue will automatically be closed in the near future if no further activity occurs. Thank you for all your contributions.
|
||||
|
||||
|
||||
stale-pr-message: >
|
||||
This PR has been automatically marked as stale because it has not had recent activity. The resources of the Feishin team are limited, and so we are asking for your help.
|
||||
|
||||
This PR will automatically be closed in the near future if no further activity occurs. Thank you for all your contributions.
|
||||
|
||||
|
||||
stale-issue-label: 'stale'
|
||||
exempt-issue-labels: 'enhancement,keep,security'
|
||||
stale-pr-label: 'stale'
|
||||
exempt-pr-labels: 'keep,security'
|
||||
+9
-7
@@ -1,17 +1,19 @@
|
||||
{
|
||||
"customSyntax": "postcss-styled-syntax",
|
||||
"extends": [
|
||||
"stylelint-config-standard",
|
||||
"stylelint-config-styled-components",
|
||||
"stylelint-config-css-modules",
|
||||
"stylelint-config-recess-order"
|
||||
],
|
||||
"rules": {
|
||||
"declaration-empty-line-before": null,
|
||||
"declaration-block-no-redundant-longhand-properties": null,
|
||||
"selector-class-pattern": null,
|
||||
"block-no-empty": null,
|
||||
"selector-type-case": ["lower", { "ignoreTypes": ["/^\\$\\w+/"] }],
|
||||
"selector-type-no-unknown": [true, { "ignoreTypes": ["/-styled-mixin/", "/^\\$\\w+/"] }],
|
||||
"declaration-colon-newline-after": null,
|
||||
"property-no-vendor-prefix": null
|
||||
"declaration-block-no-shorthand-property-overrides": null,
|
||||
"declaration-block-no-redundant-longhand-properties": null,
|
||||
"at-rule-no-unknown": [true, { "ignoreAtRules": ["mixin", "value"] }],
|
||||
"function-no-unknown": [true, { "ignoreFunctions": ["darken", "alpha", "lighten"] }],
|
||||
"declaration-property-value-no-unknown": null,
|
||||
"no-descending-specificity": null,
|
||||
"no-empty-source": null
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
+9
-7
@@ -26,10 +26,6 @@
|
||||
"source.formatDocument": "explicit"
|
||||
},
|
||||
"css.validate": true,
|
||||
"less.validate": false,
|
||||
"scss.validate": true,
|
||||
"scss.lint.unknownAtRules": "warning",
|
||||
"scss.lint.unknownProperties": "warning",
|
||||
"javascript.validate.enable": false,
|
||||
"javascript.format.enable": false,
|
||||
"typescript.format.enable": false,
|
||||
@@ -49,8 +45,14 @@
|
||||
"i18n-ally.localesPaths": ["src/i18n", "src/i18n/locales"],
|
||||
"typescript.tsdk": "node_modules\\typescript\\lib",
|
||||
"typescript.preferences.importModuleSpecifier": "non-relative",
|
||||
"stylelint.validate": ["css", "scss", "typescript", "typescriptreact"],
|
||||
"stylelint.config": null,
|
||||
"stylelint.validate": ["css", "postcss"],
|
||||
"typescript.updateImportsOnFileMove.enabled": "always",
|
||||
"typescript.preferences.autoImportFileExcludePatterns": [
|
||||
"@mantine/core",
|
||||
"@mantine/modals",
|
||||
"@mantine/dates"
|
||||
],
|
||||
"[typescriptreact]": { "editor.defaultFormatter": "esbenp.prettier-vscode" },
|
||||
"typescript.format.insertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces": true,
|
||||
"folderTemplates.structures": [
|
||||
@@ -63,14 +65,14 @@
|
||||
"template": "Functional Component with CSS Modules"
|
||||
},
|
||||
{
|
||||
"fileName": "<FTName | kebabcase>.module.scss"
|
||||
"fileName": "<FTName | kebabcase>.module.css"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"folderTemplates.fileTemplates": {
|
||||
"Functional Component with CSS Modules": [
|
||||
"import styles from './<FTName | kebabcase>.module.scss';",
|
||||
"import styles from './<FTName | kebabcase>.module.css';",
|
||||
"",
|
||||
"interface <FTName | pascalcase>Props {}",
|
||||
"",
|
||||
|
||||
+5
-3
@@ -1,9 +1,11 @@
|
||||
# --- Builder stage
|
||||
FROM node:18-alpine as builder
|
||||
FROM node:23-alpine as builder
|
||||
WORKDIR /app
|
||||
|
||||
# Copy package.json first to cache node_modules
|
||||
COPY package.json package-lock.json .
|
||||
COPY package.json pnpm-lock.yaml .
|
||||
|
||||
RUN npm install -g pnpm
|
||||
|
||||
RUN pnpm install
|
||||
|
||||
@@ -14,7 +16,7 @@ RUN pnpm run build:web
|
||||
# --- Production stage
|
||||
FROM nginx:alpine-slim
|
||||
|
||||
COPY --chown=nginx:nginx --from=builder /app/release/app/dist/web /usr/share/nginx/html
|
||||
COPY --chown=nginx:nginx --from=builder /app/out/web /usr/share/nginx/html
|
||||
COPY ./settings.js.template /etc/nginx/templates/settings.js.template
|
||||
COPY ng.conf.template /etc/nginx/templates/default.conf.template
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<img src="assets/icons/icon.png" alt="logo" title="feishin" align="right" height="60px" />
|
||||
<img src="assets/icons/icon.png" alt="logo" title="feishin" align="right" height="60px" width="60px" />
|
||||
|
||||
# Feishin
|
||||
|
||||
@@ -116,11 +116,11 @@ First thing to do is check that your MPV binary path is correct. Navigate to the
|
||||
|
||||
### What music servers does Feishin support?
|
||||
|
||||
Feishin supports any music server that implements a [Navidrome](https://www.navidrome.org/) or [Jellyfin](https://jellyfin.org/) API. **Subsonic API is not currently supported**. This will likely be added in [later when the new Subsonic API is decided on](https://support.symfonium.app/t/subsonic-servers-participation/1233).
|
||||
Feishin supports any music server that implements a [Navidrome](https://www.navidrome.org/), [Jellyfin](https://jellyfin.org/), or [OpenSubsonic compatible](https://opensubsonic.netlify.app/) API.
|
||||
|
||||
- [Navidrome](https://github.com/navidrome/navidrome)
|
||||
- [Jellyfin](https://github.com/jellyfin/jellyfin)
|
||||
- Subsonic-compatible servers
|
||||
- [OpenSubsonic](https://opensubsonic.netlify.app/) compatible servers, such as...
|
||||
- [Airsonic-Advanced](https://github.com/airsonic-advanced/airsonic-advanced)
|
||||
- [Ampache](https://ampache.org)
|
||||
- [Astiga](https://asti.ga/)
|
||||
|
||||
Binary file not shown.
@@ -13,6 +13,7 @@ const config: UserConfig = {
|
||||
rollupOptions: {
|
||||
external: ['source-map-support'],
|
||||
},
|
||||
sourcemap: true,
|
||||
},
|
||||
define: {
|
||||
'import.meta.env.IS_LINUX': JSON.stringify(currentOSEnv === 'linux'),
|
||||
@@ -46,7 +47,7 @@ const config: UserConfig = {
|
||||
renderer: {
|
||||
css: {
|
||||
modules: {
|
||||
generateScopedName: '[name]__[local]__[hash:base64:5]',
|
||||
generateScopedName: 'fs-[name]-[local]',
|
||||
localsConvention: 'camelCase',
|
||||
},
|
||||
},
|
||||
|
||||
@@ -46,6 +46,7 @@ export default tseslint.config(
|
||||
'react-refresh/only-export-components': 'off',
|
||||
'react/display-name': 'off',
|
||||
semi: ['error', 'always'],
|
||||
'single-attribute-per-line': 'off',
|
||||
},
|
||||
},
|
||||
eslintConfigPrettier,
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
server {
|
||||
listen 9180;
|
||||
sendfile on;
|
||||
default_type application/octet-stream;
|
||||
|
||||
gzip on;
|
||||
gzip_http_version 1.1;
|
||||
gzip_disable "MSIE [1-6]\.";
|
||||
gzip_min_length 256;
|
||||
gzip_vary on;
|
||||
gzip_proxied expired no-cache no-store private auth;
|
||||
gzip_types text/plain text/css application/json application/javascript application/x-javascript text/xml application/xml application/xml+rss text/javascript;
|
||||
gzip_comp_level 9;
|
||||
|
||||
location ${PUBLIC_PATH} {
|
||||
alias /usr/share/nginx/html/;
|
||||
try_files $uri $uri/ /index.html =404;
|
||||
}
|
||||
|
||||
location ${PUBLIC_PATH}settings.js {
|
||||
alias /etc/nginx/conf.d/settings.js;
|
||||
}
|
||||
|
||||
location ${PUBLIC_PATH}/settings.js {
|
||||
alias /etc/nginx/conf.d/settings.js;
|
||||
}
|
||||
}
|
||||
+36
-32
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "feishin",
|
||||
"version": "0.14.0",
|
||||
"version": "0.18.0",
|
||||
"description": "A modern self-hosted music player.",
|
||||
"keywords": [
|
||||
"subsonic",
|
||||
@@ -25,11 +25,16 @@
|
||||
"build:remote": "vite build --config remote.vite.config.ts",
|
||||
"build:web": "vite build --config web.vite.config.ts",
|
||||
"dev": "electron-vite dev",
|
||||
"dev:remote": "vite dev --config remote.vite.config.ts",
|
||||
"dev:watch": "electron-vite dev --watch",
|
||||
"i18next": "i18next -c src/i18n/i18next-parser.config.js",
|
||||
"postinstall": "electron-builder install-app-deps",
|
||||
"lint": "eslint --cache .",
|
||||
"lint:fix": "eslint --cache --fix .",
|
||||
"lint": "pnpm run lint-code && pnpm run lint-styles",
|
||||
"lint-code": "eslint --cache .",
|
||||
"lint-code:fix": "eslint --cache --fix .",
|
||||
"lint-styles": "stylelint 'src/**/*.{css,scss}'",
|
||||
"lint-styles:fix": "stylelint 'src/**/*.{css,scss}' --fix",
|
||||
"lint:fix": "pnpm run lint-code:fix && pnpm run lint-styles:fix",
|
||||
"package": "pnpm run build && electron-builder",
|
||||
"package:dev": "pnpm run build && electron-builder --dir",
|
||||
"package:linux": "pnpm run build && electron-builder --linux",
|
||||
@@ -54,16 +59,18 @@
|
||||
"@ag-grid-community/infinite-row-model": "^28.2.1",
|
||||
"@ag-grid-community/react": "^28.2.1",
|
||||
"@ag-grid-community/styles": "^28.2.1",
|
||||
"@atlaskit/pragmatic-drag-and-drop": "1.4.0",
|
||||
"@atlaskit/pragmatic-drag-and-drop-auto-scroll": "^2.1.0",
|
||||
"@atlaskit/pragmatic-drag-and-drop-hitbox": "^1.0.3",
|
||||
"@electron-toolkit/preload": "^3.0.1",
|
||||
"@electron-toolkit/utils": "^4.0.0",
|
||||
"@emotion/react": "^11.10.4",
|
||||
"@mantine/core": "^6.0.22",
|
||||
"@mantine/dates": "^6.0.22",
|
||||
"@mantine/form": "^6.0.22",
|
||||
"@mantine/hooks": "^6.0.22",
|
||||
"@mantine/modals": "^6.0.22",
|
||||
"@mantine/notifications": "^6.0.22",
|
||||
"@mantine/utils": "^6.0.22",
|
||||
"@mantine/colors-generator": "^8.1.1",
|
||||
"@mantine/core": "^8.1.1",
|
||||
"@mantine/dates": "^8.1.1",
|
||||
"@mantine/form": "^8.1.1",
|
||||
"@mantine/hooks": "^8.1.1",
|
||||
"@mantine/modals": "^8.1.1",
|
||||
"@mantine/notifications": "^8.1.1",
|
||||
"@tanstack/react-query": "^4.32.1",
|
||||
"@tanstack/react-query-devtools": "^4.32.1",
|
||||
"@tanstack/react-query-persist-client": "^4.32.1",
|
||||
@@ -84,7 +91,6 @@
|
||||
"electron-updater": "^6.3.9",
|
||||
"fast-average-color": "^9.3.0",
|
||||
"format-duration": "^2.0.0",
|
||||
"framer-motion": "^11.0.0",
|
||||
"fuse.js": "^6.6.2",
|
||||
"i18next": "^21.10.0",
|
||||
"idb-keyval": "^6.2.1",
|
||||
@@ -93,43 +99,46 @@
|
||||
"lodash": "^4.17.21",
|
||||
"md5": "^2.3.0",
|
||||
"memoize-one": "^6.0.0",
|
||||
"motion": "^12.18.1",
|
||||
"mpris-service": "^2.1.2",
|
||||
"nanoid": "^3.3.3",
|
||||
"node-mpv": "github:jeffvli/Node-MPV#32b4d64395289ad710c41d481d2707a7acfc228f",
|
||||
"overlayscrollbars": "^2.11.1",
|
||||
"overlayscrollbars-react": "^0.5.6",
|
||||
"qs": "^6.14.0",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"react-error-boundary": "^3.1.4",
|
||||
"react-i18next": "^11.18.6",
|
||||
"react-icons": "^4.10.1",
|
||||
"react-icons": "^5.5.0",
|
||||
"react-image": "^4.1.0",
|
||||
"react-loading-skeleton": "^3.5.0",
|
||||
"react-player": "^2.11.0",
|
||||
"react-router": "^6.16.0",
|
||||
"react-router-dom": "^6.16.0",
|
||||
"react-simple-img": "^3.0.0",
|
||||
"react-virtualized-auto-sizer": "^1.0.17",
|
||||
"react-window": "^1.8.9",
|
||||
"react-window-infinite-loader": "^1.0.9",
|
||||
"semver": "^7.5.4",
|
||||
"styled-components": "^6.0.8",
|
||||
"swiper": "^9.3.1",
|
||||
"use-sync-external-store": "^1.5.0",
|
||||
"ws": "^8.18.2",
|
||||
"zod": "^3.22.3",
|
||||
"zustand": "^4.3.9"
|
||||
"zustand": "^5.0.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@electron-toolkit/eslint-config-prettier": "^3.0.0",
|
||||
"@electron-toolkit/eslint-config-ts": "^3.0.0",
|
||||
"@electron-toolkit/tsconfig": "^1.0.1",
|
||||
"@types/electron-localshortcut": "^3.1.0",
|
||||
"@types/lodash": "^4.14.188",
|
||||
"@types/md5": "^2.3.2",
|
||||
"@types/node": "^22.14.1",
|
||||
"@types/react": "^18.3.1",
|
||||
"@types/react-dom": "^18.3.1",
|
||||
"@types/lodash": "^4.17.18",
|
||||
"@types/md5": "^2.3.5",
|
||||
"@types/node": "^22.15.32",
|
||||
"@types/react": "^18.3.23",
|
||||
"@types/react-dom": "^18.3.7",
|
||||
"@types/react-window": "^1.8.5",
|
||||
"@types/react-window-infinite-loader": "^1.0.6",
|
||||
"@types/source-map-support": "^0.5.10",
|
||||
"@types/styled-components": "^5.1.26",
|
||||
"@types/ws": "^8.18.1",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"concurrently": "^7.1.0",
|
||||
@@ -145,20 +154,15 @@
|
||||
"eslint-plugin-react-hooks": "^5.2.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.19",
|
||||
"i18next-parser": "^9.0.2",
|
||||
"postcss-styled-syntax": "^0.5.0",
|
||||
"postcss-preset-mantine": "^1.17.0",
|
||||
"prettier": "^3.5.3",
|
||||
"prettier-plugin-packagejson": "^2.5.14",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"sass-embedded": "^1.89.0",
|
||||
"stylelint": "^15.10.3",
|
||||
"stylelint-config-css-modules": "^4.3.0",
|
||||
"stylelint-config-recess-order": "^4.3.0",
|
||||
"stylelint-config-standard": "^34.0.0",
|
||||
"stylelint-config-standard-scss": "^4.0.0",
|
||||
"stylelint-config-styled-components": "^0.1.1",
|
||||
"stylelint": "^16.14.1",
|
||||
"stylelint-config-css-modules": "^4.4.0",
|
||||
"stylelint-config-recess-order": "^7.1.0",
|
||||
"stylelint-config-standard": "^38.0.0",
|
||||
"typescript": "^5.8.3",
|
||||
"typescript-plugin-styled-components": "^3.0.0",
|
||||
"vite": "^6.3.5",
|
||||
"vite-plugin-conditional-import": "^0.1.7",
|
||||
"vite-plugin-dynamic-import": "^1.6.0",
|
||||
|
||||
Generated
+742
-1312
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,5 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
'postcss-preset-mantine': {},
|
||||
},
|
||||
};
|
||||
@@ -23,6 +23,13 @@ export default defineConfig({
|
||||
entryFileNames: '[name].js',
|
||||
},
|
||||
},
|
||||
sourcemap: true,
|
||||
},
|
||||
css: {
|
||||
modules: {
|
||||
generateScopedName: 'fs-[name]-[local]',
|
||||
localsConvention: 'camelCase',
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
react(),
|
||||
|
||||
@@ -19,6 +19,7 @@ import nl from './locales/nl.json';
|
||||
import pl from './locales/pl.json';
|
||||
import ptBr from './locales/pt-BR.json';
|
||||
import ru from './locales/ru.json';
|
||||
import sl from './locales/sl.json';
|
||||
import sr from './locales/sr.json';
|
||||
import sv from './locales/sv.json';
|
||||
import ta from './locales/ta.json';
|
||||
@@ -43,6 +44,7 @@ const resources = {
|
||||
pl: { translation: pl },
|
||||
'pt-BR': { translation: ptBr },
|
||||
ru: { translation: ru },
|
||||
sl: { translation: sl },
|
||||
sr: { translation: sr },
|
||||
sv: { translation: sv },
|
||||
ta: { translation: ta },
|
||||
@@ -119,6 +121,10 @@ export const languages = [
|
||||
label: 'Русский',
|
||||
value: 'ru',
|
||||
},
|
||||
{
|
||||
label: 'Slovenščina',
|
||||
value: 'sl',
|
||||
},
|
||||
{
|
||||
label: 'Srpski',
|
||||
value: 'sr',
|
||||
|
||||
@@ -124,7 +124,7 @@
|
||||
"hotkey_toggleShuffle": "přepnutí náhodného přehrávání",
|
||||
"theme": "motiv",
|
||||
"playbackStyle_description": "nastavení způsobu přehrávání pro přehrávač zvuku",
|
||||
"discordRichPresence_description": "povolit stav přehrávání v {{discord}} rich presence. Klíče obrázků jsou: {{icon}}, {{playing}}, {{paused}} ",
|
||||
"discordRichPresence_description": "povolit stav přehrávání v {{discord}} rich presence. Klíče obrázků jsou: {{icon}}, {{playing}}, {{paused}}",
|
||||
"mpvExecutablePath": "cesta ke spustitelnému souboru mpv",
|
||||
"audioDevice": "zvukové zařízení",
|
||||
"hotkey_rate2": "hodnocení 2 hvězdami",
|
||||
@@ -171,7 +171,7 @@
|
||||
"hotkey_zoomOut": "oddálení",
|
||||
"hotkey_unfavoriteCurrentSong": "zrušení oblíbení u $t(common.currentSong)",
|
||||
"hotkey_rate0": "vymazání hodnocení",
|
||||
"discordApplicationId": "aplikační id pro {{discord}}",
|
||||
"discordApplicationId": "id aplikace pro {{discord}}",
|
||||
"applicationHotkeys_description": "nastavení klávesových zkratek aplikace. přepněte pole pro nastavení jako globální zkratku (pouze na počítači)",
|
||||
"floatingQueueArea_description": "zobrazit ikonu přejetí myší na pravé straně obrazovky pro zobrazení fronty",
|
||||
"hotkey_volumeMute": "ztlumení",
|
||||
@@ -259,7 +259,19 @@
|
||||
"lastfmApiKey": "klíč API {{lastfm}}",
|
||||
"lastfmApiKey_description": "klíč API pro {{lastfm}}. vyžadováno pro obaly alb",
|
||||
"discordServeImage": "načítat obrázky {{discord}} ze serveru",
|
||||
"discordServeImage_description": "sdílet obaly alb pro {{discord}} rich presence ze samotného serveru, dostupné pouze pro jellyfin a navidrome"
|
||||
"discordServeImage_description": "sdílet obaly alb pro {{discord}} rich presence ze samotného serveru, dostupné pouze pro jellyfin a navidrome",
|
||||
"lastfm": "zobrazit odkazy na last.fm",
|
||||
"lastfm_description": "na stránkách umělců a alb zobrazit odkazy na last.fm",
|
||||
"musicbrainz": "zobrazit odkazy na musicbrainz",
|
||||
"musicbrainz_description": "na stránkách umělců a alb, kde existuje mbid, zobrazit odkazy na musicbrainz",
|
||||
"neteaseTranslation": "Povolit překlady NetEase",
|
||||
"neteaseTranslation_description": "Pokud je povoleno, načte a zobrazí přeložené texty ze služby NetEase, pokud jsou dostupné.",
|
||||
"preferLocalLyrics": "preferovat místní texty",
|
||||
"preferLocalLyrics_description": "preferovat místní texty před vzdálenými, pokud jsou dostupné",
|
||||
"discordPausedStatus": "zobrazit rich presence při pozastavení",
|
||||
"discordPausedStatus_description": "pokud je povoleno, bude při pozastavení přehrávače zobrazen stav",
|
||||
"preservePitch": "zachovat výšku",
|
||||
"preservePitch_description": "zachová výšku při úpravě rychlosti přehrávání"
|
||||
},
|
||||
"action": {
|
||||
"editPlaylist": "upravit $t(entity.playlist_one)",
|
||||
@@ -367,7 +379,7 @@
|
||||
"size": "velikost",
|
||||
"biography": "biografie",
|
||||
"note": "poznámka",
|
||||
"albumGain": "zisk (gain) alba",
|
||||
"albumGain": "gain alba",
|
||||
"albumPeak": "vrchol alba",
|
||||
"close": "zavřít",
|
||||
"mbid": "ID MusicBrainz",
|
||||
@@ -379,14 +391,18 @@
|
||||
"preview": "náhled",
|
||||
"translation": "překlad",
|
||||
"additionalParticipants": "další přispívající",
|
||||
"tags": "štítky"
|
||||
"tags": "štítky",
|
||||
"viewReleaseNotes": "zobrazit seznam změn",
|
||||
"newVersion": "byla nainstalována nová verze ({{version}})"
|
||||
},
|
||||
"table": {
|
||||
"config": {
|
||||
"view": {
|
||||
"card": "karta",
|
||||
"table": "tabulka",
|
||||
"poster": "plakát"
|
||||
"poster": "plakát",
|
||||
"list": "seznam",
|
||||
"grid": "mřížka"
|
||||
},
|
||||
"general": {
|
||||
"displayType": "typ zobrazení",
|
||||
@@ -538,7 +554,8 @@
|
||||
"home": "$t(common.home)",
|
||||
"artists": "$t(entity.artist_other)",
|
||||
"albumArtists": "$t(entity.albumArtist_other)",
|
||||
"shared": "$t(entity.playlist_other) sdíleny"
|
||||
"shared": "$t(entity.playlist_other) sdíleny",
|
||||
"myLibrary": "moje knihovna"
|
||||
},
|
||||
"fullscreenPlayer": {
|
||||
"config": {
|
||||
@@ -714,7 +731,8 @@
|
||||
},
|
||||
"queryEditor": {
|
||||
"input_optionMatchAll": "shoda všeho",
|
||||
"input_optionMatchAny": "shoda libovolného"
|
||||
"input_optionMatchAny": "shoda libovolného",
|
||||
"title": "editor dotazů"
|
||||
},
|
||||
"lyricSearch": {
|
||||
"input_name": "$t(common.name)",
|
||||
|
||||
@@ -113,7 +113,9 @@
|
||||
"trackPeak": "Track-Spitzenpegel",
|
||||
"codec": "Codec",
|
||||
"albumPeak": "Album-Spitzenpegel",
|
||||
"albumGain": "Album-Pegelverstärkung"
|
||||
"albumGain": "Album-Pegelverstärkung",
|
||||
"tags": "tags",
|
||||
"viewReleaseNotes": "Release Notes anzeigen"
|
||||
},
|
||||
"error": {
|
||||
"remotePortWarning": "Starten Sie den Server neu, um den neuen Port anzuwenden",
|
||||
@@ -237,7 +239,8 @@
|
||||
"description": "Beschreibung",
|
||||
"setExpiration": "Ablaufdatum setzen",
|
||||
"expireInvalid": "Ablaufdatum muss in der Zukunft liegen",
|
||||
"allowDownloading": "Herunterladen zulassen"
|
||||
"allowDownloading": "Herunterladen zulassen",
|
||||
"success": "Link in die Zwischenablage kopiert (oder hier klicken um zu öffnen)"
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
@@ -429,7 +432,8 @@
|
||||
"home": "$t(common.home)",
|
||||
"artists": "$t(entity.artist_other)",
|
||||
"albumArtists": "$t(entity.albumArtist_other)",
|
||||
"shared": "$t(entity.playlist_other) geteilt"
|
||||
"shared": "$t(entity.playlist_other) geteilt",
|
||||
"myLibrary": "meine bibliothek"
|
||||
},
|
||||
"setting": {
|
||||
"playbackTab": "Wiedergabe",
|
||||
@@ -516,7 +520,7 @@
|
||||
"playSimilarSongs": "Ähnliche Lieder abspielen"
|
||||
},
|
||||
"setting": {
|
||||
"audioDevice_description": "Wählen Sie das Audiogerät aus, das für die Wiedergabe verwendet werden soll (nur Webplayer).",
|
||||
"audioDevice_description": "Wählen Sie das Audiogerät aus, das für die Wiedergabe verwendet werden soll (nur Webplayer)",
|
||||
"audioExclusiveMode": "Audio-Exklusivmodus",
|
||||
"audioDevice": "Audiogerät",
|
||||
"accentColor": "Akzentfarbe",
|
||||
@@ -670,12 +674,23 @@
|
||||
"windowBarStyle_description": "Wähle den Stil der Windows-Leiste",
|
||||
"hotkey_toggleCurrentSongFavorite": "$t(common.currentSong) zu Favoriten hinzufügen",
|
||||
"clearQueryCache_description": "\"Weiches\" Zurücksetzen. Dies wird Playlisten, Musik-Metadaten und gespeicherte Liedtexte zurücksetzen, Zugangsinformationen und zwischengespeicherte Bilder werden behalten",
|
||||
"discordRichPresence_description": "Zeige deinen Wiedergabe-Status in {{discord}} als rich presence an. Angezeigte Bilder sind: {{icon}}, {{playing}}, und {{paused}} ",
|
||||
"discordRichPresence_description": "Zeige deinen Wiedergabe-Status in {{discord}} als rich presence an. Angezeigte Bilder sind: {{icon}}, {{playing}}, und {{paused}}",
|
||||
"clearCache": "Browser-Zwischenspeicher löschen",
|
||||
"clearQueryCache": "feishins Zwischenspeicher leeren",
|
||||
"clearCache_description": "Hartes Zurücksetzen. Neben feishins Zwischenspeicher wird auch der des Browsers gelöscht (Bilder und andere Daten). Zugangsinformationen und Einstellungen werden behalten",
|
||||
"sidePlayQueueStyle": "Wiedergabelistenstil in der Seitenleiste",
|
||||
"zoom_description": "Setzt den Zoom (in %) für das Programm",
|
||||
"zoom": "Zoom"
|
||||
"zoom": "Zoom",
|
||||
"albumBackground": "Album Hintergrund",
|
||||
"customCss": "Benutzerdefiniert css",
|
||||
"homeConfiguration": "Startseite Konfiguration",
|
||||
"lastfmApiKey": "{{lastfm}} API-Schlüssel",
|
||||
"lastfmApiKey_description": "Der API-Schlüssel für {{lastfm}}. wird für benötigt",
|
||||
"discordListening": "Status als hört zu anzeigen",
|
||||
"discordListening_description": "Status als hört zu statt als spielt anzeigen",
|
||||
"lastfm": "zeige last.fm links",
|
||||
"lastfm_description": "zeige links zu last.fm auf dem Künstler/Album-Seiten",
|
||||
"musicbrainz": "Zeig musicbrainz links",
|
||||
"customCssEnable": "aktiviere Benutzerdefinierte css"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,8 @@
|
||||
"action_other": "actions",
|
||||
"add": "add",
|
||||
"additionalParticipants": "additional participants",
|
||||
"newVersion": "a new version has been installed ({{version}})",
|
||||
"viewReleaseNotes": "view release notes",
|
||||
"albumGain": "album gain",
|
||||
"albumPeak": "album peak",
|
||||
"areYouSure": "are you sure?",
|
||||
@@ -169,6 +171,7 @@
|
||||
"loginRateError": "too many login attempts, please try again in a few seconds",
|
||||
"mpvRequired": "MPV required",
|
||||
"networkError": "a network error occurred",
|
||||
"notificationDenied": "permissions for notifications were denied. this setting has no effect",
|
||||
"openError": "could not open file",
|
||||
"playbackError": "an error occurred when trying to play the media",
|
||||
"remoteDisableError": "an error occurred when trying to $t(common.disable) the remote server",
|
||||
@@ -268,6 +271,7 @@
|
||||
"title": "lyric search"
|
||||
},
|
||||
"queryEditor": {
|
||||
"title": "query editor",
|
||||
"input_optionMatchAll": "match all",
|
||||
"input_optionMatchAny": "match any"
|
||||
},
|
||||
@@ -421,6 +425,7 @@
|
||||
"folders": "$t(entity.folder_other)",
|
||||
"genres": "$t(entity.genre_other)",
|
||||
"home": "$t(common.home)",
|
||||
"myLibrary": "my library",
|
||||
"nowPlaying": "now playing",
|
||||
"playlists": "$t(entity.playlist_other)",
|
||||
"search": "$t(common.search)",
|
||||
@@ -510,12 +515,14 @@
|
||||
"disableLibraryUpdateOnStartup": "disable checking for new versions on startup",
|
||||
"discordApplicationId": "{{discord}} application id",
|
||||
"discordApplicationId_description": "the application id for {{discord}} rich presence (defaults to {{defaultId}})",
|
||||
"discordPausedStatus": "show rich presence when paused",
|
||||
"discordPausedStatus_description": "when enabled, status will show when player is paused",
|
||||
"discordIdleStatus": "show rich presence idle status",
|
||||
"discordIdleStatus_description": "when enabled, update status while player is idle",
|
||||
"discordListening": "show status as listening",
|
||||
"discordListening_description": "show status as listening instead of playing",
|
||||
"discordRichPresence": "{{discord}} rich presence",
|
||||
"discordRichPresence_description": "enable playback status in {{discord}} rich presence. Image keys are: {{icon}}, {{playing}}, and {{paused}} ",
|
||||
"discordRichPresence_description": "enable playback status in {{discord}} rich presence. Image keys are: {{icon}}, {{playing}}, and {{paused}}",
|
||||
"discordServeImage": "serve {{discord}} images from server",
|
||||
"discordServeImage_description": "share cover art for {{discord}} rich presence from server itself, only available for jellyfin and navidrome",
|
||||
"discordUpdateInterval": "{{discord}} rich presence update interval",
|
||||
@@ -532,6 +539,8 @@
|
||||
"floatingQueueArea_description": "display a hover icon on the right side of the screen to view the play queue",
|
||||
"followLyric": "follow current lyric",
|
||||
"followLyric_description": "scroll the lyric to the current playing position",
|
||||
"preferLocalLyrics": "prefer local lyrics",
|
||||
"preferLocalLyrics_description": "prefer local lyrics over remote lyrics when available",
|
||||
"font": "font",
|
||||
"font_description": "sets the font to use for the application",
|
||||
"fontType": "font type",
|
||||
@@ -587,6 +596,8 @@
|
||||
"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_description": "sets the language for the application ($t(common.restartRequired))",
|
||||
"lastfm": "show last.fm links",
|
||||
"lastfm_description": "show links to last.fm on artist/album pages",
|
||||
"lastfmApiKey": "{{lastfm}} API key",
|
||||
"lastfmApiKey_description": "the API key for {{lastfm}}. required for cover art",
|
||||
"lyricFetch": "fetch lyrics from the internet",
|
||||
@@ -595,6 +606,8 @@
|
||||
"lyricFetchProvider_description": "select the providers to fetch lyrics from. the order of the providers is the order in which they will be queried",
|
||||
"lyricOffset": "lyric offset (ms)",
|
||||
"lyricOffset_description": "offset the lyric by the specified amount of milliseconds",
|
||||
"notify": "enable song notifications",
|
||||
"notify_description": "show notifications when changing the current song",
|
||||
"minimizeToTray": "minimize to tray",
|
||||
"minimizeToTray_description": "minimize the application to the system tray",
|
||||
"minimumScrobblePercentage": "minimum scrobble duration (percentage)",
|
||||
@@ -605,6 +618,10 @@
|
||||
"mpvExecutablePath_description": "sets the path to the mpv executable. if left empty, the default path will be used",
|
||||
"mpvExtraParameters": "mpv parameters",
|
||||
"mpvExtraParameters_help": "one per line",
|
||||
"musicbrainz": "show musicbrainz links",
|
||||
"musicbrainz_description": "show links to musicbrainz on artist/album pages, where mbid exists",
|
||||
"neteaseTranslation": "Enable NetEase translations",
|
||||
"neteaseTranslation_description": "When enabled, fetches and displays translated lyrics from NetEase if available.",
|
||||
"passwordStore": "passwords/secret store",
|
||||
"passwordStore_description": "what password/secret store to use. change this if you are having issues storing passwords.",
|
||||
"playbackStyle": "playback style",
|
||||
@@ -693,6 +710,8 @@
|
||||
"volumeWidth_description": "the width of the volume slider",
|
||||
"webAudio": "use web audio",
|
||||
"webAudio_description": "use web audio. this enables advanced features like replaygain. disable if you experience otherwise",
|
||||
"preservePitch": "preserve pitch",
|
||||
"preservePitch_description": "preserves pitch when modifying playback speed",
|
||||
"windowBarStyle": "window bar style",
|
||||
"windowBarStyle_description": "select the style of the window bar",
|
||||
"zoom": "zoom percentage",
|
||||
@@ -768,6 +787,8 @@
|
||||
},
|
||||
"view": {
|
||||
"card": "card",
|
||||
"grid": "grid",
|
||||
"list": "list",
|
||||
"poster": "poster",
|
||||
"table": "table"
|
||||
}
|
||||
|
||||
@@ -120,7 +120,7 @@
|
||||
"hotkey_toggleShuffle": "alterna aleatorio",
|
||||
"theme": "tema",
|
||||
"playbackStyle_description": "selecciona el estilo de reproducción a usar por el reproductor de audio",
|
||||
"discordRichPresence_description": "activa el estado de reproducción en el estado de actividad de {{discord}}. Las teclas de imagen son: {{icon}}, {{playing}}, y {{paused}} ",
|
||||
"discordRichPresence_description": "activa el estado de reproducción en el estado de actividad de {{discord}}. Las teclas de imagen son: {{icon}}, {{playing}}, y {{paused}}",
|
||||
"mpvExecutablePath": "ruta del ejecutable mpv",
|
||||
"audioDevice": "dispositivo de audio",
|
||||
"hotkey_rate2": "calificar con 2 estrellas",
|
||||
@@ -259,7 +259,19 @@
|
||||
"lastfmApiKey_description": "la clave API para {{lastfm}}. Requerida para la portada",
|
||||
"lastfmApiKey": "Clave API para {{lastfm}}",
|
||||
"discordServeImage": "Servir imágenes de {{discord}} desde el servidor",
|
||||
"discordServeImage_description": "Comparte el arte de la portada para el estado de actividad de {{discord}} desde el propio servidor, solo disponible para Jellyfin y Navidrome"
|
||||
"discordServeImage_description": "Comparte el arte de la portada para el estado de actividad de {{discord}} desde el propio servidor, solo disponible para Jellyfin y Navidrome",
|
||||
"lastfm": "Mostrar enlaces de last.fm",
|
||||
"lastfm_description": "Muestra enlaces a last.fm en las páginas de artistas/álbumes",
|
||||
"musicbrainz": "Mostrar enlaces de MusicBrainz",
|
||||
"musicbrainz_description": "Muestra enlaces a MusicBrainz en las páginas de artistas/álbumes, donde exista mbid",
|
||||
"neteaseTranslation": "Activar traducciones de NetEase",
|
||||
"neteaseTranslation_description": "Cuando se habilita, busca y muestra letras traducidas desde NetEase si está disponible.",
|
||||
"preferLocalLyrics_description": "Prefiere letras locales sobre letras remotas cuando esté disponible",
|
||||
"preferLocalLyrics": "Preferir letras locales",
|
||||
"discordPausedStatus": "Mostrar estado de actividad cuando esté en pausa",
|
||||
"discordPausedStatus_description": "Cuando está activado, el estado mostrará cuando el reproductor esté en pausa",
|
||||
"preservePitch": "Mantener el tono",
|
||||
"preservePitch_description": "Mantiene el tono cuando se modifica la velocidad de reproducción"
|
||||
},
|
||||
"action": {
|
||||
"editPlaylist": "editar $t(entity.playlist_one)",
|
||||
@@ -379,7 +391,9 @@
|
||||
"preview": "Vista previa",
|
||||
"translation": "traducción",
|
||||
"additionalParticipants": "Participantes adicionales",
|
||||
"tags": "Etiquetas"
|
||||
"tags": "Etiquetas",
|
||||
"newVersion": "Una nueva versión ha sido instalada ({{version}})",
|
||||
"viewReleaseNotes": "Ver notas de lanzamiento"
|
||||
},
|
||||
"error": {
|
||||
"remotePortWarning": "reiniciar el servidor para aplicar el nuevo puerto",
|
||||
@@ -463,7 +477,8 @@
|
||||
"home": "$t(common.home)",
|
||||
"artists": "$t(entity.artist_other)",
|
||||
"albumArtists": "$t(entity.albumArtist_other)",
|
||||
"shared": "compartido $t(entity.playlist_other)"
|
||||
"shared": "compartido $t(entity.playlist_other)",
|
||||
"myLibrary": "Mi biblioteca"
|
||||
},
|
||||
"appMenu": {
|
||||
"selectServer": "seleccionar servidor",
|
||||
@@ -649,7 +664,8 @@
|
||||
},
|
||||
"queryEditor": {
|
||||
"input_optionMatchAll": "coincidir todos",
|
||||
"input_optionMatchAny": "coincidir cualquiera"
|
||||
"input_optionMatchAny": "coincidir cualquiera",
|
||||
"title": "Editor de consultas"
|
||||
},
|
||||
"shareItem": {
|
||||
"createFailed": "No se pudo crear el recurso compartido (¿está habilitado el uso compartido?)",
|
||||
@@ -731,7 +747,9 @@
|
||||
"view": {
|
||||
"card": "tarjeta",
|
||||
"table": "tabla",
|
||||
"poster": "cartel"
|
||||
"poster": "cartel",
|
||||
"list": "Lista",
|
||||
"grid": "Cuadrícula"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -88,7 +88,11 @@
|
||||
"albumGain": "albumin vahvistus (gain)",
|
||||
"albumPeak": "albumin huippu (peak)",
|
||||
"trackGain": "raidan vahvistus (gain)",
|
||||
"trackPeak": "kappaleen huippu (peak)"
|
||||
"trackPeak": "kappaleen huippu (peak)",
|
||||
"additionalParticipants": "muut osallistujat",
|
||||
"tags": "tägit",
|
||||
"newVersion": "uusi versio on asennettu ({{version}})",
|
||||
"viewReleaseNotes": "katsele julkaisutietoja"
|
||||
},
|
||||
"entity": {
|
||||
"album_one": "albumi",
|
||||
@@ -173,7 +177,8 @@
|
||||
"localFontAccessDenied": "paikallisiin fontteihin pääsy on kielletty",
|
||||
"playbackError": "mediaa toistaessa tapahtui virhe",
|
||||
"remotePortWarning": "käynnistä palvelin uudestaan ottaaksesi uuden portin käyttöön",
|
||||
"endpointNotImplementedError": "päätepiste {{endpoint}} ei ole toteutettu {{serverType}} varten"
|
||||
"endpointNotImplementedError": "päätepiste {{endpoint}} ei ole toteutettu {{serverType}} varten",
|
||||
"badValue": "kelpaamaton optio \"{{value}}\". tätä arvoa ei ole enää olemassa"
|
||||
},
|
||||
"filter": {
|
||||
"album": "$t(entity.album_one)",
|
||||
@@ -276,7 +281,8 @@
|
||||
},
|
||||
"queryEditor": {
|
||||
"input_optionMatchAny": "sovita joku",
|
||||
"input_optionMatchAll": "sovita kaikki"
|
||||
"input_optionMatchAll": "sovita kaikki",
|
||||
"title": "kyselyeditori"
|
||||
}
|
||||
},
|
||||
"setting": {
|
||||
@@ -356,7 +362,7 @@
|
||||
"doubleClickBehavior": "lisää kaikki haetut kappaleet soittojonoon tuplaklikkauksella",
|
||||
"discordUpdateInterval_description": "päivitysväli sekunnteina (vähintään 15 sekunttia)",
|
||||
"discordRichPresence": "{{discord}} rich presence",
|
||||
"discordRichPresence_description": "ota toiston tila käyttöön {{discord}}n rich presence-toiminnossa. Kuvakkeiden avaimet ovat {{icon}}, {{playing}} ja {{paused}}. ",
|
||||
"discordRichPresence_description": "ota toiston tila käyttöön {{discord}}n rich presence-toiminnossa. Kuvakkeiden avaimet ovat {{icon}}, {{playing}} ja {{paused}}",
|
||||
"discordUpdateInterval": "{{discord}} rich presencen päivitysväli",
|
||||
"enableRemote": "aktivoi etäohjauspalvelin",
|
||||
"externalLinks_description": "ottaa ulkoiset linkit (Last.fm, MusicBrainz) artistien/albumien sivuilla",
|
||||
@@ -506,7 +512,19 @@
|
||||
"useSystemTheme": "käytä järjestelmän teemaa",
|
||||
"volumeWheelStep": "äänenvoimakkuusrullan askel",
|
||||
"discordServeImage": "jaa {{discord}} kuvat palvelimelta",
|
||||
"discordServeImage_description": "jaa kansikuvat {{discord}}n rich presenceä varten suoraan palvelimelta. saatavilla vain jellyfinille ja navidromelle"
|
||||
"discordServeImage_description": "jaa kansikuvat {{discord}}n rich presenceä varten suoraan palvelimelta. saatavilla vain jellyfinille ja navidromelle",
|
||||
"musicbrainz_description": "näytä linkit musicbrainz sivulle artistin/albumin sivuilla, jos musicbrainz-id löytyy",
|
||||
"lastfm": "näytä last.fm linkit",
|
||||
"lastfm_description": "näytä linkit last.fm sivulle artistin/albumin sivuilla",
|
||||
"musicbrainz": "näytä musicbrainz linkit",
|
||||
"neteaseTranslation": "Ota NetEasen käännökset käyttöön",
|
||||
"neteaseTranslation_description": "Käytöss ollessa noutaa ja näyttää käännetyt sanat NetEasesta, jos ne ovat saatavilla.",
|
||||
"preferLocalLyrics_description": "suosi paikallisia sanoituksia ulkoisten sijasta, kun saatavilla",
|
||||
"preferLocalLyrics": "suosi paikallisia sanoituksia",
|
||||
"discordPausedStatus": "näytä rich presence tauotettuna",
|
||||
"discordPausedStatus_description": "ollessak käytössä, status näyttää milloin soitin on tautotettuna",
|
||||
"preservePitch": "säilytä sävelkorkeus",
|
||||
"preservePitch_description": "säilytä sävelkorkeus toistonopeutta muokatessa"
|
||||
},
|
||||
"page": {
|
||||
"itemDetail": {
|
||||
@@ -575,7 +593,8 @@
|
||||
"home": "$t(common.home)",
|
||||
"nowPlaying": "nyt soi",
|
||||
"playlists": "$t(entity.playlist_other)",
|
||||
"search": "$t(common.search)"
|
||||
"search": "$t(common.search)",
|
||||
"myLibrary": "oma kirjasto"
|
||||
},
|
||||
"setting": {
|
||||
"generalTab": "yleinen",
|
||||
@@ -736,7 +755,9 @@
|
||||
"view": {
|
||||
"table": "taulukko",
|
||||
"card": "kortti",
|
||||
"poster": "juliste"
|
||||
"poster": "juliste",
|
||||
"grid": "ruudukko",
|
||||
"list": "lista"
|
||||
}
|
||||
},
|
||||
"column": {
|
||||
|
||||
@@ -100,6 +100,9 @@
|
||||
"cancel": "annuler",
|
||||
"forceRestartRequired": "redémarrer pour appliquer les changements… fermer la notification pour redémarrer",
|
||||
"setting": "paramètre",
|
||||
"setting_one": "paramètre",
|
||||
"setting_many": "",
|
||||
"setting_other": "paramètres",
|
||||
"version": "version",
|
||||
"title": "titre",
|
||||
"filter_one": "filtre",
|
||||
@@ -150,7 +153,9 @@
|
||||
"codec": "codec",
|
||||
"translation": "traduction",
|
||||
"additionalParticipants": "participants additionnels",
|
||||
"tags": "tags"
|
||||
"tags": "tags",
|
||||
"newVersion": "une nouvelle version vient d'être installé ({{version}})",
|
||||
"viewReleaseNotes": "voir la note de version"
|
||||
},
|
||||
"error": {
|
||||
"remotePortWarning": "redémarrer le serveur pour appliquer le nouveau port",
|
||||
@@ -234,7 +239,8 @@
|
||||
"home": "$t(common.home)",
|
||||
"artists": "$t(entity.artist_other)",
|
||||
"albumArtists": "$t(entity.albumArtist_other)",
|
||||
"shared": "partagé $t(entity.playlist_other)"
|
||||
"shared": "partagé $t(entity.playlist_other)",
|
||||
"myLibrary": "ma bibliothèque"
|
||||
},
|
||||
"fullscreenPlayer": {
|
||||
"config": {
|
||||
@@ -395,7 +401,7 @@
|
||||
"discordIdleStatus_description": "quand activé, mettre à jour le status pendant que le lecteur est inactif",
|
||||
"showSkipButtons": "affiche les boutons suivants et précédents",
|
||||
"minimumScrobblePercentage": "durée minimal du scobble (pourcentage)",
|
||||
"lyricFetch": "récupère les paroles depuis internet",
|
||||
"lyricFetch": "récupérer les paroles depuis internet",
|
||||
"scrobble": "scrobble",
|
||||
"enableRemote_description": "activer le serveur de contrôle à distance, qui permet à d'autres appareils de contrôler l'application",
|
||||
"fontType_optionSystem": "police système",
|
||||
@@ -446,7 +452,7 @@
|
||||
"playbackStyle": "style de lecture",
|
||||
"hotkey_toggleShuffle": "basculer la lecture aléatoire",
|
||||
"playbackStyle_description": "sélectionnez le style de lecture à utiliser pour le lecteur audio",
|
||||
"discordRichPresence_description": "active l'état de lecteur dans le status d'activité {{discord}}. Les images clés sont : {{icon}}, {{playing}}, et {{paused}} ",
|
||||
"discordRichPresence_description": "active l'état de lecteur dans le status d'activité {{discord}}. Les images clés sont : {{icon}}, {{playing}}, et {{paused}}",
|
||||
"mpvExecutablePath": "chemin de l'exécutable mpv",
|
||||
"hotkey_rate2": "noter 2 étoiles",
|
||||
"playButtonBehavior_description": "définit le comportement par défaut du bouton play, lors de l'ajout de chanson à la file d'attente",
|
||||
@@ -573,7 +579,7 @@
|
||||
"artistConfiguration": "page de configuration de l'artiste de l'album",
|
||||
"artistConfiguration_description": "configurer les éléments et l'ordre à afficher, sur la page de l'artiste de l'album",
|
||||
"doubleClickBehavior": "mettre en file d'attente toutes les pistes recherchées lors d'un double clic",
|
||||
"contextMenu": "configuration du menu contexte (clic droit)",
|
||||
"contextMenu": "configuration du menu contextuel (clic droit)",
|
||||
"contextMenu_description": "permet de masquer les éléments qui s'affichent dans le menu lorsque vous cliquez avec le bouton droit de la souris sur un élément. les éléments qui ne sont pas cochés seront masqués",
|
||||
"albumBackground": "image d'arrière-plan de l'album",
|
||||
"albumBackground_description": "ajoute une image d'arrière-plan pour les pages de l'album contenant les illustrations de l'album",
|
||||
@@ -598,7 +604,19 @@
|
||||
"lastfmApiKey": "clé API {{lastfm}}",
|
||||
"lastfmApiKey_description": "la clé API pour {{lastfm}} . requise pour la pochette d'album",
|
||||
"discordServeImage": "servir l'image {{discord}} depuis le serveur",
|
||||
"discordServeImage_description": "partage pochette du status d'activité {{discord}} depuis le serveur lui même, disponible uniquement pour jellyfin et navidrome"
|
||||
"discordServeImage_description": "partage pochette du status d'activité {{discord}} depuis le serveur lui même, disponible uniquement pour jellyfin et navidrome",
|
||||
"lastfm": "affiche les liens de last.fm",
|
||||
"musicbrainz_description": "affiches les liens vers musicbrainz sur les pages des artistes/albums, quand mbid existes",
|
||||
"lastfm_description": "affiche les liens vers last.fm sur les pages des artistes/albums",
|
||||
"musicbrainz": "affiches les liens musicbrainz",
|
||||
"neteaseTranslation": "Activer les traductions NetEase",
|
||||
"neteaseTranslation_description": "Lorsque cette option est activée, récupère et affiche les paroles traduites de NetEase si elles sont disponibles.",
|
||||
"preferLocalLyrics_description": "privilégier les paroles locales aux paroles distantes lorsqu'elles sont disponibles",
|
||||
"preferLocalLyrics": "privilégier les paroles locales",
|
||||
"discordPausedStatus_description": "quand activé, le status s'affichera lorsque le lecteur est en pause",
|
||||
"discordPausedStatus": "afficher le status d'activité en pause",
|
||||
"preservePitch": "préserver la hauteur",
|
||||
"preservePitch_description": "préserver la hauteur lors du changement de la vitesse de lecture"
|
||||
},
|
||||
"form": {
|
||||
"deletePlaylist": {
|
||||
@@ -639,7 +657,8 @@
|
||||
},
|
||||
"queryEditor": {
|
||||
"input_optionMatchAll": "correspondre à tous",
|
||||
"input_optionMatchAny": "correspondre à n'importe quel"
|
||||
"input_optionMatchAny": "correspondre à n'importe quel",
|
||||
"title": "éditeur de requête"
|
||||
},
|
||||
"editPlaylist": {
|
||||
"title": "modifier $t(entity.playlist_one)",
|
||||
@@ -729,7 +748,9 @@
|
||||
"view": {
|
||||
"table": "liste",
|
||||
"poster": "poster",
|
||||
"card": "Carte"
|
||||
"card": "Carte",
|
||||
"grid": "grille",
|
||||
"list": "liste"
|
||||
},
|
||||
"label": {
|
||||
"releaseDate": "date de sortie",
|
||||
|
||||
@@ -163,7 +163,7 @@
|
||||
"remotePortWarning": "indítsd újra a szervert az új PORT használatához",
|
||||
"genericError": "hiba történt",
|
||||
"endpointNotImplementedError": "a(z) {{endpoint}} végpont nincs implementálva a következőhöz: {{serverType}}",
|
||||
"badAlbum": "azért látod ezt az oldalt mert ez a zeneszám nem része egy albumnak. ez általában akkor történik amikor egy szám a zenekönyvtárad gyökerébe kerül. a Jellyfin csak mappákba rendezett számokat csoportosít",
|
||||
"badAlbum": "azért látod ezt az oldalt mert ez a zeneszám nem része egy albumnak. ez általában akkor történik amikor egy szám a zenekönyvtárad gyökerébe kerül. a Jellyfin csak mappákba rendezett számokat csoportosít.",
|
||||
"loginRateError": "túl sok bejelentkezési kísérlet, kérlek próbáld újra pár másodperc múlva",
|
||||
"mpvRequired": "MPV szükséges",
|
||||
"invalidServer": "érvénytelen szerver",
|
||||
|
||||
@@ -491,7 +491,7 @@
|
||||
"discordListening": "Tampilkan status sebagai mendengarkan",
|
||||
"discordListening_description": "tampilkan status sebagai mendengarkan alih-alih bermain",
|
||||
"discordRichPresence": "status aktivitas {{discord}}",
|
||||
"discordRichPresence_description": "aktifkan status pemutaran di status aktivitas {{discord}}. Gambar tombol adalah: {{icon}}, {{playing}}, dan {{paused}} ",
|
||||
"discordRichPresence_description": "aktifkan status pemutaran di status aktivitas {{discord}}. Gambar tombol adalah: {{icon}}, {{playing}}, dan {{paused}}",
|
||||
"discordUpdateInterval": "interval pembaruan status aktivitas {{discord}}",
|
||||
"discordUpdateInterval_description": "waktu dalam detik antara setiap pembaruan (minimal 15 detik)",
|
||||
"doubleClickBehavior": "masukkan semua lagu yang dicari saat mengklik dua kali",
|
||||
|
||||
+204
-26
@@ -16,7 +16,12 @@
|
||||
"toggleSmartPlaylistEditor": "attiva/disattiva editor $t(entity.smartPlaylist)",
|
||||
"removeFromFavorites": "rimuovi da $t(entity.favorite_other)",
|
||||
"moveToTop": "sposta in cima",
|
||||
"moveToBottom": "sposta in fondo"
|
||||
"moveToBottom": "sposta in fondo",
|
||||
"moveToNext": "passa al successivo",
|
||||
"openIn": {
|
||||
"lastfm": "Apri in Last.fm",
|
||||
"musicbrainz": "Apri in MusicBrainz"
|
||||
}
|
||||
},
|
||||
"common": {
|
||||
"backward": "indietro",
|
||||
@@ -99,7 +104,22 @@
|
||||
"yes": "si",
|
||||
"random": "casuale",
|
||||
"size": "dimensione",
|
||||
"note": "nota"
|
||||
"note": "nota",
|
||||
"additionalParticipants": "partecipanti aggiuntivi",
|
||||
"newVersion": "è stata installata una nuova versione ({{version}})",
|
||||
"viewReleaseNotes": "mostra le note di rilascio",
|
||||
"albumGain": "guadagno (gain) dell'album",
|
||||
"albumPeak": "picco di volume dell'album",
|
||||
"close": "chiudi",
|
||||
"codec": "codec",
|
||||
"mbid": "MusicBrainz ID",
|
||||
"preview": "anteprima",
|
||||
"reload": "ricarica",
|
||||
"share": "condividi",
|
||||
"tags": "tags",
|
||||
"trackGain": "normalizzazione (gain) del brano",
|
||||
"trackPeak": "picco di volume del brano",
|
||||
"translation": "traduzione"
|
||||
},
|
||||
"player": {
|
||||
"repeat_all": "ripeti coda",
|
||||
@@ -113,7 +133,7 @@
|
||||
"skip_back": "salta indietro",
|
||||
"favorite": "preferito",
|
||||
"next": "successivo",
|
||||
"shuffle": "mescola",
|
||||
"shuffle": "riproduzione casuale",
|
||||
"playbackFetchNoResults": "nessuna canzone trovata",
|
||||
"playbackFetchInProgress": "caricamento canzoni…",
|
||||
"addNext": "aggiungi successivo",
|
||||
@@ -130,7 +150,9 @@
|
||||
"shuffle_off": "non mescolare",
|
||||
"addLast": "aggiungi in coda",
|
||||
"mute": "silenzia",
|
||||
"skip_forward": "salta avanti"
|
||||
"skip_forward": "salta avanti",
|
||||
"playSimilarSongs": "riproduci brani simili",
|
||||
"viewQueue": "visualizza coda"
|
||||
},
|
||||
"setting": {
|
||||
"crossfadeStyle_description": "seleziona lo stile dissolvenza da usare per il player audio",
|
||||
@@ -150,7 +172,7 @@
|
||||
"skipDuration_description": "imposta la durata da saltare quando vengono usati i pulsanti di salto nella barra del player",
|
||||
"enableRemote_description": "abilita il controllo remoto del server per permettere ad altri dispositivi di controllare l'applicazione",
|
||||
"fontType_optionSystem": "font di sistema",
|
||||
"mpvExecutablePath_description": "imposta il percorso dell'eseguibile di mpv",
|
||||
"mpvExecutablePath_description": "imposta il percorso dell'eseguibile mpv. se lasciato vuoto, verrà utilizzato il percorso predefinito",
|
||||
"hotkey_favoriteCurrentSong": "$t(common.currentSong) preferita",
|
||||
"crossfadeStyle": "stile dissolvenza",
|
||||
"sidebarConfiguration": "configurazione barra laterale",
|
||||
@@ -209,7 +231,7 @@
|
||||
"hotkey_toggleShuffle": "attiva/disattiva mescolamento",
|
||||
"theme": "tema",
|
||||
"playbackStyle_description": "selezione lo stile di riproduzione da usare per il player audio",
|
||||
"discordRichPresence_description": "abilita lo status del playback nello stato attività di {{discord}}. Le chiavi immagine sono: {{icon}}, {{playing}} e {{paused}} ",
|
||||
"discordRichPresence_description": "abilita lo status del playback nello stato attività di {{discord}}. Le chiavi immagine sono: {{icon}}, {{playing}} e {{paused}}",
|
||||
"mpvExecutablePath": "percorso eseguibile mpv",
|
||||
"audioDevice": "device audio",
|
||||
"hotkey_rate2": "voto 2 stelle",
|
||||
@@ -268,7 +290,7 @@
|
||||
"replayGainMode_description": "aggiusta il volume secondo i valori {{ReplayGain}} salvati nei metadati del file",
|
||||
"showSkipButtons": "mostra pulsanti per saltare",
|
||||
"sampleRate": "frequenza di campionamento",
|
||||
"sampleRate_description": "seleziona la frequenza di campionamento di output da usare se la frequenza di campionamento selezionata è diversa da quella della del media attuale",
|
||||
"sampleRate_description": "seleziona la frequenza di campionamento di output da utilizzare se quella selezionata è diversa da quella del file sorgente in riproduzione. Un valore inferiore a 8000 utilizzerà la frequenza predefinita",
|
||||
"hotkey_togglePreviousSongFavorite": "imposta/rimuovi $t(common.previousSong) favorito",
|
||||
"hotkey_unfavoritePreviousSong": "rimuovi $t(common.previousSong) dai preferiti",
|
||||
"showSkipButton_description": "mostra o nascondi i pulsanti per saltare nella barra del player",
|
||||
@@ -293,7 +315,85 @@
|
||||
"clearQueryCache": "pulisci cache di feishin",
|
||||
"buttonSize_description": "Dimensione bottoni nella barra di riproduzione",
|
||||
"clearCache": "pulisci la cache del browser",
|
||||
"clearQueryCache_description": "\"leggera\" pulizia di feishin. verranno aggiornate le playlist, metadata delle tracce e i testi salvati. impostazioni, credenziali del server e le immagini salvate saranno mantenute"
|
||||
"clearQueryCache_description": "\"leggera\" pulizia di feishin. verranno aggiornate le playlist, metadata delle tracce e i testi salvati. impostazioni, credenziali del server e le immagini salvate saranno mantenute",
|
||||
"albumBackground": "immagine di sfondo dell'album",
|
||||
"albumBackground_description": "aggiunge un'immagine di sfondo per le pagine degli album contenenti l'album art",
|
||||
"albumBackgroundBlur": "intensità sfocatura immagine di sfondo dell'album",
|
||||
"albumBackgroundBlur_description": "regola la quantità di sfocatura applicata all'immagine di sfondo dell'album",
|
||||
"artistConfiguration": "configurazione della pagina artista dell’album",
|
||||
"artistConfiguration_description": "configurare quali elementi vengono visualizzati, e in quale ordine, nella pagina dell'artista dell'album",
|
||||
"buttonSize": "dimensione del bottone nella barra di riproduzione",
|
||||
"clearCacheSuccess": "cache pulita correttamente",
|
||||
"contextMenu": "configurazione menu contestuale (clic destro)",
|
||||
"contextMenu_description": "consente di nascondere gli elementi che vengono visualizzati nel menu quando si fa clic destro su un elemento. gli oggetti non selezionati saranno nascosti",
|
||||
"customCssEnable": "abilita css personalizzato",
|
||||
"customCssEnable_description": "consente di scrivere css personalizzati.",
|
||||
"customCssNotice": "Attenzione: sebbene ci sia una certa sanitizzazione (vengono bloccati url() e content:), l’uso di CSS personalizzati può comunque comportare dei rischi modificando l’interfaccia.",
|
||||
"customCss": "css personalizzato",
|
||||
"customCss_description": "contenuto CSS personalizzato. Nota: le proprietà content e gli URL remoti non sono consentiti. Di seguito è mostrata un’anteprima del tuo contenuto. Sono presenti anche altri campi non impostati da te a causa della sanitizzazione.",
|
||||
"discordPausedStatus": "mostra rich presence di Discord quando la riproduzione è in pausa",
|
||||
"discordPausedStatus_description": "quando abilitato, verrà mostrato lo stato del lettore in standby/pausa (nessun brano in riproduzione)",
|
||||
"discordListening": "mostra stato come in ascolto",
|
||||
"discordListening_description": "mostra lo stato come in ascolto invece che in riproduzione",
|
||||
"discordServeImage": "recupera le immagini di {{discord}} dal server",
|
||||
"discordServeImage_description": "condividi la copertina per la rich presence di {{discord}} direttamente dal server, disponibile solo per Jellyfin e Navidrome",
|
||||
"doubleClickBehavior": "aggiungi alla coda tutte le tracce cercate, con un doppio clic",
|
||||
"doubleClickBehavior_description": "se attivato, tutte le tracce corrispondenti alla ricerca verranno aggiunte alla coda. altrimenti, verrà aggiunta alla coda solo la traccia selezionata",
|
||||
"externalLinks": "mostra link esterni",
|
||||
"externalLinks_description": "consente di visualizzare link esterni (Last.fm, MusicBrainz) sulle pagine di artista/album",
|
||||
"preferLocalLyrics": "utilizza i testi locali",
|
||||
"preferLocalLyrics_description": "usa i testi locali anziché quelli online, quando disponibili",
|
||||
"genreBehavior": "comportamento predefinito della pagina genere",
|
||||
"genreBehavior_description": "determina se cliccando su un genere si apre di default la lista dei brani o degli album",
|
||||
"homeConfiguration": "configurazione della home page",
|
||||
"homeConfiguration_description": "configura quali elementi vengono mostrati e in quale ordine nella home page",
|
||||
"homeFeature": "carosello in evidenza nella home page",
|
||||
"homeFeature_description": "controlla se mostrare il grande carosello in evidenza nella pagina principale",
|
||||
"imageAspectRatio": "usa dimensioni originali(aspect ratio) della copertina",
|
||||
"imageAspectRatio_description": "se abilitato, la copertina verrà mostrata utilizzando le dimesioni originali. per le immagini con rapporto diverso da 1:1, lo spazio residuo resterà vuoto",
|
||||
"lastfm": "mostra links last.fm",
|
||||
"lastfm_description": "mostra i link per last.fm sulle pagine di artista/album",
|
||||
"lastfmApiKey": "{{lastfm}} chiave API",
|
||||
"lastfmApiKey_description": "chiave API per {{lastfm}}. necessaria per visualizzare le copertine",
|
||||
"mpvExtraParameters_help": "uno per linea",
|
||||
"musicbrainz": "mostra links musicbrainz",
|
||||
"musicbrainz_description": "mostra link a musicbrainz sulle pagine degli artisti/album, se è disponibile un mbid",
|
||||
"neteaseTranslation": "Abilita traduzioni di NetEase",
|
||||
"neteaseTranslation_description": "Se abilitato, recupera e mostra i testi tradotti da NetEase, se disponibili.",
|
||||
"passwordStore": "Archivio di password/segreti",
|
||||
"passwordStore_description": "specifica quale archivio di password e segreti utilizzare. modificalo in caso di problemi nel salvataggio delle credenziali.",
|
||||
"playButtonBehavior_optionPlayShuffled": "$t(player.shuffle)",
|
||||
"playerAlbumArtResolution": "risoluzione della copertina nel lettore",
|
||||
"playerAlbumArtResolution_description": "la risoluzione dell’anteprima della copertina nel lettore in formato grande. valori più alti la rendono più nitida, ma possono rallentare il caricamento. Il valore predefinito è 0, che indica la modalità automatica",
|
||||
"sidePlayQueueStyle_optionAttached": "fissata",
|
||||
"sidePlayQueueStyle_optionDetached": "sganciata",
|
||||
"startMinimized": "avvia minimizzato",
|
||||
"startMinimized_description": "avvia l'app nella barra di sistema",
|
||||
"transcodeNote": "ha effetto dopo 1 brano (web) - 2 brani (mpv)",
|
||||
"transcode": "abilita la transcodifica",
|
||||
"transcode_description": "abilita la transcodifica in formati diversi",
|
||||
"playerbarOpenDrawer": "attiva/disattiva schermo intero",
|
||||
"playerbarOpenDrawer_description": "consente di cliccare sulla barra del lettore per aprire il lettore a schermo intero",
|
||||
"replayGainClipping": "clipping di {{ReplayGain}}",
|
||||
"replayGainFallback": "metodo alternativo di {{ReplayGain}}",
|
||||
"transcodeBitrate": "bitrate per la transcodifica",
|
||||
"transcodeBitrate_description": "seleziona il bitrate per la transcodifica. 0 significa lasciare che sia il server a scegliere",
|
||||
"transcodeFormat": "formato per la transcodifica",
|
||||
"transcodeFormat_description": "seleziona il formato per la transcodifica. se vuoto viene decisco dal server",
|
||||
"translationApiProvider": "translation api provider",
|
||||
"translationApiProvider_description": "api provider for translation",
|
||||
"translationApiKey": "chiave api translation",
|
||||
"translationApiKey_description": "chiave api per la traduzione (supporta solo endpoint di servizio globali)",
|
||||
"translationTargetLanguage": "lingua di destinazione della traduzione",
|
||||
"translationTargetLanguage_description": "lingua di destinazione per la traduzione",
|
||||
"trayEnabled": "Mostra icona app nella barra di sistema",
|
||||
"trayEnabled_description": "mostra/nascondi icona app nella barra si sistema. se disabilitato, disattiva anche minimizza/chiudi nella barra di sistema",
|
||||
"volumeWidth": "larghezza della barra del volume",
|
||||
"webAudio": "use audio web",
|
||||
"webAudio_description": "usa audio web. abilita funzionalità avanzate come ReplayGain. disabilita se riscontri problemi",
|
||||
"preservePitch": "mantieni tono (pitch)",
|
||||
"preservePitch_description": "mantiene il tono (pitch) durante la modifica della velocità di riproduzione",
|
||||
"volumeWidth_description": "larghezza del cursore del volume"
|
||||
},
|
||||
"error": {
|
||||
"remotePortWarning": "riavvia il server per applicare la nuova porta",
|
||||
@@ -314,7 +414,11 @@
|
||||
"mpvRequired": "MPV richiesto",
|
||||
"audioDeviceFetchError": "si è verificato un errore nel provare ad ottenre i device audio",
|
||||
"invalidServer": "server non valido",
|
||||
"loginRateError": "troppi tentativi di accesso, per favore riprova tra qualche secondo"
|
||||
"loginRateError": "troppi tentativi di accesso, per favore riprova tra qualche secondo",
|
||||
"badAlbum": "stai visualizzando questa pagina perché questa canzone non fa parte di un album. probabilmente vedi questo messaggio perché hai una canzone posizionata direttamente nella cartella principale della tua libreria musicale. jellyfin raggruppa le tracce solo se si trovano all’interno di una cartella.",
|
||||
"badValue": "opzione non valida \"{{value}}\". valore inesistente",
|
||||
"networkError": "si è verificato un errore di rete",
|
||||
"openError": "impossibile aprire il file"
|
||||
},
|
||||
"filter": {
|
||||
"mostPlayed": "più riprodotti",
|
||||
@@ -372,7 +476,9 @@
|
||||
"settings": "$t(common.setting_other)",
|
||||
"home": "$t(common.home)",
|
||||
"artists": "$t(entity.artist_other)",
|
||||
"albumArtists": "$t(entity.albumArtist_other)"
|
||||
"albumArtists": "$t(entity.albumArtist_other)",
|
||||
"myLibrary": "la mia libreria",
|
||||
"shared": "condivisa $t(entity.playlist_other)"
|
||||
},
|
||||
"fullscreenPlayer": {
|
||||
"config": {
|
||||
@@ -386,11 +492,16 @@
|
||||
"unsynchronized": "non sinncronizzato",
|
||||
"lyricAlignment": "allineamento testo",
|
||||
"useImageAspectRatio": "usa le proporzioni dell'immagine",
|
||||
"lyricGap": "gap testo"
|
||||
"lyricGap": "gap testo",
|
||||
"dynamicImageBlur": "intensità sfocatura immagine",
|
||||
"dynamicIsImage": "abilita immagine di sfondo",
|
||||
"lyricOffset": "ritardo testi (ms)"
|
||||
},
|
||||
"upNext": "successivamente",
|
||||
"lyrics": "testi",
|
||||
"related": "correlati"
|
||||
"related": "correlati",
|
||||
"visualizer": "visualizzatore audio",
|
||||
"noLyrics": "nessun testo trovato"
|
||||
},
|
||||
"appMenu": {
|
||||
"selectServer": "seleziona server",
|
||||
@@ -420,7 +531,13 @@
|
||||
"addFavorite": "$t(action.addToFavorites)",
|
||||
"play": "$t(player.play)",
|
||||
"numberSelected": "{{count}} selezionati",
|
||||
"removeFromQueue": "$t(action.removeFromQueue)"
|
||||
"removeFromQueue": "$t(action.removeFromQueue)",
|
||||
"download": "download",
|
||||
"moveToNext": "$t(action.moveToNext)",
|
||||
"playSimilarSongs": "$t(player.playSimilarSongs)",
|
||||
"playShuffled": "$t(player.shuffle)",
|
||||
"shareItem": "condividi elemento",
|
||||
"showDetails": "mostra info"
|
||||
},
|
||||
"home": {
|
||||
"mostPlayed": "più riprodotti",
|
||||
@@ -431,22 +548,28 @@
|
||||
},
|
||||
"albumDetail": {
|
||||
"moreFromArtist": "di più da questo $t(entity.artist_one)",
|
||||
"moreFromGeneric": "di più da {{item}}"
|
||||
"moreFromGeneric": "di più da {{item}}",
|
||||
"released": "rilasciato"
|
||||
},
|
||||
"setting": {
|
||||
"playbackTab": "riproduzione",
|
||||
"generalTab": "generale",
|
||||
"hotkeysTab": "tasti a scelta rapida",
|
||||
"windowTab": "finestra"
|
||||
"windowTab": "finestra",
|
||||
"advanced": "avanzate"
|
||||
},
|
||||
"albumArtistList": {
|
||||
"title": "$t(entity.albumArtist_other)"
|
||||
},
|
||||
"genreList": {
|
||||
"title": "$t(entity.genre_other)"
|
||||
"title": "$t(entity.genre_other)",
|
||||
"showAlbums": "mostra $t(entity.genre_one) $t(entity.album_other)",
|
||||
"showTracks": "mostra $t(entity.genre_one) $t(entity.track_other)"
|
||||
},
|
||||
"trackList": {
|
||||
"title": "$t(entity.track_other)"
|
||||
"title": "$t(entity.track_other)",
|
||||
"artistTracks": "tracce di {{artist}}",
|
||||
"genreTracks": "\"{{genre}}\" $t(entity.track_other)"
|
||||
},
|
||||
"globalSearch": {
|
||||
"commands": {
|
||||
@@ -460,7 +583,36 @@
|
||||
"title": "$t(entity.playlist_other)"
|
||||
},
|
||||
"albumList": {
|
||||
"title": "$t(entity.album_other)"
|
||||
"title": "$t(entity.album_other)",
|
||||
"artistAlbums": "albums di {{artist}}",
|
||||
"genreAlbums": "\"{{genre}}\" $t(entity.album_other)"
|
||||
},
|
||||
"albumArtistDetail": {
|
||||
"about": "Info {{artist}}",
|
||||
"appearsOn": "compare su",
|
||||
"recentReleases": "uscite recenti",
|
||||
"viewDiscography": "mostra discografia",
|
||||
"relatedArtists": "correlati $t(entity.artist_other)",
|
||||
"topSongs": "brani migliori",
|
||||
"topSongsFrom": "brani migliori da {{title}}",
|
||||
"viewAll": "mostra tutto",
|
||||
"viewAllTracks": "mostra tutto $t(entity.track_other)"
|
||||
},
|
||||
"manageServers": {
|
||||
"title": "gestisci servers",
|
||||
"serverDetails": "dettagli server",
|
||||
"url": "URL",
|
||||
"username": "nome utente",
|
||||
"editServerDetailsTooltip": "modifica dettagli server",
|
||||
"removeServer": "rimuovi server"
|
||||
},
|
||||
"itemDetail": {
|
||||
"copyPath": "copia percorso negli appunti",
|
||||
"copiedPath": "percorso copiato con successo",
|
||||
"openFile": "mostra traccia nel gestore file"
|
||||
},
|
||||
"playlist": {
|
||||
"reorder": "riordino abilitato solo quando si ordina per id"
|
||||
}
|
||||
},
|
||||
"form": {
|
||||
@@ -491,7 +643,7 @@
|
||||
"error_savePassword": "si è verificato un errore quando si è provato a salvare la password"
|
||||
},
|
||||
"addToPlaylist": {
|
||||
"success": "aggiunto {{message}} $t(entity.track_other) a {{numOfPlaylists}} $t(entity.playlist_other)",
|
||||
"success": "aggiunto $t(entity.trackWithCount, {\"count\": {{message}} }) a $t(entity.playlistWithCount, {\"count\": {{numOfPlaylists}} })",
|
||||
"title": "aggiungi a $t(entity.playlist_one)",
|
||||
"input_skipDuplicates": "salta duplicati",
|
||||
"input_playlists": "$t(entity.playlist_other)"
|
||||
@@ -502,7 +654,8 @@
|
||||
},
|
||||
"queryEditor": {
|
||||
"input_optionMatchAll": "soddisfa tutti",
|
||||
"input_optionMatchAny": "soddisfa qualsiasi"
|
||||
"input_optionMatchAny": "soddisfa qualsiasi",
|
||||
"title": "editor di query"
|
||||
},
|
||||
"lyricSearch": {
|
||||
"input_name": "$t(common.name)",
|
||||
@@ -510,7 +663,17 @@
|
||||
"title": "cerca testi"
|
||||
},
|
||||
"editPlaylist": {
|
||||
"title": "modifica $t(entity.playlist_one)"
|
||||
"title": "modifica $t(entity.playlist_one)",
|
||||
"publicJellyfinNote": "Jellyfin non mostra se una playlist è pubblica o meno. Se vuoi che rimanga pubblica, assicurati di selezionare l’opzione seguente",
|
||||
"success": "$t(entity.playlist_one) aggiornato con successo"
|
||||
},
|
||||
"shareItem": {
|
||||
"allowDownloading": "consentire il download",
|
||||
"description": "descrizione",
|
||||
"setExpiration": "imposta scadenza",
|
||||
"success": "link di condivisione copiato negli appunti (o clicca qui per aprirlo)",
|
||||
"expireInvalid": "la scadenza deve essere nel futuro",
|
||||
"createFailed": "condivisione fallita (è abilitata la condivisione?)"
|
||||
}
|
||||
},
|
||||
"table": {
|
||||
@@ -520,11 +683,17 @@
|
||||
"gap": "$t(common.gap)",
|
||||
"tableColumns": "tabella colonne",
|
||||
"autoFitColumns": "adatta colonne automaticamente",
|
||||
"size": "$t(common.size)"
|
||||
"size": "$t(common.size)",
|
||||
"followCurrentSong": "segui il brano corrente",
|
||||
"itemGap": "spaziatura tra gli elementi (px)",
|
||||
"itemSize": "dimensione dell’elemento (px)"
|
||||
},
|
||||
"view": {
|
||||
"table": "tabella",
|
||||
"card": "Scheda"
|
||||
"card": "Scheda",
|
||||
"grid": "griglia",
|
||||
"list": "lista",
|
||||
"poster": "poster"
|
||||
},
|
||||
"label": {
|
||||
"releaseDate": "data rilascio",
|
||||
@@ -552,7 +721,9 @@
|
||||
"discNumber": "numero disco",
|
||||
"favorite": "$t(common.favorite)",
|
||||
"year": "$t(common.year)",
|
||||
"albumArtist": "$t(entity.albumArtist_one)"
|
||||
"albumArtist": "$t(entity.albumArtist_one)",
|
||||
"codec": "$t(common.codec)",
|
||||
"songCount": "$t(entity.track_other)"
|
||||
}
|
||||
},
|
||||
"column": {
|
||||
@@ -578,7 +749,8 @@
|
||||
"path": "percorso",
|
||||
"discNumber": "disco",
|
||||
"channels": "$t(common.channel_other)",
|
||||
"size": "$t(common.size)"
|
||||
"size": "$t(common.size)",
|
||||
"codec": "$t(common.codec)"
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
@@ -627,6 +799,12 @@
|
||||
"genreWithCount_other": "{{count}} generi",
|
||||
"trackWithCount_one": "{{count}} traccia",
|
||||
"trackWithCount_many": "{{count}} tracce",
|
||||
"trackWithCount_other": "{{count}} tracce"
|
||||
"trackWithCount_other": "{{count}} tracce",
|
||||
"play_one": "{{count}} riproduzione",
|
||||
"play_many": "{{count}} riproduzioni",
|
||||
"play_other": "{{count}} riproduzioni",
|
||||
"song_one": "traccia",
|
||||
"song_many": "tracce",
|
||||
"song_other": "tracce"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,7 +122,7 @@
|
||||
"hotkey_toggleShuffle": "シャッフルの切り替え",
|
||||
"theme": "テーマ",
|
||||
"playbackStyle_description": "オーディオプレーヤーに使用する再生スタイルを選択します",
|
||||
"discordRichPresence_description": "{{discord}} のRich Presenceに再生ステータスを表示するようにします。画像キー: {{icon}}, {{playing}}, {{paused}} ",
|
||||
"discordRichPresence_description": "{{discord}} のRich Presenceに再生ステータスを表示するようにします。画像キー: {{icon}}, {{playing}}, {{paused}}",
|
||||
"mpvExecutablePath": "mpv 実行ファイルパス",
|
||||
"audioDevice": "オーディオデバイス",
|
||||
"hotkey_rate2": "2つ星で評価",
|
||||
|
||||
@@ -533,7 +533,7 @@
|
||||
"crossfadeDuration_description": "ustaw czas trwania efektu przenikania",
|
||||
"language": "język",
|
||||
"hotkey_toggleShuffle": "przełącz kolejność losową",
|
||||
"discordRichPresence_description": "włącz status odtwarzania w {{discord}} (rich presence). Dzięki temu będą wyświetlane informacje takie jak: {{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",
|
||||
"hotkey_rate2": "oceń na 2 gwiazdki",
|
||||
"exitToTray": "zamknij do zasobnika",
|
||||
|
||||
@@ -91,7 +91,11 @@
|
||||
"albumGain": "ganho do álbum",
|
||||
"trackPeak": "pico da faixa",
|
||||
"albumPeak": "pico do álbum",
|
||||
"trackGain": "ganho da faixa"
|
||||
"trackGain": "ganho da faixa",
|
||||
"additionalParticipants": "participantes adicionais",
|
||||
"tags": "tags",
|
||||
"newVersion": "uma nova versão foi instalada ({{version}})",
|
||||
"viewReleaseNotes": "ver notas de lançamento"
|
||||
},
|
||||
"action": {
|
||||
"goToPage": "vá para página",
|
||||
@@ -205,7 +209,18 @@
|
||||
"buttonSize_description": "o tamanho dos botões da barra de reprodução",
|
||||
"albumBackgroundBlur": "tamanho de desfoque da imagem de fundo do álbum",
|
||||
"albumBackgroundBlur_description": "ajusta a quantidade de desfoque aplicada para a imagem de fundo do álbum",
|
||||
"albumBackground": "imagem de fundo do álbum"
|
||||
"albumBackground": "imagem de fundo do álbum",
|
||||
"contextMenu_description": "permite esconder itens exibidos no menu quando você clica em um item com o botão direito. itens não selecionados serão escondidos",
|
||||
"customCssEnable": "habilitar css customizado",
|
||||
"customCssEnable_description": "permite escrever css customizado.",
|
||||
"crossfadeDuration": "duraçao de crossfade",
|
||||
"customCss": "css customizado",
|
||||
"crossfadeDuration_description": "define a duração do efeito crossfade",
|
||||
"customCssNotice": "Aviso: apesar de existir alguma higienização (url() e content: não são permitidas), o uso de CSS personalizado ainda pode representar riscos ao alterar a interface.",
|
||||
"crossfadeStyle": "estilo do crossfade",
|
||||
"crossfadeStyle_description": "seleciona qual estilo de crossfade usado no player de áudio",
|
||||
"disableAutomaticUpdates": "desabilitar atualizações automáticas",
|
||||
"disableLibraryUpdateOnStartup": "desabilitar a verificação de novas versões na inicialização"
|
||||
},
|
||||
"table": {
|
||||
"config": {
|
||||
@@ -262,7 +277,8 @@
|
||||
"nowPlaying": "tocando agora",
|
||||
"playlists": "$t(entity.playlist_other)",
|
||||
"search": "$t(common.search)",
|
||||
"settings": "$t(common.setting_other)"
|
||||
"settings": "$t(common.setting_other)",
|
||||
"myLibrary": "minha biblioteca"
|
||||
},
|
||||
"playlistList": {
|
||||
"title": "$t(entity.playlist_other)"
|
||||
@@ -524,6 +540,7 @@
|
||||
"loginRateError": "muitas tentativas de login, tente novamente em alguns segundos",
|
||||
"badAlbum": "você está vendo este erro por que está música não é parte de algum album. um motivo comum para você estar vendo este erro é se a sua música estiver na raiz da sua pasta de músicas. o jellyfin apenas agrupa as músicas se elas estiveram na mesma pasta.",
|
||||
"networkError": "ocorreu um erro na internet",
|
||||
"openError": "não foi possível abrir o arquivo"
|
||||
"openError": "não foi possível abrir o arquivo",
|
||||
"badValue": "opção inválida \"{{value}}\". este valor não existe no momento"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,7 +86,7 @@
|
||||
"confirm": "подтвердить",
|
||||
"resetToDefault": "сбросить настройки",
|
||||
"home": "главная",
|
||||
"comingSoon": "скоро...",
|
||||
"comingSoon": "скоро…",
|
||||
"reset": "сбросить",
|
||||
"channel_one": "канал",
|
||||
"channel_few": "канала",
|
||||
@@ -333,7 +333,7 @@
|
||||
"next": "следующий",
|
||||
"shuffle": "перемешать",
|
||||
"playbackFetchNoResults": "песни не найдены",
|
||||
"playbackFetchInProgress": "загрузка песен..",
|
||||
"playbackFetchInProgress": "загрузка песен…",
|
||||
"addNext": "воспроизвести следующим",
|
||||
"playbackSpeed": "скорость воспроизведения",
|
||||
"playbackFetchCancel": "пожалуйста, подождите немного... закройте уведомление для отмены",
|
||||
@@ -759,7 +759,7 @@
|
||||
"artistConfiguration": "конфигурация страницы альбомов исполнителей",
|
||||
"artistConfiguration_description": "позволяет настроить видимость и порядок элементов на странице альбомов исполнителей",
|
||||
"fontType_description": "встроенный позволяет выбрать один из шрифтов, предоставляемых Feishin. системный позволяет выбрать любой шрифт, предоставляемый вашей операционной системой. пользовательский позволяет выбрать свой собственный шрифт",
|
||||
"discordRichPresence_description": "включить статус воспроизведения в статус профиля в {{discord}}. Ключи изображений: {{icon}}, {{playing}} и {{paused}} ",
|
||||
"discordRichPresence_description": "включить статус воспроизведения в статус профиля в {{discord}}. Ключи изображений: {{icon}}, {{playing}} и {{paused}}",
|
||||
"lyricOffset": "синхронизация текста треков (мс)"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,647 @@
|
||||
{
|
||||
"action": {
|
||||
"addToFavorites": "dodaj na $t(entity.favorite_other)",
|
||||
"addToPlaylist": "dodaj na $t(entity.playlist_one)",
|
||||
"clearQueue": "počisti čakalno vrsto",
|
||||
"createPlaylist": "ustvari $t(entity.playlist_one)",
|
||||
"deletePlaylist": "izbriši $t(entity.playlist_one)",
|
||||
"deselectAll": "odizberi vse",
|
||||
"editPlaylist": "uredi $t(entity.playlist_one)",
|
||||
"goToPage": "pojdi na stran",
|
||||
"moveToNext": "pojdi na naslednjo",
|
||||
"moveToBottom": "pojdi na dno",
|
||||
"moveToTop": "pojdi na vrh",
|
||||
"refresh": "$t(common.refresh)",
|
||||
"removeFromFavorites": "odstrani iz $t(entity.favorite_other)",
|
||||
"removeFromPlaylist": "odstrani iz seznama predvajanja",
|
||||
"removeFromQueue": "odstrani iz čakalne vrste",
|
||||
"setRating": "nastavi oceno",
|
||||
"toggleSmartPlaylistEditor": "preklopi urejevalnik $t(entity.smartPlaylist)",
|
||||
"viewPlaylists": "poglej $t(entity.playlist_other)",
|
||||
"openIn": {
|
||||
"lastfm": "Odpri v Last.fm",
|
||||
"musicbrainz": "Odpri v MusicBrainz"
|
||||
}
|
||||
},
|
||||
"common": {
|
||||
"action_one": "dejanje",
|
||||
"action_two": "dejanji",
|
||||
"action_few": "dejanja",
|
||||
"action_other": "dejanj",
|
||||
"add": "dodaj",
|
||||
"additionalParticipants": "dodatni udeleženci",
|
||||
"newVersion": "nova verzija je bila nameščena ({{version}})",
|
||||
"viewReleaseNotes": "poglej zapiske o različici",
|
||||
"albumGain": "ojačitev albuma",
|
||||
"albumPeak": "vrh albuma",
|
||||
"areYouSure": "ali si prepričan?",
|
||||
"ascending": "naraščajoče",
|
||||
"backward": "nazaj",
|
||||
"biography": "biografija",
|
||||
"bitrate": "bitna hitrost",
|
||||
"bpm": "unm",
|
||||
"cancel": "prekliči",
|
||||
"center": "center",
|
||||
"channel_one": "kanal",
|
||||
"channel_two": "kanala",
|
||||
"channel_few": "kanali",
|
||||
"channel_other": "kanalov",
|
||||
"clear": "počisti",
|
||||
"close": "zapri",
|
||||
"codec": "kodek",
|
||||
"collapse": "strni",
|
||||
"comingSoon": "prihaja kmalu …",
|
||||
"configure": "prilagodi",
|
||||
"confirm": "potrdi",
|
||||
"create": "ustvari",
|
||||
"currentSong": "trenutna $t(entity.track_one)",
|
||||
"decrease": "zmanjšaj",
|
||||
"delete": "izbriši",
|
||||
"descending": "padajoče",
|
||||
"description": "opis",
|
||||
"disable": "onemogoči",
|
||||
"disc": "disk",
|
||||
"dismiss": "spreglej",
|
||||
"duration": "trajanje",
|
||||
"edit": "uredi",
|
||||
"enable": "omogoči",
|
||||
"expand": "razširi",
|
||||
"favorite": "najljubša",
|
||||
"filter_one": "filter",
|
||||
"filter_two": "filtra",
|
||||
"filter_few": "filtri",
|
||||
"filter_other": "filtrov",
|
||||
"filters": "filtri",
|
||||
"forceRestartRequired": "znova zaženi, da potrdiš spremembe ... zapri obvestilo, da znova zaženeš",
|
||||
"forward": "naprej",
|
||||
"gap": "reža",
|
||||
"home": "domov",
|
||||
"increase": "povišaj",
|
||||
"limit": "omeji",
|
||||
"manage": "upravljaj",
|
||||
"maximize": "maksimiziraj",
|
||||
"menu": "meni",
|
||||
"minimize": "pomanjšaj",
|
||||
"modified": "spremenjeno",
|
||||
"mbid": "MusicBrainz identifikator (ID)",
|
||||
"left": "levo",
|
||||
"no": "ne",
|
||||
"none": "noben",
|
||||
"noResultsFromQuery": "poizvedba ni vrnila rezultatov",
|
||||
"note": "opomba",
|
||||
"ok": "ok",
|
||||
"owner": "lastnik",
|
||||
"path": "pot",
|
||||
"playerMustBePaused": "predvajalnik mora biti ustavljen",
|
||||
"preview": "predogled",
|
||||
"previousSong": "prejšnja $t(entity.track_one)",
|
||||
"quit": "izhod",
|
||||
"random": "naključno",
|
||||
"rating": "ocena",
|
||||
"refresh": "osveži",
|
||||
"reload": "ponovno naloži",
|
||||
"reset": "ponastavi",
|
||||
"resetToDefault": "ponastavi na privzeto",
|
||||
"restartRequired": "zahtevan je ponovni zagon",
|
||||
"right": "desno",
|
||||
"save": "shrani",
|
||||
"saveAndReplace": "shrani in zamenjaj",
|
||||
"saveAs": "shrani kot",
|
||||
"search": "išči",
|
||||
"setting": "nastavitev",
|
||||
"share": "deli",
|
||||
"size": "velikost",
|
||||
"sortOrder": "vrstni red",
|
||||
"tags": "oznake",
|
||||
"title": "naslov",
|
||||
"trackNumber": "skladba",
|
||||
"trackGain": "glasnost skladbe",
|
||||
"trackPeak": "vrhunec skladbe",
|
||||
"translation": "prevod",
|
||||
"unknown": "neznan",
|
||||
"version": "verzija",
|
||||
"year": "leto",
|
||||
"yes": "da",
|
||||
"name": "ime"
|
||||
},
|
||||
"entity": {
|
||||
"album_one": "album",
|
||||
"album_two": "albuma",
|
||||
"album_few": "albumi",
|
||||
"album_other": "albumov",
|
||||
"albumArtist_one": "izvajalec albuma",
|
||||
"albumArtist_two": "izvajalec albumov",
|
||||
"albumArtist_few": "izvajalec albumov",
|
||||
"albumArtist_other": "izvajalec albumov",
|
||||
"albumArtistCount_one": "{{count}} izvajalec albuma",
|
||||
"albumArtistCount_two": "{{count}} izvajalca albuma",
|
||||
"albumArtistCount_few": "{{count}} izvajalci albuma",
|
||||
"albumArtistCount_other": "{{count}} izvajalcev albuma",
|
||||
"albumWithCount_one": "{{count}} album",
|
||||
"albumWithCount_two": "{{count}} albuma",
|
||||
"albumWithCount_few": "{{count}} albumi",
|
||||
"albumWithCount_other": "{{count}} albumov",
|
||||
"artist_one": "izvajalec",
|
||||
"artist_two": "izvajalca",
|
||||
"artist_few": "izvajalci",
|
||||
"artist_other": "izvajalcev",
|
||||
"artistWithCount_one": "{{count}} izvajalec",
|
||||
"artistWithCount_two": "{{count}} izvajalca",
|
||||
"artistWithCount_few": "{{count}} izvajalci",
|
||||
"artistWithCount_other": "{{count}} izvajalcev",
|
||||
"favorite_one": "priljubljen",
|
||||
"favorite_two": "priljubljena",
|
||||
"favorite_few": "priljubljeni",
|
||||
"favorite_other": "priljubljenih",
|
||||
"folder_one": "mapa",
|
||||
"folder_two": "mapi",
|
||||
"folder_few": "mape",
|
||||
"folder_other": "map",
|
||||
"folderWithCount_one": "{{count}} mapa",
|
||||
"folderWithCount_two": "{{count}} mapi",
|
||||
"folderWithCount_few": "{{count}} mape",
|
||||
"folderWithCount_other": "{{count}} map",
|
||||
"genre_one": "zvrst",
|
||||
"genre_two": "zvrsti",
|
||||
"genre_few": "zvrsti",
|
||||
"genre_other": "zvrsti",
|
||||
"genreWithCount_one": "{{count}} zvrst",
|
||||
"genreWithCount_two": "{{count}} zvrsti",
|
||||
"genreWithCount_few": "{{count}} zvrsti",
|
||||
"genreWithCount_other": "{{count}} zvrsti",
|
||||
"playlist_one": "seznam predvajanja",
|
||||
"playlist_two": "seznama predvajanja",
|
||||
"playlist_few": "seznami predvajanja",
|
||||
"playlist_other": "seznamov predvajanja",
|
||||
"play_one": "{{count}} predvajanje",
|
||||
"play_two": "{{count}} predvajanji",
|
||||
"play_few": "{{count}} predvajanja",
|
||||
"play_other": "{{count}} predvajanj",
|
||||
"playlistWithCount_one": "{{count}} seznam predvajanja",
|
||||
"playlistWithCount_two": "{{count}} seznama predvajanja",
|
||||
"playlistWithCount_few": "{{count}} seznami predvajanja",
|
||||
"playlistWithCount_other": "{{count}} seznamov predvajanja",
|
||||
"smartPlaylist": "pametni $t(entity.playlist_one)",
|
||||
"track_one": "skladba",
|
||||
"track_two": "skladbi",
|
||||
"track_few": "skladbe",
|
||||
"track_other": "skladb",
|
||||
"song_one": "pesem",
|
||||
"song_two": "pesmi",
|
||||
"song_few": "pesmi",
|
||||
"song_other": "pesmi",
|
||||
"trackWithCount_one": "{{count}} skladba",
|
||||
"trackWithCount_two": "{{count}} skladbi",
|
||||
"trackWithCount_few": "{{count}} skladbe",
|
||||
"trackWithCount_other": "{{count}} skladb"
|
||||
},
|
||||
"error": {
|
||||
"apiRouteError": "preusmeritev zahteve ni bila mogoča",
|
||||
"audioDeviceFetchError": "napaka pri poskusu pridobivanja avdio naprav",
|
||||
"authenticationFailed": "napaka pri avtentikaciji",
|
||||
"badAlbum": "ta stran je prikazana ker skladba ne pripada nobenemu albumu. skladba se verjetno nahaja na vrhu datotečne strukture direktorija z glasbo. jellyfin razporedi skladbe v skupine samo v primeru, ko se nahajajo v direktoriju.",
|
||||
"badValue": "neveljavna možnost \"{{value}}\". ta vrednost ne obstaja več",
|
||||
"credentialsRequired": "zahtevana prijava",
|
||||
"endpointNotImplementedError": "{{serverType}} ne implementira končne točke {{endpoint}}",
|
||||
"genericError": "prišlo je do napake",
|
||||
"invalidServer": "neveljaven strežnik",
|
||||
"localFontAccessDenied": "dostop do lokalnih pisav je bil zavrnjen",
|
||||
"loginRateError": "preveč poskusov prijave, prosimo, poskusite čez nekaj sekund",
|
||||
"mpvRequired": "obvezen MPV",
|
||||
"networkError": "prišlo je do mrežne napake",
|
||||
"openError": "datoteke ni mogoče odpreti",
|
||||
"playbackError": "prišlo je do napake pri poskusu predvajanja skladbe",
|
||||
"remoteDisableError": "oddaljenega strežnika ni bilo mogoče $t(common.disable)ti",
|
||||
"remoteEnableError": "oddaljenega strežnika ni bilo mogoče $t(common.enable)ti",
|
||||
"remotePortError": "pri nastavljanju vrat oddaljenega strežnika je prišlo do napake",
|
||||
"remotePortWarning": "ponovno zaženite strežnik da aplicirate spremembo strežniških vrat",
|
||||
"serverNotSelectedError": "izbran ni bil noben strežnik",
|
||||
"serverRequired": "strežnik zahtevan",
|
||||
"sessionExpiredError": "vaša seja se je iztekla",
|
||||
"systemFontError": "napaka pri pridobivanju sistemskih pisav"
|
||||
},
|
||||
"filter": {
|
||||
"album": "$t(entity.album_one)",
|
||||
"albumArtist": "$t(entity.albumArtist_one)",
|
||||
"albumCount": "število $t(entity.album_other)",
|
||||
"artist": "$t(entity.artist_one)",
|
||||
"biography": "biografija",
|
||||
"bitrate": "bitna hitrost",
|
||||
"bpm": "bpm",
|
||||
"channels": "$t(common.channel_other)",
|
||||
"comment": "komentar",
|
||||
"communityRating": "ocena skupnosti",
|
||||
"criticRating": "ocena kritikov",
|
||||
"dateAdded": "dodano",
|
||||
"disc": "disk",
|
||||
"duration": "trajanje",
|
||||
"favorited": "priljubljeno",
|
||||
"fromYear": "od leta",
|
||||
"genre": "$t(entity.genre_one)",
|
||||
"id": "identifikator",
|
||||
"isCompilation": "je kompilacija",
|
||||
"isFavorited": "je dodan med priljubljene",
|
||||
"isPublic": "je javno",
|
||||
"isRated": "je ocenjen",
|
||||
"isRecentlyPlayed": "je bil nedavno predvajan",
|
||||
"lastPlayed": "zadnje predvajano",
|
||||
"mostPlayed": "najpogosteje predvajano",
|
||||
"name": "ime",
|
||||
"note": "opomba",
|
||||
"owner": "$t(common.owner)",
|
||||
"path": "pot",
|
||||
"playCount": "število predvajanj",
|
||||
"random": "naključno",
|
||||
"rating": "ocena",
|
||||
"recentlyAdded": "nedavno dodano",
|
||||
"recentlyPlayed": "nedavno predvajano",
|
||||
"recentlyUpdated": "nedavno posodobljeno",
|
||||
"releaseDate": "datum izida",
|
||||
"releaseYear": "leto izida",
|
||||
"search": "išči",
|
||||
"songCount": "število pesmi",
|
||||
"title": "naslov",
|
||||
"toYear": "do leta",
|
||||
"trackNumber": "skladba"
|
||||
},
|
||||
"form": {
|
||||
"addServer": {
|
||||
"error_savePassword": "pri shranjevanju gesla je prišlo do napake",
|
||||
"ignoreCors": "ignoriraj cors $t(common.restartRequired)",
|
||||
"ignoreSsl": "ignoriraj ssl $t(common.restartRequired)",
|
||||
"input_legacyAuthentication": "omogoči legacy avtentikacijo",
|
||||
"input_name": "ime strežnika",
|
||||
"input_password": "geslo",
|
||||
"input_savePassword": "shrani geslo",
|
||||
"input_url": "url",
|
||||
"input_username": "uporabniško ime",
|
||||
"success": "dodajanje strežnika uspešno",
|
||||
"title": "dodaj strežnik"
|
||||
},
|
||||
"addToPlaylist": {
|
||||
"input_playlists": "$t(entity.playlist_other)",
|
||||
"input_skipDuplicates": "preskoči duplikate",
|
||||
"success": "$t(entity.trackWithCount, {\"count\": {{message}} }) dodan v $t(entity.playlistWithCount, {\"count\": {{numOfPlaylists}} })",
|
||||
"title": "dodaj v $t(entity.playlist_one)"
|
||||
},
|
||||
"createPlaylist": {
|
||||
"input_description": "$t(common.description)",
|
||||
"input_name": "$t(common.name)",
|
||||
"input_owner": "$t(common.owner)",
|
||||
"input_public": "javno",
|
||||
"success": "$t(entity.playlist_one) je bil uspešno ustvarjen",
|
||||
"title": "ustvari $t(entity.playlist_one)"
|
||||
},
|
||||
"deletePlaylist": {
|
||||
"input_confirm": "vpišite ime $t(entity.playlist_one) za potrditev",
|
||||
"success": "$t(entity.playlist_one) uspešno izbrisan",
|
||||
"title": "izbriši $t(entity.playlist_one)"
|
||||
},
|
||||
"editPlaylist": {
|
||||
"publicJellyfinNote": "Jellyfin ne poda informacij o tem, ali gre za javni ali zasebni seznam predvajanja. Če želite, da seznam predvajanja ostane javen, izberite naslednji vnos",
|
||||
"success": "$t(entity.playlist_one) uspešno posodobljen",
|
||||
"title": "uredi $t(entity.playlist_one)"
|
||||
},
|
||||
"lyricSearch": {
|
||||
"input_artist": "$t(entity.artist_one)",
|
||||
"input_name": "$t(common.name)",
|
||||
"title": "iskanje po besedilu"
|
||||
},
|
||||
"queryEditor": {
|
||||
"title": "urejevalnik poizvedb",
|
||||
"input_optionMatchAll": "ujemanje vseh",
|
||||
"input_optionMatchAny": "ujemanje z najmanj enim"
|
||||
},
|
||||
"shareItem": {
|
||||
"allowDownloading": "dovoli prenašanje",
|
||||
"description": "opis",
|
||||
"setExpiration": "nastavi datum poteka veljavnosti",
|
||||
"success": "deli povezavo v odložišču (ali klikni tukaj za odpiranje)",
|
||||
"expireInvalid": "datum poteka veljavnosti mora biti v prihodnosti",
|
||||
"createFailed": "deljenje ni uspelo (je deljenje omogočeno?)"
|
||||
},
|
||||
"updateServer": {
|
||||
"success": "strežnik uspešno posodobljen",
|
||||
"title": "posodobi strežnik"
|
||||
}
|
||||
},
|
||||
"page": {
|
||||
"albumArtistDetail": {
|
||||
"about": "O izvajalcu",
|
||||
"appearsOn": "se pojavi na",
|
||||
"recentReleases": "zadnje izdaje",
|
||||
"viewDiscography": "poglej diskografijo",
|
||||
"relatedArtists": "sorodni $t(entity.artist_other)",
|
||||
"topSongs": "najboljše skladbe",
|
||||
"topSongsFrom": "najboljše skladbe iz {{title}}",
|
||||
"viewAll": "poglej vse",
|
||||
"viewAllTracks": "poglej vse $t(entity.track_other)"
|
||||
},
|
||||
"albumArtistList": {
|
||||
"title": "$t(entity.albumArtist_other)"
|
||||
},
|
||||
"albumDetail": {
|
||||
"moreFromArtist": "več od $t(entity.artist_one)",
|
||||
"moreFromGeneric": "več iz {{item}}",
|
||||
"released": "izdano"
|
||||
},
|
||||
"albumList": {
|
||||
"artistAlbums": "albumi izvajalca {{artist}}",
|
||||
"genreAlbums": "\"{{genre}}\" $t(entity.album_other)",
|
||||
"title": "$t(entity.album_other)"
|
||||
},
|
||||
"appMenu": {
|
||||
"collapseSidebar": "skrij stransko vrstico",
|
||||
"expandSidebar": "razširi stransko vrstico",
|
||||
"goBack": "nazaj",
|
||||
"goForward": "naprej",
|
||||
"manageServers": "urejanje strežnikov",
|
||||
"openBrowserDevtools": "odpri orodja za razvijalce brskalnika",
|
||||
"quit": "$t(common.quit)",
|
||||
"selectServer": "izberi strežnik",
|
||||
"settings": "$t(common.setting_other)",
|
||||
"version": "verzija {{version}}"
|
||||
},
|
||||
"manageServers": {
|
||||
"title": "urejanje strežnikov",
|
||||
"serverDetails": "podrobosti o strežniku",
|
||||
"url": "URL",
|
||||
"username": "uporabniško ime",
|
||||
"editServerDetailsTooltip": "urejanje podrobnosti strežnika",
|
||||
"removeServer": "odstrani strežnik"
|
||||
},
|
||||
"contextMenu": {
|
||||
"addFavorite": "$t(action.addToFavorites)",
|
||||
"addLast": "$t(player.addLast)",
|
||||
"addNext": "$t(player.addNext)",
|
||||
"addToFavorites": "$t(action.addToFavorites)",
|
||||
"addToPlaylist": "$t(action.addToPlaylist)",
|
||||
"createPlaylist": "$t(action.createPlaylist)",
|
||||
"deletePlaylist": "$t(action.deletePlaylist)",
|
||||
"deselectAll": "$t(action.deselectAll)",
|
||||
"download": "prenesi",
|
||||
"moveToNext": "$t(action.moveToNext)",
|
||||
"moveToBottom": "$t(action.moveToBottom)",
|
||||
"moveToTop": "$t(action.moveToTop)",
|
||||
"numberSelected": "{{count}} izbranih",
|
||||
"play": "$t(player.play)",
|
||||
"playSimilarSongs": "$t(player.playSimilarSongs)",
|
||||
"removeFromFavorites": "$t(action.removeFromFavorites)",
|
||||
"removeFromPlaylist": "$t(action.removeFromPlaylist)",
|
||||
"removeFromQueue": "$t(action.removeFromQueue)",
|
||||
"setRating": "$t(action.setRating)",
|
||||
"playShuffled": "$t(player.shuffle)",
|
||||
"shareItem": "deli",
|
||||
"showDetails": "pridobi informacije"
|
||||
},
|
||||
"fullscreenPlayer": {
|
||||
"config": {
|
||||
"dynamicBackground": "dinamično ozadje",
|
||||
"dynamicImageBlur": "velikost zameglitve slike",
|
||||
"dynamicIsImage": "omogoči sliko v ozadju",
|
||||
"followCurrentLyric": "sledi besedilu",
|
||||
"lyricAlignment": "poravnava besedila",
|
||||
"lyricOffset": "zamik besedila (ms)",
|
||||
"lyricGap": "razmik besedila",
|
||||
"lyricSize": "velikost besedila",
|
||||
"opacity": "prosojnost",
|
||||
"showLyricMatch": "prikaži ujemanje besedila",
|
||||
"showLyricProvider": "pokaži ponudnika besedila",
|
||||
"synchronized": "sinhronizirano",
|
||||
"unsynchronized": "nesinhronizirano",
|
||||
"useImageAspectRatio": "uporabi razmerje stranic slike"
|
||||
},
|
||||
"lyrics": "besedilo",
|
||||
"related": "sorodno",
|
||||
"upNext": "sledi",
|
||||
"visualizer": "vizualizator",
|
||||
"noLyrics": "ni bilo najdenih besedil"
|
||||
},
|
||||
"genreList": {
|
||||
"showAlbums": "prikaži $t(entity.genre_one) $t(entity.album_other)",
|
||||
"showTracks": "prikaži $t(entity.genre_one) $t(entity.track_other)",
|
||||
"title": "$t(entity.genre_other)"
|
||||
},
|
||||
"globalSearch": {
|
||||
"commands": {
|
||||
"goToPage": "pojdi na stran",
|
||||
"searchFor": "išči {{query}}",
|
||||
"serverCommands": "strežniški ukazi"
|
||||
},
|
||||
"title": "ukazi"
|
||||
},
|
||||
"home": {
|
||||
"explore": "razišči knjižnico",
|
||||
"mostPlayed": "najpogosteje predvajano",
|
||||
"newlyAdded": "zadnje dodane izdaje",
|
||||
"recentlyPlayed": "nedavno predvajano",
|
||||
"title": "$t(common.home)"
|
||||
},
|
||||
"itemDetail": {
|
||||
"copyPath": "kopiraj v odložišče",
|
||||
"copiedPath": "kopiranje poti uspešno",
|
||||
"openFile": "prikaži skladbo v upravitelju datotek"
|
||||
},
|
||||
"playlist": {
|
||||
"reorder": "preurejanje je omogočeno samo pri razvrščanju po identifikatorju"
|
||||
},
|
||||
"playlistList": {
|
||||
"title": "$t(entity.playlist_other)"
|
||||
},
|
||||
"setting": {
|
||||
"advanced": "napredno",
|
||||
"generalTab": "splošno",
|
||||
"hotkeysTab": "blžnjice",
|
||||
"playbackTab": "predvajanje",
|
||||
"windowTab": "okno"
|
||||
},
|
||||
"sidebar": {
|
||||
"albumArtists": "$t(entity.albumArtist_other)",
|
||||
"albums": "$t(entity.album_other)",
|
||||
"artists": "$t(entity.artist_other)",
|
||||
"folders": "$t(entity.folder_other)",
|
||||
"genres": "$t(entity.genre_other)",
|
||||
"home": "$t(common.home)",
|
||||
"myLibrary": "moja knjižnica",
|
||||
"nowPlaying": "trenutno se predvaja",
|
||||
"playlists": "$t(entity.playlist_other)",
|
||||
"search": "$t(common.search)",
|
||||
"settings": "$t(common.setting_other)",
|
||||
"shared": "deljen $t(entity.playlist_other)",
|
||||
"tracks": "$t(entity.track_other)"
|
||||
},
|
||||
"trackList": {
|
||||
"artistTracks": "skladbe po {{artist}}",
|
||||
"genreTracks": "\"{{genre}}\" $t(entity.track_other)",
|
||||
"title": "$t(entity.track_other)"
|
||||
}
|
||||
},
|
||||
"player": {
|
||||
"addLast": "dodaj zadnje",
|
||||
"addNext": "dodaj naslednje",
|
||||
"favorite": "dodaj med priljubljene",
|
||||
"mute": "utišaj",
|
||||
"muted": "utišano",
|
||||
"next": "naslednje",
|
||||
"play": "predvajaj",
|
||||
"playbackFetchCancel": "akcija traja dlje časa... zaprite obvestilo za preklic",
|
||||
"playbackFetchInProgress": "nalaganje pesmi…",
|
||||
"playbackFetchNoResults": "nobena pesem ni bila najdena",
|
||||
"playbackSpeed": "hitrost predvajanja",
|
||||
"playRandom": "predvajaj naključno",
|
||||
"playSimilarSongs": "predvajaj sorodne pesmi",
|
||||
"previous": "prejšnje",
|
||||
"queue_clear": "počisti čakalno vrsto",
|
||||
"queue_moveToBottom": "premakni izbrano na vrh",
|
||||
"queue_moveToTop": "premakni izbrano na dno",
|
||||
"queue_remove": "odstrani izbrano",
|
||||
"repeat": "ponovi",
|
||||
"repeat_all": "ponovi vse",
|
||||
"repeat_off": "ne ponavljaj",
|
||||
"shuffle": "predvajaj v naključnem vrstnem redu",
|
||||
"shuffle_off": "prevajanje v naključnem vrstnem redu izključeno",
|
||||
"skip": "preskoči",
|
||||
"skip_back": "preskoči nazaj",
|
||||
"skip_forward": "preskoči naprej",
|
||||
"stop": "ustavi",
|
||||
"toggleFullscreenPlayer": "preklopi predvajalnik v celozaslonski način",
|
||||
"unfavorite": "odstrani iz priljubljenih",
|
||||
"pause": "premor",
|
||||
"viewQueue": "poglej čakalno vrsto"
|
||||
},
|
||||
"setting": {
|
||||
"accentColor": "barva poudarka",
|
||||
"accentColor_description": "nastavi barva poudarka aplikacije",
|
||||
"albumBackground": "slika ozadja albuma",
|
||||
"albumBackground_description": "doda sliko ozadja za strani albuma",
|
||||
"albumBackgroundBlur": "velikost zameglitve slike ozadja albuma",
|
||||
"albumBackgroundBlur_description": "spremeni moč zameglitve slike ozadja albuma",
|
||||
"applicationHotkeys": "bližnjične tipke aplikacije",
|
||||
"applicationHotkeys_description": "konfigurira bližnjične tipke aplikacije. obkljukajte da nastavite globalne bližnjico na tipkovnici (samo na namizju)",
|
||||
"artistConfiguration": "konfiguracija strani izvajalca albuma",
|
||||
"artistConfiguration_description": "konfiguriranje vsebine in vrstnega reda prikaza na strani izvajalca albuma",
|
||||
"audioDevice": "avdio naprava",
|
||||
"audioDevice_description": "izberite avdio napravo za predvajanje (samo v spletnem predvajalniku)",
|
||||
"audioExclusiveMode": "avdio način",
|
||||
"audioExclusiveMode_description": "omogoči način ekskluzivnega predvajanja. V tem načinu je sistem običajno zaklenjen in samo mpv lahko oddaja zvok",
|
||||
"audioPlayer": "avdio predvajalnik",
|
||||
"audioPlayer_description": "izberite avdio predvajalnik za predvajanje",
|
||||
"buttonSize": "velikost gumbov vrstice predvajalnika",
|
||||
"buttonSize_description": "velikost gumbov v vrstici predvajalnika",
|
||||
"clearCache": "izbriši začasni pomnilnik",
|
||||
"clearCache_description": "poleg brisanja feishinovega začasnega pomnilnika bo izbrisan tudi začasni pomnilnik brskalnika. nastavitve in prijavni podatki strežnikov se ohranijo",
|
||||
"clearQueryCache": "počisti feishinov začasni pomnilnik",
|
||||
"clearQueryCache_description": "osveži sezname predvajanja, metapodatke in ponastavi shranjena besedila. nastavitve, prijavni podatki za strežnike in slike se ohranijo",
|
||||
"clearCacheSuccess": "začasni pomnilnik uspešno izbrisan",
|
||||
"contextMenu": "konfiguracija kontekstnega menija (desni klik)",
|
||||
"contextMenu_description": "omogoči skrivanje vrstic v meniju, prikazanem ob desnem kliku. odznačeni predmeti bodo skriti",
|
||||
"crossfadeDuration": "trajanje prehoda",
|
||||
"crossfadeDuration_description": "nastavi čas trajanja prehoda med pesmimi",
|
||||
"crossfadeStyle": "tip prehoda",
|
||||
"crossfadeStyle_description": "izbira tipa efekta prehoda",
|
||||
"customCssEnable": "omogoči css po meri",
|
||||
"customCssEnable_description": "omogoča urejanje css-ja po meri.",
|
||||
"customCssNotice": "Opozorilo: kljub določenim varnostnim ukrepom (prepoved url() in content:) lahko uporaba CSS po meri s spreminjanjem vmesnika še vedno predstavlja tveganje.",
|
||||
"customCss": "css po meri",
|
||||
"customCss_description": "vsebina css po meri. Opomba: vsebina in oddaljeni url-ji so prepovedane lastnosti. Spodaj je prikazan predogled vaše vsebine. Dodatna polja, ki jih niste nastavili, so prisotna zaradi prečiščevanja.",
|
||||
"customFontPath": "pot za pisavo po meri",
|
||||
"customFontPath_description": "nastavi pot do pisave po meri",
|
||||
"disableAutomaticUpdates": "onemogoči samodejne posodobitve",
|
||||
"disableLibraryUpdateOnStartup": "onemogoči prevejranje novih verzij ob zagonu",
|
||||
"discordApplicationId": "{{discord}} identifikator aplikacije",
|
||||
"discordApplicationId_description": "identifikator aplikacije za {{discord}} bogato prezenco (privzeto {{defaultId}})",
|
||||
"discordPausedStatus": "prikaži bogato prezenco med ustavljenim predvajanjem",
|
||||
"discordPausedStatus_description": "ko je nastavitev omogočena, se bo status prikazal tudi ko je predvajanje začasno zaustavljeno",
|
||||
"discordIdleStatus": "prikaže stanje mirovanja v bogati prezenci",
|
||||
"discordIdleStatus_description": "ko je nastavitev omogočena, se bo status posodabljal ko predvajalnik miruje",
|
||||
"discordListening": "prikaži status poslušanja",
|
||||
"discordListening_description": "prikaži status poslušanja namesto predvajanja",
|
||||
"discordRichPresence": "{{discord}} bogata prezenca",
|
||||
"discordRichPresence_description": "omogoči prikaz statusa predvajanja v {{discord}} bogati prezenci. Oznake slike so: {{icon}}, {{playing}} in {{paused}}",
|
||||
"discordServeImage": "pošiljaj {{discord}} u slike iz strežnika",
|
||||
"discordServeImage_description": "deli naslovne slike za {{discord}} bogato prisotnost iz samega strežnika, na voljo samo za jellyfin in navidrome",
|
||||
"discordUpdateInterval": "interval posodabljanja {{discord}} bogate prezence",
|
||||
"discordUpdateInterval_description": "čas v sekundah med posameznimi posodobitvami (najmanj 15 sekund)",
|
||||
"doubleClickBehavior": "dvojni klik doda vse iskane skladbe v čakalno vrsto",
|
||||
"doubleClickBehavior_description": "če je nastavitev vklopljena se bodo v čakalno vrsto dodale vse skladbe, ki ustrezajo iskanju. v nasprotnem primeru se v čakalno vrsto doda samo izbrana skladba",
|
||||
"enableRemote": "omogoči oddaljeno upravljanje strežnika",
|
||||
"enableRemote_description": "omogoči oddaljeno nadzorovanje strežnika in s tem dovoli drugim napravam da upravljajo aplikacijo",
|
||||
"externalLinks": "prikaži zunanje povezave",
|
||||
"externalLinks_description": "omogoči prikaz zunanjih povezav (Last.fm, MusicBrainz) na straneh albumov,izvajalcev",
|
||||
"exitToTray": "minimiziraj",
|
||||
"exitToTray_description": "ob izhodu se aplikacija minimizira v opravilno vrstico",
|
||||
"floatingQueueArea": "prikaži območje plavajoče čakalne vrste",
|
||||
"floatingQueueArea_description": "na desni strani zaslona prikažite ikono za ogled čakalne vrste predvajanja",
|
||||
"followLyric": "sledenje besedilu",
|
||||
"followLyric_description": "pomaknite besedilo pesmi do trenutnega položaja predvajanja",
|
||||
"preferLocalLyrics": "prioritiziraj lokalna besedila",
|
||||
"preferLocalLyrics_description": "prioritiziraj lokalna besedila pred oddaljenimi, kadar so na voljo",
|
||||
"font": "pisava",
|
||||
"font_description": "nastavi pisavo, ki jo bo aplikacija uporabljala",
|
||||
"fontType": "tip pisave",
|
||||
"fontType_description": "vgrajena pisava izbere eno od pisav, ki jih ponuja Feishin. sistemska pisava vam omogoča, da izberete katero koli pisavo, ki jo ponuja vaš operacijski sistem. po meri lahko izberete svojo pisavo",
|
||||
"fontType_optionBuiltIn": "vgrajena pisava",
|
||||
"fontType_optionCustom": "pisava po meri",
|
||||
"fontType_optionSystem": "sistemska pisava",
|
||||
"gaplessAudio": "neprekinjen avdio",
|
||||
"gaplessAudio_description": "nastavi neprekinjen avdio za mpv",
|
||||
"gaplessAudio_optionWeak": "šibko (priporočeno)",
|
||||
"genreBehavior": "privzeto vedenje strani z zvrstmi",
|
||||
"genreBehavior_description": "določa, ali se ob kliku na zvrst privzeto odpre seznam skladb ali albumov",
|
||||
"globalMediaHotkeys": "globalne bližnjične tipke za vsebino",
|
||||
"globalMediaHotkeys_description": "omogočite ali onemogočite uporabo bližnjic za sistemske medije za nadzor predvajanja",
|
||||
"homeConfiguration": "konfiguracija domače strani",
|
||||
"homeConfiguration_description": "konfigurirajte, kateri elementi so prikazani na domači strani in v kakšnem vrstnem redu",
|
||||
"homeFeature": "tekoči trak na domači strani",
|
||||
"homeFeature_description": "nadzoruje, ali naj se na domači strani prikaže velik tekoči trak",
|
||||
"hotkey_browserBack": "nazaj (brskalnik)",
|
||||
"hotkey_browserForward": "naprej (brskalnik)",
|
||||
"hotkey_favoriteCurrentSong": "dodaj $t(common.currentSong) med priljubljene",
|
||||
"hotkey_favoritePreviousSong": "dodaj $t(common.previousSong) med priljubljene",
|
||||
"hotkey_globalSearch": "globalno iskanje",
|
||||
"hotkey_localSearch": "iskanje na strani",
|
||||
"hotkey_playbackNext": "naslednja skladba",
|
||||
"hotkey_playbackPause": "pavza",
|
||||
"hotkey_playbackPlay": "predvajaj",
|
||||
"hotkey_playbackPlayPause": "predvajaj / pavza",
|
||||
"hotkey_playbackPrevious": "prejšnja skladba",
|
||||
"hotkey_playbackStop": "ustavi",
|
||||
"hotkey_rate0": "počisti oceno",
|
||||
"hotkey_rate1": "oceni z 1 zvezdico",
|
||||
"hotkey_rate2": "oceni z 2 zvezdicama",
|
||||
"hotkey_rate3": "oceni s 3 zvezdicami",
|
||||
"hotkey_rate4": "oceni s 4 zvezdicami",
|
||||
"hotkey_rate5": "oceni s 5 zvezdicami",
|
||||
"hotkey_skipBackward": "preskoči nazaj",
|
||||
"hotkey_skipForward": "preskoči naprej",
|
||||
"hotkey_toggleCurrentSongFavorite": "dodaj/odstrani $t(common.currentSong) iz seznama priljubljenih",
|
||||
"hotkey_toggleFullScreenPlayer": "preklopi predvajalnik na celozaslonski način",
|
||||
"hotkey_togglePreviousSongFavorite": "dodaj/odstrani $t(common.previousSong) iz seznama priljubljenih",
|
||||
"hotkey_toggleQueue": "preklopi čakalno vrsto",
|
||||
"hotkey_toggleRepeat": "preklopi ponovitve",
|
||||
"hotkey_toggleShuffle": "preklopi naključni vrstni red predvajanja",
|
||||
"hotkey_unfavoriteCurrentSong": "odstrani $t(common.currentSong) iz seznama priljubljenih",
|
||||
"hotkey_unfavoritePreviousSong": "odstrani $t(common.previousSong) iz seznama priljubljenih",
|
||||
"hotkey_volumeDown": "znižaj glasnost",
|
||||
"hotkey_volumeMute": "utišaj",
|
||||
"hotkey_volumeUp": "povišaj glasnost",
|
||||
"hotkey_zoomIn": "povečaj",
|
||||
"hotkey_zoomOut": "pomanjšaj",
|
||||
"imageAspectRatio": "uporabi razmerje stranic izvorne naslovnice",
|
||||
"imageAspectRatio_description": "če je omogočeno, bo naslovnica prikazana z izvornim razmerjem stranic. za slike, ki niso 1:1, bo preostali prostor prazen",
|
||||
"language": "jezik",
|
||||
"language_description": "nastavi jezik aplikacije ($t(common.restartRequired))",
|
||||
"lastfm": "prikaži last.fm povezave",
|
||||
"lastfm_description": "prikaži povezave do last.fm na straneh izvajalcev/albumov",
|
||||
"lastfmApiKey": "API ključ {{lastfm}}",
|
||||
"lastfmApiKey_description": "API ključ za {{lastfm}}. potreben za naslovnico albuma",
|
||||
"lyricFetch": "pridobi besedila iz interneta",
|
||||
"lyricFetch_description": "pridobivanje besedil iz različnih internetnih virov",
|
||||
"lyricFetchProvider": "ponudniki za pridobivanje besedil",
|
||||
"lyricFetchProvider_description": "izberite ponudnike, od katerih želite pridobiti besedila. vrstni red ponudnikov je vrstni red, v katerem bodo poizvedovani",
|
||||
"lyricOffset": "zamik besedila (ms)",
|
||||
"lyricOffset_description": "zamakni besedilo za določeno število milisekund",
|
||||
"minimizeToTray": "minimiziraj v sistemsko vrstico",
|
||||
"minimizeToTray_description": "minimizirajte aplikacijo v sistemsko vrstico"
|
||||
}
|
||||
}
|
||||
@@ -112,7 +112,7 @@
|
||||
"hotkey_localSearch": "pretraživanje na stranici",
|
||||
"hotkey_toggleQueue": "promeni listu za reprodukciju",
|
||||
"zoom_description": "postavlja stepen zumiranja za aplikaciju",
|
||||
"remotePassword_description": "postavlja lozinku za daljinsku kontrolu servera. Ove informacije se prenose nezaštićeno, pa biste trebali koristiti jedinstvenu lozinku koja vam nije važna.",
|
||||
"remotePassword_description": "postavlja lozinku za daljinsku kontrolu servera. Ove informacije se prenose nezaštićeno, pa biste trebali koristiti jedinstvenu lozinku koja vam nije važna",
|
||||
"hotkey_rate5": "oceni sa 5 zvezdica",
|
||||
"hotkey_playbackPrevious": "prethodna pesma",
|
||||
"showSkipButtons_description": "prikaži ili sakrij dugmad za preskakanje na traci za reprodukciju",
|
||||
@@ -122,7 +122,7 @@
|
||||
"hotkey_toggleShuffle": "promeni slučajan redosled",
|
||||
"theme": "tema",
|
||||
"playbackStyle_description": "izaberite stil reprodukcije za audio plejer",
|
||||
"discordRichPresence_description": "omogućava status reprodukcije u {{discord}} bogatom prikazu. Ključevi slika su: {{icon}}, {{playing}}, i {{paused}} ",
|
||||
"discordRichPresence_description": "omogućava status reprodukcije u {{discord}} bogatom prikazu. Ključevi slika su: {{icon}}, {{playing}}, i {{paused}}",
|
||||
"mpvExecutablePath": "putanja do mpv izvršne datoteke",
|
||||
"audioDevice": "audio uređaj",
|
||||
"hotkey_rate2": "oceni sa 2 zvezdice",
|
||||
@@ -158,7 +158,7 @@
|
||||
"useSystemTheme_description": "prati sistemski određene postavke za svetlu ili tamnu temu",
|
||||
"playButtonBehavior_optionAddNext": "$t(player.addNext)",
|
||||
"lyricFetch_description": "preuzimanje tekstova sa različitih izvora na internetu",
|
||||
"lyricFetchProvider_description": "izaberite pružatelje tekstova za preuzimanje. Redosled pružatelja je redosled upita.",
|
||||
"lyricFetchProvider_description": "izaberite pružatelje tekstova za preuzimanje. Redosled pružatelja je redosled upita",
|
||||
"globalMediaHotkeys_description": "omogućava ili onemogućava korišćenje medijskih tastera sistema za kontrolu reprodukcije",
|
||||
"customFontPath": "prilagođena putanja fonta",
|
||||
"followLyric": "prati trenutni tekst pesme",
|
||||
|
||||
@@ -224,7 +224,7 @@
|
||||
"input_password": "கடவுச்சொல்",
|
||||
"error_savePassword": "கடவுச்சொல்லை சேமிக்க முயற்சிக்கும்போது பிழை ஏற்பட்டது",
|
||||
"ignoreCors": "CORS ஐ புறக்கணிக்கவும் ($ t (Common.RestartRequired))",
|
||||
"ignoreSsl": "SSL ஐ புறக்கணிக்கவும் ($ t (பொதுவானது.",
|
||||
"ignoreSsl": "SSL ஐ புறக்கணிக்கவும் ($ t (பொதுவானது",
|
||||
"input_legacyAuthentication": "மரபு அங்கீகாரத்தை இயக்கவும்",
|
||||
"input_name": "சேவையக பெயர்",
|
||||
"input_savePassword": "கடவுச்சொல்லைச் சேமிக்கவும்",
|
||||
@@ -521,7 +521,7 @@
|
||||
"hotkey_volumeMute": "தொகுதி முடக்கு",
|
||||
"hotkey_volumeUp": "தொகுதி",
|
||||
"language": "மொழி",
|
||||
"language_description": "பயன்பாட்டிற்கான மொழியை அமைக்கிறது ($ t (பொதுவானது.",
|
||||
"language_description": "பயன்பாட்டிற்கான மொழியை அமைக்கிறது ($ t (பொதுவானது",
|
||||
"lastfmApiKey": "{{lastfm}} பநிஇ key",
|
||||
"lastfmApiKey_description": "{{lastfm} க்கு க்கான பநிஇ விசை. கவர் கலைக்கு தேவை",
|
||||
"lyricFetch": "இணையத்திலிருந்து வரிகளை பெறுங்கள்",
|
||||
@@ -615,7 +615,7 @@
|
||||
"discordIdleStatus_description": "இயக்கப்பட்டால், பிளேயர் சும்மா இருக்கும்போது நிலையைப் புதுப்பிக்கவும்",
|
||||
"discordListening_description": "விளையாடுவதற்குப் பதிலாக கேட்பது என்று அந்த நிலையைக் காட்டுங்கள்",
|
||||
"discordRichPresence": "{{discord}} பணக்கார இருப்பு",
|
||||
"discordRichPresence_description": "{{discord}} பணக்கார இருப்பில் பின்னணி நிலையை இயக்கவும். பட விசைகள்: {{icon}}, {{playing}}, மற்றும் {{paused}} ",
|
||||
"discordRichPresence_description": "{{discord}} பணக்கார இருப்பில் பின்னணி நிலையை இயக்கவும். பட விசைகள்: {{icon}}, {{playing}}, மற்றும் {{paused}}",
|
||||
"customCss_description": "தனிப்பயன் சிஎச்எச் உள்ளடக்கம். குறிப்பு: உள்ளடக்கம் மற்றும் தொலைநிலை முகவரி கள் அனுமதிக்கப்படாத பண்புகள். உங்கள் உள்ளடக்கத்தின் முன்னோட்டம் கீழே காட்டப்பட்டுள்ளது. நீங்கள் அமைக்காத கூடுதல் புலங்கள் சுத்திகரிப்பு காரணமாக உள்ளன.",
|
||||
"doubleClickBehavior": "இரட்டை சொடுக்கு செய்யும் போது தேடப்பட்ட அனைத்து தடங்களையும் வரிசைப்படுத்தவும்",
|
||||
"doubleClickBehavior_description": "உண்மை என்றால், தட தேடலில் பொருந்தக்கூடிய அனைத்து தடங்களும் வரிசையில் நிற்கப்படும். இல்லையெனில், சொடுக்கு செய்யப்பட்ட ஒன்று மட்டுமே வரிசையில் நிற்கப்படும்",
|
||||
@@ -705,14 +705,14 @@
|
||||
"rowIndex": "வரிசை அட்டவணை",
|
||||
"size": "$ t (common.size)",
|
||||
"trackNumber": "ட்ராக் எண்",
|
||||
"year": "$ t (பொதுவானது.",
|
||||
"year": "$ t (பொதுவானது",
|
||||
"lastPlayed": "கடைசியாக விளையாடியது",
|
||||
"note": "$ t (பொதுவானது. குறிப்பு)",
|
||||
"owner": "$ t (பொதுவானவர்)",
|
||||
"actions": "$ t (common.action_other)",
|
||||
"albumArtist": "$ t (entity.albumartist_one)",
|
||||
"discNumber": "வட்டு எண்",
|
||||
"duration": "$ t (பொதுவானது.",
|
||||
"duration": "$ t (பொதுவானது",
|
||||
"favorite": "$ t (common.foavorite)",
|
||||
"genre": "$ t (entity.genre_one)",
|
||||
"path": "$ t (common.path)",
|
||||
|
||||
@@ -111,7 +111,9 @@
|
||||
"preview": "预览",
|
||||
"translation": "翻译",
|
||||
"additionalParticipants": "其他参与者",
|
||||
"tags": "标签"
|
||||
"tags": "标签",
|
||||
"viewReleaseNotes": "查看发行说明",
|
||||
"newVersion": "已安装新版本 ({{version}})"
|
||||
},
|
||||
"entity": {
|
||||
"albumArtist_other": "专辑艺术家",
|
||||
@@ -180,6 +182,8 @@
|
||||
"followLyric_description": "滚动歌词到当前播放位置",
|
||||
"audioExclusiveMode": "音频独占模式",
|
||||
"font": "字体",
|
||||
"neteaseTranslation": "启用网易云歌词翻译",
|
||||
"neteaseTranslation_description": "启用后,在获取歌词时将包含并显示网易云音乐提供的翻译(如果存在)。",
|
||||
"crossfadeDuration_description": "设置淡入淡出持续时间",
|
||||
"audioDevice": "音频设备",
|
||||
"enableRemote": "启用远程控制服务器",
|
||||
@@ -321,7 +325,7 @@
|
||||
"discordUpdateInterval": "{{discord}} rich presence 更新间隔",
|
||||
"discordApplicationId_description": "{{discord}} rich presence 应用 id(默认为 {{defaultId}})",
|
||||
"discordUpdateInterval_description": "更新间隔秒数(至少 15 秒)",
|
||||
"discordRichPresence_description": "在 {{discord}} rich presence 中显示播放状态。图片键为:{{icon}}、{{playing}} 和 {{paused}} ",
|
||||
"discordRichPresence_description": "在 {{discord}} rich presence 中显示播放状态。图片键为:{{icon}}、{{playing}} 和 {{paused}}",
|
||||
"accentColor": "强调色",
|
||||
"accentColor_description": "设置应用的强调色",
|
||||
"replayGainPreamp_description": "调整应用在{{ReplayGain}}值上的前置放大增益",
|
||||
@@ -393,7 +397,17 @@
|
||||
"lastfmApiKey": "{{lastfm}} API 密钥",
|
||||
"lastfmApiKey_description": "{{lastfm}} 的 API 密钥。封面艺术图所需",
|
||||
"discordServeImage": "从服务器提供 {{discord}} 图像",
|
||||
"discordServeImage_description": "分享 {{discord}} 封面艺术图,来自 rich presence 服务器,仅适用于 jellyfin 和 navidrome"
|
||||
"discordServeImage_description": "分享 {{discord}} 封面艺术图,来自 rich presence 服务器,仅适用于 jellyfin 和 navidrome",
|
||||
"musicbrainz": "显示 musicbrainz 链接",
|
||||
"musicbrainz_description": "在 mbid 的艺术家/专辑页面上显示 musicbrainz 的链接",
|
||||
"lastfm": "显示 last.fm 链接",
|
||||
"lastfm_description": "在艺术家/专辑页面上显示 last.fm 的链接",
|
||||
"preferLocalLyrics_description": "优先选择本地歌词(如有),而不是远程歌词",
|
||||
"preferLocalLyrics": "首选本地歌词",
|
||||
"discordPausedStatus": "暂停时显示rich presence",
|
||||
"discordPausedStatus_description": "启用后将在播放器暂停时显示状态",
|
||||
"preservePitch": "保持音高",
|
||||
"preservePitch_description": "在调整播放速度时保持音高"
|
||||
},
|
||||
"error": {
|
||||
"remotePortWarning": "重启服务器使新端口生效",
|
||||
@@ -477,7 +491,8 @@
|
||||
"home": "$t(common.home)",
|
||||
"artists": "$t(entity.artist_other)",
|
||||
"albumArtists": "$t(entity.albumArtist_other)",
|
||||
"shared": "共享$t(entity.playlist_other)"
|
||||
"shared": "共享$t(entity.playlist_other)",
|
||||
"myLibrary": "我的媒体库"
|
||||
},
|
||||
"fullscreenPlayer": {
|
||||
"config": {
|
||||
@@ -653,7 +668,8 @@
|
||||
},
|
||||
"queryEditor": {
|
||||
"input_optionMatchAll": "匹配全部",
|
||||
"input_optionMatchAny": "匹配任何"
|
||||
"input_optionMatchAny": "匹配任何",
|
||||
"title": "查询编辑器"
|
||||
},
|
||||
"editPlaylist": {
|
||||
"title": "编辑$t(entity.playlist_one)",
|
||||
@@ -689,7 +705,9 @@
|
||||
"view": {
|
||||
"table": "表格",
|
||||
"poster": "海报",
|
||||
"card": "卡片"
|
||||
"card": "卡片",
|
||||
"grid": "网格",
|
||||
"list": "列表"
|
||||
},
|
||||
"label": {
|
||||
"releaseDate": "发布日期",
|
||||
|
||||
@@ -261,7 +261,7 @@
|
||||
"discordApplicationId_description": "{{discord}} rich presence 應用 id(默認爲 {{defaultId}})",
|
||||
"discordIdleStatus": "顯示 rich presence 閑置狀態",
|
||||
"discordIdleStatus_description": "啓用後將會在播放器閑置時更新狀態",
|
||||
"discordRichPresence_description": "在 {{discord}} rich presence 中顯示播放狀態。圖片鍵爲:{{icon}}、{{playing}} 和 {{paused}} ",
|
||||
"discordRichPresence_description": "在 {{discord}} rich presence 中顯示播放狀態。圖片鍵爲:{{icon}}、{{playing}} 和 {{paused}}",
|
||||
"discordUpdateInterval": "{{discord}} rich presence 更新間隔",
|
||||
"discordUpdateInterval_description": "更新間隔秒數(至少 15 秒)",
|
||||
"enableRemote": "啓用遠程控制服務器",
|
||||
|
||||
@@ -5,16 +5,20 @@ const FEISHIN_DISCORD_APPLICATION_ID = '1165957668758900787';
|
||||
|
||||
let client: Client | null = null;
|
||||
|
||||
const createClient = (clientId?: string) => {
|
||||
const createClient = async (clientId?: string) => {
|
||||
client = new Client({
|
||||
clientId: clientId || FEISHIN_DISCORD_APPLICATION_ID,
|
||||
});
|
||||
|
||||
client.login();
|
||||
await client.login();
|
||||
|
||||
return client;
|
||||
};
|
||||
|
||||
const isConnected = () => {
|
||||
return client?.isConnected;
|
||||
};
|
||||
|
||||
const setActivity = (activity: SetActivity) => {
|
||||
if (client) {
|
||||
client.user?.setActivity({
|
||||
@@ -35,8 +39,12 @@ const quit = () => {
|
||||
}
|
||||
};
|
||||
|
||||
ipcMain.handle('discord-rpc-initialize', (_event, clientId?: string) => {
|
||||
createClient(clientId);
|
||||
ipcMain.handle('discord-rpc-initialize', async (_event, clientId?: string) => {
|
||||
await createClient(clientId);
|
||||
});
|
||||
|
||||
ipcMain.handle('discord-rpc-is-connected', () => {
|
||||
return isConnected();
|
||||
});
|
||||
|
||||
ipcMain.handle('discord-rpc-set-activity', (_event, activity: SetActivity) => {
|
||||
@@ -58,6 +66,7 @@ ipcMain.handle('discord-rpc-quit', () => {
|
||||
export const discordRpc = {
|
||||
clearActivity,
|
||||
createClient,
|
||||
isConnected,
|
||||
quit,
|
||||
setActivity,
|
||||
};
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
LyricSearchQuery,
|
||||
LyricSource,
|
||||
} from '.';
|
||||
import { store } from '../settings';
|
||||
import { orderSearchResults } from './shared';
|
||||
|
||||
const SEARCH_URL = 'https://music.163.com/api/search/get';
|
||||
@@ -76,14 +77,20 @@ export async function getLyricsBySongId(songId: string): Promise<null | string>
|
||||
id: songId,
|
||||
kv: '-1',
|
||||
lv: '-1',
|
||||
tv: '-1',
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('NetEase lyrics request got an error!', e);
|
||||
return null;
|
||||
}
|
||||
|
||||
return result.data.klyric?.lyric || result.data.lrc?.lyric;
|
||||
const enableTranslation = store.get('enableNeteaseTranslation', false) as boolean;
|
||||
const originalLrc = result.data.lrc?.lyric;
|
||||
if (!enableTranslation) {
|
||||
return originalLrc || null;
|
||||
}
|
||||
const translatedLrc = result.data.tlyric?.lyric;
|
||||
return mergeLyrics(originalLrc, translatedLrc);
|
||||
}
|
||||
|
||||
export async function getSearchResults(
|
||||
@@ -166,3 +173,52 @@ async function getMatchedLyrics(
|
||||
|
||||
return firstMatch;
|
||||
}
|
||||
|
||||
function mergeLyrics(original: string | undefined, translated: string | undefined): null | string {
|
||||
if (!original) {
|
||||
return null;
|
||||
}
|
||||
if (!translated) {
|
||||
return original;
|
||||
}
|
||||
|
||||
const lrcLineRegex = /\[(\d{2}:\d{2}\.\d{2,3})\](.*)/;
|
||||
const translatedMap = new Map<string, string>();
|
||||
|
||||
// Parse the translated LRC and store it in a Map for efficient timestamp-based lookups.
|
||||
translated.split('\n').forEach((line) => {
|
||||
const match = line.match(lrcLineRegex);
|
||||
if (match) {
|
||||
const timestamp = match[1];
|
||||
const text = match[2].trim();
|
||||
if (text) {
|
||||
translatedMap.set(timestamp, text);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (translatedMap.size === 0) {
|
||||
return original;
|
||||
}
|
||||
|
||||
// Iterate through each line of the original LRC. If a translation exists for the same timestamp, append the translated text after the original text.
|
||||
const finalLines = original.split('\n').map((line) => {
|
||||
const match = line.match(lrcLineRegex);
|
||||
|
||||
if (match) {
|
||||
const timestamp = match[1];
|
||||
const originalText = match[2].trim();
|
||||
const translatedText = translatedMap.get(timestamp);
|
||||
|
||||
if (translatedText && originalText) {
|
||||
// Append and add a break delimiter to separate the original and translated text
|
||||
return [`[${timestamp}]${originalText}`, translatedText].join('_BREAK_');
|
||||
}
|
||||
}
|
||||
|
||||
// If no match or no translation is found, return the original line unchanged.
|
||||
return line;
|
||||
});
|
||||
|
||||
return finalLines.join('\n');
|
||||
}
|
||||
|
||||
@@ -105,7 +105,7 @@ const createMpv = async (data: {
|
||||
try {
|
||||
await mpv.start();
|
||||
} catch (error: any) {
|
||||
console.log('mpv failed to start', error);
|
||||
console.error('mpv failed to start', error);
|
||||
} finally {
|
||||
await mpv.setMultipleProperties(properties || {});
|
||||
}
|
||||
|
||||
@@ -405,7 +405,7 @@ const enableServer = (config: RemoteConfig): Promise<void> => {
|
||||
}
|
||||
case 'proxy': {
|
||||
const toFetch = currentState.song?.imageUrl?.replaceAll(
|
||||
/&(size|width|height=\d+)/g,
|
||||
/&(size|width|height)=\d+/g,
|
||||
'',
|
||||
);
|
||||
|
||||
|
||||
@@ -177,7 +177,7 @@ ipcMain.on('update-song', (_event, song: QueueSong | undefined) => {
|
||||
'xesam:userRating': song.userRating ? song.userRating / 5 : null,
|
||||
};
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
console.error(err);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
+9
-7
@@ -49,7 +49,7 @@ export default class AppUpdater {
|
||||
protocol.registerSchemesAsPrivileged([{ privileges: { bypassCSP: true }, scheme: 'feishin' }]);
|
||||
|
||||
process.on('uncaughtException', (error: any) => {
|
||||
console.log('Error in main process', error);
|
||||
console.error('Error in main process', error);
|
||||
});
|
||||
|
||||
if (store.get('ignore_ssl')) {
|
||||
@@ -421,9 +421,6 @@ async function createWindow(first = true): Promise<void> {
|
||||
store.set('fullscreen', mainWindow?.isFullScreen());
|
||||
|
||||
if (!exitFromTray && store.get('window_exit_to_tray')) {
|
||||
if (isMacOS() && !forceQuit) {
|
||||
exitFromTray = true;
|
||||
}
|
||||
event.preventDefault();
|
||||
mainWindow?.hide();
|
||||
}
|
||||
@@ -432,8 +429,6 @@ async function createWindow(first = true): Promise<void> {
|
||||
event.preventDefault();
|
||||
saved = true;
|
||||
|
||||
getMainWindow()?.webContents.send('renderer-save-queue');
|
||||
|
||||
ipcMain.once('player-save-queue', async (_event, data: Record<string, any>) => {
|
||||
const queueLocation = join(app.getPath('userData'), 'queue');
|
||||
const serialized = JSON.stringify(data);
|
||||
@@ -457,12 +452,19 @@ async function createWindow(first = true): Promise<void> {
|
||||
} catch (error) {
|
||||
console.error('error saving queue state: ', error);
|
||||
} finally {
|
||||
mainWindow?.close();
|
||||
if (!isMacOS()) {
|
||||
mainWindow?.close();
|
||||
}
|
||||
if (forceQuit) {
|
||||
app.exit();
|
||||
}
|
||||
}
|
||||
});
|
||||
getMainWindow()?.webContents.send('renderer-save-queue');
|
||||
} else {
|
||||
if (forceQuit) {
|
||||
app.exit();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -6,6 +6,11 @@ const initialize = (clientId: string) => {
|
||||
return client;
|
||||
};
|
||||
|
||||
const isConnected = () => {
|
||||
const isConnected = ipcRenderer.invoke('discord-rpc-is-connected');
|
||||
return isConnected;
|
||||
};
|
||||
|
||||
const clearActivity = () => {
|
||||
ipcRenderer.invoke('discord-rpc-clear-activity');
|
||||
};
|
||||
@@ -21,6 +26,7 @@ const quit = () => {
|
||||
export const discordRpc = {
|
||||
clearActivity,
|
||||
initialize,
|
||||
isConnected,
|
||||
quit,
|
||||
setActivity,
|
||||
};
|
||||
|
||||
+11
-65
@@ -1,10 +1,15 @@
|
||||
import { MantineProvider } from '@mantine/core';
|
||||
import { useEffect } from 'react';
|
||||
import '@mantine/core/styles.css';
|
||||
import '@mantine/notifications/styles.css';
|
||||
|
||||
import './styles/global.scss';
|
||||
import '/@/shared/styles/global.css';
|
||||
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import { Shell } from '/@/remote/components/shell';
|
||||
import { useIsDark, useReconnect } from '/@/remote/store';
|
||||
import { useAppTheme } from '/@/renderer/themes/use-app-theme';
|
||||
import { AppTheme } from '/@/shared/themes/app-theme-types';
|
||||
|
||||
export const App = () => {
|
||||
const isDark = useIsDark();
|
||||
@@ -14,71 +19,12 @@ export const App = () => {
|
||||
reconnect();
|
||||
}, [reconnect]);
|
||||
|
||||
const { mode, theme } = useAppTheme(isDark ? AppTheme.DEFAULT_DARK : AppTheme.DEFAULT_LIGHT);
|
||||
|
||||
return (
|
||||
<MantineProvider
|
||||
theme={{
|
||||
colorScheme: isDark ? 'dark' : 'light',
|
||||
components: {
|
||||
AppShell: {
|
||||
styles: {
|
||||
body: {
|
||||
height: '100vh',
|
||||
overflow: 'scroll',
|
||||
},
|
||||
},
|
||||
},
|
||||
Modal: {
|
||||
styles: {
|
||||
body: {
|
||||
background: 'var(--modal-bg)',
|
||||
height: '100vh',
|
||||
},
|
||||
close: { marginRight: '0.5rem' },
|
||||
content: { borderRadius: '5px' },
|
||||
header: {
|
||||
background: 'var(--modal-header-bg)',
|
||||
paddingBottom: '1rem',
|
||||
},
|
||||
title: { fontSize: 'medium', fontWeight: 500 },
|
||||
},
|
||||
},
|
||||
},
|
||||
defaultRadius: 'xs',
|
||||
dir: 'ltr',
|
||||
focusRing: 'auto',
|
||||
focusRingStyles: {
|
||||
inputStyles: () => ({
|
||||
border: '1px solid var(--primary-color)',
|
||||
}),
|
||||
resetStyles: () => ({ outline: 'none' }),
|
||||
styles: () => ({
|
||||
outline: '1px solid var(--primary-color)',
|
||||
outlineOffset: '-1px',
|
||||
}),
|
||||
},
|
||||
fontFamily: 'var(--content-font-family)',
|
||||
fontSizes: {
|
||||
lg: '1.1rem',
|
||||
md: '1rem',
|
||||
sm: '0.9rem',
|
||||
xl: '1.5rem',
|
||||
xs: '0.8rem',
|
||||
},
|
||||
headings: {
|
||||
fontFamily: 'var(--content-font-family)',
|
||||
fontWeight: 700,
|
||||
},
|
||||
other: {},
|
||||
spacing: {
|
||||
lg: '2rem',
|
||||
md: '1rem',
|
||||
sm: '0.5rem',
|
||||
xl: '4rem',
|
||||
xs: '0rem',
|
||||
},
|
||||
}}
|
||||
withGlobalStyles
|
||||
withNormalizeCSS
|
||||
defaultColorScheme={mode}
|
||||
theme={theme}
|
||||
>
|
||||
<Shell />
|
||||
</MantineProvider>
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
import { CiImageOff, CiImageOn } from 'react-icons/ci';
|
||||
|
||||
import { RemoteButton } from '/@/remote/components/buttons/remote-button';
|
||||
import { useShowImage, useToggleShowImage } from '/@/remote/store';
|
||||
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
|
||||
|
||||
export const ImageButton = () => {
|
||||
const showImage = useShowImage();
|
||||
const toggleImage = useToggleShowImage();
|
||||
|
||||
return (
|
||||
<RemoteButton
|
||||
mr={5}
|
||||
<ActionIcon
|
||||
onClick={() => toggleImage()}
|
||||
size="xl"
|
||||
tooltip={showImage ? 'Hide Image' : 'Show Image'}
|
||||
tooltip={{
|
||||
label: showImage ? 'Hide Image' : 'Show Image',
|
||||
}}
|
||||
variant="default"
|
||||
>
|
||||
{showImage ? <CiImageOff size={30} /> : <CiImageOn size={30} />}
|
||||
</RemoteButton>
|
||||
</ActionIcon>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,22 +1,24 @@
|
||||
import { RiRestartLine } from 'react-icons/ri';
|
||||
|
||||
import { RemoteButton } from '/@/remote/components/buttons/remote-button';
|
||||
import { useConnected, useReconnect } from '/@/remote/store';
|
||||
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
|
||||
|
||||
export const ReconnectButton = () => {
|
||||
const connected = useConnected();
|
||||
const reconnect = useReconnect();
|
||||
|
||||
return (
|
||||
<RemoteButton
|
||||
$active={!connected}
|
||||
mr={5}
|
||||
<ActionIcon
|
||||
onClick={() => reconnect()}
|
||||
size="xl"
|
||||
tooltip={connected ? 'Reconnect' : 'Not connected. Reconnect.'}
|
||||
tooltip={{
|
||||
label: connected ? 'Reconnect' : 'Not connected. Reconnect.',
|
||||
}}
|
||||
variant="default"
|
||||
>
|
||||
<RiRestartLine size={30} />
|
||||
</RemoteButton>
|
||||
<RiRestartLine
|
||||
color={connected ? 'var(--theme-colors-primary)' : 'var(--theme-colors-foreground)'}
|
||||
size={30}
|
||||
/>
|
||||
</ActionIcon>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
import { Button, type ButtonProps as MantineButtonProps, Tooltip } from '@mantine/core';
|
||||
import { forwardRef, MouseEvent, ReactNode, Ref } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
export interface ButtonProps extends StyledButtonProps {
|
||||
tooltip: string;
|
||||
}
|
||||
|
||||
interface StyledButtonProps extends MantineButtonProps {
|
||||
$active?: boolean;
|
||||
children: ReactNode;
|
||||
onClick?: (e: MouseEvent<HTMLButtonElement, MouseEvent>) => void;
|
||||
onMouseDown?: (e: MouseEvent<HTMLButtonElement, MouseEvent>) => void;
|
||||
ref: Ref<HTMLButtonElement>;
|
||||
}
|
||||
|
||||
const StyledButton = styled(Button)<StyledButtonProps>`
|
||||
svg {
|
||||
display: flex;
|
||||
fill: ${({ $active: active }) =>
|
||||
active ? 'var(--primary-color)' : 'var(--playerbar-btn-fg)'};
|
||||
stroke: var(--playerbar-btn-fg);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: var(--playerbar-btn-bg-hover);
|
||||
|
||||
svg {
|
||||
fill: ${({ $active: active }) =>
|
||||
active
|
||||
? 'var(--primary-color) !important'
|
||||
: 'var(--playerbar-btn-fg-hover) !important'};
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const RemoteButton = forwardRef<HTMLButtonElement, any>(
|
||||
({ children, tooltip, ...props }: any, ref) => {
|
||||
return (
|
||||
<Tooltip
|
||||
label={tooltip}
|
||||
withinPortal
|
||||
>
|
||||
<StyledButton
|
||||
{...props}
|
||||
ref={ref}
|
||||
>
|
||||
{children}
|
||||
</StyledButton>
|
||||
</Tooltip>
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -1,28 +1,34 @@
|
||||
import { useEffect } from 'react';
|
||||
import { RiMoonLine, RiSunLine } from 'react-icons/ri';
|
||||
|
||||
import { RemoteButton } from '/@/remote/components/buttons/remote-button';
|
||||
import { useIsDark, useToggleDark } from '/@/remote/store';
|
||||
import { AppTheme } from '/@/shared/types/domain-types';
|
||||
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
|
||||
import { Icon } from '/@/shared/components/icon/icon';
|
||||
|
||||
export const ThemeButton = () => {
|
||||
const isDark = useIsDark();
|
||||
const toggleDark = useToggleDark();
|
||||
|
||||
useEffect(() => {
|
||||
const targetTheme: AppTheme = isDark ? AppTheme.DEFAULT_DARK : AppTheme.DEFAULT_LIGHT;
|
||||
document.body.setAttribute('data-theme', targetTheme);
|
||||
}, [isDark]);
|
||||
const handleToggleTheme = () => {
|
||||
toggleDark();
|
||||
};
|
||||
|
||||
return (
|
||||
<RemoteButton
|
||||
mr={5}
|
||||
onClick={() => toggleDark()}
|
||||
size="xl"
|
||||
tooltip="Toggle Theme"
|
||||
<ActionIcon
|
||||
onClick={handleToggleTheme}
|
||||
tooltip={{
|
||||
label: 'Toggle Theme',
|
||||
}}
|
||||
variant="default"
|
||||
>
|
||||
{isDark ? <RiSunLine size={30} /> : <RiMoonLine size={30} />}
|
||||
</RemoteButton>
|
||||
{isDark ? (
|
||||
<Icon
|
||||
icon="themeLight"
|
||||
size={30}
|
||||
/>
|
||||
) : (
|
||||
<Icon
|
||||
icon="themeDark"
|
||||
size={30}
|
||||
/>
|
||||
)}
|
||||
</ActionIcon>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
.container {
|
||||
width: 100%;
|
||||
height: 40vh;
|
||||
aspect-ratio: 1/1;
|
||||
object-fit: var(--theme-image-fit);
|
||||
border-radius: var(--theme-radius-md);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import styles from './player-image.module.css';
|
||||
|
||||
import { useSend } from '/@/remote/store';
|
||||
|
||||
interface PlayerImageProps {
|
||||
src?: null | string;
|
||||
}
|
||||
export const PlayerImage = ({ src }: PlayerImageProps) => {
|
||||
const send = useSend();
|
||||
|
||||
return (
|
||||
<img
|
||||
className={styles.container}
|
||||
onError={() => send({ event: 'proxy' })}
|
||||
src={src?.replaceAll(/&(size|width|height)=\d+/g, '')}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -1,22 +1,18 @@
|
||||
import { Group, Image, Rating, Text, Title, Tooltip } from '@mantine/core';
|
||||
import formatDuration from 'format-duration';
|
||||
import debounce from 'lodash/debounce';
|
||||
import { useCallback } from 'react';
|
||||
import {
|
||||
RiHeartLine,
|
||||
RiPauseFill,
|
||||
RiPlayFill,
|
||||
RiRepeat2Line,
|
||||
RiRepeatOneLine,
|
||||
RiShuffleFill,
|
||||
RiSkipBackFill,
|
||||
RiSkipForwardFill,
|
||||
RiVolumeUpFill,
|
||||
} from 'react-icons/ri';
|
||||
import { RiPauseFill, RiPlayFill, RiVolumeUpFill } from 'react-icons/ri';
|
||||
|
||||
import { RemoteButton } from '/@/remote/components/buttons/remote-button';
|
||||
import { WrapperSlider } from '/@/remote/components/wrapped-slider';
|
||||
import { PlayerImage } from '/@/remote/components/player-image';
|
||||
import { WrappedSlider } from '/@/remote/components/wrapped-slider';
|
||||
import { useInfo, useSend, useShowImage } from '/@/remote/store';
|
||||
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
|
||||
import { Flex } from '/@/shared/components/flex/flex';
|
||||
import { Group } from '/@/shared/components/group/group';
|
||||
import { Rating } from '/@/shared/components/rating/rating';
|
||||
import { Stack } from '/@/shared/components/stack/stack';
|
||||
import { Text } from '/@/shared/components/text/text';
|
||||
import { Tooltip } from '/@/shared/components/tooltip/tooltip';
|
||||
import { PlayerRepeat, PlayerStatus } from '/@/shared/types/types';
|
||||
|
||||
export const RemoteContainer = () => {
|
||||
@@ -36,107 +32,81 @@ export const RemoteContainer = () => {
|
||||
const debouncedSetRating = debounce(setRating, 400);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack
|
||||
gap="md"
|
||||
h="100dvh"
|
||||
w="100%"
|
||||
>
|
||||
{showImage && (
|
||||
<Flex
|
||||
align="center"
|
||||
justify="center"
|
||||
w="100%"
|
||||
>
|
||||
<PlayerImage src={song?.imageUrl} />
|
||||
</Flex>
|
||||
)}
|
||||
{id && (
|
||||
<>
|
||||
<Title order={1}>{song.name}</Title>
|
||||
<Group align="flex-end">
|
||||
<Title order={2}>Album: {song.album}</Title>
|
||||
<Title order={2}>Artist: {song.artistName}</Title>
|
||||
</Group>
|
||||
<Group position="apart">
|
||||
<Title order={3}>Duration: {formatDuration(song.duration)}</Title>
|
||||
<Stack gap="xs">
|
||||
<Text
|
||||
fw={700}
|
||||
size="xl"
|
||||
style={{
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
>
|
||||
{song.name}
|
||||
</Text>
|
||||
<Text
|
||||
isMuted
|
||||
style={{
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
>
|
||||
{song.album}
|
||||
</Text>
|
||||
<Text
|
||||
isMuted
|
||||
style={{
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
>
|
||||
{song.artistName}
|
||||
</Text>
|
||||
<Group justify="space-between">
|
||||
{song.releaseDate && (
|
||||
<Title order={3}>
|
||||
Released: {new Date(song.releaseDate).toLocaleDateString()}
|
||||
</Title>
|
||||
<Text isMuted>{new Date(song.releaseDate).toLocaleDateString()}</Text>
|
||||
)}
|
||||
<Title order={3}>Plays: {song.playCount}</Title>
|
||||
<Text isMuted>Plays: {song.playCount}</Text>
|
||||
</Group>
|
||||
</>
|
||||
</Stack>
|
||||
)}
|
||||
<Group
|
||||
gap={0}
|
||||
grow
|
||||
spacing={0}
|
||||
>
|
||||
<RemoteButton
|
||||
<ActionIcon
|
||||
disabled={!id}
|
||||
onClick={() => send({ event: 'previous' })}
|
||||
tooltip="Previous track"
|
||||
variant="default"
|
||||
>
|
||||
<RiSkipBackFill size={25} />
|
||||
</RemoteButton>
|
||||
<RemoteButton
|
||||
disabled={!id}
|
||||
onClick={() => {
|
||||
if (status === PlayerStatus.PLAYING) {
|
||||
send({ event: 'pause' });
|
||||
} else if (status === PlayerStatus.PAUSED) {
|
||||
send({ event: 'play' });
|
||||
}
|
||||
icon="favorite"
|
||||
iconProps={{
|
||||
fill: song?.userFavorite ? 'primary' : 'default',
|
||||
}}
|
||||
tooltip={id && status === PlayerStatus.PLAYING ? 'Pause' : 'Play'}
|
||||
variant="default"
|
||||
>
|
||||
{id && status === PlayerStatus.PLAYING ? (
|
||||
<RiPauseFill size={25} />
|
||||
) : (
|
||||
<RiPlayFill size={25} />
|
||||
)}
|
||||
</RemoteButton>
|
||||
<RemoteButton
|
||||
disabled={!id}
|
||||
onClick={() => send({ event: 'next' })}
|
||||
tooltip="Next track"
|
||||
variant="default"
|
||||
>
|
||||
<RiSkipForwardFill size={25} />
|
||||
</RemoteButton>
|
||||
</Group>
|
||||
<Group
|
||||
grow
|
||||
spacing={0}
|
||||
>
|
||||
<RemoteButton
|
||||
$active={shuffle || false}
|
||||
onClick={() => send({ event: 'shuffle' })}
|
||||
tooltip={shuffle ? 'Shuffle tracks' : 'Shuffle disabled'}
|
||||
variant="default"
|
||||
>
|
||||
<RiShuffleFill size={25} />
|
||||
</RemoteButton>
|
||||
<RemoteButton
|
||||
$active={repeat !== undefined && repeat !== PlayerRepeat.NONE}
|
||||
onClick={() => send({ event: 'repeat' })}
|
||||
tooltip={`Repeat ${
|
||||
repeat === PlayerRepeat.ONE
|
||||
? 'One'
|
||||
: repeat === PlayerRepeat.ALL
|
||||
? 'all'
|
||||
: 'none'
|
||||
}`}
|
||||
variant="default"
|
||||
>
|
||||
{repeat === undefined || repeat === PlayerRepeat.ONE ? (
|
||||
<RiRepeatOneLine size={25} />
|
||||
) : (
|
||||
<RiRepeat2Line size={25} />
|
||||
)}
|
||||
</RemoteButton>
|
||||
<RemoteButton
|
||||
$active={song?.userFavorite}
|
||||
disabled={!id}
|
||||
onClick={() => {
|
||||
if (!id) return;
|
||||
|
||||
send({ event: 'favorite', favorite: !song.userFavorite, id });
|
||||
}}
|
||||
tooltip={song?.userFavorite ? 'Unfavorite' : 'Favorite'}
|
||||
variant="default"
|
||||
>
|
||||
<RiHeartLine size={25} />
|
||||
</RemoteButton>
|
||||
tooltip={{
|
||||
label: song?.userFavorite ? 'Unfavorite' : 'Favorite',
|
||||
}}
|
||||
variant="transparent"
|
||||
/>
|
||||
{(song?.serverType === 'navidrome' || song?.serverType === 'subsonic') && (
|
||||
<div style={{ margin: 'auto' }}>
|
||||
<Tooltip
|
||||
@@ -146,43 +116,132 @@ export const RemoteContainer = () => {
|
||||
<Rating
|
||||
onChange={debouncedSetRating}
|
||||
onDoubleClick={() => debouncedSetRating(0)}
|
||||
sx={{ margin: 'auto' }}
|
||||
style={{ margin: 'auto' }}
|
||||
value={song.userRating ?? 0}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
</Group>
|
||||
{id && position !== undefined && (
|
||||
<WrapperSlider
|
||||
label={(value) => formatDuration(value * 1e3)}
|
||||
leftLabel={formatDuration(position * 1e3)}
|
||||
max={song.duration / 1e3}
|
||||
onChangeEnd={(e) => send({ event: 'position', position: e })}
|
||||
rightLabel={formatDuration(song.duration)}
|
||||
value={position}
|
||||
<Group
|
||||
gap="xs"
|
||||
grow
|
||||
>
|
||||
<ActionIcon
|
||||
disabled={!id}
|
||||
icon="mediaPrevious"
|
||||
iconProps={{
|
||||
fill: 'default',
|
||||
size: 'lg',
|
||||
}}
|
||||
onClick={() => send({ event: 'previous' })}
|
||||
tooltip={{
|
||||
label: 'Previous track',
|
||||
}}
|
||||
variant="default"
|
||||
/>
|
||||
)}
|
||||
<WrapperSlider
|
||||
leftLabel={<RiVolumeUpFill size={20} />}
|
||||
max={100}
|
||||
onChangeEnd={(e) => send({ event: 'volume', volume: e })}
|
||||
rightLabel={
|
||||
<Text
|
||||
size="xs"
|
||||
weight={600}
|
||||
>
|
||||
{volume ?? 0}
|
||||
</Text>
|
||||
}
|
||||
value={volume ?? 0}
|
||||
/>
|
||||
{showImage && (
|
||||
<Image
|
||||
onError={() => send({ event: 'proxy' })}
|
||||
src={song?.imageUrl?.replaceAll(/&(size|width|height=\d+)/g, '')}
|
||||
<ActionIcon
|
||||
disabled={!id}
|
||||
onClick={() => {
|
||||
if (status === PlayerStatus.PLAYING) {
|
||||
send({ event: 'pause' });
|
||||
} else if (status === PlayerStatus.PAUSED) {
|
||||
send({ event: 'play' });
|
||||
}
|
||||
}}
|
||||
tooltip={{
|
||||
label: id && status === PlayerStatus.PLAYING ? 'Pause' : 'Play',
|
||||
}}
|
||||
variant="default"
|
||||
>
|
||||
{id && status === PlayerStatus.PLAYING ? (
|
||||
<RiPauseFill size={25} />
|
||||
) : (
|
||||
<RiPlayFill size={25} />
|
||||
)}
|
||||
</ActionIcon>
|
||||
<ActionIcon
|
||||
disabled={!id}
|
||||
icon="mediaNext"
|
||||
iconProps={{
|
||||
fill: 'default',
|
||||
size: 'lg',
|
||||
}}
|
||||
onClick={() => send({ event: 'next' })}
|
||||
tooltip={{
|
||||
label: 'Next track',
|
||||
}}
|
||||
variant="default"
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
</Group>
|
||||
<Group
|
||||
gap="xs"
|
||||
grow
|
||||
>
|
||||
<ActionIcon
|
||||
icon="mediaShuffle"
|
||||
iconProps={{
|
||||
fill: shuffle ? 'primary' : 'default',
|
||||
size: 'lg',
|
||||
}}
|
||||
onClick={() => send({ event: 'shuffle' })}
|
||||
tooltip={{
|
||||
label: shuffle ? 'Shuffle tracks' : 'Shuffle disabled',
|
||||
}}
|
||||
variant="default"
|
||||
/>
|
||||
<ActionIcon
|
||||
icon={
|
||||
repeat === undefined || repeat === PlayerRepeat.ONE
|
||||
? 'mediaRepeatOne'
|
||||
: 'mediaRepeat'
|
||||
}
|
||||
iconProps={{
|
||||
fill:
|
||||
repeat !== undefined && repeat !== PlayerRepeat.NONE
|
||||
? 'primary'
|
||||
: 'default',
|
||||
size: 'lg',
|
||||
}}
|
||||
onClick={() => send({ event: 'repeat' })}
|
||||
tooltip={{
|
||||
label: `Repeat ${
|
||||
repeat === PlayerRepeat.ONE
|
||||
? 'One'
|
||||
: repeat === PlayerRepeat.ALL
|
||||
? 'all'
|
||||
: 'none'
|
||||
}`,
|
||||
}}
|
||||
variant="default"
|
||||
/>
|
||||
</Group>
|
||||
<Stack gap="lg">
|
||||
{id && position !== undefined && (
|
||||
<WrappedSlider
|
||||
label={(value) => formatDuration(value * 1e3)}
|
||||
leftLabel={formatDuration(position * 1e3)}
|
||||
max={song.duration / 1e3}
|
||||
onChangeEnd={(e) => send({ event: 'position', position: e })}
|
||||
rightLabel={formatDuration(song.duration)}
|
||||
value={position}
|
||||
/>
|
||||
)}
|
||||
<WrappedSlider
|
||||
leftLabel={<RiVolumeUpFill size={20} />}
|
||||
max={100}
|
||||
onChangeEnd={(e) => send({ event: 'volume', volume: e })}
|
||||
rightLabel={
|
||||
<Text
|
||||
fw={600}
|
||||
size="xs"
|
||||
>
|
||||
{volume ?? 0}
|
||||
</Text>
|
||||
}
|
||||
value={volume ?? 0}
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,76 +1,71 @@
|
||||
import {
|
||||
AppShell,
|
||||
Container,
|
||||
Flex,
|
||||
Grid,
|
||||
Header,
|
||||
Image,
|
||||
MediaQuery,
|
||||
Skeleton,
|
||||
Title,
|
||||
} from '@mantine/core';
|
||||
import { AppShell, Flex, Grid, Image } from '@mantine/core';
|
||||
|
||||
import { ImageButton } from '/@/remote/components/buttons/image-button';
|
||||
import { ReconnectButton } from '/@/remote/components/buttons/reconnect-button';
|
||||
import { ThemeButton } from '/@/remote/components/buttons/theme-button';
|
||||
import { RemoteContainer } from '/@/remote/components/remote-container';
|
||||
import { useConnected } from '/@/remote/store';
|
||||
import { Center } from '/@/shared/components/center/center';
|
||||
import { Group } from '/@/shared/components/group/group';
|
||||
import { Spinner } from '/@/shared/components/spinner/spinner';
|
||||
|
||||
export const Shell = () => {
|
||||
const connected = useConnected();
|
||||
|
||||
return (
|
||||
<AppShell
|
||||
header={
|
||||
<Header height={60}>
|
||||
<Grid>
|
||||
<Grid.Col span="auto">
|
||||
<div>
|
||||
<Image
|
||||
fit="contain"
|
||||
height={60}
|
||||
src="/favicon.ico"
|
||||
width={60}
|
||||
/>
|
||||
</div>
|
||||
</Grid.Col>
|
||||
<MediaQuery
|
||||
smallerThan="sm"
|
||||
styles={{ display: 'none' }}
|
||||
>
|
||||
<Grid.Col
|
||||
sm={6}
|
||||
xs={0}
|
||||
>
|
||||
<Title ta="center">Feishin Remote</Title>
|
||||
</Grid.Col>
|
||||
</MediaQuery>
|
||||
|
||||
<Grid.Col span="auto">
|
||||
<Flex
|
||||
direction="row"
|
||||
justify="right"
|
||||
>
|
||||
<ReconnectButton />
|
||||
<ImageButton />
|
||||
<ThemeButton />
|
||||
</Flex>
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
</Header>
|
||||
}
|
||||
h="100vh"
|
||||
padding="md"
|
||||
w="100vw"
|
||||
>
|
||||
<Container>
|
||||
<AppShell.Header style={{ background: 'var(--theme-colors-surface)' }}>
|
||||
<Grid
|
||||
px="md"
|
||||
py="sm"
|
||||
>
|
||||
<Grid.Col span={4}>
|
||||
<Flex
|
||||
align="center"
|
||||
direction="row"
|
||||
h="100%"
|
||||
justify="flex-start"
|
||||
style={{
|
||||
justifySelf: 'flex-start',
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
fit="contain"
|
||||
height={32}
|
||||
src="/favicon.ico"
|
||||
width={32}
|
||||
/>
|
||||
</Flex>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={8}>
|
||||
<Group
|
||||
gap="sm"
|
||||
justify="flex-end"
|
||||
wrap="nowrap"
|
||||
>
|
||||
<ReconnectButton />
|
||||
<ImageButton />
|
||||
<ThemeButton />
|
||||
</Group>
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
</AppShell.Header>
|
||||
<AppShell.Main pt="60px">
|
||||
{connected ? (
|
||||
<RemoteContainer />
|
||||
) : (
|
||||
<Skeleton
|
||||
height={300}
|
||||
width="100%"
|
||||
/>
|
||||
<Center
|
||||
h="100vh"
|
||||
w="100vw"
|
||||
>
|
||||
<Spinner />
|
||||
</Center>
|
||||
)}
|
||||
</Container>
|
||||
</AppShell.Main>
|
||||
</AppShell>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,40 +1,17 @@
|
||||
import { rem, Slider, SliderProps } from '@mantine/core';
|
||||
import { ReactNode, useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
const SliderContainer = styled.div`
|
||||
display: flex;
|
||||
width: 95%;
|
||||
height: 20px;
|
||||
margin: 10px 0;
|
||||
`;
|
||||
|
||||
const SliderValueWrapper = styled.div<{ $position: 'left' | 'right' }>`
|
||||
display: flex;
|
||||
flex: 1;
|
||||
align-self: flex-end;
|
||||
justify-content: center;
|
||||
max-width: 50px;
|
||||
`;
|
||||
|
||||
const SliderWrapper = styled.div`
|
||||
display: flex;
|
||||
flex: 6;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
`;
|
||||
import { Group } from '/@/shared/components/group/group';
|
||||
import { Text } from '/@/shared/components/text/text';
|
||||
|
||||
const PlayerbarSlider = ({ ...props }: SliderProps) => {
|
||||
return (
|
||||
<Slider
|
||||
styles={{
|
||||
bar: {
|
||||
backgroundColor: 'var(--playerbar-slider-track-progress-bg)',
|
||||
transition: 'background-color 0.2s ease',
|
||||
},
|
||||
label: {
|
||||
backgroundColor: 'var(--tooltip-bg)',
|
||||
color: 'var(--tooltip-fg)',
|
||||
fontSize: '1.1rem',
|
||||
fontWeight: 600,
|
||||
padding: '0 1rem',
|
||||
@@ -59,9 +36,9 @@ const PlayerbarSlider = ({ ...props }: SliderProps) => {
|
||||
},
|
||||
track: {
|
||||
'&::before': {
|
||||
backgroundColor: 'var(--playerbar-slider-track-bg)',
|
||||
right: 'calc(0.1rem * -1)',
|
||||
},
|
||||
height: '1rem',
|
||||
},
|
||||
}}
|
||||
{...props}
|
||||
@@ -79,31 +56,32 @@ export interface WrappedProps extends Omit<SliderProps, 'onChangeEnd'> {
|
||||
value: number;
|
||||
}
|
||||
|
||||
export const WrapperSlider = ({ leftLabel, rightLabel, value, ...props }: WrappedProps) => {
|
||||
export const WrappedSlider = ({ leftLabel, rightLabel, value, ...props }: WrappedProps) => {
|
||||
const [isSeeking, setIsSeeking] = useState(false);
|
||||
const [seek, setSeek] = useState(0);
|
||||
|
||||
return (
|
||||
<SliderContainer>
|
||||
{leftLabel && <SliderValueWrapper $position="left">{leftLabel}</SliderValueWrapper>}
|
||||
<SliderWrapper>
|
||||
<PlayerbarSlider
|
||||
{...props}
|
||||
min={0}
|
||||
onChange={(e) => {
|
||||
setIsSeeking(true);
|
||||
setSeek(e);
|
||||
}}
|
||||
onChangeEnd={(e) => {
|
||||
props.onChangeEnd(e);
|
||||
setIsSeeking(false);
|
||||
}}
|
||||
size={6}
|
||||
value={!isSeeking ? (value ?? 0) : seek}
|
||||
w="100%"
|
||||
/>
|
||||
</SliderWrapper>
|
||||
{rightLabel && <SliderValueWrapper $position="right">{rightLabel}</SliderValueWrapper>}
|
||||
</SliderContainer>
|
||||
<Group
|
||||
align="center"
|
||||
wrap="nowrap"
|
||||
>
|
||||
{leftLabel && <Text size="sm">{leftLabel}</Text>}
|
||||
<PlayerbarSlider
|
||||
{...props}
|
||||
min={0}
|
||||
onChange={(e) => {
|
||||
setIsSeeking(true);
|
||||
setSeek(e);
|
||||
}}
|
||||
onChangeEnd={(e) => {
|
||||
props.onChangeEnd(e);
|
||||
setIsSeeking(false);
|
||||
}}
|
||||
size={6}
|
||||
value={!isSeeking ? (value ?? 0) : seek}
|
||||
w="100%"
|
||||
/>
|
||||
{rightLabel && <Text size="sm">{rightLabel}</Text>}
|
||||
</Group>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -5,6 +5,9 @@
|
||||
<meta charset="utf-8" />
|
||||
<meta http-equiv="Content-Security-Policy" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
|
||||
<meta http-equiv="Pragma" content="no-cache" />
|
||||
<meta http-equiv="Expires" content="0" />
|
||||
<title>Feishin Remote</title>
|
||||
<link rel="manifest" href="manifest.json">
|
||||
<script>
|
||||
|
||||
+1
-10
@@ -1,4 +1,3 @@
|
||||
import { Notifications } from '@mantine/notifications';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
|
||||
import { App } from '/@/remote/app';
|
||||
@@ -6,12 +5,4 @@ import { App } from '/@/remote/app';
|
||||
const container = document.getElementById('root')! as HTMLElement;
|
||||
const root = createRoot(container);
|
||||
|
||||
root.render(
|
||||
<>
|
||||
<Notifications
|
||||
containerWidth="300px"
|
||||
position="bottom-center"
|
||||
/>
|
||||
<App />
|
||||
</>,
|
||||
);
|
||||
root.render(<App />);
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import type { NotificationProps as MantineNotificationProps } from '@mantine/notifications';
|
||||
|
||||
import { hideNotification, showNotification } from '@mantine/notifications';
|
||||
import merge from 'lodash/merge';
|
||||
import { create } from 'zustand';
|
||||
import { devtools, persist } from 'zustand/middleware';
|
||||
import { immer } from 'zustand/middleware/immer';
|
||||
import { createWithEqualityFn } from 'zustand/traditional';
|
||||
|
||||
import { toast } from '/@/shared/components/toast/toast';
|
||||
import { ClientEvent, ServerEvent, SongUpdateSocket } from '/@/shared/types/remote-types';
|
||||
|
||||
export interface SettingsSlice extends SettingsState {
|
||||
@@ -36,55 +34,7 @@ const initialState: SettingsState = {
|
||||
showImage: true,
|
||||
};
|
||||
|
||||
interface NotificationProps extends MantineNotificationProps {
|
||||
type?: 'error' | 'warning';
|
||||
}
|
||||
|
||||
const showToast = ({ type, ...props }: NotificationProps) => {
|
||||
const color = type === 'warning' ? 'var(--warning-color)' : 'var(--danger-color)';
|
||||
|
||||
const defaultTitle = type === 'warning' ? 'Warning' : 'Error';
|
||||
|
||||
const defaultDuration = type === 'error' ? 2000 : 1000;
|
||||
|
||||
return showNotification({
|
||||
autoClose: defaultDuration,
|
||||
styles: () => ({
|
||||
closeButton: {
|
||||
'&:hover': {
|
||||
background: 'transparent',
|
||||
},
|
||||
},
|
||||
description: {
|
||||
color: 'var(--toast-description-fg)',
|
||||
fontSize: '1rem',
|
||||
},
|
||||
loader: {
|
||||
margin: '1rem',
|
||||
},
|
||||
root: {
|
||||
'&::before': { backgroundColor: color },
|
||||
background: 'var(--toast-bg)',
|
||||
border: '2px solid var(--generic-border-color)',
|
||||
bottom: '90px',
|
||||
},
|
||||
title: {
|
||||
color: 'var(--toast-title-fg)',
|
||||
fontSize: '1.3rem',
|
||||
},
|
||||
}),
|
||||
title: defaultTitle,
|
||||
...props,
|
||||
});
|
||||
};
|
||||
|
||||
const toast = {
|
||||
error: (props: NotificationProps) => showToast({ type: 'error', ...props }),
|
||||
hide: hideNotification,
|
||||
warn: (props: NotificationProps) => showToast({ type: 'warning', ...props }),
|
||||
};
|
||||
|
||||
export const useRemoteStore = create<SettingsSlice>()(
|
||||
export const useRemoteStore = createWithEqualityFn<SettingsSlice>()(
|
||||
persist(
|
||||
devtools(
|
||||
immer((set, get) => ({
|
||||
|
||||
@@ -1,127 +0,0 @@
|
||||
@use '../../renderer/themes/default.scss';
|
||||
@use '../../renderer/themes/dark.scss';
|
||||
@use '../../renderer/themes/light.scss';
|
||||
@use '../../renderer/styles/ag-grid.scss';
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body,
|
||||
html {
|
||||
position: absolute;
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow-x: hidden;
|
||||
overflow-y: hidden;
|
||||
color: var(--content-text-color);
|
||||
background: var(--content-bg);
|
||||
font-family: var(--content-font-family);
|
||||
font-size: var(--root-font-size);
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 639px) {
|
||||
body,
|
||||
html {
|
||||
overflow-x: auto;
|
||||
}
|
||||
}
|
||||
|
||||
#app {
|
||||
height: inherit;
|
||||
}
|
||||
|
||||
*,
|
||||
*:before,
|
||||
*:after {
|
||||
box-sizing: border-box;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
||||
-webkit-text-size-adjust: none;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-corner {
|
||||
background: var(--scrollbar-track-bg);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: var(--scrollbar-track-bg);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: var(--scrollbar-thumb-bg);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--scrollbar-thumb-bg-hover);
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
button {
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
|
||||
.overlay-scrollbar {
|
||||
overflow-y: overlay !important;
|
||||
overflow-x: overlay !important;
|
||||
}
|
||||
|
||||
.hide-scrollbar {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: transparent transparent;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 1px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.hide-scrollbar::-webkit-scrollbar {
|
||||
display: none; /* Safari and Chrome */
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeOut {
|
||||
from {
|
||||
opacity: 1;
|
||||
}
|
||||
to {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.mantine-ScrollArea-thumb[data-state='visible'] {
|
||||
animation: fadeIn 0.3s forwards;
|
||||
}
|
||||
|
||||
.mantine-ScrollArea-scrollbar[data-state='hidden'] {
|
||||
animation: fadeOut 0.2s forwards;
|
||||
}
|
||||
@@ -2,8 +2,8 @@ import i18n from '/@/i18n/i18n';
|
||||
import { JellyfinController } from '/@/renderer/api/jellyfin/jellyfin-controller';
|
||||
import { NavidromeController } from '/@/renderer/api/navidrome/navidrome-controller';
|
||||
import { SubsonicController } from '/@/renderer/api/subsonic/subsonic-controller';
|
||||
import { toast } from '/@/renderer/components/toast/index';
|
||||
import { useAuthStore } from '/@/renderer/store';
|
||||
import { toast } from '/@/shared/components/toast/toast';
|
||||
import {
|
||||
AuthenticationResponse,
|
||||
ControllerEndpoint,
|
||||
|
||||
@@ -290,19 +290,32 @@ export const JellyfinController: ControllerEndpoint = {
|
||||
|
||||
const yearsFilter = yearsGroup.length ? yearsGroup.join(',') : undefined;
|
||||
|
||||
let artistQuery:
|
||||
| Omit<z.infer<typeof jfType._parameters.albumList>, 'IncludeItemTypes'>
|
||||
| undefined;
|
||||
|
||||
if (query.artistIds) {
|
||||
// Based mostly off of observation, this is the behavior I've seen:
|
||||
// ContributingArtistIds is the _closest_ to where the album is a compilation and the artist is involved
|
||||
// AlbumArtistIds is where the artist is an album artist
|
||||
// ArtistIds is all credits
|
||||
if (query.compilation) {
|
||||
artistQuery = {
|
||||
ContributingArtistIds: formatCommaDelimitedString(query.artistIds),
|
||||
};
|
||||
} else if (query.compilation === false) {
|
||||
artistQuery = { AlbumArtistIds: formatCommaDelimitedString(query.artistIds) };
|
||||
} else {
|
||||
artistQuery = { ArtistIds: formatCommaDelimitedString(query.artistIds) };
|
||||
}
|
||||
}
|
||||
|
||||
const res = await jfApiClient(apiClientProps).getAlbumList({
|
||||
params: {
|
||||
userId: apiClientProps.server?.userId,
|
||||
},
|
||||
query: {
|
||||
...(!query.compilation &&
|
||||
query.artistIds && {
|
||||
AlbumArtistIds: formatCommaDelimitedString(query.artistIds),
|
||||
}),
|
||||
...(query.compilation &&
|
||||
query.artistIds && {
|
||||
ContributingArtistIds: query.artistIds[0],
|
||||
}),
|
||||
...artistQuery,
|
||||
Fields: 'People, Tags',
|
||||
GenreIds: query.genres ? query.genres.join(',') : undefined,
|
||||
IncludeItemTypes: 'MusicAlbum',
|
||||
|
||||
@@ -7,10 +7,10 @@ import qs from 'qs';
|
||||
|
||||
import i18n from '/@/i18n/i18n';
|
||||
import { authenticationFailure } from '/@/renderer/api/utils';
|
||||
import { toast } from '/@/renderer/components';
|
||||
import { useAuthStore } from '/@/renderer/store';
|
||||
import { ndType } from '/@/shared/api/navidrome/navidrome-types';
|
||||
import { resultWithHeaders } from '/@/shared/api/utils';
|
||||
import { toast } from '/@/shared/components/toast/toast';
|
||||
import { ServerListItem } from '/@/shared/types/domain-types';
|
||||
|
||||
const localSettings = isElectron() ? window.api.localSettings : null;
|
||||
|
||||
@@ -5,8 +5,8 @@ import qs from 'qs';
|
||||
import { z } from 'zod';
|
||||
|
||||
import i18n from '/@/i18n/i18n';
|
||||
import { toast } from '/@/renderer/components/toast/index';
|
||||
import { ssType } from '/@/shared/api/subsonic/subsonic-types';
|
||||
import { toast } from '/@/shared/components/toast/toast';
|
||||
import { ServerListItem } from '/@/shared/types/domain-types';
|
||||
|
||||
const c = initContract();
|
||||
|
||||
@@ -1,12 +1,19 @@
|
||||
import type { ServerInferResponses } from '@ts-rest/core';
|
||||
|
||||
import dayjs from 'dayjs';
|
||||
import filter from 'lodash/filter';
|
||||
import orderBy from 'lodash/orderBy';
|
||||
import md5 from 'md5';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { ssApiClient } from '/@/renderer/api/subsonic/subsonic-api';
|
||||
import { contract, ssApiClient } from '/@/renderer/api/subsonic/subsonic-api';
|
||||
import { randomString } from '/@/renderer/utils';
|
||||
import { ssNormalize } from '/@/shared/api/subsonic/subsonic-normalize';
|
||||
import { AlbumListSortType, SubsonicExtensions } from '/@/shared/api/subsonic/subsonic-types';
|
||||
import {
|
||||
AlbumListSortType,
|
||||
ssType,
|
||||
SubsonicExtensions,
|
||||
} from '/@/shared/api/subsonic/subsonic-types';
|
||||
import {
|
||||
AlbumListSort,
|
||||
ControllerEndpoint,
|
||||
@@ -287,7 +294,7 @@ export const SubsonicController: ControllerEndpoint = {
|
||||
let type = ALBUM_LIST_SORT_MAPPING[query.sortBy] ?? AlbumListSortType.ALPHABETICAL_BY_NAME;
|
||||
|
||||
if (query.artistIds) {
|
||||
const promises: any[] = [];
|
||||
const promises: Promise<ServerInferResponses<typeof contract.getArtist>>[] = [];
|
||||
|
||||
for (const artistId of query.artistIds) {
|
||||
promises.push(
|
||||
@@ -309,8 +316,10 @@ export const SubsonicController: ControllerEndpoint = {
|
||||
return artist.body.artist.album ?? [];
|
||||
});
|
||||
|
||||
const items = albums.map((album) => ssNormalize.album(album, apiClientProps.server));
|
||||
|
||||
return {
|
||||
items: albums.map((album) => ssNormalize.album(album, apiClientProps.server)),
|
||||
items: sortAlbumList(items, query.sortBy, query.sortOrder),
|
||||
startIndex: 0,
|
||||
totalRecordCount: albums.length,
|
||||
};
|
||||
@@ -347,8 +356,8 @@ export const SubsonicController: ControllerEndpoint = {
|
||||
type = AlbumListSortType.BY_YEAR;
|
||||
}
|
||||
|
||||
let fromYear;
|
||||
let toYear;
|
||||
let fromYear: number | undefined;
|
||||
let toYear: number | undefined;
|
||||
|
||||
if (query.minYear) {
|
||||
fromYear = query.minYear;
|
||||
@@ -435,6 +444,33 @@ export const SubsonicController: ControllerEndpoint = {
|
||||
return totalRecordCount;
|
||||
}
|
||||
|
||||
if (query.artistIds) {
|
||||
const promises: Promise<ServerInferResponses<typeof contract.getArtist>>[] = [];
|
||||
|
||||
for (const artistId of query.artistIds) {
|
||||
promises.push(
|
||||
ssApiClient(apiClientProps).getArtist({
|
||||
query: {
|
||||
id: artistId,
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
const artistResult = await Promise.all(promises);
|
||||
|
||||
const albums = artistResult.reduce((total: number, artist) => {
|
||||
if (artist.status !== 200) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const length = artist.body.artist.album?.length ?? 0;
|
||||
return length + total;
|
||||
}, 0);
|
||||
|
||||
return albums;
|
||||
}
|
||||
|
||||
if (query.favorite) {
|
||||
const res = await ssApiClient(apiClientProps).getStarred({
|
||||
query: {
|
||||
@@ -463,8 +499,8 @@ export const SubsonicController: ControllerEndpoint = {
|
||||
type = AlbumListSortType.BY_YEAR;
|
||||
}
|
||||
|
||||
let fromYear;
|
||||
let toYear;
|
||||
let fromYear: number | undefined;
|
||||
let toYear: number | undefined;
|
||||
|
||||
if (query.minYear) {
|
||||
fromYear = query.minYear;
|
||||
@@ -858,9 +894,8 @@ export const SubsonicController: ControllerEndpoint = {
|
||||
return ssNormalize.song(res.body.song, apiClientProps.server);
|
||||
},
|
||||
getSongList: async ({ apiClientProps, query }) => {
|
||||
const fromAlbumPromises: any[] = [];
|
||||
const artistDetailPromises: any[] = [];
|
||||
let results: any[] = [];
|
||||
const fromAlbumPromises: Promise<ServerInferResponses<typeof contract.getAlbum>>[] = [];
|
||||
const artistDetailPromises: Promise<ServerInferResponses<typeof contract.getArtist>>[] = [];
|
||||
|
||||
if (query.searchTerm) {
|
||||
const res = await ssApiClient(apiClientProps).search3({
|
||||
@@ -984,6 +1019,8 @@ export const SubsonicController: ControllerEndpoint = {
|
||||
}
|
||||
}
|
||||
|
||||
let results: z.infer<typeof ssType._response.song>[] = [];
|
||||
|
||||
if (fromAlbumPromises) {
|
||||
const albumsResult = await Promise.all(fromAlbumPromises);
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { toast } from '/@/renderer/components';
|
||||
import { useAuthStore } from '/@/renderer/store';
|
||||
import { toast } from '/@/shared/components/toast/toast';
|
||||
import { ServerListItem } from '/@/shared/types/types';
|
||||
|
||||
export const authenticationFailure = (currentServer: null | ServerListItem) => {
|
||||
@@ -10,7 +10,7 @@ export const authenticationFailure = (currentServer: null | ServerListItem) => {
|
||||
if (currentServer) {
|
||||
const serverId = currentServer.id;
|
||||
const token = currentServer.ndCredential;
|
||||
console.log(`token is expired: ${token}`);
|
||||
console.error(`token is expired: ${token}`);
|
||||
useAuthStore.getState().actions.updateServer(serverId, { ndCredential: undefined });
|
||||
useAuthStore.getState().actions.setCurrentServer(null);
|
||||
}
|
||||
|
||||
+18
-109
@@ -2,17 +2,20 @@ import { ClientSideRowModelModule } from '@ag-grid-community/client-side-row-mod
|
||||
import { ModuleRegistry } from '@ag-grid-community/core';
|
||||
import { InfiniteRowModelModule } from '@ag-grid-community/infinite-row-model';
|
||||
import { MantineProvider } from '@mantine/core';
|
||||
import { Notifications } from '@mantine/notifications';
|
||||
import isElectron from 'is-electron';
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { initSimpleImg } from 'react-simple-img';
|
||||
import '@mantine/core/styles.css';
|
||||
import '@mantine/notifications/styles.css';
|
||||
import '@mantine/dates/styles.css';
|
||||
|
||||
import './styles/global.scss';
|
||||
import '/@/shared/styles/global.css';
|
||||
|
||||
import '@ag-grid-community/styles/ag-grid.css';
|
||||
import 'overlayscrollbars/overlayscrollbars.css';
|
||||
|
||||
import '/styles/overlayscrollbars.css';
|
||||
import i18n from '/@/i18n/i18n';
|
||||
import { toast } from '/@/renderer/components';
|
||||
import { ContextMenuProvider } from '/@/renderer/features/context-menu';
|
||||
import { useDiscordRpc } from '/@/renderer/features/discord-rpc/use-discord-rpc';
|
||||
import { PlayQueueHandlerContext } from '/@/renderer/features/player';
|
||||
@@ -20,7 +23,6 @@ import { WebAudioContext } from '/@/renderer/features/player/context/webaudio-co
|
||||
import { useHandlePlayQueueAdd } from '/@/renderer/features/player/hooks/use-handle-playqueue-add';
|
||||
import { updateSong } from '/@/renderer/features/player/update-remote-song';
|
||||
import { getMpvProperties } from '/@/renderer/features/settings/components/playback/mpv-settings';
|
||||
import { useTheme } from '/@/renderer/hooks';
|
||||
import { useServerVersion } from '/@/renderer/hooks/use-server-version';
|
||||
import { IsUpdatedDialog } from '/@/renderer/is-updated-dialog';
|
||||
import { AppRouter } from '/@/renderer/router/app-router';
|
||||
@@ -34,71 +36,33 @@ import {
|
||||
useRemoteSettings,
|
||||
useSettingsStore,
|
||||
} from '/@/renderer/store';
|
||||
import { useAppTheme } from '/@/renderer/themes/use-app-theme';
|
||||
import { sanitizeCss } from '/@/renderer/utils/sanitize';
|
||||
import { setQueue } from '/@/renderer/utils/set-transcoded-queue-data';
|
||||
import { FontType, PlaybackType, PlayerStatus, WebAudio } from '/@/shared/types/types';
|
||||
import { toast } from '/@/shared/components/toast/toast';
|
||||
import { PlaybackType, PlayerStatus, WebAudio } from '/@/shared/types/types';
|
||||
|
||||
ModuleRegistry.registerModules([ClientSideRowModelModule, InfiniteRowModelModule]);
|
||||
|
||||
initSimpleImg({ threshold: 0.05 }, true);
|
||||
|
||||
const mpvPlayer = isElectron() ? window.api.mpvPlayer : null;
|
||||
const ipc = isElectron() ? window.api.ipc : null;
|
||||
const remote = isElectron() ? window.api.remote : null;
|
||||
const utils = isElectron() ? window.api.utils : null;
|
||||
|
||||
export const App = () => {
|
||||
const theme = useTheme();
|
||||
const accent = useSettingsStore((store) => store.general.accent);
|
||||
const { mode, theme } = useAppTheme();
|
||||
const language = useSettingsStore((store) => store.general.language);
|
||||
const nativeImageAspect = useSettingsStore((store) => store.general.nativeAspectRatio);
|
||||
const { builtIn, custom, system, type } = useSettingsStore((state) => state.font);
|
||||
|
||||
const { content, enabled } = useCssSettings();
|
||||
const { type: playbackType } = usePlaybackSettings();
|
||||
const { bindings } = useHotkeySettings();
|
||||
const handlePlayQueueAdd = useHandlePlayQueueAdd();
|
||||
const { clearQueue, restoreQueue } = useQueueControls();
|
||||
const remoteSettings = useRemoteSettings();
|
||||
const textStyleRef = useRef<HTMLStyleElement | null>(null);
|
||||
const cssRef = useRef<HTMLStyleElement | null>(null);
|
||||
useDiscordRpc();
|
||||
useServerVersion();
|
||||
|
||||
useEffect(() => {
|
||||
if (type === FontType.SYSTEM && system) {
|
||||
const root = document.documentElement;
|
||||
root.style.setProperty('--content-font-family', 'dynamic-font');
|
||||
|
||||
if (!textStyleRef.current) {
|
||||
textStyleRef.current = document.createElement('style');
|
||||
document.body.appendChild(textStyleRef.current);
|
||||
}
|
||||
|
||||
textStyleRef.current.textContent = `
|
||||
@font-face {
|
||||
font-family: "dynamic-font";
|
||||
src: local("${system}");
|
||||
}`;
|
||||
} else if (type === FontType.CUSTOM && custom) {
|
||||
const root = document.documentElement;
|
||||
root.style.setProperty('--content-font-family', 'dynamic-font');
|
||||
|
||||
if (!textStyleRef.current) {
|
||||
textStyleRef.current = document.createElement('style');
|
||||
document.body.appendChild(textStyleRef.current);
|
||||
}
|
||||
|
||||
textStyleRef.current.textContent = `
|
||||
@font-face {
|
||||
font-family: "dynamic-font";
|
||||
src: url("feishin://${custom}");
|
||||
}`;
|
||||
} else {
|
||||
const root = document.documentElement;
|
||||
root.style.setProperty('--content-font-family', builtIn);
|
||||
}
|
||||
}, [builtIn, custom, system, type]);
|
||||
|
||||
const [webAudio, setWebAudio] = useState<WebAudio>();
|
||||
|
||||
useEffect(() => {
|
||||
@@ -121,16 +85,6 @@ export const App = () => {
|
||||
return () => {};
|
||||
}, [content, enabled]);
|
||||
|
||||
useEffect(() => {
|
||||
const root = document.documentElement;
|
||||
root.style.setProperty('--primary-color', accent);
|
||||
}, [accent]);
|
||||
|
||||
useEffect(() => {
|
||||
const root = document.documentElement;
|
||||
root.style.setProperty('--image-fit', nativeImageAspect ? 'contain' : 'cover');
|
||||
}, [nativeImageAspect]);
|
||||
|
||||
const providerValue = useMemo(() => {
|
||||
return { handlePlayQueueAdd };
|
||||
}, [handlePlayQueueAdd]);
|
||||
@@ -237,59 +191,14 @@ export const App = () => {
|
||||
|
||||
return (
|
||||
<MantineProvider
|
||||
theme={{
|
||||
colorScheme: theme as 'dark' | 'light',
|
||||
components: {
|
||||
Modal: {
|
||||
styles: {
|
||||
body: { background: 'var(--modal-bg)', padding: '1rem !important' },
|
||||
close: { marginRight: '0.5rem' },
|
||||
content: { borderRadius: '5px' },
|
||||
header: {
|
||||
background: 'var(--modal-header-bg)',
|
||||
paddingBottom: '1rem',
|
||||
},
|
||||
title: { fontSize: 'medium', fontWeight: 500 },
|
||||
},
|
||||
},
|
||||
},
|
||||
defaultRadius: 'xs',
|
||||
dir: 'ltr',
|
||||
focusRing: 'auto',
|
||||
focusRingStyles: {
|
||||
inputStyles: () => ({
|
||||
border: '1px solid var(--primary-color)',
|
||||
}),
|
||||
resetStyles: () => ({ outline: 'none' }),
|
||||
styles: () => ({
|
||||
outline: '1px solid var(--primary-color)',
|
||||
outlineOffset: '-1px',
|
||||
}),
|
||||
},
|
||||
fontFamily: 'var(--content-font-family)',
|
||||
fontSizes: {
|
||||
lg: '1.1rem',
|
||||
md: '1rem',
|
||||
sm: '0.9rem',
|
||||
xl: '1.5rem',
|
||||
xs: '0.8rem',
|
||||
},
|
||||
headings: {
|
||||
fontFamily: 'var(--content-font-family)',
|
||||
fontWeight: 700,
|
||||
},
|
||||
other: {},
|
||||
spacing: {
|
||||
lg: '2rem',
|
||||
md: '1rem',
|
||||
sm: '0.5rem',
|
||||
xl: '4rem',
|
||||
xs: '0rem',
|
||||
},
|
||||
}}
|
||||
withGlobalStyles
|
||||
withNormalizeCSS
|
||||
defaultColorScheme={mode as 'dark' | 'light'}
|
||||
theme={theme}
|
||||
>
|
||||
<Notifications
|
||||
containerWidth="300px"
|
||||
position="bottom-center"
|
||||
zIndex={5}
|
||||
/>
|
||||
<PlayQueueHandlerContext.Provider value={providerValue}>
|
||||
<ContextMenuProvider>
|
||||
<WebAudioContext.Provider value={webAudioProvider}>
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
import type { AccordionProps as MantineAccordionProps } from '@mantine/core';
|
||||
|
||||
import { Accordion as MantineAccordion } from '@mantine/core';
|
||||
import styled from 'styled-components';
|
||||
|
||||
type AccordionProps = MantineAccordionProps;
|
||||
|
||||
const StyledAccordion = styled(MantineAccordion)`
|
||||
& .mantine-Accordion-panel {
|
||||
background: var(--paper-bg);
|
||||
}
|
||||
|
||||
.mantine-Accordion-control {
|
||||
background: var(--paper-bg);
|
||||
}
|
||||
`;
|
||||
|
||||
export const Accordion = ({ children, ...props }: AccordionProps) => {
|
||||
return <StyledAccordion {...props}>{children}</StyledAccordion>;
|
||||
};
|
||||
|
||||
Accordion.Control = StyledAccordion.Control;
|
||||
Accordion.Item = StyledAccordion.Item;
|
||||
Accordion.Panel = StyledAccordion.Panel;
|
||||
@@ -19,10 +19,10 @@ import {
|
||||
crossfadeHandler,
|
||||
gaplessHandler,
|
||||
} from '/@/renderer/components/audio-player/utils/list-handlers';
|
||||
import { toast } from '/@/renderer/components/toast';
|
||||
import { useWebAudio } from '/@/renderer/features/player/hooks/use-webaudio';
|
||||
import { getServerById, TranscodingConfig, usePlaybackSettings, useSpeed } from '/@/renderer/store';
|
||||
import { useSettingsStore, useSettingsStoreActions } from '/@/renderer/store/settings.store';
|
||||
import { toast } from '/@/shared/components/toast/toast';
|
||||
import { PlaybackStyle, PlayerStatus } from '/@/shared/types/types';
|
||||
|
||||
export type AudioPlayerProgress = {
|
||||
@@ -120,6 +120,7 @@ export const AudioPlayer = forwardRef<AudioPlayerRef, AudioPlayerProps>((props,
|
||||
const audioDeviceId = useSettingsStore((state) => state.playback.audioDeviceId);
|
||||
const playback = useSettingsStore((state) => state.playback.mpvProperties);
|
||||
const shouldUseWebAudio = useSettingsStore((state) => state.playback.webAudio);
|
||||
const preservesPitch = useSettingsStore((state) => state.playback.preservePitch);
|
||||
const { resetSampleRate } = useSettingsStoreActions();
|
||||
const playbackSpeed = useSpeed();
|
||||
const { transcode } = usePlaybackSettings();
|
||||
@@ -230,21 +231,23 @@ export const AudioPlayer = forwardRef<AudioPlayerRef, AudioPlayerProps>((props,
|
||||
// calling play() is not necessarily a safe option (https://developer.chrome.com/blog/play-request-was-interrupted)
|
||||
// In practice, this failure is only likely to happen when using the 0-second wav:
|
||||
// play() + play() in rapid succession will cause problems as the frist one ends the track.
|
||||
player1Ref.current
|
||||
?.getInternalPlayer()
|
||||
?.play()
|
||||
.catch(() => {});
|
||||
const internalPlayer = player1Ref.current?.getInternalPlayer();
|
||||
if (internalPlayer) {
|
||||
internalPlayer.preservesPitch = preservesPitch;
|
||||
internalPlayer.play().catch(() => {});
|
||||
}
|
||||
} else {
|
||||
player2Ref.current
|
||||
?.getInternalPlayer()
|
||||
?.play()
|
||||
.catch(() => {});
|
||||
const internalPlayer = player2Ref.current?.getInternalPlayer();
|
||||
if (internalPlayer) {
|
||||
internalPlayer.preservesPitch = preservesPitch;
|
||||
internalPlayer.play().catch(() => {});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
player1Ref.current?.getInternalPlayer()?.pause();
|
||||
player2Ref.current?.getInternalPlayer()?.pause();
|
||||
}
|
||||
}, [currentPlayer, status]);
|
||||
}, [currentPlayer, status, preservesPitch]);
|
||||
|
||||
const handleCrossfade1 = useCallback(
|
||||
(e: AudioPlayerProgress) => {
|
||||
@@ -319,10 +322,8 @@ export const AudioPlayer = forwardRef<AudioPlayerRef, AudioPlayerProps>((props,
|
||||
if (isElectron() && webAudio && 'setSinkId' in webAudio.context && audioDeviceId) {
|
||||
const setSink = async () => {
|
||||
try {
|
||||
if (audioDeviceId !== 'default') {
|
||||
if (webAudio.context.state !== 'closed') {
|
||||
await (webAudio.context as any).setSinkId(audioDeviceId);
|
||||
} else {
|
||||
await (webAudio.context as any).setSinkId('');
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error({ message: `Error setting sink: ${(error as Error).message}` });
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
import type { BadgeProps as MantineBadgeProps } from '@mantine/core';
|
||||
|
||||
import { createPolymorphicComponent, Badge as MantineBadge } from '@mantine/core';
|
||||
import styled from 'styled-components';
|
||||
|
||||
export type BadgeProps = MantineBadgeProps;
|
||||
|
||||
const StyledBadge = styled(MantineBadge)<BadgeProps>`
|
||||
border-radius: var(--badge-radius);
|
||||
|
||||
.mantine-Badge-root {
|
||||
color: var(--badge-fg);
|
||||
}
|
||||
|
||||
.mantine-Badge-inner {
|
||||
color: var(--badge-fg);
|
||||
}
|
||||
`;
|
||||
|
||||
const _Badge = ({ children, ...props }: BadgeProps) => {
|
||||
return (
|
||||
<StyledBadge
|
||||
radius="md"
|
||||
size="sm"
|
||||
styles={{
|
||||
root: { background: 'var(--badge-bg)' },
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</StyledBadge>
|
||||
);
|
||||
};
|
||||
|
||||
export const Badge = createPolymorphicComponent<'button', BadgeProps>(_Badge);
|
||||
@@ -1,180 +0,0 @@
|
||||
import type { ButtonProps as MantineButtonProps, TooltipProps } from '@mantine/core';
|
||||
import type { Ref } from 'react';
|
||||
|
||||
import { createPolymorphicComponent, Button as MantineButton } from '@mantine/core';
|
||||
import { useTimeout } from '@mantine/hooks';
|
||||
import React, { forwardRef, useCallback, useRef, useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { Spinner } from '/@/renderer/components/spinner';
|
||||
import { Tooltip } from '/@/renderer/components/tooltip';
|
||||
|
||||
export interface ButtonProps extends MantineButtonProps {
|
||||
children: React.ReactNode;
|
||||
loading?: boolean;
|
||||
onClick?: (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
|
||||
onMouseDown?: (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
|
||||
tooltip?: Omit<TooltipProps, 'children'>;
|
||||
}
|
||||
|
||||
interface StyledButtonProps extends ButtonProps {
|
||||
ref: Ref<HTMLButtonElement>;
|
||||
}
|
||||
|
||||
const StyledButton = styled(MantineButton)<StyledButtonProps>`
|
||||
color: ${(props) => `var(--btn-${props.variant}-fg)`};
|
||||
background: ${(props) => `var(--btn-${props.variant}-bg)`};
|
||||
border: ${(props) => `var(--btn-${props.variant}-border)`};
|
||||
border-radius: ${(props) => `var(--btn-${props.variant}-radius)`};
|
||||
transition:
|
||||
background 0.2s ease-in-out,
|
||||
color 0.2s ease-in-out,
|
||||
border 0.2s ease-in-out;
|
||||
|
||||
svg {
|
||||
fill: ${(props) => `var(--btn-${props.variant}-fg)`};
|
||||
transition: fill 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
color: ${(props) => `var(--btn-${props.variant}-fg)`};
|
||||
background: ${(props) => `var(--btn-${props.variant}-bg)`};
|
||||
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
&:not([data-disabled])&:hover {
|
||||
color: ${(props) => `var(--btn-${props.variant}-fg) !important`};
|
||||
background: ${(props) => `var(--btn-${props.variant}-bg)`};
|
||||
filter: brightness(85%);
|
||||
border: ${(props) => `var(--btn-${props.variant}-border-hover)`};
|
||||
|
||||
svg {
|
||||
fill: ${(props) => `var(--btn-${props.variant}-fg-hover)`};
|
||||
}
|
||||
}
|
||||
|
||||
&:not([data-disabled])&:focus-visible {
|
||||
color: ${(props) => `var(--btn-${props.variant}-fg-hover)`};
|
||||
background: ${(props) => `var(--btn-${props.variant}-bg)`};
|
||||
filter: brightness(85%);
|
||||
}
|
||||
|
||||
& .mantine-Button-centerLoader {
|
||||
display: none;
|
||||
}
|
||||
|
||||
& .mantine-Button-leftIcon {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
.mantine-Button-rightIcon {
|
||||
display: flex;
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
`;
|
||||
|
||||
const ButtonChildWrapper = styled.span<{ $loading?: boolean }>`
|
||||
color: ${(props) => props.$loading && 'transparent !important'};
|
||||
`;
|
||||
|
||||
const SpinnerWrapper = styled.div`
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate3d(-50%, -50%, 0);
|
||||
`;
|
||||
|
||||
export const _Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ children, tooltip, ...props }: ButtonProps, ref) => {
|
||||
if (tooltip) {
|
||||
return (
|
||||
<Tooltip
|
||||
withinPortal
|
||||
{...tooltip}
|
||||
>
|
||||
<StyledButton
|
||||
loaderPosition="center"
|
||||
ref={ref}
|
||||
{...props}
|
||||
>
|
||||
<ButtonChildWrapper $loading={props.loading}>{children}</ButtonChildWrapper>
|
||||
{props.loading && (
|
||||
<SpinnerWrapper>
|
||||
<Spinner />
|
||||
</SpinnerWrapper>
|
||||
)}
|
||||
</StyledButton>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledButton
|
||||
loaderPosition="center"
|
||||
ref={ref}
|
||||
{...props}
|
||||
>
|
||||
<ButtonChildWrapper $loading={props.loading}>{children}</ButtonChildWrapper>
|
||||
{props.loading && (
|
||||
<SpinnerWrapper>
|
||||
<Spinner />
|
||||
</SpinnerWrapper>
|
||||
)}
|
||||
</StyledButton>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
export const Button = createPolymorphicComponent<'button', ButtonProps>(_Button);
|
||||
|
||||
interface HoldButtonProps extends ButtonProps {
|
||||
timeoutProps: {
|
||||
callback: () => void;
|
||||
duration: number;
|
||||
};
|
||||
}
|
||||
|
||||
export const TimeoutButton = ({ timeoutProps, ...props }: HoldButtonProps) => {
|
||||
const [, setTimeoutRemaining] = useState(timeoutProps.duration);
|
||||
const [isRunning, setIsRunning] = useState(false);
|
||||
const intervalRef = useRef(0);
|
||||
|
||||
const callback = () => {
|
||||
timeoutProps.callback();
|
||||
setTimeoutRemaining(timeoutProps.duration);
|
||||
clearInterval(intervalRef.current);
|
||||
setIsRunning(false);
|
||||
};
|
||||
|
||||
const { clear, start } = useTimeout(callback, timeoutProps.duration);
|
||||
|
||||
const startTimeout = useCallback(() => {
|
||||
if (isRunning) {
|
||||
clearInterval(intervalRef.current);
|
||||
setIsRunning(false);
|
||||
clear();
|
||||
} else {
|
||||
setIsRunning(true);
|
||||
start();
|
||||
|
||||
const intervalId = window.setInterval(() => {
|
||||
setTimeoutRemaining((prev) => prev - 100);
|
||||
}, 100);
|
||||
|
||||
intervalRef.current = intervalId;
|
||||
}
|
||||
}, [clear, isRunning, start]);
|
||||
|
||||
return (
|
||||
<Button
|
||||
onClick={startTimeout}
|
||||
sx={{ color: 'var(--danger-color)' }}
|
||||
{...props}
|
||||
>
|
||||
{isRunning ? 'Cancel' : props.children}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
@@ -1,223 +0,0 @@
|
||||
import type { CardRoute, CardRow, Play, PlayQueueAddOptions } from '/@/shared/types/types';
|
||||
|
||||
import { Center } from '@mantine/core';
|
||||
import { useCallback } from 'react';
|
||||
import { RiAlbumFill } from 'react-icons/ri';
|
||||
import { generatePath, useNavigate } from 'react-router';
|
||||
import { SimpleImg } from 'react-simple-img';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { CardControls } from '/@/renderer/components/card/card-controls';
|
||||
import { CardRows } from '/@/renderer/components/card/card-rows';
|
||||
import { Skeleton } from '/@/renderer/components/skeleton';
|
||||
import { Album, AlbumArtist, Artist, LibraryItem } from '/@/shared/types/domain-types';
|
||||
|
||||
const CardWrapper = styled.div<{
|
||||
link?: boolean;
|
||||
}>`
|
||||
padding: 1rem;
|
||||
cursor: ${({ link }) => link && 'pointer'};
|
||||
background: var(--card-default-bg);
|
||||
border-radius: var(--card-default-radius);
|
||||
transition:
|
||||
border 0.2s ease-in-out,
|
||||
background 0.2s ease-in-out;
|
||||
|
||||
&:hover {
|
||||
background: var(--card-default-bg-hover);
|
||||
}
|
||||
|
||||
&:hover div {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&:hover * {
|
||||
&::before {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 1px solid #fff;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledCard = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 0;
|
||||
border-radius: var(--card-default-radius);
|
||||
`;
|
||||
|
||||
const ImageSection = styled.div`
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
border-radius: var(--card-default-radius);
|
||||
|
||||
&::before {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 1;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
content: '';
|
||||
user-select: none;
|
||||
background: linear-gradient(0deg, rgb(0 0 0 / 100%) 35%, rgb(0 0 0 / 0%) 100%);
|
||||
opacity: 0;
|
||||
transition: all 0.2s ease-in-out;
|
||||
}
|
||||
`;
|
||||
|
||||
const Image = styled(SimpleImg)`
|
||||
border-radius: var(--card-default-radius);
|
||||
box-shadow: 2px 2px 10px 2px rgb(0 0 0 / 20%);
|
||||
`;
|
||||
|
||||
const ControlsContainer = styled.div`
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
z-index: 50;
|
||||
width: 100%;
|
||||
opacity: 0;
|
||||
transition: all 0.2s ease-in-out;
|
||||
`;
|
||||
|
||||
const DetailSection = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
`;
|
||||
|
||||
const Row = styled.div<{ $secondary?: boolean }>`
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
height: 22px;
|
||||
padding: 0 0.2rem;
|
||||
overflow: hidden;
|
||||
color: ${({ $secondary }) => ($secondary ? 'var(--main-fg-secondary)' : 'var(--main-fg)')};
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
user-select: none;
|
||||
`;
|
||||
|
||||
interface BaseGridCardProps {
|
||||
controls: {
|
||||
cardRows: CardRow<Album | AlbumArtist | Artist>[];
|
||||
itemType: LibraryItem;
|
||||
playButtonBehavior: Play;
|
||||
route: CardRoute;
|
||||
};
|
||||
data: any;
|
||||
handlePlayQueueAdd?: (options: PlayQueueAddOptions) => void;
|
||||
loading?: boolean;
|
||||
size: number;
|
||||
}
|
||||
|
||||
export const AlbumCard = ({
|
||||
controls,
|
||||
data,
|
||||
handlePlayQueueAdd,
|
||||
loading,
|
||||
size,
|
||||
}: BaseGridCardProps) => {
|
||||
const navigate = useNavigate();
|
||||
const { cardRows, itemType, route } = controls;
|
||||
|
||||
const handleNavigate = useCallback(() => {
|
||||
navigate(
|
||||
generatePath(
|
||||
route.route as string,
|
||||
route.slugs?.reduce((acc, slug) => {
|
||||
return {
|
||||
...acc,
|
||||
[slug.slugProperty]: data[slug.idProperty],
|
||||
};
|
||||
}, {}),
|
||||
),
|
||||
);
|
||||
}, [data, navigate, route.route, route.slugs]);
|
||||
|
||||
if (!loading) {
|
||||
return (
|
||||
<CardWrapper
|
||||
link
|
||||
onClick={handleNavigate}
|
||||
>
|
||||
<StyledCard>
|
||||
<ImageSection>
|
||||
{data?.imageUrl ? (
|
||||
<Image
|
||||
animationDuration={0.3}
|
||||
height={size}
|
||||
imgStyle={{ objectFit: 'cover' }}
|
||||
placeholder={data?.imagePlaceholderUrl || 'var(--card-default-bg)'}
|
||||
src={data?.imageUrl}
|
||||
width={size}
|
||||
/>
|
||||
) : (
|
||||
<Center
|
||||
sx={{
|
||||
background: 'var(--placeholder-bg)',
|
||||
borderRadius: 'var(--card-default-radius)',
|
||||
height: `${size}px`,
|
||||
width: `${size}px`,
|
||||
}}
|
||||
>
|
||||
<RiAlbumFill
|
||||
color="var(--placeholder-fg)"
|
||||
size={35}
|
||||
/>
|
||||
</Center>
|
||||
)}
|
||||
<ControlsContainer>
|
||||
<CardControls
|
||||
handlePlayQueueAdd={handlePlayQueueAdd}
|
||||
itemData={data}
|
||||
itemType={itemType}
|
||||
/>
|
||||
</ControlsContainer>
|
||||
</ImageSection>
|
||||
<DetailSection>
|
||||
<CardRows
|
||||
data={data}
|
||||
rows={cardRows}
|
||||
/>
|
||||
</DetailSection>
|
||||
</StyledCard>
|
||||
</CardWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<CardWrapper>
|
||||
<StyledCard style={{ alignItems: 'center', display: 'flex' }}>
|
||||
<Skeleton
|
||||
height={size}
|
||||
radius="sm"
|
||||
visible
|
||||
width={size}
|
||||
>
|
||||
<ImageSection />
|
||||
</Skeleton>
|
||||
<DetailSection style={{ width: '100%' }}>
|
||||
{(cardRows || []).map((_row: CardRow<Album>, index: number) => (
|
||||
<Skeleton
|
||||
height={15}
|
||||
key={`skeleton-${data?.id}-${index}`}
|
||||
my={3}
|
||||
radius="md"
|
||||
visible
|
||||
width={!data ? (index > 0 ? '50%' : '90%') : '100%'}
|
||||
>
|
||||
<Row />
|
||||
</Skeleton>
|
||||
))}
|
||||
</DetailSection>
|
||||
</StyledCard>
|
||||
</CardWrapper>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,71 @@
|
||||
.play-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
background-color: rgb(255 255 255);
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
opacity: 0.8;
|
||||
transition: opacity 0.2s ease-in-out;
|
||||
transition: scale 0.2s linear;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
scale: 1.1;
|
||||
}
|
||||
|
||||
&:active {
|
||||
opacity: 1;
|
||||
scale: 1;
|
||||
}
|
||||
|
||||
svg {
|
||||
fill: rgb(0 0 0);
|
||||
stroke: rgb(0 0 0);
|
||||
}
|
||||
}
|
||||
|
||||
.secondary-button {
|
||||
opacity: 0.8;
|
||||
transition: opacity 0.2s ease-in-out;
|
||||
transition: scale 0.2s linear;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
scale: 1.1;
|
||||
}
|
||||
|
||||
&:active {
|
||||
opacity: 1;
|
||||
scale: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.grid-card-controls-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.controls-row {
|
||||
width: 100%;
|
||||
height: calc(100% / 3);
|
||||
}
|
||||
|
||||
.bottom-controls {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: space-between;
|
||||
padding: 1rem 0.5rem;
|
||||
}
|
||||
|
||||
.favorite-wrapper {
|
||||
svg {
|
||||
fill: var(--theme-colors-primary-filled);
|
||||
}
|
||||
}
|
||||
@@ -1,110 +1,21 @@
|
||||
import type { PlayQueueAddOptions } from '/@/shared/types/types';
|
||||
import type { UnstyledButtonProps } from '@mantine/core';
|
||||
import type { MouseEvent } from 'react';
|
||||
|
||||
import { Group } from '@mantine/core';
|
||||
import React from 'react';
|
||||
import { RiHeartFill, RiHeartLine, RiMore2Fill, RiPlayFill } from 'react-icons/ri';
|
||||
import styled from 'styled-components';
|
||||
import styles from './card-controls.module.css';
|
||||
|
||||
import { _Button } from '/@/renderer/components/button';
|
||||
import {
|
||||
ALBUM_CONTEXT_MENU_ITEMS,
|
||||
ARTIST_CONTEXT_MENU_ITEMS,
|
||||
} from '/@/renderer/features/context-menu/context-menu-items';
|
||||
import { useHandleGeneralContextMenu } from '/@/renderer/features/context-menu/hooks/use-handle-context-menu';
|
||||
import { usePlayButtonBehavior } from '/@/renderer/store/settings.store';
|
||||
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
|
||||
import { Button } from '/@/shared/components/button/button';
|
||||
import { Group } from '/@/shared/components/group/group';
|
||||
import { Icon } from '/@/shared/components/icon/icon';
|
||||
import { LibraryItem } from '/@/shared/types/domain-types';
|
||||
import { Play } from '/@/shared/types/types';
|
||||
|
||||
type PlayButtonType = React.ComponentPropsWithoutRef<'button'> & UnstyledButtonProps;
|
||||
|
||||
const PlayButton = styled.button<PlayButtonType>`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
background-color: rgb(255 255 255);
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
opacity: 0.8;
|
||||
transition: opacity 0.2s ease-in-out;
|
||||
transition: scale 0.2s linear;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
scale: 1.1;
|
||||
}
|
||||
|
||||
&:active {
|
||||
opacity: 1;
|
||||
scale: 1;
|
||||
}
|
||||
|
||||
svg {
|
||||
fill: rgb(0 0 0);
|
||||
stroke: rgb(0 0 0);
|
||||
}
|
||||
`;
|
||||
|
||||
const SecondaryButton = styled(_Button)`
|
||||
opacity: 0.8;
|
||||
transition: opacity 0.2s ease-in-out;
|
||||
transition: scale 0.2s linear;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
scale: 1.1;
|
||||
}
|
||||
|
||||
&:active {
|
||||
opacity: 1;
|
||||
scale: 1;
|
||||
}
|
||||
`;
|
||||
|
||||
const GridCardControlsContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
`;
|
||||
|
||||
const ControlsRow = styled.div`
|
||||
width: 100%;
|
||||
height: calc(100% / 3);
|
||||
`;
|
||||
|
||||
// const TopControls = styled(ControlsRow)`
|
||||
// display: flex;
|
||||
// align-items: flex-start;
|
||||
// justify-content: space-between;
|
||||
// padding: 0.5rem;
|
||||
// `;
|
||||
|
||||
// const CenterControls = styled(ControlsRow)`
|
||||
// display: flex;
|
||||
// align-items: center;
|
||||
// justify-content: center;
|
||||
// padding: 0.5rem;
|
||||
// `;
|
||||
|
||||
const BottomControls = styled(ControlsRow)`
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: space-between;
|
||||
padding: 1rem 0.5rem;
|
||||
`;
|
||||
|
||||
const FavoriteWrapper = styled.span<{ isFavorite: boolean }>`
|
||||
svg {
|
||||
fill: ${(props) => props.isFavorite && 'var(--primary-color)'};
|
||||
}
|
||||
`;
|
||||
|
||||
export const CardControls = ({
|
||||
handlePlayQueueAdd,
|
||||
itemData,
|
||||
@@ -134,46 +45,45 @@ export const CardControls = ({
|
||||
);
|
||||
|
||||
return (
|
||||
<GridCardControlsContainer>
|
||||
<BottomControls>
|
||||
<PlayButton onClick={handlePlay}>
|
||||
<RiPlayFill size={25} />
|
||||
</PlayButton>
|
||||
<Group spacing="xs">
|
||||
<SecondaryButton
|
||||
<div className={styles.gridCardControlsContainer}>
|
||||
<div className={styles.bottomControls}>
|
||||
<button
|
||||
className={styles.playButton}
|
||||
onClick={handlePlay}
|
||||
>
|
||||
<Icon icon="mediaPlay" />
|
||||
</button>
|
||||
<Group gap="xs">
|
||||
<Button
|
||||
className={styles.secondaryButton}
|
||||
disabled
|
||||
p={5}
|
||||
sx={{ svg: { fill: 'white !important' } }}
|
||||
style={{ svg: { fill: 'white !important' } }}
|
||||
variant="subtle"
|
||||
>
|
||||
<FavoriteWrapper isFavorite={itemData?.isFavorite}>
|
||||
<div className={itemData?.isFavorite ? styles.favoriteWrapper : ''}>
|
||||
{itemData?.isFavorite ? (
|
||||
<RiHeartFill size={20} />
|
||||
<Icon icon="favorite" />
|
||||
) : (
|
||||
<RiHeartLine
|
||||
color="white"
|
||||
size={20}
|
||||
/>
|
||||
<Icon icon="favorite" />
|
||||
)}
|
||||
</FavoriteWrapper>
|
||||
</SecondaryButton>
|
||||
<SecondaryButton
|
||||
onClick={(e) => {
|
||||
</div>
|
||||
</Button>
|
||||
<ActionIcon
|
||||
className={styles.secondaryButton}
|
||||
onClick={(e: any) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
handleContextMenu(e, [itemData]);
|
||||
}}
|
||||
p={5}
|
||||
sx={{ svg: { fill: 'white !important' } }}
|
||||
style={{ svg: { fill: 'white !important' } }}
|
||||
variant="subtle"
|
||||
>
|
||||
<RiMore2Fill
|
||||
color="white"
|
||||
size={20}
|
||||
/>
|
||||
</SecondaryButton>
|
||||
<Icon icon="ellipsisHorizontal" />
|
||||
</ActionIcon>
|
||||
</Group>
|
||||
</BottomControls>
|
||||
</GridCardControlsContainer>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
.row {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
height: 22px;
|
||||
padding: 0 0.2rem;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
color: var(--theme-colors-foreground);
|
||||
white-space: nowrap;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.row.secondary {
|
||||
color: var(--theme-colors-foreground-muted);
|
||||
}
|
||||
@@ -1,27 +1,17 @@
|
||||
import clsx from 'clsx';
|
||||
import formatDuration from 'format-duration';
|
||||
import React from 'react';
|
||||
import { generatePath } from 'react-router';
|
||||
import { Link } from 'react-router-dom';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { Text } from '/@/renderer/components/text';
|
||||
import styles from './card-rows.module.css';
|
||||
|
||||
import { AppRoute } from '/@/renderer/router/routes';
|
||||
import { formatDateAbsolute, formatDateRelative, formatRating } from '/@/renderer/utils/format';
|
||||
import { Text } from '/@/shared/components/text/text';
|
||||
import { Album, AlbumArtist, Artist, Playlist, Song } from '/@/shared/types/domain-types';
|
||||
import { CardRow } from '/@/shared/types/types';
|
||||
|
||||
const Row = styled.div<{ $secondary?: boolean }>`
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
height: 22px;
|
||||
padding: 0 0.2rem;
|
||||
overflow: hidden;
|
||||
color: ${({ $secondary }) => ($secondary ? 'var(--main-fg-secondary)' : 'var(--main-fg)')};
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
user-select: none;
|
||||
`;
|
||||
|
||||
interface CardRowsProps {
|
||||
data: any;
|
||||
rows: CardRow<Album>[] | CardRow<AlbumArtist>[] | CardRow<Artist>[];
|
||||
@@ -33,17 +23,19 @@ export const CardRows = ({ data, rows }: CardRowsProps) => {
|
||||
{rows.map((row, index: number) => {
|
||||
if (row.arrayProperty && row.route) {
|
||||
return (
|
||||
<Row
|
||||
$secondary={index > 0}
|
||||
<div
|
||||
className={clsx(styles.row, {
|
||||
[styles.secondary]: index > 0,
|
||||
})}
|
||||
key={`row-${row.property}-${index}`}
|
||||
>
|
||||
{data[row.property].map((item: any, itemIndex: number) => (
|
||||
<React.Fragment key={`${data.id}-${item.id}`}>
|
||||
{itemIndex > 0 && (
|
||||
<Text
|
||||
$noSelect
|
||||
$secondary
|
||||
sx={{
|
||||
isMuted
|
||||
isNoSelect
|
||||
style={{
|
||||
display: 'inline-block',
|
||||
padding: '0 2px 0 1px',
|
||||
}}
|
||||
@@ -52,10 +44,10 @@ export const CardRows = ({ data, rows }: CardRowsProps) => {
|
||||
</Text>
|
||||
)}{' '}
|
||||
<Text
|
||||
$link
|
||||
$noSelect
|
||||
$secondary={index > 0}
|
||||
component={Link}
|
||||
isLink
|
||||
isMuted={index > 0}
|
||||
isNoSelect
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
overflow="hidden"
|
||||
size={index > 0 ? 'sm' : 'md'}
|
||||
@@ -79,17 +71,22 @@ export const CardRows = ({ data, rows }: CardRowsProps) => {
|
||||
</Text>
|
||||
</React.Fragment>
|
||||
))}
|
||||
</Row>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (row.arrayProperty) {
|
||||
return (
|
||||
<Row key={`row-${row.property}`}>
|
||||
<div
|
||||
className={clsx(styles.row, {
|
||||
[styles.secondary]: index > 0,
|
||||
})}
|
||||
key={`row-${row.property}`}
|
||||
>
|
||||
{data[row.property].map((item: any) => (
|
||||
<Text
|
||||
$noSelect
|
||||
$secondary={index > 0}
|
||||
isMuted={index > 0}
|
||||
isNoSelect
|
||||
key={`${data.id}-${item.id}`}
|
||||
overflow="hidden"
|
||||
size={index > 0 ? 'sm' : 'md'}
|
||||
@@ -98,17 +95,22 @@ export const CardRows = ({ data, rows }: CardRowsProps) => {
|
||||
(row.format ? row.format(item) : item[row.arrayProperty])}
|
||||
</Text>
|
||||
))}
|
||||
</Row>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Row key={`row-${row.property}`}>
|
||||
<div
|
||||
className={clsx(styles.row, {
|
||||
[styles.secondary]: index > 0,
|
||||
})}
|
||||
key={`row-${row.property}`}
|
||||
>
|
||||
{row.route ? (
|
||||
<Text
|
||||
$link
|
||||
$noSelect
|
||||
component={Link}
|
||||
isLink
|
||||
isNoSelect
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
overflow="hidden"
|
||||
to={generatePath(
|
||||
@@ -125,15 +127,15 @@ export const CardRows = ({ data, rows }: CardRowsProps) => {
|
||||
</Text>
|
||||
) : (
|
||||
<Text
|
||||
$noSelect
|
||||
$secondary={index > 0}
|
||||
isMuted={index > 0}
|
||||
isNoSelect
|
||||
overflow="hidden"
|
||||
size={index > 0 ? 'sm' : 'md'}
|
||||
>
|
||||
{data && (row.format ? row.format(data) : data[row.property])}
|
||||
</Text>
|
||||
)}
|
||||
</Row>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
export * from './album-card';
|
||||
export * from './card-rows';
|
||||
@@ -0,0 +1,67 @@
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
pointer-events: auto;
|
||||
|
||||
&:global(.card-controls) {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.container.hidden {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.image-container {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
aspect-ratio: 1/1;
|
||||
overflow: hidden;
|
||||
background: var(--theme-card-default-bg);
|
||||
border-radius: var(--theme-card-poster-radius);
|
||||
|
||||
&::before {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 1;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
user-select: none;
|
||||
content: '';
|
||||
background: linear-gradient(0deg, rgb(0 0 0 / 100%) 35%, rgb(0 0 0 / 0%) 100%);
|
||||
opacity: 0;
|
||||
transition: all 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
&::before {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover:global(.card-controls) {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.image {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
height: 100% !important;
|
||||
max-height: 100%;
|
||||
border: 0;
|
||||
|
||||
img {
|
||||
height: 100%;
|
||||
object-fit: var(--theme-image-fit);
|
||||
}
|
||||
}
|
||||
|
||||
.detail-container {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
@@ -1,12 +1,13 @@
|
||||
import { Center, Stack } from '@mantine/core';
|
||||
import { RiAlbumFill, RiPlayListFill, RiUserVoiceFill } from 'react-icons/ri';
|
||||
import { useState } from 'react';
|
||||
import { generatePath, Link } from 'react-router-dom';
|
||||
import { SimpleImg } from 'react-simple-img';
|
||||
import styled, { css } from 'styled-components';
|
||||
|
||||
import { CardRows } from '/@/renderer/components/card';
|
||||
import { Skeleton } from '/@/renderer/components/skeleton';
|
||||
import styles from './poster-card.module.css';
|
||||
|
||||
import { CardRows } from '/@/renderer/components/card/card-rows';
|
||||
import { GridCardControls } from '/@/renderer/components/virtual-grid/grid-card/grid-card-controls';
|
||||
import { Image } from '/@/shared/components/image/image';
|
||||
import { Skeleton } from '/@/shared/components/skeleton/skeleton';
|
||||
import { Stack } from '/@/shared/components/stack/stack';
|
||||
import { Album, AlbumArtist, Artist, LibraryItem } from '/@/shared/types/domain-types';
|
||||
import { CardRoute, CardRow, Play, PlayQueueAddOptions } from '/@/shared/types/types';
|
||||
|
||||
@@ -28,85 +29,14 @@ interface BaseGridCardProps {
|
||||
isLoading?: boolean;
|
||||
}
|
||||
|
||||
const PosterCardContainer = styled.div<{ $isHidden?: boolean }>`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
pointer-events: auto;
|
||||
opacity: ${({ $isHidden }) => ($isHidden ? 0 : 1)};
|
||||
|
||||
.card-controls {
|
||||
opacity: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
const ImageContainerStyles = css`
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
aspect-ratio: 1/1;
|
||||
overflow: hidden;
|
||||
background: var(--card-default-bg);
|
||||
border-radius: var(--card-poster-radius);
|
||||
|
||||
&::before {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 1;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
content: '';
|
||||
user-select: none;
|
||||
background: linear-gradient(0deg, rgb(0 0 0 / 100%) 35%, rgb(0 0 0 / 0%) 100%);
|
||||
opacity: 0;
|
||||
transition: all 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
&::before {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover .card-controls {
|
||||
opacity: 1;
|
||||
}
|
||||
`;
|
||||
|
||||
const ImageContainer = styled(Link)<{ $isFavorite?: boolean }>`
|
||||
${ImageContainerStyles}
|
||||
`;
|
||||
|
||||
const ImageContainerSkeleton = styled.div`
|
||||
${ImageContainerStyles}
|
||||
`;
|
||||
|
||||
const Image = styled(SimpleImg)`
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
height: 100% !important;
|
||||
max-height: 100%;
|
||||
border: 0;
|
||||
|
||||
img {
|
||||
height: 100%;
|
||||
object-fit: var(--image-fit);
|
||||
}
|
||||
`;
|
||||
|
||||
const DetailContainer = styled.div`
|
||||
margin-top: 0.5rem;
|
||||
`;
|
||||
|
||||
export const PosterCard = ({
|
||||
controls,
|
||||
data,
|
||||
isLoading,
|
||||
uniqueId,
|
||||
}: BaseGridCardProps & { uniqueId: string }) => {
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
|
||||
if (!isLoading) {
|
||||
const path = generatePath(
|
||||
controls.route.route as string,
|
||||
@@ -118,90 +48,57 @@ export const PosterCard = ({
|
||||
}, {}),
|
||||
);
|
||||
|
||||
let Placeholder = RiAlbumFill;
|
||||
|
||||
switch (controls.itemType) {
|
||||
case LibraryItem.ALBUM:
|
||||
Placeholder = RiAlbumFill;
|
||||
break;
|
||||
case LibraryItem.ALBUM_ARTIST:
|
||||
Placeholder = RiUserVoiceFill;
|
||||
break;
|
||||
case LibraryItem.ARTIST:
|
||||
Placeholder = RiUserVoiceFill;
|
||||
break;
|
||||
case LibraryItem.PLAYLIST:
|
||||
Placeholder = RiPlayListFill;
|
||||
break;
|
||||
default:
|
||||
Placeholder = RiAlbumFill;
|
||||
break;
|
||||
}
|
||||
|
||||
return (
|
||||
<PosterCardContainer key={`${uniqueId}-${data.id}`}>
|
||||
<ImageContainer
|
||||
$isFavorite={data?.userFavorite}
|
||||
<div
|
||||
className={styles.container}
|
||||
key={`${uniqueId}-${data.id}`}
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
>
|
||||
<Link
|
||||
className={styles.imageContainer}
|
||||
to={path}
|
||||
>
|
||||
{data?.imageUrl ? (
|
||||
<Image
|
||||
importance="auto"
|
||||
placeholder={data?.imagePlaceholderUrl || 'var(--card-default-bg)'}
|
||||
src={data?.imageUrl}
|
||||
/>
|
||||
) : (
|
||||
<Center
|
||||
sx={{
|
||||
background: 'var(--placeholder-bg)',
|
||||
borderRadius: 'var(--card-default-radius)',
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
<Placeholder
|
||||
color="var(--placeholder-fg)"
|
||||
size={35}
|
||||
/>
|
||||
</Center>
|
||||
)}
|
||||
<Image
|
||||
className={styles.image}
|
||||
src={data?.imageUrl}
|
||||
/>
|
||||
<GridCardControls
|
||||
handleFavorite={controls.handleFavorite}
|
||||
handlePlayQueueAdd={controls.handlePlayQueueAdd}
|
||||
isHovered={isHovered}
|
||||
itemData={data}
|
||||
itemType={controls.itemType}
|
||||
/>
|
||||
</ImageContainer>
|
||||
<DetailContainer>
|
||||
</Link>
|
||||
<div className={styles.detailContainer}>
|
||||
<CardRows
|
||||
data={data}
|
||||
rows={controls.cardRows}
|
||||
/>
|
||||
</DetailContainer>
|
||||
</PosterCardContainer>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<PosterCardContainer key={`placeholder-${uniqueId}-${data.id}`}>
|
||||
<Skeleton
|
||||
radius="sm"
|
||||
visible
|
||||
>
|
||||
<ImageContainerSkeleton />
|
||||
</Skeleton>
|
||||
<DetailContainer>
|
||||
<Stack spacing="sm">
|
||||
<div
|
||||
className={styles.container}
|
||||
key={`placeholder-${uniqueId}-${data.id}`}
|
||||
>
|
||||
<div className={styles.imageContainer}>
|
||||
<Skeleton className={styles.image} />
|
||||
</div>
|
||||
<div className={styles.detailContainer}>
|
||||
<Stack gap="xs">
|
||||
{(controls?.cardRows || []).map((row, index) => (
|
||||
<Skeleton
|
||||
height={14}
|
||||
key={`${index}-${row.arrayProperty}`}
|
||||
radius="sm"
|
||||
visible
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
</DetailContainer>
|
||||
</PosterCardContainer>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
import { CheckboxProps, Checkbox as MantineCheckbox } from '@mantine/core';
|
||||
import { forwardRef } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledCheckbox = styled(MantineCheckbox)`
|
||||
& .mantine-Checkbox-input {
|
||||
background-color: var(--input-bg);
|
||||
|
||||
&:checked {
|
||||
background-color: var(--primary-color);
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
|
||||
&:hover:not(:checked) {
|
||||
background-color: var(--primary-color);
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
transition: none;
|
||||
}
|
||||
`;
|
||||
|
||||
export const Checkbox = forwardRef<HTMLInputElement, CheckboxProps>(
|
||||
({ ...props }: CheckboxProps, ref) => {
|
||||
return (
|
||||
<StyledCheckbox
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -0,0 +1,35 @@
|
||||
.container {
|
||||
position: absolute;
|
||||
z-index: 1000;
|
||||
padding: var(--theme-spacing-xs);
|
||||
background: var(--theme-colors-surface);
|
||||
border: 1px solid var(--theme-colors-border);
|
||||
border-radius: var(--theme-radius-md);
|
||||
box-shadow: 2px 2px 10px 2px rgb(0 0 0 / 40%);
|
||||
}
|
||||
|
||||
.context-menu-button {
|
||||
display: flex;
|
||||
padding: var(--theme-spacing-sm);
|
||||
font-family: var(--theme-content-font-family);
|
||||
font-size: var(--theme-font-size-sm);
|
||||
font-weight: 500;
|
||||
color: var(--theme-colors-surface-foreground);
|
||||
text-align: left;
|
||||
cursor: default;
|
||||
background: var(--theme-colors-surface);
|
||||
border: none;
|
||||
|
||||
&:hover {
|
||||
background: lighten(var(--theme-colors-surface), 10%);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
background: transparent;
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
|
||||
.left {
|
||||
margin-right: 3rem;
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
import { motion, Variants } from 'motion/react';
|
||||
import { ComponentPropsWithoutRef, forwardRef, ReactNode, Ref } from 'react';
|
||||
|
||||
import styles from './context-menu.module.css';
|
||||
|
||||
import { Group } from '/@/shared/components/group/group';
|
||||
|
||||
interface ContextMenuProps {
|
||||
children: ReactNode;
|
||||
maxWidth?: number;
|
||||
minWidth?: number;
|
||||
xPos: number;
|
||||
yPos: number;
|
||||
}
|
||||
|
||||
export const ContextMenuButton = forwardRef(
|
||||
(
|
||||
{
|
||||
children,
|
||||
leftIcon,
|
||||
rightIcon,
|
||||
...props
|
||||
}: ComponentPropsWithoutRef<'button'> & {
|
||||
leftIcon?: ReactNode;
|
||||
rightIcon?: ReactNode;
|
||||
},
|
||||
ref: any,
|
||||
) => {
|
||||
return (
|
||||
<button
|
||||
{...props}
|
||||
className={styles.contextMenuButton}
|
||||
disabled={props.disabled}
|
||||
key={props.key}
|
||||
onClick={props.onClick}
|
||||
ref={ref}
|
||||
>
|
||||
<Group
|
||||
justify="space-between"
|
||||
w="100%"
|
||||
>
|
||||
<Group
|
||||
className={styles.left}
|
||||
gap="md"
|
||||
>
|
||||
{leftIcon}
|
||||
{children}
|
||||
</Group>
|
||||
{rightIcon}
|
||||
</Group>
|
||||
</button>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
const variants: Variants = {
|
||||
closed: {
|
||||
opacity: 0,
|
||||
transition: {
|
||||
duration: 0.1,
|
||||
},
|
||||
},
|
||||
open: {
|
||||
opacity: 1,
|
||||
transition: {
|
||||
duration: 0.1,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const ContextMenu = forwardRef(
|
||||
({ children, maxWidth, minWidth, xPos, yPos }: ContextMenuProps, ref: Ref<HTMLDivElement>) => {
|
||||
return (
|
||||
<motion.div
|
||||
animate="open"
|
||||
className={styles.container}
|
||||
initial="closed"
|
||||
ref={ref}
|
||||
style={{
|
||||
left: xPos,
|
||||
maxWidth,
|
||||
minWidth,
|
||||
top: yPos,
|
||||
}}
|
||||
variants={variants}
|
||||
>
|
||||
{children}
|
||||
</motion.div>
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -1,127 +0,0 @@
|
||||
import { Box, Group, UnstyledButton, UnstyledButtonProps } from '@mantine/core';
|
||||
import { motion, Variants } from 'framer-motion';
|
||||
import { ComponentPropsWithoutRef, forwardRef, ReactNode, Ref } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
interface ContextMenuProps {
|
||||
children: ReactNode;
|
||||
maxWidth?: number;
|
||||
minWidth?: number;
|
||||
xPos: number;
|
||||
yPos: number;
|
||||
}
|
||||
|
||||
const ContextMenuContainer = styled(motion.div)<Omit<ContextMenuProps, 'children'>>`
|
||||
position: absolute;
|
||||
top: ${({ yPos }) => yPos}px !important;
|
||||
left: ${({ xPos }) => xPos}px !important;
|
||||
z-index: 1000;
|
||||
min-width: ${({ minWidth }) => minWidth}px;
|
||||
max-width: ${({ maxWidth }) => maxWidth}px;
|
||||
background: var(--dropdown-menu-bg);
|
||||
border-radius: var(--dropdown-menu-border-radius);
|
||||
box-shadow: 2px 2px 10px 2px rgb(0 0 0 / 40%);
|
||||
|
||||
button:first-child {
|
||||
border-top-left-radius: var(--dropdown-menu-border-radius);
|
||||
border-top-right-radius: var(--dropdown-menu-border-radius);
|
||||
}
|
||||
|
||||
button:last-child {
|
||||
border-bottom-right-radius: var(--dropdown-menu-border-radius);
|
||||
border-bottom-left-radius: var(--dropdown-menu-border-radius);
|
||||
}
|
||||
`;
|
||||
|
||||
export const StyledContextMenuButton = styled(UnstyledButton)`
|
||||
padding: var(--dropdown-menu-item-padding);
|
||||
font-family: var(--content-font-family);
|
||||
font-weight: 500;
|
||||
color: var(--dropdown-menu-fg);
|
||||
text-align: left;
|
||||
cursor: default;
|
||||
background: var(--dropdown-menu-bg);
|
||||
border: none;
|
||||
|
||||
& .mantine-Button-inner {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: var(--dropdown-menu-bg-hover);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
background: transparent;
|
||||
opacity: 0.6;
|
||||
}
|
||||
`;
|
||||
|
||||
export const ContextMenuButton = forwardRef(
|
||||
(
|
||||
{
|
||||
children,
|
||||
leftIcon,
|
||||
rightIcon,
|
||||
...props
|
||||
}: ComponentPropsWithoutRef<'button'> &
|
||||
UnstyledButtonProps & {
|
||||
leftIcon?: ReactNode;
|
||||
rightIcon?: ReactNode;
|
||||
},
|
||||
ref: any,
|
||||
) => {
|
||||
return (
|
||||
<StyledContextMenuButton
|
||||
{...props}
|
||||
as="button"
|
||||
disabled={props.disabled}
|
||||
key={props.key}
|
||||
onClick={props.onClick}
|
||||
ref={ref}
|
||||
>
|
||||
<Group position="apart">
|
||||
<Group spacing="md">
|
||||
<Box>{leftIcon}</Box>
|
||||
<Box mr="2rem">{children}</Box>
|
||||
</Group>
|
||||
<Box>{rightIcon}</Box>
|
||||
</Group>
|
||||
</StyledContextMenuButton>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
const variants: Variants = {
|
||||
closed: {
|
||||
opacity: 0,
|
||||
transition: {
|
||||
duration: 0.1,
|
||||
},
|
||||
},
|
||||
open: {
|
||||
opacity: 1,
|
||||
transition: {
|
||||
duration: 0.1,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const ContextMenu = forwardRef(
|
||||
({ children, maxWidth, minWidth, xPos, yPos }: ContextMenuProps, ref: Ref<HTMLDivElement>) => {
|
||||
return (
|
||||
<ContextMenuContainer
|
||||
animate="open"
|
||||
initial="closed"
|
||||
maxWidth={maxWidth}
|
||||
minWidth={minWidth}
|
||||
ref={ref}
|
||||
variants={variants}
|
||||
xPos={xPos}
|
||||
yPos={yPos}
|
||||
>
|
||||
{children}
|
||||
</ContextMenuContainer>
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -1,45 +0,0 @@
|
||||
import type { DatePickerProps as MantineDatePickerProps } from '@mantine/dates';
|
||||
|
||||
import { DatePicker as MantineDatePicker } from '@mantine/dates';
|
||||
import styled from 'styled-components';
|
||||
|
||||
interface DatePickerProps extends MantineDatePickerProps {
|
||||
maxWidth?: number | string;
|
||||
width?: number | string;
|
||||
}
|
||||
|
||||
const StyledDatePicker = styled(MantineDatePicker)<DatePickerProps>`
|
||||
& .mantine-DatePicker-input {
|
||||
color: var(--input-fg);
|
||||
background: var(--input-bg);
|
||||
|
||||
&::placeholder {
|
||||
color: var(--input-placeholder-fg);
|
||||
}
|
||||
}
|
||||
|
||||
& .mantine-DatePicker-icon {
|
||||
color: var(--input-placeholder-fg);
|
||||
}
|
||||
|
||||
& .mantine-DatePicker-required {
|
||||
color: var(--secondary-color);
|
||||
}
|
||||
|
||||
& .mantine-DatePicker-label {
|
||||
font-family: var(--label-font-family);
|
||||
}
|
||||
|
||||
& .mantine-DateRangePicker-disabled {
|
||||
opacity: 0.6;
|
||||
}
|
||||
`;
|
||||
|
||||
export const DatePicker = ({ maxWidth, width, ...props }: DatePickerProps) => {
|
||||
return (
|
||||
<StyledDatePicker
|
||||
{...props}
|
||||
sx={{ maxWidth, width }}
|
||||
/>
|
||||
);
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user