mirror of
https://github.com/jeffvli/feishin.git
synced 2026-06-11 14:53:47 +02:00
Compare commits
60 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 |
@@ -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:
|
contact_links:
|
||||||
- name: Question
|
- name: Questions or help
|
||||||
url: https://github.com/jeffvli/feishin/discussions
|
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
|
|
||||||
@@ -24,11 +24,9 @@ jobs:
|
|||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
- name: Extract metadata (tags, labels) for Docker
|
- name: Extract metadata (tags, labels) for Docker
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7
|
uses: docker/metadata-action@v5
|
||||||
with:
|
with:
|
||||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||||
tags: |
|
|
||||||
type=raw,value=latest,enable={{is_default_branch}}
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v3
|
uses: docker/setup-qemu-action@v3
|
||||||
- name: Setup Docker buildx
|
- name: Setup Docker buildx
|
||||||
@@ -41,6 +39,6 @@ jobs:
|
|||||||
tags: ${{ steps.meta.outputs.tags }}
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
platforms: |
|
platforms: |
|
||||||
linux/amd64
|
linux/amd64
|
||||||
linux/arm/v7
|
linux/arm/v7
|
||||||
linux/arm64/v8
|
linux/arm64/v8
|
||||||
|
|||||||
+1
-1
@@ -10,7 +10,7 @@
|
|||||||
"selector-type-no-unknown": [true, { "ignoreTypes": ["/-styled-mixin/", "/^\\$\\w+/"] }],
|
"selector-type-no-unknown": [true, { "ignoreTypes": ["/-styled-mixin/", "/^\\$\\w+/"] }],
|
||||||
"declaration-block-no-shorthand-property-overrides": null,
|
"declaration-block-no-shorthand-property-overrides": null,
|
||||||
"declaration-block-no-redundant-longhand-properties": null,
|
"declaration-block-no-redundant-longhand-properties": null,
|
||||||
"at-rule-no-unknown": [true, { "ignoreAtRules": ["mixin"] }],
|
"at-rule-no-unknown": [true, { "ignoreAtRules": ["mixin", "value"] }],
|
||||||
"function-no-unknown": [true, { "ignoreFunctions": ["darken", "alpha", "lighten"] }],
|
"function-no-unknown": [true, { "ignoreFunctions": ["darken", "alpha", "lighten"] }],
|
||||||
"declaration-property-value-no-unknown": null,
|
"declaration-property-value-no-unknown": null,
|
||||||
"no-descending-specificity": null,
|
"no-descending-specificity": null,
|
||||||
|
|||||||
Binary file not shown.
+1
-3
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "feishin",
|
"name": "feishin",
|
||||||
"version": "0.15.0",
|
"version": "0.17.0",
|
||||||
"description": "A modern self-hosted music player.",
|
"description": "A modern self-hosted music player.",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"subsonic",
|
"subsonic",
|
||||||
@@ -170,9 +170,7 @@
|
|||||||
},
|
},
|
||||||
"pnpm": {
|
"pnpm": {
|
||||||
"onlyBuiltDependencies": [
|
"onlyBuiltDependencies": [
|
||||||
"abstract-socket",
|
|
||||||
"electron",
|
"electron",
|
||||||
"electron-winstaller",
|
|
||||||
"esbuild"
|
"esbuild"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -124,7 +124,7 @@
|
|||||||
"hotkey_toggleShuffle": "přepnutí náhodného přehrávání",
|
"hotkey_toggleShuffle": "přepnutí náhodného přehrávání",
|
||||||
"theme": "motiv",
|
"theme": "motiv",
|
||||||
"playbackStyle_description": "nastavení způsobu přehrávání pro přehrávač zvuku",
|
"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",
|
"mpvExecutablePath": "cesta ke spustitelnému souboru mpv",
|
||||||
"audioDevice": "zvukové zařízení",
|
"audioDevice": "zvukové zařízení",
|
||||||
"hotkey_rate2": "hodnocení 2 hvězdami",
|
"hotkey_rate2": "hodnocení 2 hvězdami",
|
||||||
@@ -171,7 +171,7 @@
|
|||||||
"hotkey_zoomOut": "oddálení",
|
"hotkey_zoomOut": "oddálení",
|
||||||
"hotkey_unfavoriteCurrentSong": "zrušení oblíbení u $t(common.currentSong)",
|
"hotkey_unfavoriteCurrentSong": "zrušení oblíbení u $t(common.currentSong)",
|
||||||
"hotkey_rate0": "vymazání hodnocení",
|
"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)",
|
"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",
|
"floatingQueueArea_description": "zobrazit ikonu přejetí myší na pravé straně obrazovky pro zobrazení fronty",
|
||||||
"hotkey_volumeMute": "ztlumení",
|
"hotkey_volumeMute": "ztlumení",
|
||||||
@@ -265,7 +265,13 @@
|
|||||||
"musicbrainz": "zobrazit odkazy na musicbrainz",
|
"musicbrainz": "zobrazit odkazy na musicbrainz",
|
||||||
"musicbrainz_description": "na stránkách umělců a alb, kde existuje mbid, 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": "Povolit překlady NetEase",
|
||||||
"neteaseTranslation_description": "Pokud je povoleno, načte a zobrazí přeložené texty ze služby NetEase, pokud jsou dostupné."
|
"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": {
|
"action": {
|
||||||
"editPlaylist": "upravit $t(entity.playlist_one)",
|
"editPlaylist": "upravit $t(entity.playlist_one)",
|
||||||
@@ -373,7 +379,7 @@
|
|||||||
"size": "velikost",
|
"size": "velikost",
|
||||||
"biography": "biografie",
|
"biography": "biografie",
|
||||||
"note": "poznámka",
|
"note": "poznámka",
|
||||||
"albumGain": "zisk (gain) alba",
|
"albumGain": "gain alba",
|
||||||
"albumPeak": "vrchol alba",
|
"albumPeak": "vrchol alba",
|
||||||
"close": "zavřít",
|
"close": "zavřít",
|
||||||
"mbid": "ID MusicBrainz",
|
"mbid": "ID MusicBrainz",
|
||||||
@@ -385,14 +391,18 @@
|
|||||||
"preview": "náhled",
|
"preview": "náhled",
|
||||||
"translation": "překlad",
|
"translation": "překlad",
|
||||||
"additionalParticipants": "další přispívající",
|
"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": {
|
"table": {
|
||||||
"config": {
|
"config": {
|
||||||
"view": {
|
"view": {
|
||||||
"card": "karta",
|
"card": "karta",
|
||||||
"table": "tabulka",
|
"table": "tabulka",
|
||||||
"poster": "plakát"
|
"poster": "plakát",
|
||||||
|
"list": "seznam",
|
||||||
|
"grid": "mřížka"
|
||||||
},
|
},
|
||||||
"general": {
|
"general": {
|
||||||
"displayType": "typ zobrazení",
|
"displayType": "typ zobrazení",
|
||||||
@@ -544,7 +554,8 @@
|
|||||||
"home": "$t(common.home)",
|
"home": "$t(common.home)",
|
||||||
"artists": "$t(entity.artist_other)",
|
"artists": "$t(entity.artist_other)",
|
||||||
"albumArtists": "$t(entity.albumArtist_other)",
|
"albumArtists": "$t(entity.albumArtist_other)",
|
||||||
"shared": "$t(entity.playlist_other) sdíleny"
|
"shared": "$t(entity.playlist_other) sdíleny",
|
||||||
|
"myLibrary": "moje knihovna"
|
||||||
},
|
},
|
||||||
"fullscreenPlayer": {
|
"fullscreenPlayer": {
|
||||||
"config": {
|
"config": {
|
||||||
@@ -720,7 +731,8 @@
|
|||||||
},
|
},
|
||||||
"queryEditor": {
|
"queryEditor": {
|
||||||
"input_optionMatchAll": "shoda všeho",
|
"input_optionMatchAll": "shoda všeho",
|
||||||
"input_optionMatchAny": "shoda libovolného"
|
"input_optionMatchAny": "shoda libovolného",
|
||||||
|
"title": "editor dotazů"
|
||||||
},
|
},
|
||||||
"lyricSearch": {
|
"lyricSearch": {
|
||||||
"input_name": "$t(common.name)",
|
"input_name": "$t(common.name)",
|
||||||
|
|||||||
@@ -113,7 +113,9 @@
|
|||||||
"trackPeak": "Track-Spitzenpegel",
|
"trackPeak": "Track-Spitzenpegel",
|
||||||
"codec": "Codec",
|
"codec": "Codec",
|
||||||
"albumPeak": "Album-Spitzenpegel",
|
"albumPeak": "Album-Spitzenpegel",
|
||||||
"albumGain": "Album-Pegelverstärkung"
|
"albumGain": "Album-Pegelverstärkung",
|
||||||
|
"tags": "tags",
|
||||||
|
"viewReleaseNotes": "Release Notes anzeigen"
|
||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
"remotePortWarning": "Starten Sie den Server neu, um den neuen Port anzuwenden",
|
"remotePortWarning": "Starten Sie den Server neu, um den neuen Port anzuwenden",
|
||||||
@@ -237,7 +239,8 @@
|
|||||||
"description": "Beschreibung",
|
"description": "Beschreibung",
|
||||||
"setExpiration": "Ablaufdatum setzen",
|
"setExpiration": "Ablaufdatum setzen",
|
||||||
"expireInvalid": "Ablaufdatum muss in der Zukunft liegen",
|
"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": {
|
"entity": {
|
||||||
@@ -429,7 +432,8 @@
|
|||||||
"home": "$t(common.home)",
|
"home": "$t(common.home)",
|
||||||
"artists": "$t(entity.artist_other)",
|
"artists": "$t(entity.artist_other)",
|
||||||
"albumArtists": "$t(entity.albumArtist_other)",
|
"albumArtists": "$t(entity.albumArtist_other)",
|
||||||
"shared": "$t(entity.playlist_other) geteilt"
|
"shared": "$t(entity.playlist_other) geteilt",
|
||||||
|
"myLibrary": "meine bibliothek"
|
||||||
},
|
},
|
||||||
"setting": {
|
"setting": {
|
||||||
"playbackTab": "Wiedergabe",
|
"playbackTab": "Wiedergabe",
|
||||||
@@ -516,7 +520,7 @@
|
|||||||
"playSimilarSongs": "Ähnliche Lieder abspielen"
|
"playSimilarSongs": "Ähnliche Lieder abspielen"
|
||||||
},
|
},
|
||||||
"setting": {
|
"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",
|
"audioExclusiveMode": "Audio-Exklusivmodus",
|
||||||
"audioDevice": "Audiogerät",
|
"audioDevice": "Audiogerät",
|
||||||
"accentColor": "Akzentfarbe",
|
"accentColor": "Akzentfarbe",
|
||||||
@@ -670,12 +674,23 @@
|
|||||||
"windowBarStyle_description": "Wähle den Stil der Windows-Leiste",
|
"windowBarStyle_description": "Wähle den Stil der Windows-Leiste",
|
||||||
"hotkey_toggleCurrentSongFavorite": "$t(common.currentSong) zu Favoriten hinzufügen",
|
"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",
|
"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",
|
"clearCache": "Browser-Zwischenspeicher löschen",
|
||||||
"clearQueryCache": "feishins Zwischenspeicher leeren",
|
"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",
|
"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",
|
"sidePlayQueueStyle": "Wiedergabelistenstil in der Seitenleiste",
|
||||||
"zoom_description": "Setzt den Zoom (in %) für das Programm",
|
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -514,12 +514,14 @@
|
|||||||
"disableLibraryUpdateOnStartup": "disable checking for new versions on startup",
|
"disableLibraryUpdateOnStartup": "disable checking for new versions on startup",
|
||||||
"discordApplicationId": "{{discord}} application id",
|
"discordApplicationId": "{{discord}} application id",
|
||||||
"discordApplicationId_description": "the application id for {{discord}} rich presence (defaults to {{defaultId}})",
|
"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": "show rich presence idle status",
|
||||||
"discordIdleStatus_description": "when enabled, update status while player is idle",
|
"discordIdleStatus_description": "when enabled, update status while player is idle",
|
||||||
"discordListening": "show status as listening",
|
"discordListening": "show status as listening",
|
||||||
"discordListening_description": "show status as listening instead of playing",
|
"discordListening_description": "show status as listening instead of playing",
|
||||||
"discordRichPresence": "{{discord}} rich presence",
|
"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": "serve {{discord}} images from server",
|
||||||
"discordServeImage_description": "share cover art for {{discord}} rich presence from server itself, only available for jellyfin and navidrome",
|
"discordServeImage_description": "share cover art for {{discord}} rich presence from server itself, only available for jellyfin and navidrome",
|
||||||
"discordUpdateInterval": "{{discord}} rich presence update interval",
|
"discordUpdateInterval": "{{discord}} rich presence update interval",
|
||||||
@@ -536,6 +538,8 @@
|
|||||||
"floatingQueueArea_description": "display a hover icon on the right side of the screen to view the play queue",
|
"floatingQueueArea_description": "display a hover icon on the right side of the screen to view the play queue",
|
||||||
"followLyric": "follow current lyric",
|
"followLyric": "follow current lyric",
|
||||||
"followLyric_description": "scroll the lyric to the current playing position",
|
"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": "font",
|
||||||
"font_description": "sets the font to use for the application",
|
"font_description": "sets the font to use for the application",
|
||||||
"fontType": "font type",
|
"fontType": "font type",
|
||||||
@@ -703,6 +707,8 @@
|
|||||||
"volumeWidth_description": "the width of the volume slider",
|
"volumeWidth_description": "the width of the volume slider",
|
||||||
"webAudio": "use web audio",
|
"webAudio": "use web audio",
|
||||||
"webAudio_description": "use web audio. this enables advanced features like replaygain. disable if you experience otherwise",
|
"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": "window bar style",
|
||||||
"windowBarStyle_description": "select the style of the window bar",
|
"windowBarStyle_description": "select the style of the window bar",
|
||||||
"zoom": "zoom percentage",
|
"zoom": "zoom percentage",
|
||||||
|
|||||||
@@ -120,7 +120,7 @@
|
|||||||
"hotkey_toggleShuffle": "alterna aleatorio",
|
"hotkey_toggleShuffle": "alterna aleatorio",
|
||||||
"theme": "tema",
|
"theme": "tema",
|
||||||
"playbackStyle_description": "selecciona el estilo de reproducción a usar por el reproductor de audio",
|
"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",
|
"mpvExecutablePath": "ruta del ejecutable mpv",
|
||||||
"audioDevice": "dispositivo de audio",
|
"audioDevice": "dispositivo de audio",
|
||||||
"hotkey_rate2": "calificar con 2 estrellas",
|
"hotkey_rate2": "calificar con 2 estrellas",
|
||||||
@@ -265,7 +265,13 @@
|
|||||||
"musicbrainz": "Mostrar enlaces de MusicBrainz",
|
"musicbrainz": "Mostrar enlaces de MusicBrainz",
|
||||||
"musicbrainz_description": "Muestra enlaces a MusicBrainz en las páginas de artistas/álbumes, donde exista mbid",
|
"musicbrainz_description": "Muestra enlaces a MusicBrainz en las páginas de artistas/álbumes, donde exista mbid",
|
||||||
"neteaseTranslation": "Activar traducciones de NetEase",
|
"neteaseTranslation": "Activar traducciones de NetEase",
|
||||||
"neteaseTranslation_description": "Cuando se habilita, busca y muestra letras traducidas desde NetEase si está disponible."
|
"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": {
|
"action": {
|
||||||
"editPlaylist": "editar $t(entity.playlist_one)",
|
"editPlaylist": "editar $t(entity.playlist_one)",
|
||||||
@@ -385,7 +391,9 @@
|
|||||||
"preview": "Vista previa",
|
"preview": "Vista previa",
|
||||||
"translation": "traducción",
|
"translation": "traducción",
|
||||||
"additionalParticipants": "Participantes adicionales",
|
"additionalParticipants": "Participantes adicionales",
|
||||||
"tags": "Etiquetas"
|
"tags": "Etiquetas",
|
||||||
|
"newVersion": "Una nueva versión ha sido instalada ({{version}})",
|
||||||
|
"viewReleaseNotes": "Ver notas de lanzamiento"
|
||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
"remotePortWarning": "reiniciar el servidor para aplicar el nuevo puerto",
|
"remotePortWarning": "reiniciar el servidor para aplicar el nuevo puerto",
|
||||||
@@ -469,7 +477,8 @@
|
|||||||
"home": "$t(common.home)",
|
"home": "$t(common.home)",
|
||||||
"artists": "$t(entity.artist_other)",
|
"artists": "$t(entity.artist_other)",
|
||||||
"albumArtists": "$t(entity.albumArtist_other)",
|
"albumArtists": "$t(entity.albumArtist_other)",
|
||||||
"shared": "compartido $t(entity.playlist_other)"
|
"shared": "compartido $t(entity.playlist_other)",
|
||||||
|
"myLibrary": "Mi biblioteca"
|
||||||
},
|
},
|
||||||
"appMenu": {
|
"appMenu": {
|
||||||
"selectServer": "seleccionar servidor",
|
"selectServer": "seleccionar servidor",
|
||||||
@@ -655,7 +664,8 @@
|
|||||||
},
|
},
|
||||||
"queryEditor": {
|
"queryEditor": {
|
||||||
"input_optionMatchAll": "coincidir todos",
|
"input_optionMatchAll": "coincidir todos",
|
||||||
"input_optionMatchAny": "coincidir cualquiera"
|
"input_optionMatchAny": "coincidir cualquiera",
|
||||||
|
"title": "Editor de consultas"
|
||||||
},
|
},
|
||||||
"shareItem": {
|
"shareItem": {
|
||||||
"createFailed": "No se pudo crear el recurso compartido (¿está habilitado el uso compartido?)",
|
"createFailed": "No se pudo crear el recurso compartido (¿está habilitado el uso compartido?)",
|
||||||
@@ -737,7 +747,9 @@
|
|||||||
"view": {
|
"view": {
|
||||||
"card": "tarjeta",
|
"card": "tarjeta",
|
||||||
"table": "tabla",
|
"table": "tabla",
|
||||||
"poster": "cartel"
|
"poster": "cartel",
|
||||||
|
"list": "Lista",
|
||||||
|
"grid": "Cuadrícula"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -90,7 +90,9 @@
|
|||||||
"trackGain": "raidan vahvistus (gain)",
|
"trackGain": "raidan vahvistus (gain)",
|
||||||
"trackPeak": "kappaleen huippu (peak)",
|
"trackPeak": "kappaleen huippu (peak)",
|
||||||
"additionalParticipants": "muut osallistujat",
|
"additionalParticipants": "muut osallistujat",
|
||||||
"tags": "tägit"
|
"tags": "tägit",
|
||||||
|
"newVersion": "uusi versio on asennettu ({{version}})",
|
||||||
|
"viewReleaseNotes": "katsele julkaisutietoja"
|
||||||
},
|
},
|
||||||
"entity": {
|
"entity": {
|
||||||
"album_one": "albumi",
|
"album_one": "albumi",
|
||||||
@@ -279,7 +281,8 @@
|
|||||||
},
|
},
|
||||||
"queryEditor": {
|
"queryEditor": {
|
||||||
"input_optionMatchAny": "sovita joku",
|
"input_optionMatchAny": "sovita joku",
|
||||||
"input_optionMatchAll": "sovita kaikki"
|
"input_optionMatchAll": "sovita kaikki",
|
||||||
|
"title": "kyselyeditori"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"setting": {
|
"setting": {
|
||||||
@@ -359,7 +362,7 @@
|
|||||||
"doubleClickBehavior": "lisää kaikki haetut kappaleet soittojonoon tuplaklikkauksella",
|
"doubleClickBehavior": "lisää kaikki haetut kappaleet soittojonoon tuplaklikkauksella",
|
||||||
"discordUpdateInterval_description": "päivitysväli sekunnteina (vähintään 15 sekunttia)",
|
"discordUpdateInterval_description": "päivitysväli sekunnteina (vähintään 15 sekunttia)",
|
||||||
"discordRichPresence": "{{discord}} rich presence",
|
"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",
|
"discordUpdateInterval": "{{discord}} rich presencen päivitysväli",
|
||||||
"enableRemote": "aktivoi etäohjauspalvelin",
|
"enableRemote": "aktivoi etäohjauspalvelin",
|
||||||
"externalLinks_description": "ottaa ulkoiset linkit (Last.fm, MusicBrainz) artistien/albumien sivuilla",
|
"externalLinks_description": "ottaa ulkoiset linkit (Last.fm, MusicBrainz) artistien/albumien sivuilla",
|
||||||
@@ -515,7 +518,13 @@
|
|||||||
"lastfm_description": "näytä linkit last.fm sivulle artistin/albumin sivuilla",
|
"lastfm_description": "näytä linkit last.fm sivulle artistin/albumin sivuilla",
|
||||||
"musicbrainz": "näytä musicbrainz linkit",
|
"musicbrainz": "näytä musicbrainz linkit",
|
||||||
"neteaseTranslation": "Ota NetEasen käännökset käyttöön",
|
"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."
|
"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": {
|
"page": {
|
||||||
"itemDetail": {
|
"itemDetail": {
|
||||||
@@ -584,7 +593,8 @@
|
|||||||
"home": "$t(common.home)",
|
"home": "$t(common.home)",
|
||||||
"nowPlaying": "nyt soi",
|
"nowPlaying": "nyt soi",
|
||||||
"playlists": "$t(entity.playlist_other)",
|
"playlists": "$t(entity.playlist_other)",
|
||||||
"search": "$t(common.search)"
|
"search": "$t(common.search)",
|
||||||
|
"myLibrary": "oma kirjasto"
|
||||||
},
|
},
|
||||||
"setting": {
|
"setting": {
|
||||||
"generalTab": "yleinen",
|
"generalTab": "yleinen",
|
||||||
@@ -745,7 +755,9 @@
|
|||||||
"view": {
|
"view": {
|
||||||
"table": "taulukko",
|
"table": "taulukko",
|
||||||
"card": "kortti",
|
"card": "kortti",
|
||||||
"poster": "juliste"
|
"poster": "juliste",
|
||||||
|
"grid": "ruudukko",
|
||||||
|
"list": "lista"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"column": {
|
"column": {
|
||||||
|
|||||||
@@ -150,7 +150,9 @@
|
|||||||
"codec": "codec",
|
"codec": "codec",
|
||||||
"translation": "traduction",
|
"translation": "traduction",
|
||||||
"additionalParticipants": "participants additionnels",
|
"additionalParticipants": "participants additionnels",
|
||||||
"tags": "tags"
|
"tags": "tags",
|
||||||
|
"newVersion": "une nouvelle version vient d'être installé ({{version}})",
|
||||||
|
"viewReleaseNotes": "voir la note de version"
|
||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
"remotePortWarning": "redémarrer le serveur pour appliquer le nouveau port",
|
"remotePortWarning": "redémarrer le serveur pour appliquer le nouveau port",
|
||||||
@@ -234,7 +236,8 @@
|
|||||||
"home": "$t(common.home)",
|
"home": "$t(common.home)",
|
||||||
"artists": "$t(entity.artist_other)",
|
"artists": "$t(entity.artist_other)",
|
||||||
"albumArtists": "$t(entity.albumArtist_other)",
|
"albumArtists": "$t(entity.albumArtist_other)",
|
||||||
"shared": "partagé $t(entity.playlist_other)"
|
"shared": "partagé $t(entity.playlist_other)",
|
||||||
|
"myLibrary": "ma bibliothèque"
|
||||||
},
|
},
|
||||||
"fullscreenPlayer": {
|
"fullscreenPlayer": {
|
||||||
"config": {
|
"config": {
|
||||||
@@ -446,7 +449,7 @@
|
|||||||
"playbackStyle": "style de lecture",
|
"playbackStyle": "style de lecture",
|
||||||
"hotkey_toggleShuffle": "basculer la lecture aléatoire",
|
"hotkey_toggleShuffle": "basculer la lecture aléatoire",
|
||||||
"playbackStyle_description": "sélectionnez le style de lecture à utiliser pour le lecteur audio",
|
"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",
|
"mpvExecutablePath": "chemin de l'exécutable mpv",
|
||||||
"hotkey_rate2": "noter 2 étoiles",
|
"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",
|
"playButtonBehavior_description": "définit le comportement par défaut du bouton play, lors de l'ajout de chanson à la file d'attente",
|
||||||
@@ -602,7 +605,15 @@
|
|||||||
"lastfm": "affiche les liens de last.fm",
|
"lastfm": "affiche les liens de last.fm",
|
||||||
"musicbrainz_description": "affiches les liens vers musicbrainz sur les pages des artistes/albums, quand mbid existes",
|
"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",
|
"lastfm_description": "affiche les liens vers last.fm sur les pages des artistes/albums",
|
||||||
"musicbrainz": "affiches les liens musicbrainz"
|
"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": {
|
"form": {
|
||||||
"deletePlaylist": {
|
"deletePlaylist": {
|
||||||
@@ -643,7 +654,8 @@
|
|||||||
},
|
},
|
||||||
"queryEditor": {
|
"queryEditor": {
|
||||||
"input_optionMatchAll": "correspondre à tous",
|
"input_optionMatchAll": "correspondre à tous",
|
||||||
"input_optionMatchAny": "correspondre à n'importe quel"
|
"input_optionMatchAny": "correspondre à n'importe quel",
|
||||||
|
"title": "éditeur de requête"
|
||||||
},
|
},
|
||||||
"editPlaylist": {
|
"editPlaylist": {
|
||||||
"title": "modifier $t(entity.playlist_one)",
|
"title": "modifier $t(entity.playlist_one)",
|
||||||
@@ -733,7 +745,9 @@
|
|||||||
"view": {
|
"view": {
|
||||||
"table": "liste",
|
"table": "liste",
|
||||||
"poster": "poster",
|
"poster": "poster",
|
||||||
"card": "Carte"
|
"card": "Carte",
|
||||||
|
"grid": "grille",
|
||||||
|
"list": "liste"
|
||||||
},
|
},
|
||||||
"label": {
|
"label": {
|
||||||
"releaseDate": "date de sortie",
|
"releaseDate": "date de sortie",
|
||||||
|
|||||||
@@ -163,7 +163,7 @@
|
|||||||
"remotePortWarning": "indítsd újra a szervert az új PORT használatához",
|
"remotePortWarning": "indítsd újra a szervert az új PORT használatához",
|
||||||
"genericError": "hiba történt",
|
"genericError": "hiba történt",
|
||||||
"endpointNotImplementedError": "a(z) {{endpoint}} végpont nincs implementálva a következőhöz: {{serverType}}",
|
"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",
|
"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",
|
"mpvRequired": "MPV szükséges",
|
||||||
"invalidServer": "érvénytelen szerver",
|
"invalidServer": "érvénytelen szerver",
|
||||||
|
|||||||
@@ -491,7 +491,7 @@
|
|||||||
"discordListening": "Tampilkan status sebagai mendengarkan",
|
"discordListening": "Tampilkan status sebagai mendengarkan",
|
||||||
"discordListening_description": "tampilkan status sebagai mendengarkan alih-alih bermain",
|
"discordListening_description": "tampilkan status sebagai mendengarkan alih-alih bermain",
|
||||||
"discordRichPresence": "status aktivitas {{discord}}",
|
"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": "interval pembaruan status aktivitas {{discord}}",
|
||||||
"discordUpdateInterval_description": "waktu dalam detik antara setiap pembaruan (minimal 15 detik)",
|
"discordUpdateInterval_description": "waktu dalam detik antara setiap pembaruan (minimal 15 detik)",
|
||||||
"doubleClickBehavior": "masukkan semua lagu yang dicari saat mengklik dua kali",
|
"doubleClickBehavior": "masukkan semua lagu yang dicari saat mengklik dua kali",
|
||||||
|
|||||||
@@ -209,7 +209,7 @@
|
|||||||
"hotkey_toggleShuffle": "attiva/disattiva mescolamento",
|
"hotkey_toggleShuffle": "attiva/disattiva mescolamento",
|
||||||
"theme": "tema",
|
"theme": "tema",
|
||||||
"playbackStyle_description": "selezione lo stile di riproduzione da usare per il player audio",
|
"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",
|
"mpvExecutablePath": "percorso eseguibile mpv",
|
||||||
"audioDevice": "device audio",
|
"audioDevice": "device audio",
|
||||||
"hotkey_rate2": "voto 2 stelle",
|
"hotkey_rate2": "voto 2 stelle",
|
||||||
|
|||||||
@@ -122,7 +122,7 @@
|
|||||||
"hotkey_toggleShuffle": "シャッフルの切り替え",
|
"hotkey_toggleShuffle": "シャッフルの切り替え",
|
||||||
"theme": "テーマ",
|
"theme": "テーマ",
|
||||||
"playbackStyle_description": "オーディオプレーヤーに使用する再生スタイルを選択します",
|
"playbackStyle_description": "オーディオプレーヤーに使用する再生スタイルを選択します",
|
||||||
"discordRichPresence_description": "{{discord}} のRich Presenceに再生ステータスを表示するようにします。画像キー: {{icon}}, {{playing}}, {{paused}} ",
|
"discordRichPresence_description": "{{discord}} のRich Presenceに再生ステータスを表示するようにします。画像キー: {{icon}}, {{playing}}, {{paused}}",
|
||||||
"mpvExecutablePath": "mpv 実行ファイルパス",
|
"mpvExecutablePath": "mpv 実行ファイルパス",
|
||||||
"audioDevice": "オーディオデバイス",
|
"audioDevice": "オーディオデバイス",
|
||||||
"hotkey_rate2": "2つ星で評価",
|
"hotkey_rate2": "2つ星で評価",
|
||||||
|
|||||||
@@ -533,7 +533,7 @@
|
|||||||
"crossfadeDuration_description": "ustaw czas trwania efektu przenikania",
|
"crossfadeDuration_description": "ustaw czas trwania efektu przenikania",
|
||||||
"language": "język",
|
"language": "język",
|
||||||
"hotkey_toggleShuffle": "przełącz kolejność losową",
|
"hotkey_toggleShuffle": "przełącz kolejność losową",
|
||||||
"discordRichPresence_description": "włącz status odtwarzania w {{discord}} (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",
|
"audioDevice": "urządzenia dźwiękowe",
|
||||||
"hotkey_rate2": "oceń na 2 gwiazdki",
|
"hotkey_rate2": "oceń na 2 gwiazdki",
|
||||||
"exitToTray": "zamknij do zasobnika",
|
"exitToTray": "zamknij do zasobnika",
|
||||||
|
|||||||
@@ -86,7 +86,7 @@
|
|||||||
"confirm": "подтвердить",
|
"confirm": "подтвердить",
|
||||||
"resetToDefault": "сбросить настройки",
|
"resetToDefault": "сбросить настройки",
|
||||||
"home": "главная",
|
"home": "главная",
|
||||||
"comingSoon": "скоро...",
|
"comingSoon": "скоро…",
|
||||||
"reset": "сбросить",
|
"reset": "сбросить",
|
||||||
"channel_one": "канал",
|
"channel_one": "канал",
|
||||||
"channel_few": "канала",
|
"channel_few": "канала",
|
||||||
@@ -333,7 +333,7 @@
|
|||||||
"next": "следующий",
|
"next": "следующий",
|
||||||
"shuffle": "перемешать",
|
"shuffle": "перемешать",
|
||||||
"playbackFetchNoResults": "песни не найдены",
|
"playbackFetchNoResults": "песни не найдены",
|
||||||
"playbackFetchInProgress": "загрузка песен..",
|
"playbackFetchInProgress": "загрузка песен…",
|
||||||
"addNext": "воспроизвести следующим",
|
"addNext": "воспроизвести следующим",
|
||||||
"playbackSpeed": "скорость воспроизведения",
|
"playbackSpeed": "скорость воспроизведения",
|
||||||
"playbackFetchCancel": "пожалуйста, подождите немного... закройте уведомление для отмены",
|
"playbackFetchCancel": "пожалуйста, подождите немного... закройте уведомление для отмены",
|
||||||
@@ -759,7 +759,7 @@
|
|||||||
"artistConfiguration": "конфигурация страницы альбомов исполнителей",
|
"artistConfiguration": "конфигурация страницы альбомов исполнителей",
|
||||||
"artistConfiguration_description": "позволяет настроить видимость и порядок элементов на странице альбомов исполнителей",
|
"artistConfiguration_description": "позволяет настроить видимость и порядок элементов на странице альбомов исполнителей",
|
||||||
"fontType_description": "встроенный позволяет выбрать один из шрифтов, предоставляемых Feishin. системный позволяет выбрать любой шрифт, предоставляемый вашей операционной системой. пользовательский позволяет выбрать свой собственный шрифт",
|
"fontType_description": "встроенный позволяет выбрать один из шрифтов, предоставляемых Feishin. системный позволяет выбрать любой шрифт, предоставляемый вашей операционной системой. пользовательский позволяет выбрать свой собственный шрифт",
|
||||||
"discordRichPresence_description": "включить статус воспроизведения в статус профиля в {{discord}}. Ключи изображений: {{icon}}, {{playing}} и {{paused}} ",
|
"discordRichPresence_description": "включить статус воспроизведения в статус профиля в {{discord}}. Ключи изображений: {{icon}}, {{playing}} и {{paused}}",
|
||||||
"lyricOffset": "синхронизация текста треков (мс)"
|
"lyricOffset": "синхронизация текста треков (мс)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -112,7 +112,7 @@
|
|||||||
"hotkey_localSearch": "pretraživanje na stranici",
|
"hotkey_localSearch": "pretraživanje na stranici",
|
||||||
"hotkey_toggleQueue": "promeni listu za reprodukciju",
|
"hotkey_toggleQueue": "promeni listu za reprodukciju",
|
||||||
"zoom_description": "postavlja stepen zumiranja za aplikaciju",
|
"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_rate5": "oceni sa 5 zvezdica",
|
||||||
"hotkey_playbackPrevious": "prethodna pesma",
|
"hotkey_playbackPrevious": "prethodna pesma",
|
||||||
"showSkipButtons_description": "prikaži ili sakrij dugmad za preskakanje na traci za reprodukciju",
|
"showSkipButtons_description": "prikaži ili sakrij dugmad za preskakanje na traci za reprodukciju",
|
||||||
@@ -122,7 +122,7 @@
|
|||||||
"hotkey_toggleShuffle": "promeni slučajan redosled",
|
"hotkey_toggleShuffle": "promeni slučajan redosled",
|
||||||
"theme": "tema",
|
"theme": "tema",
|
||||||
"playbackStyle_description": "izaberite stil reprodukcije za audio plejer",
|
"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",
|
"mpvExecutablePath": "putanja do mpv izvršne datoteke",
|
||||||
"audioDevice": "audio uređaj",
|
"audioDevice": "audio uređaj",
|
||||||
"hotkey_rate2": "oceni sa 2 zvezdice",
|
"hotkey_rate2": "oceni sa 2 zvezdice",
|
||||||
@@ -158,7 +158,7 @@
|
|||||||
"useSystemTheme_description": "prati sistemski određene postavke za svetlu ili tamnu temu",
|
"useSystemTheme_description": "prati sistemski određene postavke za svetlu ili tamnu temu",
|
||||||
"playButtonBehavior_optionAddNext": "$t(player.addNext)",
|
"playButtonBehavior_optionAddNext": "$t(player.addNext)",
|
||||||
"lyricFetch_description": "preuzimanje tekstova sa različitih izvora na internetu",
|
"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",
|
"globalMediaHotkeys_description": "omogućava ili onemogućava korišćenje medijskih tastera sistema za kontrolu reprodukcije",
|
||||||
"customFontPath": "prilagođena putanja fonta",
|
"customFontPath": "prilagođena putanja fonta",
|
||||||
"followLyric": "prati trenutni tekst pesme",
|
"followLyric": "prati trenutni tekst pesme",
|
||||||
|
|||||||
@@ -224,7 +224,7 @@
|
|||||||
"input_password": "கடவுச்சொல்",
|
"input_password": "கடவுச்சொல்",
|
||||||
"error_savePassword": "கடவுச்சொல்லை சேமிக்க முயற்சிக்கும்போது பிழை ஏற்பட்டது",
|
"error_savePassword": "கடவுச்சொல்லை சேமிக்க முயற்சிக்கும்போது பிழை ஏற்பட்டது",
|
||||||
"ignoreCors": "CORS ஐ புறக்கணிக்கவும் ($ t (Common.RestartRequired))",
|
"ignoreCors": "CORS ஐ புறக்கணிக்கவும் ($ t (Common.RestartRequired))",
|
||||||
"ignoreSsl": "SSL ஐ புறக்கணிக்கவும் ($ t (பொதுவானது.",
|
"ignoreSsl": "SSL ஐ புறக்கணிக்கவும் ($ t (பொதுவானது",
|
||||||
"input_legacyAuthentication": "மரபு அங்கீகாரத்தை இயக்கவும்",
|
"input_legacyAuthentication": "மரபு அங்கீகாரத்தை இயக்கவும்",
|
||||||
"input_name": "சேவையக பெயர்",
|
"input_name": "சேவையக பெயர்",
|
||||||
"input_savePassword": "கடவுச்சொல்லைச் சேமிக்கவும்",
|
"input_savePassword": "கடவுச்சொல்லைச் சேமிக்கவும்",
|
||||||
@@ -521,7 +521,7 @@
|
|||||||
"hotkey_volumeMute": "தொகுதி முடக்கு",
|
"hotkey_volumeMute": "தொகுதி முடக்கு",
|
||||||
"hotkey_volumeUp": "தொகுதி",
|
"hotkey_volumeUp": "தொகுதி",
|
||||||
"language": "மொழி",
|
"language": "மொழி",
|
||||||
"language_description": "பயன்பாட்டிற்கான மொழியை அமைக்கிறது ($ t (பொதுவானது.",
|
"language_description": "பயன்பாட்டிற்கான மொழியை அமைக்கிறது ($ t (பொதுவானது",
|
||||||
"lastfmApiKey": "{{lastfm}} பநிஇ key",
|
"lastfmApiKey": "{{lastfm}} பநிஇ key",
|
||||||
"lastfmApiKey_description": "{{lastfm} க்கு க்கான பநிஇ விசை. கவர் கலைக்கு தேவை",
|
"lastfmApiKey_description": "{{lastfm} க்கு க்கான பநிஇ விசை. கவர் கலைக்கு தேவை",
|
||||||
"lyricFetch": "இணையத்திலிருந்து வரிகளை பெறுங்கள்",
|
"lyricFetch": "இணையத்திலிருந்து வரிகளை பெறுங்கள்",
|
||||||
@@ -615,7 +615,7 @@
|
|||||||
"discordIdleStatus_description": "இயக்கப்பட்டால், பிளேயர் சும்மா இருக்கும்போது நிலையைப் புதுப்பிக்கவும்",
|
"discordIdleStatus_description": "இயக்கப்பட்டால், பிளேயர் சும்மா இருக்கும்போது நிலையைப் புதுப்பிக்கவும்",
|
||||||
"discordListening_description": "விளையாடுவதற்குப் பதிலாக கேட்பது என்று அந்த நிலையைக் காட்டுங்கள்",
|
"discordListening_description": "விளையாடுவதற்குப் பதிலாக கேட்பது என்று அந்த நிலையைக் காட்டுங்கள்",
|
||||||
"discordRichPresence": "{{discord}} பணக்கார இருப்பு",
|
"discordRichPresence": "{{discord}} பணக்கார இருப்பு",
|
||||||
"discordRichPresence_description": "{{discord}} பணக்கார இருப்பில் பின்னணி நிலையை இயக்கவும். பட விசைகள்: {{icon}}, {{playing}}, மற்றும் {{paused}} ",
|
"discordRichPresence_description": "{{discord}} பணக்கார இருப்பில் பின்னணி நிலையை இயக்கவும். பட விசைகள்: {{icon}}, {{playing}}, மற்றும் {{paused}}",
|
||||||
"customCss_description": "தனிப்பயன் சிஎச்எச் உள்ளடக்கம். குறிப்பு: உள்ளடக்கம் மற்றும் தொலைநிலை முகவரி கள் அனுமதிக்கப்படாத பண்புகள். உங்கள் உள்ளடக்கத்தின் முன்னோட்டம் கீழே காட்டப்பட்டுள்ளது. நீங்கள் அமைக்காத கூடுதல் புலங்கள் சுத்திகரிப்பு காரணமாக உள்ளன.",
|
"customCss_description": "தனிப்பயன் சிஎச்எச் உள்ளடக்கம். குறிப்பு: உள்ளடக்கம் மற்றும் தொலைநிலை முகவரி கள் அனுமதிக்கப்படாத பண்புகள். உங்கள் உள்ளடக்கத்தின் முன்னோட்டம் கீழே காட்டப்பட்டுள்ளது. நீங்கள் அமைக்காத கூடுதல் புலங்கள் சுத்திகரிப்பு காரணமாக உள்ளன.",
|
||||||
"doubleClickBehavior": "இரட்டை சொடுக்கு செய்யும் போது தேடப்பட்ட அனைத்து தடங்களையும் வரிசைப்படுத்தவும்",
|
"doubleClickBehavior": "இரட்டை சொடுக்கு செய்யும் போது தேடப்பட்ட அனைத்து தடங்களையும் வரிசைப்படுத்தவும்",
|
||||||
"doubleClickBehavior_description": "உண்மை என்றால், தட தேடலில் பொருந்தக்கூடிய அனைத்து தடங்களும் வரிசையில் நிற்கப்படும். இல்லையெனில், சொடுக்கு செய்யப்பட்ட ஒன்று மட்டுமே வரிசையில் நிற்கப்படும்",
|
"doubleClickBehavior_description": "உண்மை என்றால், தட தேடலில் பொருந்தக்கூடிய அனைத்து தடங்களும் வரிசையில் நிற்கப்படும். இல்லையெனில், சொடுக்கு செய்யப்பட்ட ஒன்று மட்டுமே வரிசையில் நிற்கப்படும்",
|
||||||
@@ -705,14 +705,14 @@
|
|||||||
"rowIndex": "வரிசை அட்டவணை",
|
"rowIndex": "வரிசை அட்டவணை",
|
||||||
"size": "$ t (common.size)",
|
"size": "$ t (common.size)",
|
||||||
"trackNumber": "ட்ராக் எண்",
|
"trackNumber": "ட்ராக் எண்",
|
||||||
"year": "$ t (பொதுவானது.",
|
"year": "$ t (பொதுவானது",
|
||||||
"lastPlayed": "கடைசியாக விளையாடியது",
|
"lastPlayed": "கடைசியாக விளையாடியது",
|
||||||
"note": "$ t (பொதுவானது. குறிப்பு)",
|
"note": "$ t (பொதுவானது. குறிப்பு)",
|
||||||
"owner": "$ t (பொதுவானவர்)",
|
"owner": "$ t (பொதுவானவர்)",
|
||||||
"actions": "$ t (common.action_other)",
|
"actions": "$ t (common.action_other)",
|
||||||
"albumArtist": "$ t (entity.albumartist_one)",
|
"albumArtist": "$ t (entity.albumartist_one)",
|
||||||
"discNumber": "வட்டு எண்",
|
"discNumber": "வட்டு எண்",
|
||||||
"duration": "$ t (பொதுவானது.",
|
"duration": "$ t (பொதுவானது",
|
||||||
"favorite": "$ t (common.foavorite)",
|
"favorite": "$ t (common.foavorite)",
|
||||||
"genre": "$ t (entity.genre_one)",
|
"genre": "$ t (entity.genre_one)",
|
||||||
"path": "$ t (common.path)",
|
"path": "$ t (common.path)",
|
||||||
|
|||||||
@@ -111,7 +111,9 @@
|
|||||||
"preview": "预览",
|
"preview": "预览",
|
||||||
"translation": "翻译",
|
"translation": "翻译",
|
||||||
"additionalParticipants": "其他参与者",
|
"additionalParticipants": "其他参与者",
|
||||||
"tags": "标签"
|
"tags": "标签",
|
||||||
|
"viewReleaseNotes": "查看发行说明",
|
||||||
|
"newVersion": "已安装新版本 ({{version}})"
|
||||||
},
|
},
|
||||||
"entity": {
|
"entity": {
|
||||||
"albumArtist_other": "专辑艺术家",
|
"albumArtist_other": "专辑艺术家",
|
||||||
@@ -323,7 +325,7 @@
|
|||||||
"discordUpdateInterval": "{{discord}} rich presence 更新间隔",
|
"discordUpdateInterval": "{{discord}} rich presence 更新间隔",
|
||||||
"discordApplicationId_description": "{{discord}} rich presence 应用 id(默认为 {{defaultId}})",
|
"discordApplicationId_description": "{{discord}} rich presence 应用 id(默认为 {{defaultId}})",
|
||||||
"discordUpdateInterval_description": "更新间隔秒数(至少 15 秒)",
|
"discordUpdateInterval_description": "更新间隔秒数(至少 15 秒)",
|
||||||
"discordRichPresence_description": "在 {{discord}} rich presence 中显示播放状态。图片键为:{{icon}}、{{playing}} 和 {{paused}} ",
|
"discordRichPresence_description": "在 {{discord}} rich presence 中显示播放状态。图片键为:{{icon}}、{{playing}} 和 {{paused}}",
|
||||||
"accentColor": "强调色",
|
"accentColor": "强调色",
|
||||||
"accentColor_description": "设置应用的强调色",
|
"accentColor_description": "设置应用的强调色",
|
||||||
"replayGainPreamp_description": "调整应用在{{ReplayGain}}值上的前置放大增益",
|
"replayGainPreamp_description": "调整应用在{{ReplayGain}}值上的前置放大增益",
|
||||||
@@ -399,7 +401,13 @@
|
|||||||
"musicbrainz": "显示 musicbrainz 链接",
|
"musicbrainz": "显示 musicbrainz 链接",
|
||||||
"musicbrainz_description": "在 mbid 的艺术家/专辑页面上显示 musicbrainz 的链接",
|
"musicbrainz_description": "在 mbid 的艺术家/专辑页面上显示 musicbrainz 的链接",
|
||||||
"lastfm": "显示 last.fm 链接",
|
"lastfm": "显示 last.fm 链接",
|
||||||
"lastfm_description": "在艺术家/专辑页面上显示 last.fm 的链接"
|
"lastfm_description": "在艺术家/专辑页面上显示 last.fm 的链接",
|
||||||
|
"preferLocalLyrics_description": "优先选择本地歌词(如有),而不是远程歌词",
|
||||||
|
"preferLocalLyrics": "首选本地歌词",
|
||||||
|
"discordPausedStatus": "暂停时显示rich presence",
|
||||||
|
"discordPausedStatus_description": "启用后将在播放器暂停时显示状态",
|
||||||
|
"preservePitch": "保持音高",
|
||||||
|
"preservePitch_description": "在调整播放速度时保持音高"
|
||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
"remotePortWarning": "重启服务器使新端口生效",
|
"remotePortWarning": "重启服务器使新端口生效",
|
||||||
@@ -483,7 +491,8 @@
|
|||||||
"home": "$t(common.home)",
|
"home": "$t(common.home)",
|
||||||
"artists": "$t(entity.artist_other)",
|
"artists": "$t(entity.artist_other)",
|
||||||
"albumArtists": "$t(entity.albumArtist_other)",
|
"albumArtists": "$t(entity.albumArtist_other)",
|
||||||
"shared": "共享$t(entity.playlist_other)"
|
"shared": "共享$t(entity.playlist_other)",
|
||||||
|
"myLibrary": "我的媒体库"
|
||||||
},
|
},
|
||||||
"fullscreenPlayer": {
|
"fullscreenPlayer": {
|
||||||
"config": {
|
"config": {
|
||||||
@@ -659,7 +668,8 @@
|
|||||||
},
|
},
|
||||||
"queryEditor": {
|
"queryEditor": {
|
||||||
"input_optionMatchAll": "匹配全部",
|
"input_optionMatchAll": "匹配全部",
|
||||||
"input_optionMatchAny": "匹配任何"
|
"input_optionMatchAny": "匹配任何",
|
||||||
|
"title": "查询编辑器"
|
||||||
},
|
},
|
||||||
"editPlaylist": {
|
"editPlaylist": {
|
||||||
"title": "编辑$t(entity.playlist_one)",
|
"title": "编辑$t(entity.playlist_one)",
|
||||||
@@ -695,7 +705,9 @@
|
|||||||
"view": {
|
"view": {
|
||||||
"table": "表格",
|
"table": "表格",
|
||||||
"poster": "海报",
|
"poster": "海报",
|
||||||
"card": "卡片"
|
"card": "卡片",
|
||||||
|
"grid": "网格",
|
||||||
|
"list": "列表"
|
||||||
},
|
},
|
||||||
"label": {
|
"label": {
|
||||||
"releaseDate": "发布日期",
|
"releaseDate": "发布日期",
|
||||||
|
|||||||
@@ -261,7 +261,7 @@
|
|||||||
"discordApplicationId_description": "{{discord}} rich presence 應用 id(默認爲 {{defaultId}})",
|
"discordApplicationId_description": "{{discord}} rich presence 應用 id(默認爲 {{defaultId}})",
|
||||||
"discordIdleStatus": "顯示 rich presence 閑置狀態",
|
"discordIdleStatus": "顯示 rich presence 閑置狀態",
|
||||||
"discordIdleStatus_description": "啓用後將會在播放器閑置時更新狀態",
|
"discordIdleStatus_description": "啓用後將會在播放器閑置時更新狀態",
|
||||||
"discordRichPresence_description": "在 {{discord}} rich presence 中顯示播放狀態。圖片鍵爲:{{icon}}、{{playing}} 和 {{paused}} ",
|
"discordRichPresence_description": "在 {{discord}} rich presence 中顯示播放狀態。圖片鍵爲:{{icon}}、{{playing}} 和 {{paused}}",
|
||||||
"discordUpdateInterval": "{{discord}} rich presence 更新間隔",
|
"discordUpdateInterval": "{{discord}} rich presence 更新間隔",
|
||||||
"discordUpdateInterval_description": "更新間隔秒數(至少 15 秒)",
|
"discordUpdateInterval_description": "更新間隔秒數(至少 15 秒)",
|
||||||
"enableRemote": "啓用遠程控制服務器",
|
"enableRemote": "啓用遠程控制服務器",
|
||||||
|
|||||||
@@ -201,25 +201,23 @@ function mergeLyrics(original: string | undefined, translated: string | undefine
|
|||||||
return original;
|
return original;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Iterate through each line of the original LRC. If a translation exists for
|
// Iterate through each line of the original LRC. If a translation exists for the same timestamp, append the translated text after the original text.
|
||||||
// the same timestamp, insert it as a new, fully-formatted LRC line.
|
const finalLines = original.split('\n').map((line) => {
|
||||||
const finalLines = original.split('\n').flatMap((line) => {
|
|
||||||
const match = line.match(lrcLineRegex);
|
const match = line.match(lrcLineRegex);
|
||||||
|
|
||||||
if (match) {
|
if (match) {
|
||||||
const timestamp = match[1];
|
const timestamp = match[1];
|
||||||
|
const originalText = match[2].trim();
|
||||||
const translatedText = translatedMap.get(timestamp);
|
const translatedText = translatedMap.get(timestamp);
|
||||||
|
|
||||||
if (translatedText) {
|
if (translatedText && originalText) {
|
||||||
// Return an array containing both the original line and the new translated line.
|
// Append and add a break delimiter to separate the original and translated text
|
||||||
// flatMap will flatten this into the final array of lines.
|
return [`[${timestamp}]${originalText}`, translatedText].join('_BREAK_');
|
||||||
const translatedLine = `[${timestamp}]${translatedText}`;
|
|
||||||
return [line, translatedLine];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If no match or no translation is found, return only the original line.
|
// If no match or no translation is found, return the original line unchanged.
|
||||||
return [line];
|
return line;
|
||||||
});
|
});
|
||||||
|
|
||||||
return finalLines.join('\n');
|
return finalLines.join('\n');
|
||||||
|
|||||||
@@ -105,7 +105,7 @@ const createMpv = async (data: {
|
|||||||
try {
|
try {
|
||||||
await mpv.start();
|
await mpv.start();
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.log('mpv failed to start', error);
|
console.error('mpv failed to start', error);
|
||||||
} finally {
|
} finally {
|
||||||
await mpv.setMultipleProperties(properties || {});
|
await mpv.setMultipleProperties(properties || {});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -405,7 +405,7 @@ const enableServer = (config: RemoteConfig): Promise<void> => {
|
|||||||
}
|
}
|
||||||
case 'proxy': {
|
case 'proxy': {
|
||||||
const toFetch = currentState.song?.imageUrl?.replaceAll(
|
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,
|
'xesam:userRating': song.userRating ? song.userRating / 5 : null,
|
||||||
};
|
};
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log(err);
|
console.error(err);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -49,7 +49,7 @@ export default class AppUpdater {
|
|||||||
protocol.registerSchemesAsPrivileged([{ privileges: { bypassCSP: true }, scheme: 'feishin' }]);
|
protocol.registerSchemesAsPrivileged([{ privileges: { bypassCSP: true }, scheme: 'feishin' }]);
|
||||||
|
|
||||||
process.on('uncaughtException', (error: any) => {
|
process.on('uncaughtException', (error: any) => {
|
||||||
console.log('Error in main process', error);
|
console.error('Error in main process', error);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (store.get('ignore_ssl')) {
|
if (store.get('ignore_ssl')) {
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export const PlayerImage = ({ src }: PlayerImageProps) => {
|
|||||||
<img
|
<img
|
||||||
className={styles.container}
|
className={styles.container}
|
||||||
onError={() => send({ event: 'proxy' })}
|
onError={() => send({ event: 'proxy' })}
|
||||||
src={src?.replaceAll(/&(size|width|height=\d+)/g, '')}
|
src={src?.replaceAll(/&(size|width|height)=\d+/g, '')}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -398,8 +398,124 @@ export const SubsonicController: ControllerEndpoint = {
|
|||||||
totalRecordCount: null,
|
totalRecordCount: null,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
getAlbumListCount: async (args) =>
|
getAlbumListCount: async (args) => {
|
||||||
SubsonicController.getAlbumList(args).then((res) => res!.totalRecordCount!),
|
const { apiClientProps, query } = args;
|
||||||
|
|
||||||
|
if (query.searchTerm) {
|
||||||
|
let fetchNextPage = true;
|
||||||
|
let startIndex = 0;
|
||||||
|
let totalRecordCount = 0;
|
||||||
|
|
||||||
|
while (fetchNextPage) {
|
||||||
|
const res = await ssApiClient(apiClientProps).search3({
|
||||||
|
query: {
|
||||||
|
albumCount: 500,
|
||||||
|
albumOffset: startIndex,
|
||||||
|
artistCount: 0,
|
||||||
|
artistOffset: 0,
|
||||||
|
query: query.searchTerm || '""',
|
||||||
|
songCount: 0,
|
||||||
|
songOffset: 0,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.status !== 200) {
|
||||||
|
throw new Error('Failed to get album list count');
|
||||||
|
}
|
||||||
|
|
||||||
|
const albumCount = (res.body.searchResult3?.album || [])?.length;
|
||||||
|
|
||||||
|
totalRecordCount += albumCount;
|
||||||
|
startIndex += albumCount;
|
||||||
|
|
||||||
|
// The max limit size for Subsonic is 500
|
||||||
|
fetchNextPage = albumCount === 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
return totalRecordCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (query.favorite) {
|
||||||
|
const res = await ssApiClient(apiClientProps).getStarred({
|
||||||
|
query: {
|
||||||
|
musicFolderId: query.musicFolderId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.status !== 200) {
|
||||||
|
throw new Error('Failed to get album list');
|
||||||
|
}
|
||||||
|
|
||||||
|
return (res.body.starred?.album || []).length || 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
let type = AlbumListSortType.ALPHABETICAL_BY_NAME;
|
||||||
|
|
||||||
|
let fetchNextPage = true;
|
||||||
|
let startIndex = 0;
|
||||||
|
let totalRecordCount = 0;
|
||||||
|
|
||||||
|
if (query.genres?.length) {
|
||||||
|
type = AlbumListSortType.BY_GENRE;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (query.minYear || query.maxYear) {
|
||||||
|
type = AlbumListSortType.BY_YEAR;
|
||||||
|
}
|
||||||
|
|
||||||
|
let fromYear: number | undefined;
|
||||||
|
let toYear: number | undefined;
|
||||||
|
|
||||||
|
if (query.minYear) {
|
||||||
|
fromYear = query.minYear;
|
||||||
|
toYear = dayjs().year();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (query.maxYear) {
|
||||||
|
toYear = query.maxYear;
|
||||||
|
|
||||||
|
if (!query.minYear) {
|
||||||
|
fromYear = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
while (fetchNextPage) {
|
||||||
|
const res = await ssApiClient(apiClientProps).getAlbumList2({
|
||||||
|
query: {
|
||||||
|
fromYear,
|
||||||
|
genre: query.genres?.length ? query.genres[0] : undefined,
|
||||||
|
musicFolderId: query.musicFolderId,
|
||||||
|
offset: startIndex,
|
||||||
|
size: 500,
|
||||||
|
toYear,
|
||||||
|
type,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const headers = res.headers;
|
||||||
|
|
||||||
|
// Navidrome returns the total count in the header
|
||||||
|
if (headers.get('x-total-count')) {
|
||||||
|
fetchNextPage = false;
|
||||||
|
totalRecordCount = Number(headers.get('x-total-count'));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (res.status !== 200) {
|
||||||
|
throw new Error('Failed to get album list count');
|
||||||
|
}
|
||||||
|
|
||||||
|
const albumCount = res.body.albumList2.album.length;
|
||||||
|
|
||||||
|
totalRecordCount += albumCount;
|
||||||
|
startIndex += albumCount;
|
||||||
|
|
||||||
|
// The max limit size for Subsonic is 500
|
||||||
|
fetchNextPage = albumCount === 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
return totalRecordCount;
|
||||||
|
},
|
||||||
getArtistList: async (args) => {
|
getArtistList: async (args) => {
|
||||||
const { apiClientProps, query } = args;
|
const { apiClientProps, query } = args;
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ export const authenticationFailure = (currentServer: null | ServerListItem) => {
|
|||||||
if (currentServer) {
|
if (currentServer) {
|
||||||
const serverId = currentServer.id;
|
const serverId = currentServer.id;
|
||||||
const token = currentServer.ndCredential;
|
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.updateServer(serverId, { ndCredential: undefined });
|
||||||
useAuthStore.getState().actions.setCurrentServer(null);
|
useAuthStore.getState().actions.setCurrentServer(null);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -120,6 +120,7 @@ export const AudioPlayer = forwardRef<AudioPlayerRef, AudioPlayerProps>((props,
|
|||||||
const audioDeviceId = useSettingsStore((state) => state.playback.audioDeviceId);
|
const audioDeviceId = useSettingsStore((state) => state.playback.audioDeviceId);
|
||||||
const playback = useSettingsStore((state) => state.playback.mpvProperties);
|
const playback = useSettingsStore((state) => state.playback.mpvProperties);
|
||||||
const shouldUseWebAudio = useSettingsStore((state) => state.playback.webAudio);
|
const shouldUseWebAudio = useSettingsStore((state) => state.playback.webAudio);
|
||||||
|
const preservesPitch = useSettingsStore((state) => state.playback.preservePitch);
|
||||||
const { resetSampleRate } = useSettingsStoreActions();
|
const { resetSampleRate } = useSettingsStoreActions();
|
||||||
const playbackSpeed = useSpeed();
|
const playbackSpeed = useSpeed();
|
||||||
const { transcode } = usePlaybackSettings();
|
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)
|
// 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:
|
// 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.
|
// play() + play() in rapid succession will cause problems as the frist one ends the track.
|
||||||
player1Ref.current
|
const internalPlayer = player1Ref.current?.getInternalPlayer();
|
||||||
?.getInternalPlayer()
|
if (internalPlayer) {
|
||||||
?.play()
|
internalPlayer.preservesPitch = preservesPitch;
|
||||||
.catch(() => {});
|
internalPlayer.play().catch(() => {});
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
player2Ref.current
|
const internalPlayer = player2Ref.current?.getInternalPlayer();
|
||||||
?.getInternalPlayer()
|
if (internalPlayer) {
|
||||||
?.play()
|
internalPlayer.preservesPitch = preservesPitch;
|
||||||
.catch(() => {});
|
internalPlayer.play().catch(() => {});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
player1Ref.current?.getInternalPlayer()?.pause();
|
player1Ref.current?.getInternalPlayer()?.pause();
|
||||||
player2Ref.current?.getInternalPlayer()?.pause();
|
player2Ref.current?.getInternalPlayer()?.pause();
|
||||||
}
|
}
|
||||||
}, [currentPlayer, status]);
|
}, [currentPlayer, status, preservesPitch]);
|
||||||
|
|
||||||
const handleCrossfade1 = useCallback(
|
const handleCrossfade1 = useCallback(
|
||||||
(e: AudioPlayerProgress) => {
|
(e: AudioPlayerProgress) => {
|
||||||
|
|||||||
@@ -251,7 +251,7 @@ export const useVirtualTable = <TFilter extends BaseQuery<any>>({
|
|||||||
properties.table.pagination.itemsPerPage;
|
properties.table.pagination.itemsPerPage;
|
||||||
event.api?.ensureIndexVisible(currentPageStartIndex, 'top');
|
event.api?.ensureIndexVisible(currentPageStartIndex, 'top');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log(err);
|
console.error(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isSearchParams) {
|
if (isSearchParams) {
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ const RouteErrorBoundary = () => {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const error = useRouteError() as any;
|
const error = useRouteError() as any;
|
||||||
console.log('error', error);
|
console.error('error', error);
|
||||||
|
|
||||||
const handleReload = () => {
|
const handleReload = () => {
|
||||||
navigate(0);
|
navigate(0);
|
||||||
|
|||||||
@@ -62,7 +62,8 @@ const ActionRequiredRoute = () => {
|
|||||||
</Group>
|
</Group>
|
||||||
<Stack mt="2rem">
|
<Stack mt="2rem">
|
||||||
{canReturnHome && <Navigate to={AppRoute.HOME} />}
|
{canReturnHome && <Navigate to={AppRoute.HOME} />}
|
||||||
{!displayedCheck && (
|
{/* This should be displayed if a credential is required */}
|
||||||
|
{isCredentialRequired && (
|
||||||
<Group
|
<Group
|
||||||
justify="center"
|
justify="center"
|
||||||
wrap="nowrap"
|
wrap="nowrap"
|
||||||
|
|||||||
@@ -414,18 +414,23 @@ export const AlbumListHeaderFilters = ({
|
|||||||
Object.values(filter?._custom?.jellyfin).some((value) => value !== undefined);
|
Object.values(filter?._custom?.jellyfin).some((value) => value !== undefined);
|
||||||
|
|
||||||
const isSubsonicFilterApplied =
|
const isSubsonicFilterApplied =
|
||||||
server?.type === ServerType.SUBSONIC &&
|
server?.type === ServerType.SUBSONIC && (filter.maxYear || filter.minYear);
|
||||||
(filter.maxYear || filter.minYear || filter.favorite);
|
|
||||||
|
const isCompilationFilterApplied =
|
||||||
|
server?.type === ServerType.NAVIDROME && filter.compilation !== undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
isNavidromeFilterApplied ||
|
isNavidromeFilterApplied ||
|
||||||
isJellyfinFilterApplied ||
|
isJellyfinFilterApplied ||
|
||||||
isSubsonicFilterApplied ||
|
isSubsonicFilterApplied ||
|
||||||
filter.genres?.length
|
filter.genres?.length ||
|
||||||
|
filter.favorite !== undefined ||
|
||||||
|
isCompilationFilterApplied
|
||||||
);
|
);
|
||||||
}, [
|
}, [
|
||||||
filter?._custom?.jellyfin,
|
filter?._custom?.jellyfin,
|
||||||
filter?._custom?.navidrome,
|
filter?._custom?.navidrome,
|
||||||
|
filter.compilation,
|
||||||
filter.favorite,
|
filter.favorite,
|
||||||
filter.genres?.length,
|
filter.genres?.length,
|
||||||
filter.maxYear,
|
filter.maxYear,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import debounce from 'lodash/debounce';
|
import debounce from 'lodash/debounce';
|
||||||
import { ChangeEvent, useMemo, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { MultiSelectWithInvalidData } from '/@/renderer/components/select-with-invalid-data';
|
import { MultiSelectWithInvalidData } from '/@/renderer/components/select-with-invalid-data';
|
||||||
@@ -12,8 +12,8 @@ import { Group } from '/@/shared/components/group/group';
|
|||||||
import { NumberInput } from '/@/shared/components/number-input/number-input';
|
import { NumberInput } from '/@/shared/components/number-input/number-input';
|
||||||
import { SpinnerIcon } from '/@/shared/components/spinner/spinner';
|
import { SpinnerIcon } from '/@/shared/components/spinner/spinner';
|
||||||
import { Stack } from '/@/shared/components/stack/stack';
|
import { Stack } from '/@/shared/components/stack/stack';
|
||||||
import { Switch } from '/@/shared/components/switch/switch';
|
|
||||||
import { Text } from '/@/shared/components/text/text';
|
import { Text } from '/@/shared/components/text/text';
|
||||||
|
import { YesNoSelect } from '/@/shared/components/yes-no-select/yes-no-select';
|
||||||
import {
|
import {
|
||||||
AlbumArtistListSort,
|
AlbumArtistListSort,
|
||||||
AlbumListQuery,
|
AlbumListQuery,
|
||||||
@@ -72,15 +72,15 @@ export const JellyfinAlbumFilters = ({
|
|||||||
return filter?._custom?.jellyfin?.Tags?.split('|');
|
return filter?._custom?.jellyfin?.Tags?.split('|');
|
||||||
}, [filter?._custom?.jellyfin?.Tags]);
|
}, [filter?._custom?.jellyfin?.Tags]);
|
||||||
|
|
||||||
const toggleFilters = [
|
const yesNoFilter = [
|
||||||
{
|
{
|
||||||
label: t('filter.isFavorited', { postProcess: 'sentenceCase' }),
|
label: t('filter.isFavorited', { postProcess: 'sentenceCase' }),
|
||||||
onChange: (e: ChangeEvent<HTMLInputElement>) => {
|
onChange: (favorite?: boolean) => {
|
||||||
const updatedFilters = setFilter({
|
const updatedFilters = setFilter({
|
||||||
customFilters,
|
customFilters,
|
||||||
data: {
|
data: {
|
||||||
_custom: filter?._custom,
|
_custom: filter?._custom,
|
||||||
favorite: e.currentTarget.checked ? true : undefined,
|
favorite,
|
||||||
},
|
},
|
||||||
itemType: LibraryItem.ALBUM,
|
itemType: LibraryItem.ALBUM,
|
||||||
key: pageKey,
|
key: pageKey,
|
||||||
@@ -189,16 +189,16 @@ export const JellyfinAlbumFilters = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack p="0.8rem">
|
<Stack p="0.8rem">
|
||||||
{toggleFilters.map((filter) => (
|
{yesNoFilter.map((filter) => (
|
||||||
<Group
|
<Group
|
||||||
justify="space-between"
|
justify="space-between"
|
||||||
key={`nd-filter-${filter.label}`}
|
key={`nd-filter-${filter.label}`}
|
||||||
>
|
>
|
||||||
<Text>{filter.label}</Text>
|
<Text>{filter.label}</Text>
|
||||||
<Switch
|
<YesNoSelect
|
||||||
checked={filter?.value || false}
|
|
||||||
onChange={filter.onChange}
|
onChange={filter.onChange}
|
||||||
size="xs"
|
size="xs"
|
||||||
|
value={filter.value}
|
||||||
/>
|
/>
|
||||||
</Group>
|
</Group>
|
||||||
))}
|
))}
|
||||||
@@ -250,7 +250,7 @@ export const JellyfinAlbumFilters = ({
|
|||||||
searchValue={albumArtistSearchTerm}
|
searchValue={albumArtistSearchTerm}
|
||||||
/>
|
/>
|
||||||
</Group>
|
</Group>
|
||||||
{tagsQuery.data?.boolTags?.length && (
|
{tagsQuery.data?.boolTags && tagsQuery.data.boolTags.length > 0 && (
|
||||||
<Group grow>
|
<Group grow>
|
||||||
<MultiSelectWithInvalidData
|
<MultiSelectWithInvalidData
|
||||||
clearable
|
clearable
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { SpinnerIcon } from '/@/shared/components/spinner/spinner';
|
|||||||
import { Stack } from '/@/shared/components/stack/stack';
|
import { Stack } from '/@/shared/components/stack/stack';
|
||||||
import { Switch } from '/@/shared/components/switch/switch';
|
import { Switch } from '/@/shared/components/switch/switch';
|
||||||
import { Text } from '/@/shared/components/text/text';
|
import { Text } from '/@/shared/components/text/text';
|
||||||
|
import { YesNoSelect } from '/@/shared/components/yes-no-select/yes-no-select';
|
||||||
import {
|
import {
|
||||||
AlbumArtistListSort,
|
AlbumArtistListSort,
|
||||||
AlbumListQuery,
|
AlbumListQuery,
|
||||||
@@ -78,6 +79,41 @@ export const NavidromeAlbumFilters = ({
|
|||||||
serverId,
|
serverId,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const yesNoUndefinedFilters = [
|
||||||
|
{
|
||||||
|
label: t('filter.isFavorited', { postProcess: 'sentenceCase' }),
|
||||||
|
onChange: (favorite?: boolean) => {
|
||||||
|
const updatedFilters = setFilter({
|
||||||
|
customFilters,
|
||||||
|
data: {
|
||||||
|
_custom: filter._custom,
|
||||||
|
favorite,
|
||||||
|
},
|
||||||
|
itemType: LibraryItem.ALBUM,
|
||||||
|
key: pageKey,
|
||||||
|
}) as AlbumListFilter;
|
||||||
|
onFilterChange(updatedFilters);
|
||||||
|
},
|
||||||
|
value: filter.favorite,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('filter.isCompilation', { postProcess: 'sentenceCase' }),
|
||||||
|
onChange: (compilation?: boolean) => {
|
||||||
|
const updatedFilters = setFilter({
|
||||||
|
customFilters,
|
||||||
|
data: {
|
||||||
|
_custom: filter._custom,
|
||||||
|
compilation,
|
||||||
|
},
|
||||||
|
itemType: LibraryItem.ALBUM,
|
||||||
|
key: pageKey,
|
||||||
|
}) as AlbumListFilter;
|
||||||
|
onFilterChange(updatedFilters);
|
||||||
|
},
|
||||||
|
value: filter.compilation,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
const toggleFilters = [
|
const toggleFilters = [
|
||||||
{
|
{
|
||||||
label: t('filter.isRated', { postProcess: 'sentenceCase' }),
|
label: t('filter.isRated', { postProcess: 'sentenceCase' }),
|
||||||
@@ -100,38 +136,6 @@ export const NavidromeAlbumFilters = ({
|
|||||||
},
|
},
|
||||||
value: filter._custom?.navidrome?.has_rating,
|
value: filter._custom?.navidrome?.has_rating,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
label: t('filter.isFavorited', { postProcess: 'sentenceCase' }),
|
|
||||||
onChange: (e: ChangeEvent<HTMLInputElement>) => {
|
|
||||||
const updatedFilters = setFilter({
|
|
||||||
customFilters,
|
|
||||||
data: {
|
|
||||||
_custom: filter._custom,
|
|
||||||
favorite: e.currentTarget.checked ? true : undefined,
|
|
||||||
},
|
|
||||||
itemType: LibraryItem.ALBUM,
|
|
||||||
key: pageKey,
|
|
||||||
}) as AlbumListFilter;
|
|
||||||
onFilterChange(updatedFilters);
|
|
||||||
},
|
|
||||||
value: filter.favorite,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: t('filter.isCompilation', { postProcess: 'sentenceCase' }),
|
|
||||||
onChange: (e: ChangeEvent<HTMLInputElement>) => {
|
|
||||||
const updatedFilters = setFilter({
|
|
||||||
customFilters,
|
|
||||||
data: {
|
|
||||||
_custom: filter._custom,
|
|
||||||
compilation: e.currentTarget.checked ? true : undefined,
|
|
||||||
},
|
|
||||||
itemType: LibraryItem.ALBUM,
|
|
||||||
key: pageKey,
|
|
||||||
}) as AlbumListFilter;
|
|
||||||
onFilterChange(updatedFilters);
|
|
||||||
},
|
|
||||||
value: filter.compilation,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
label: t('filter.isRecentlyPlayed', { postProcess: 'sentenceCase' }),
|
label: t('filter.isRecentlyPlayed', { postProcess: 'sentenceCase' }),
|
||||||
onChange: (e: ChangeEvent<HTMLInputElement>) => {
|
onChange: (e: ChangeEvent<HTMLInputElement>) => {
|
||||||
@@ -236,6 +240,19 @@ export const NavidromeAlbumFilters = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack p="0.8rem">
|
<Stack p="0.8rem">
|
||||||
|
{yesNoUndefinedFilters.map((filter) => (
|
||||||
|
<Group
|
||||||
|
justify="space-between"
|
||||||
|
key={`nd-filter-${filter.label}`}
|
||||||
|
>
|
||||||
|
<Text>{filter.label}</Text>
|
||||||
|
<YesNoSelect
|
||||||
|
onChange={filter.onChange}
|
||||||
|
size="xs"
|
||||||
|
value={filter.value}
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
))}
|
||||||
{toggleFilters.map((filter) => (
|
{toggleFilters.map((filter) => (
|
||||||
<Group
|
<Group
|
||||||
justify="space-between"
|
justify="space-between"
|
||||||
|
|||||||
@@ -103,6 +103,7 @@ export const AlbumArtistDetailContent = ({ background }: AlbumArtistDetailConten
|
|||||||
},
|
},
|
||||||
query: {
|
query: {
|
||||||
artistIds: [routeId],
|
artistIds: [routeId],
|
||||||
|
compilation: false,
|
||||||
limit: 15,
|
limit: 15,
|
||||||
sortBy: AlbumListSort.RELEASE_DATE,
|
sortBy: AlbumListSort.RELEASE_DATE,
|
||||||
sortOrder: SortOrder.DESC,
|
sortOrder: SortOrder.DESC,
|
||||||
|
|||||||
@@ -24,8 +24,13 @@ export const useDiscordRpc = () => {
|
|||||||
current: (number | PlayerStatus | QueueSong | undefined)[],
|
current: (number | PlayerStatus | QueueSong | undefined)[],
|
||||||
previous: (number | PlayerStatus | QueueSong | undefined)[],
|
previous: (number | PlayerStatus | QueueSong | undefined)[],
|
||||||
) => {
|
) => {
|
||||||
// No current song, or we switched to a new track and the player was paused (end of album, etc.)
|
if (
|
||||||
if (!current[0] || (current[0] && current[2] === 'paused' && current[1] === 0))
|
!current[0] || // No track
|
||||||
|
(current[0] &&
|
||||||
|
current[2] === 'paused' && // Track paused
|
||||||
|
(discordSettings.showPaused ? current[1] === 0 : true)) || // Beginning of track (only if show paused setting enabled)
|
||||||
|
(discordSettings.showPaused ? false : current[1] === 0) // Beginning of track (only if show paused setting disabled)
|
||||||
|
)
|
||||||
return discordRpc?.clearActivity();
|
return discordRpc?.clearActivity();
|
||||||
|
|
||||||
// Handle change detection
|
// Handle change detection
|
||||||
@@ -122,6 +127,7 @@ export const useDiscordRpc = () => {
|
|||||||
[
|
[
|
||||||
discordSettings.showAsListening,
|
discordSettings.showAsListening,
|
||||||
discordSettings.showServerImage,
|
discordSettings.showServerImage,
|
||||||
|
discordSettings.showPaused,
|
||||||
generalSettings.lastfmApiKey,
|
generalSettings.lastfmApiKey,
|
||||||
lastUniqueId,
|
lastUniqueId,
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ const formatArtists = (artists: null | RelatedArtist[] | undefined) =>
|
|||||||
{artist.id ? (
|
{artist.id ? (
|
||||||
<Text
|
<Text
|
||||||
component={Link}
|
component={Link}
|
||||||
fw={500}
|
fw={700}
|
||||||
isLink
|
isLink
|
||||||
overflow="visible"
|
overflow="visible"
|
||||||
size="md"
|
size="md"
|
||||||
@@ -106,7 +106,7 @@ const FormatGenre = (item: Album | AlbumArtist | Playlist | Song) => {
|
|||||||
{index > 0 && <Separator />}
|
{index > 0 && <Separator />}
|
||||||
<Text
|
<Text
|
||||||
component={Link}
|
component={Link}
|
||||||
fw={500}
|
fw={700}
|
||||||
isLink
|
isLink
|
||||||
overflow="visible"
|
overflow="visible"
|
||||||
size="md"
|
size="md"
|
||||||
@@ -260,7 +260,7 @@ const SongPropertyMapping: ItemDetailRow<Song>[] = [
|
|||||||
song.album && (
|
song.album && (
|
||||||
<Text
|
<Text
|
||||||
component={Link}
|
component={Link}
|
||||||
fw={500}
|
fw={700}
|
||||||
isLink
|
isLink
|
||||||
overflow="visible"
|
overflow="visible"
|
||||||
size="md"
|
size="md"
|
||||||
|
|||||||
@@ -1,21 +1,23 @@
|
|||||||
.lyric-line {
|
.lyric-line {
|
||||||
padding: 0 1rem;
|
padding: 0 1rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
line-height: 1.2;
|
||||||
color: var(--theme-colors-foreground);
|
color: var(--theme-colors-foreground);
|
||||||
|
word-break: normal;
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
transition:
|
transition:
|
||||||
opacity 0.3s ease-in-out,
|
opacity 0.3s ease-in-out,
|
||||||
transform 0.3s ease-in-out;
|
transform 0.3s ease-in-out;
|
||||||
|
}
|
||||||
&.active {
|
|
||||||
opacity: 1;
|
.lyric-line:global(.active) {
|
||||||
}
|
opacity: 1 !important;
|
||||||
|
}
|
||||||
&.unsynchronized {
|
|
||||||
opacity: 1;
|
.lyric-line:global(.unsynchronized) {
|
||||||
}
|
opacity: 1;
|
||||||
|
}
|
||||||
&.synchronized {
|
|
||||||
cursor: pointer;
|
.lyric-line:global(.synchronized) {
|
||||||
}
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ import { ComponentPropsWithoutRef } from 'react';
|
|||||||
|
|
||||||
import styles from './lyric-line.module.css';
|
import styles from './lyric-line.module.css';
|
||||||
|
|
||||||
import { TextTitle } from '/@/shared/components/text-title/text-title';
|
import { Box } from '/@/shared/components/box/box';
|
||||||
|
import { Stack } from '/@/shared/components/stack/stack';
|
||||||
|
|
||||||
interface LyricLineProps extends ComponentPropsWithoutRef<'div'> {
|
interface LyricLineProps extends ComponentPropsWithoutRef<'div'> {
|
||||||
alignment: 'center' | 'left' | 'right';
|
alignment: 'center' | 'left' | 'right';
|
||||||
@@ -12,8 +13,10 @@ interface LyricLineProps extends ComponentPropsWithoutRef<'div'> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const LyricLine = ({ alignment, className, fontSize, text, ...props }: LyricLineProps) => {
|
export const LyricLine = ({ alignment, className, fontSize, text, ...props }: LyricLineProps) => {
|
||||||
|
const lines = text.split('_BREAK_');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TextTitle
|
<Box
|
||||||
className={clsx(styles.lyricLine, className)}
|
className={clsx(styles.lyricLine, className)}
|
||||||
style={{
|
style={{
|
||||||
fontSize,
|
fontSize,
|
||||||
@@ -21,7 +24,11 @@ export const LyricLine = ({ alignment, className, fontSize, text, ...props }: Ly
|
|||||||
}}
|
}}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{text}
|
<Stack gap={0}>
|
||||||
</TextTitle>
|
{lines.map((line, index) => (
|
||||||
|
<span key={index}>{line}</span>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ export const useSongLyricsBySong = (
|
|||||||
song: QueueSong | undefined,
|
song: QueueSong | undefined,
|
||||||
): UseQueryResult<FullLyricsMetadata | StructuredLyric[]> => {
|
): UseQueryResult<FullLyricsMetadata | StructuredLyric[]> => {
|
||||||
const { query } = args;
|
const { query } = args;
|
||||||
const { fetch } = useLyricsSettings();
|
const { fetch, preferLocalLyrics } = useLyricsSettings();
|
||||||
const server = getServerById(song?.serverId);
|
const server = getServerById(song?.serverId);
|
||||||
|
|
||||||
return useQuery({
|
return useQuery({
|
||||||
@@ -97,6 +97,9 @@ export const useSongLyricsBySong = (
|
|||||||
if (!server) throw new Error('Server not found');
|
if (!server) throw new Error('Server not found');
|
||||||
if (!song) return null;
|
if (!song) return null;
|
||||||
|
|
||||||
|
let localLyrics: FullLyricsMetadata | null | StructuredLyric[] = null;
|
||||||
|
let remoteLyrics: FullLyricsMetadata | null | StructuredLyric[] = null;
|
||||||
|
|
||||||
if (hasFeature(server, ServerFeature.LYRICS_MULTIPLE_STRUCTURED)) {
|
if (hasFeature(server, ServerFeature.LYRICS_MULTIPLE_STRUCTURED)) {
|
||||||
const subsonicLyrics = await api.controller
|
const subsonicLyrics = await api.controller
|
||||||
.getStructuredLyrics({
|
.getStructuredLyrics({
|
||||||
@@ -106,7 +109,7 @@ export const useSongLyricsBySong = (
|
|||||||
.catch(console.error);
|
.catch(console.error);
|
||||||
|
|
||||||
if (subsonicLyrics?.length) {
|
if (subsonicLyrics?.length) {
|
||||||
return subsonicLyrics;
|
localLyrics = subsonicLyrics;
|
||||||
}
|
}
|
||||||
} else if (hasFeature(server, ServerFeature.LYRICS_SINGLE_STRUCTURED)) {
|
} else if (hasFeature(server, ServerFeature.LYRICS_SINGLE_STRUCTURED)) {
|
||||||
const jfLyrics = await api.controller
|
const jfLyrics = await api.controller
|
||||||
@@ -114,10 +117,10 @@ export const useSongLyricsBySong = (
|
|||||||
apiClientProps: { server, signal },
|
apiClientProps: { server, signal },
|
||||||
query: { songId: song.id },
|
query: { songId: song.id },
|
||||||
})
|
})
|
||||||
.catch((err) => console.log(err));
|
.catch((err) => console.error(err));
|
||||||
|
|
||||||
if (jfLyrics) {
|
if (jfLyrics) {
|
||||||
return {
|
localLyrics = {
|
||||||
artist: song.artists?.[0]?.name,
|
artist: song.artists?.[0]?.name,
|
||||||
lyrics: jfLyrics,
|
lyrics: jfLyrics,
|
||||||
name: song.name,
|
name: song.name,
|
||||||
@@ -126,7 +129,7 @@ export const useSongLyricsBySong = (
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
} else if (song.lyrics) {
|
} else if (song.lyrics) {
|
||||||
return {
|
localLyrics = {
|
||||||
artist: song.artists?.[0]?.name,
|
artist: song.artists?.[0]?.name,
|
||||||
lyrics: formatLyrics(song.lyrics),
|
lyrics: formatLyrics(song.lyrics),
|
||||||
name: song.name,
|
name: song.name,
|
||||||
@@ -135,12 +138,16 @@ export const useSongLyricsBySong = (
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (preferLocalLyrics && localLyrics) {
|
||||||
|
return localLyrics;
|
||||||
|
}
|
||||||
|
|
||||||
if (fetch) {
|
if (fetch) {
|
||||||
const remoteLyricsResult: InternetProviderLyricResponse | null =
|
const remoteLyricsResult: InternetProviderLyricResponse | null =
|
||||||
await lyricsIpc?.getRemoteLyricsBySong(song);
|
await lyricsIpc?.getRemoteLyricsBySong(song);
|
||||||
|
|
||||||
if (remoteLyricsResult) {
|
if (remoteLyricsResult) {
|
||||||
return {
|
remoteLyrics = {
|
||||||
...remoteLyricsResult,
|
...remoteLyricsResult,
|
||||||
lyrics: formatLyrics(remoteLyricsResult.lyrics),
|
lyrics: formatLyrics(remoteLyricsResult.lyrics),
|
||||||
remote: true,
|
remote: true,
|
||||||
@@ -148,6 +155,14 @@ export const useSongLyricsBySong = (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (remoteLyrics) {
|
||||||
|
return remoteLyrics;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (localLyrics) {
|
||||||
|
return localLyrics;
|
||||||
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
queryKey: queryKeys.songs.lyrics(server?.id || '', query),
|
queryKey: queryKeys.songs.lyrics(server?.id || '', query),
|
||||||
@@ -183,9 +198,7 @@ export const useSongLyricsByRemoteId = (
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const remoteLyricsResult = await lyricsIpc?.getRemoteLyricsByRemoteId(
|
const remoteLyricsResult = await lyricsIpc?.getRemoteLyricsByRemoteId(query as any);
|
||||||
query as LyricGetQuery,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (remoteLyricsResult) {
|
if (remoteLyricsResult) {
|
||||||
return formatLyrics(remoteLyricsResult);
|
return formatLyrics(remoteLyricsResult);
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
.active {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
@@ -1,9 +1,8 @@
|
|||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import isElectron from 'is-electron';
|
import isElectron from 'is-electron';
|
||||||
import { useCallback, useEffect, useRef } from 'react';
|
import { Fragment, useCallback, useEffect, useRef } from 'react';
|
||||||
|
|
||||||
import styles from './synchronized-lyrics.module.css';
|
import styles from './synchronized-lyrics.module.css';
|
||||||
import './synchronized-lyrics.css';
|
|
||||||
|
|
||||||
import { LyricLine } from '/@/renderer/features/lyrics/lyric-line';
|
import { LyricLine } from '/@/renderer/features/lyrics/lyric-line';
|
||||||
import { useScrobble } from '/@/renderer/features/player/hooks/use-scrobble';
|
import { useScrobble } from '/@/renderer/features/player/hooks/use-scrobble';
|
||||||
@@ -339,7 +338,7 @@ export const SynchronizedLyrics = ({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{lyrics.map(([time, text], idx) => (
|
{lyrics.map(([time, text], idx) => (
|
||||||
<div key={idx}>
|
<Fragment key={idx}>
|
||||||
<LyricLine
|
<LyricLine
|
||||||
alignment={settings.alignment}
|
alignment={settings.alignment}
|
||||||
className="lyric-line synchronized"
|
className="lyric-line synchronized"
|
||||||
@@ -357,7 +356,7 @@ export const SynchronizedLyrics = ({
|
|||||||
text={translatedLyrics.split('\n')[idx]}
|
text={translatedLyrics.split('\n')[idx]}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</Fragment>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,8 +2,6 @@
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
object-fit: var(--theme-image-fit);
|
|
||||||
object-position: 50% 100%;
|
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
filter: drop-shadow(0 0 5px rgb(0 0 0 / 40%)) drop-shadow(0 0 5px rgb(0 0 0 / 40%));
|
filter: drop-shadow(0 0 5px rgb(0 0 0 / 40%)) drop-shadow(0 0 5px rgb(0 0 0 / 40%));
|
||||||
}
|
}
|
||||||
@@ -24,8 +22,13 @@
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
cursor: default;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
|
|
||||||
|
a {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
font-size: 3.5vh;
|
font-size: 3.5vh;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,9 +16,7 @@ import { Center } from '/@/shared/components/center/center';
|
|||||||
import { Flex } from '/@/shared/components/flex/flex';
|
import { Flex } from '/@/shared/components/flex/flex';
|
||||||
import { Group } from '/@/shared/components/group/group';
|
import { Group } from '/@/shared/components/group/group';
|
||||||
import { Icon } from '/@/shared/components/icon/icon';
|
import { Icon } from '/@/shared/components/icon/icon';
|
||||||
import { Image } from '/@/shared/components/image/image';
|
|
||||||
import { Stack } from '/@/shared/components/stack/stack';
|
import { Stack } from '/@/shared/components/stack/stack';
|
||||||
import { TextTitle } from '/@/shared/components/text-title/text-title';
|
|
||||||
import { Text } from '/@/shared/components/text/text';
|
import { Text } from '/@/shared/components/text/text';
|
||||||
import { PlayerData, QueueSong } from '/@/shared/types/domain-types';
|
import { PlayerData, QueueSong } from '/@/shared/types/domain-types';
|
||||||
|
|
||||||
@@ -52,9 +50,14 @@ const scaleImageUrl = (imageSize: number, url?: null | string) => {
|
|||||||
.replace(/&height=\d+/, `&height=${imageSize}`);
|
.replace(/&height=\d+/, `&height=${imageSize}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
const MotionImage = motion.create(Image);
|
const MotionImage = motion.img;
|
||||||
|
|
||||||
|
const ImageWithPlaceholder = ({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: HTMLMotionProps<'img'> & { placeholder?: string }) => {
|
||||||
|
const nativeAspectRatio = useSettingsStore((store) => store.general.nativeAspectRatio);
|
||||||
|
|
||||||
const ImageWithPlaceholder = ({ ...props }: HTMLMotionProps<'img'> & { placeholder?: string }) => {
|
|
||||||
if (!props.src) {
|
if (!props.src) {
|
||||||
return (
|
return (
|
||||||
<Center
|
<Center
|
||||||
@@ -76,7 +79,11 @@ const ImageWithPlaceholder = ({ ...props }: HTMLMotionProps<'img'> & { placehold
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<MotionImage
|
<MotionImage
|
||||||
className={styles.image}
|
className={clsx(styles.image, className)}
|
||||||
|
style={{
|
||||||
|
objectFit: nativeAspectRatio ? 'contain' : 'cover',
|
||||||
|
width: nativeAspectRatio ? 'auto' : '100%',
|
||||||
|
}}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -201,45 +208,35 @@ export const FullScreenPlayerImage = () => {
|
|||||||
</div>
|
</div>
|
||||||
<Stack
|
<Stack
|
||||||
className={styles.metadataContainer}
|
className={styles.metadataContainer}
|
||||||
gap="xs"
|
gap="md"
|
||||||
maw="100%"
|
maw="100%"
|
||||||
>
|
>
|
||||||
<TextTitle
|
<Text
|
||||||
fw={900}
|
fw={900}
|
||||||
order={1}
|
lh="1.2"
|
||||||
overflow="hidden"
|
overflow="hidden"
|
||||||
|
size="4xl"
|
||||||
w="100%"
|
w="100%"
|
||||||
>
|
>
|
||||||
{currentSong?.name}
|
{currentSong?.name}
|
||||||
</TextTitle>
|
</Text>
|
||||||
<TextTitle
|
<Text
|
||||||
component={Link}
|
component={Link}
|
||||||
fw={600}
|
|
||||||
isLink
|
isLink
|
||||||
order={3}
|
|
||||||
overflow="hidden"
|
overflow="hidden"
|
||||||
style={{
|
size="xl"
|
||||||
textShadow: 'var(--theme-fullscreen-player-text-shadow)',
|
|
||||||
}}
|
|
||||||
to={generatePath(AppRoute.LIBRARY_ALBUMS_DETAIL, {
|
to={generatePath(AppRoute.LIBRARY_ALBUMS_DETAIL, {
|
||||||
albumId: currentSong?.albumId || '',
|
albumId: currentSong?.albumId || '',
|
||||||
})}
|
})}
|
||||||
w="100%"
|
w="100%"
|
||||||
>
|
>
|
||||||
{currentSong?.album}{' '}
|
{currentSong?.album}
|
||||||
</TextTitle>
|
</Text>
|
||||||
<TextTitle
|
<Text key="fs-artists">
|
||||||
key="fs-artists"
|
|
||||||
order={3}
|
|
||||||
style={{
|
|
||||||
textShadow: 'var(--theme-fullscreen-player-text-shadow)',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{currentSong?.artists?.map((artist, index) => (
|
{currentSong?.artists?.map((artist, index) => (
|
||||||
<Fragment key={`fs-artist-${artist.id}`}>
|
<Fragment key={`fs-artist-${artist.id}`}>
|
||||||
{index > 0 && (
|
{index > 0 && (
|
||||||
<Text
|
<Text
|
||||||
isMuted
|
|
||||||
style={{
|
style={{
|
||||||
display: 'inline-block',
|
display: 'inline-block',
|
||||||
padding: '0 0.5rem',
|
padding: '0 0.5rem',
|
||||||
@@ -250,12 +247,7 @@ export const FullScreenPlayerImage = () => {
|
|||||||
)}
|
)}
|
||||||
<Text
|
<Text
|
||||||
component={Link}
|
component={Link}
|
||||||
fw={600}
|
|
||||||
isLink
|
isLink
|
||||||
isMuted
|
|
||||||
style={{
|
|
||||||
textShadow: 'var(--theme-fullscreen-player-text-shadow)',
|
|
||||||
}}
|
|
||||||
to={generatePath(AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL, {
|
to={generatePath(AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL, {
|
||||||
albumArtistId: artist.id,
|
albumArtistId: artist.id,
|
||||||
})}
|
})}
|
||||||
@@ -264,7 +256,7 @@ export const FullScreenPlayerImage = () => {
|
|||||||
</Text>
|
</Text>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
))}
|
))}
|
||||||
</TextTitle>
|
</Text>
|
||||||
<Group
|
<Group
|
||||||
justify="center"
|
justify="center"
|
||||||
mt="sm"
|
mt="sm"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useHotkeys } from '@mantine/hooks';
|
import { useHotkeys } from '@mantine/hooks';
|
||||||
import { motion, Variants } from 'motion/react';
|
import { motion, Variants } from 'motion/react';
|
||||||
import { CSSProperties, useLayoutEffect, useRef } from 'react';
|
import { CSSProperties, useLayoutEffect, useRef, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useLocation } from 'react-router';
|
import { useLocation } from 'react-router';
|
||||||
|
|
||||||
@@ -32,7 +32,11 @@ import { Platform } from '/@/shared/types/types';
|
|||||||
|
|
||||||
const mainBackground = 'var(--theme-colors-background)';
|
const mainBackground = 'var(--theme-colors-background)';
|
||||||
|
|
||||||
const Controls = () => {
|
interface ControlsProps {
|
||||||
|
isPageHovered: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Controls = ({ isPageHovered }: ControlsProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const {
|
const {
|
||||||
dynamicBackground,
|
dynamicBackground,
|
||||||
@@ -77,7 +81,7 @@ const Controls = () => {
|
|||||||
iconProps={{ size: 'lg' }}
|
iconProps={{ size: 'lg' }}
|
||||||
onClick={handleToggleFullScreenPlayer}
|
onClick={handleToggleFullScreenPlayer}
|
||||||
tooltip={{ label: t('common.minimize', { postProcess: 'titleCase' }) }}
|
tooltip={{ label: t('common.minimize', { postProcess: 'titleCase' }) }}
|
||||||
variant="subtle"
|
variant={isPageHovered ? 'default' : 'subtle'}
|
||||||
/>
|
/>
|
||||||
<Popover position="bottom-start">
|
<Popover position="bottom-start">
|
||||||
<Popover.Target>
|
<Popover.Target>
|
||||||
@@ -85,7 +89,7 @@ const Controls = () => {
|
|||||||
icon="settings"
|
icon="settings"
|
||||||
iconProps={{ size: 'lg' }}
|
iconProps={{ size: 'lg' }}
|
||||||
tooltip={{ label: t('common.configure', { postProcess: 'titleCase' }) }}
|
tooltip={{ label: t('common.configure', { postProcess: 'titleCase' }) }}
|
||||||
variant="subtle"
|
variant={isPageHovered ? 'default' : 'subtle'}
|
||||||
/>
|
/>
|
||||||
</Popover.Target>
|
</Popover.Target>
|
||||||
<Popover.Dropdown>
|
<Popover.Dropdown>
|
||||||
@@ -410,6 +414,8 @@ export const FullScreenPlayer = () => {
|
|||||||
const { setStore } = useFullScreenPlayerStoreActions();
|
const { setStore } = useFullScreenPlayerStoreActions();
|
||||||
const { windowBarStyle } = useWindowSettings();
|
const { windowBarStyle } = useWindowSettings();
|
||||||
|
|
||||||
|
const [isPageHovered, setIsPageHovered] = useState(false);
|
||||||
|
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const isOpenedRef = useRef<boolean | null>(null);
|
const isOpenedRef = useRef<boolean | null>(null);
|
||||||
|
|
||||||
@@ -441,10 +447,12 @@ export const FullScreenPlayer = () => {
|
|||||||
custom={{ background, backgroundImage, dynamicBackground, windowBarStyle }}
|
custom={{ background, backgroundImage, dynamicBackground, windowBarStyle }}
|
||||||
exit="closed"
|
exit="closed"
|
||||||
initial="closed"
|
initial="closed"
|
||||||
|
onMouseEnter={() => setIsPageHovered(true)}
|
||||||
|
onMouseLeave={() => setIsPageHovered(false)}
|
||||||
transition={{ duration: 2 }}
|
transition={{ duration: 2 }}
|
||||||
variants={containerVariants}
|
variants={containerVariants}
|
||||||
>
|
>
|
||||||
<Controls />
|
<Controls isPageHovered={isPageHovered} />
|
||||||
{dynamicBackground && (
|
{dynamicBackground && (
|
||||||
<div
|
<div
|
||||||
className={styles.backgroundImageOverlay}
|
className={styles.backgroundImageOverlay}
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ export const LeftControls = () => {
|
|||||||
<LayoutGroup>
|
<LayoutGroup>
|
||||||
<AnimatePresence
|
<AnimatePresence
|
||||||
initial={false}
|
initial={false}
|
||||||
mode="wait"
|
mode="popLayout"
|
||||||
>
|
>
|
||||||
{!hideImage && (
|
{!hideImage && (
|
||||||
<div className={styles.imageWrapper}>
|
<div className={styles.imageWrapper}>
|
||||||
@@ -83,7 +83,7 @@ export const LeftControls = () => {
|
|||||||
key="playerbar-image"
|
key="playerbar-image"
|
||||||
onClick={handleToggleFullScreenPlayer}
|
onClick={handleToggleFullScreenPlayer}
|
||||||
role="button"
|
role="button"
|
||||||
transition={{ duration: 0.3, ease: 'easeInOut' }}
|
transition={{ duration: 0.2, ease: 'easeIn' }}
|
||||||
>
|
>
|
||||||
<Tooltip
|
<Tooltip
|
||||||
label={t('player.toggleFullscreenPlayer', {
|
label={t('player.toggleFullscreenPlayer', {
|
||||||
|
|||||||
@@ -37,6 +37,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.main {
|
.main {
|
||||||
|
background: var(--theme-colors-foreground) !important;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
|
|
||||||
svg {
|
svg {
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ interface PlayerButtonProps extends Omit<ActionIconProps, 'icon' | 'variant'> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const PlayerButton = forwardRef<HTMLButtonElement, PlayerButtonProps>(
|
export const PlayerButton = forwardRef<HTMLButtonElement, PlayerButtonProps>(
|
||||||
({ icon, isActive, tooltip, variant, ...rest }: PlayerButtonProps) => {
|
({ icon, isActive, tooltip, variant, ...rest }: PlayerButtonProps, ref) => {
|
||||||
if (tooltip) {
|
if (tooltip) {
|
||||||
return (
|
return (
|
||||||
<Tooltip {...tooltip}>
|
<Tooltip {...tooltip}>
|
||||||
@@ -23,6 +23,7 @@ export const PlayerButton = forwardRef<HTMLButtonElement, PlayerButtonProps>(
|
|||||||
className={clsx({
|
className={clsx({
|
||||||
[styles.active]: isActive,
|
[styles.active]: isActive,
|
||||||
})}
|
})}
|
||||||
|
ref={ref}
|
||||||
{...rest}
|
{...rest}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
@@ -41,6 +42,7 @@ export const PlayerButton = forwardRef<HTMLButtonElement, PlayerButtonProps>(
|
|||||||
className={clsx(styles.playerButton, styles[variant], {
|
className={clsx(styles.playerButton, styles[variant], {
|
||||||
[styles.active]: isActive,
|
[styles.active]: isActive,
|
||||||
})}
|
})}
|
||||||
|
ref={ref}
|
||||||
{...rest}
|
{...rest}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
@@ -58,18 +60,23 @@ interface PlayButtonProps extends Omit<ActionIconProps, 'icon' | 'variant'> {
|
|||||||
isPaused?: boolean;
|
isPaused?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PlayButton = ({ isPaused, ...props }: PlayButtonProps) => {
|
export const PlayButton = forwardRef<HTMLButtonElement, PlayButtonProps>(
|
||||||
return (
|
({ isPaused, ...props }: PlayButtonProps, ref) => {
|
||||||
<ActionIcon
|
return (
|
||||||
className={styles.main}
|
<ActionIcon
|
||||||
icon={isPaused ? 'mediaPlay' : 'mediaPause'}
|
className={styles.main}
|
||||||
tooltip={
|
icon={isPaused ? 'mediaPlay' : 'mediaPause'}
|
||||||
isPaused
|
iconProps={{
|
||||||
? t('player.play', { postProcess: 'sentenceCase' })
|
size: 'lg',
|
||||||
: t('player.pause', { postProcess: 'sentenceCase' })
|
}}
|
||||||
}
|
ref={ref}
|
||||||
variant="white"
|
tooltip={{
|
||||||
{...props}
|
label: isPaused
|
||||||
/>
|
? (t('player.play', { postProcess: 'sentenceCase' }) as string)
|
||||||
);
|
: (t('player.pause', { postProcess: 'sentenceCase' }) as string),
|
||||||
};
|
}}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import AudioMotionAnalyzer from 'audiomotion-analyzer';
|
import AudioMotionAnalyzer from 'audiomotion-analyzer';
|
||||||
import { createRef, useCallback, useEffect, useState } from 'react';
|
import { createRef, useEffect, useState } from 'react';
|
||||||
|
|
||||||
import styles from './visualizer.module.css';
|
import styles from './visualizer.module.css';
|
||||||
|
|
||||||
@@ -12,8 +12,6 @@ export const Visualizer = () => {
|
|||||||
const accent = useSettingsStore((store) => store.general.accent);
|
const accent = useSettingsStore((store) => store.general.accent);
|
||||||
const [motion, setMotion] = useState<AudioMotionAnalyzer>();
|
const [motion, setMotion] = useState<AudioMotionAnalyzer>();
|
||||||
|
|
||||||
const [, setLength] = useState(500);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const { context, gain } = webAudio || {};
|
const { context, gain } = webAudio || {};
|
||||||
if (gain && context && canvasRef.current && !motion) {
|
if (gain && context && canvasRef.current && !motion) {
|
||||||
@@ -35,28 +33,6 @@ export const Visualizer = () => {
|
|||||||
return () => {};
|
return () => {};
|
||||||
}, [accent, canvasRef, motion, webAudio]);
|
}, [accent, canvasRef, motion, webAudio]);
|
||||||
|
|
||||||
const resize = useCallback(() => {
|
|
||||||
const body = document.querySelector('.full-screen-player-queue-container');
|
|
||||||
const header = document.querySelector('.full-screen-player-queue-header');
|
|
||||||
|
|
||||||
if (body && header) {
|
|
||||||
const width = body.clientWidth - 30;
|
|
||||||
const height = body.clientHeight - header.clientHeight - 30;
|
|
||||||
|
|
||||||
setLength(Math.min(width, height));
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
resize();
|
|
||||||
|
|
||||||
window.addEventListener('resize', resize);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
window.removeEventListener('resize', resize);
|
|
||||||
};
|
|
||||||
}, [resize]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={styles.container}
|
className={styles.container}
|
||||||
|
|||||||
@@ -207,7 +207,7 @@ export const PlaylistDetailSongListContent = ({ songs, tableRef }: PlaylistDetai
|
|||||||
const currentPageStartIndex = pagination.currentPage * pagination.itemsPerPage;
|
const currentPageStartIndex = pagination.currentPage * pagination.itemsPerPage;
|
||||||
event.api?.ensureIndexVisible(currentPageStartIndex, 'top');
|
event.api?.ensureIndexVisible(currentPageStartIndex, 'top');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log(err);
|
console.error(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
setPagination(playlistId, {
|
setPagination(playlistId, {
|
||||||
|
|||||||
@@ -155,6 +155,26 @@ export const AudioSettings = ({ hasFancyAudio }: { hasFancyAudio: boolean }) =>
|
|||||||
postProcess: 'sentenceCase',
|
postProcess: 'sentenceCase',
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
control: (
|
||||||
|
<Switch
|
||||||
|
defaultChecked={settings.preservePitch}
|
||||||
|
onChange={(e) => {
|
||||||
|
setSettings({
|
||||||
|
playback: { ...settings, preservePitch: e.currentTarget.checked },
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
description: t('setting.preservePitch', {
|
||||||
|
context: 'description',
|
||||||
|
postProcess: 'sentenceCase',
|
||||||
|
}),
|
||||||
|
isHidden: settings.type !== PlaybackType.WEB,
|
||||||
|
title: t('setting.preservePitch', {
|
||||||
|
postProcess: 'sentenceCase',
|
||||||
|
}),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
control: (
|
control: (
|
||||||
<Slider
|
<Slider
|
||||||
|
|||||||
@@ -43,6 +43,28 @@ export const LyricSettings = () => {
|
|||||||
}),
|
}),
|
||||||
title: t('setting.followLyric', { postProcess: 'sentenceCase' }),
|
title: t('setting.followLyric', { postProcess: 'sentenceCase' }),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
control: (
|
||||||
|
<Switch
|
||||||
|
aria-label="Prefer local lyrics"
|
||||||
|
defaultChecked={settings.preferLocalLyrics}
|
||||||
|
onChange={(e) => {
|
||||||
|
setSettings({
|
||||||
|
lyrics: {
|
||||||
|
...settings,
|
||||||
|
preferLocalLyrics: e.currentTarget.checked,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
description: t('setting.preferLocalLyrics', {
|
||||||
|
context: 'description',
|
||||||
|
postProcess: 'sentenceCase',
|
||||||
|
}),
|
||||||
|
isHidden: !isElectron(),
|
||||||
|
title: t('setting.preferLocalLyrics', { postProcess: 'sentenceCase' }),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
control: (
|
control: (
|
||||||
<Switch
|
<Switch
|
||||||
|
|||||||
@@ -74,6 +74,29 @@ export const DiscordSettings = () => {
|
|||||||
postProcess: 'sentenceCase',
|
postProcess: 'sentenceCase',
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
control: (
|
||||||
|
<Switch
|
||||||
|
checked={settings.showPaused}
|
||||||
|
onChange={(e) => {
|
||||||
|
setSettings({
|
||||||
|
discord: {
|
||||||
|
...settings,
|
||||||
|
showPaused: e.currentTarget.checked,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
description: t('setting.discordPausedStatus', {
|
||||||
|
context: 'description',
|
||||||
|
postProcess: 'sentenceCase',
|
||||||
|
}),
|
||||||
|
isHidden: !isElectron(),
|
||||||
|
title: t('setting.discordPausedStatus', {
|
||||||
|
postProcess: 'sentenceCase',
|
||||||
|
}),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
control: (
|
control: (
|
||||||
<Switch
|
<Switch
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ export const FolderButton = ({ isActive, ...props }: FolderButtonProps) => {
|
|||||||
...props.iconProps,
|
...props.iconProps,
|
||||||
}}
|
}}
|
||||||
tooltip={{
|
tooltip={{
|
||||||
label: t('entity.folder', { postProcess: 'sentenceCase' }),
|
label: t('entity.folder', { count: 1, postProcess: 'sentenceCase' }),
|
||||||
...props.tooltip,
|
...props.tooltip,
|
||||||
}}
|
}}
|
||||||
variant="subtle"
|
variant="subtle"
|
||||||
|
|||||||
@@ -14,8 +14,7 @@ export const PlayButton = ({ className, ...props }: PlayButtonProps) => {
|
|||||||
className={clsx(styles.button, className)}
|
className={clsx(styles.button, className)}
|
||||||
icon="mediaPlay"
|
icon="mediaPlay"
|
||||||
iconProps={{
|
iconProps={{
|
||||||
fill: 'default',
|
size: 'xl',
|
||||||
size: 'lg',
|
|
||||||
}}
|
}}
|
||||||
variant="filled"
|
variant="filled"
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
max-height: calc(100vh - 119px);
|
max-height: calc(100vh - 119px);
|
||||||
user-select: none;
|
user-select: none;
|
||||||
background: var(--theme-colors-background-alternate);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-container.web,
|
.sidebar-container.web,
|
||||||
|
|||||||
@@ -9,6 +9,20 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.inner {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-start;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
display: block;
|
||||||
|
align-content: center;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
.link {
|
.link {
|
||||||
display: flex;
|
display: flex;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|||||||
@@ -10,13 +10,20 @@ interface SidebarItemProps extends ButtonProps {
|
|||||||
to: LinkProps['to'];
|
to: LinkProps['to'];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SidebarItem = ({ children, to, ...props }: SidebarItemProps) => {
|
export const SidebarItem = ({ children, className, to, ...props }: SidebarItemProps) => {
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
className={clsx({
|
className={clsx(
|
||||||
[styles.disabled]: props.disabled,
|
{
|
||||||
[styles.link]: true,
|
[styles.disabled]: props.disabled,
|
||||||
})}
|
[styles.link]: true,
|
||||||
|
},
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
classNames={{
|
||||||
|
inner: styles.inner,
|
||||||
|
label: styles.label,
|
||||||
|
}}
|
||||||
component={Link}
|
component={Link}
|
||||||
to={to}
|
to={to}
|
||||||
variant="subtle"
|
variant="subtle"
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
@value label from './sidebar-item.module.css';
|
||||||
|
|
||||||
.list {
|
.list {
|
||||||
padding: var(--theme-spacing-sm) var(--theme-spacing-md);
|
padding: var(--theme-spacing-sm) var(--theme-spacing-md);
|
||||||
}
|
}
|
||||||
@@ -8,6 +10,12 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.row-hover {
|
||||||
|
:global(.label) {
|
||||||
|
margin-right: 135px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.controls {
|
.controls {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { closeAllModals, openModal } from '@mantine/modals';
|
import { closeAllModals, openModal } from '@mantine/modals';
|
||||||
|
import clsx from 'clsx';
|
||||||
import { MouseEvent, useCallback, useMemo, useState } from 'react';
|
import { MouseEvent, useCallback, useMemo, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { generatePath } from 'react-router';
|
import { generatePath } from 'react-router';
|
||||||
@@ -43,6 +44,9 @@ const PlaylistRowButton = ({ name, onPlay, to, ...props }: PlaylistRowButtonProp
|
|||||||
onMouseLeave={() => setIsHovered(false)}
|
onMouseLeave={() => setIsHovered(false)}
|
||||||
>
|
>
|
||||||
<SidebarItem
|
<SidebarItem
|
||||||
|
className={clsx({
|
||||||
|
[styles.rowHover]: isHovered,
|
||||||
|
})}
|
||||||
to={url}
|
to={url}
|
||||||
variant="subtle"
|
variant="subtle"
|
||||||
{...props}
|
{...props}
|
||||||
@@ -181,7 +185,9 @@ export const SidebarPlaylistList = () => {
|
|||||||
const owned: Array<[boolean, () => void] | Playlist> = [];
|
const owned: Array<[boolean, () => void] | Playlist> = [];
|
||||||
|
|
||||||
for (const playlist of data.items) {
|
for (const playlist of data.items) {
|
||||||
owned.push(playlist);
|
if (!playlist.owner || playlist.owner === server.username) {
|
||||||
|
owned.push(playlist);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return { ...base, items: owned };
|
return { ...base, items: owned };
|
||||||
|
|||||||
@@ -8,6 +8,10 @@
|
|||||||
background: var(--theme-colors-background-alternate);
|
background: var(--theme-colors-background-alternate);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.container.custom-bar {
|
||||||
|
max-height: calc(100vh - 120px);
|
||||||
|
}
|
||||||
|
|
||||||
.scroll-area {
|
.scroll-area {
|
||||||
padding: 0 var(--theme-spacing-md) var(--theme-spacing-md) var(--theme-spacing-md);
|
padding: 0 var(--theme-spacing-md) var(--theme-spacing-md) var(--theme-spacing-md);
|
||||||
}
|
}
|
||||||
@@ -24,6 +28,7 @@
|
|||||||
|
|
||||||
.image-container {
|
.image-container {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
width: var(--sidebar-image-height);
|
||||||
height: var(--sidebar-image-height);
|
height: var(--sidebar-image-height);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
animation: fade-in 0.2s ease-in-out;
|
animation: fade-in 0.2s ease-in-out;
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import clsx from 'clsx';
|
||||||
import { AnimatePresence, motion } from 'motion/react';
|
import { AnimatePresence, motion } from 'motion/react';
|
||||||
import { CSSProperties, useMemo } from 'react';
|
import { CSSProperties, useMemo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
@@ -19,13 +20,18 @@ import {
|
|||||||
useSetFullScreenPlayerStore,
|
useSetFullScreenPlayerStore,
|
||||||
useSidebarStore,
|
useSidebarStore,
|
||||||
} from '/@/renderer/store';
|
} from '/@/renderer/store';
|
||||||
import { SidebarItemType, useGeneralSettings } from '/@/renderer/store/settings.store';
|
import {
|
||||||
|
SidebarItemType,
|
||||||
|
useGeneralSettings,
|
||||||
|
useWindowSettings,
|
||||||
|
} from '/@/renderer/store/settings.store';
|
||||||
import { Accordion } from '/@/shared/components/accordion/accordion';
|
import { Accordion } from '/@/shared/components/accordion/accordion';
|
||||||
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
|
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
|
||||||
import { Group } from '/@/shared/components/group/group';
|
import { Group } from '/@/shared/components/group/group';
|
||||||
import { ScrollArea } from '/@/shared/components/scroll-area/scroll-area';
|
import { ScrollArea } from '/@/shared/components/scroll-area/scroll-area';
|
||||||
import { Text } from '/@/shared/components/text/text';
|
import { Text } from '/@/shared/components/text/text';
|
||||||
import { Tooltip } from '/@/shared/components/tooltip/tooltip';
|
import { Tooltip } from '/@/shared/components/tooltip/tooltip';
|
||||||
|
import { Platform } from '/@/shared/types/types';
|
||||||
|
|
||||||
export const Sidebar = () => {
|
export const Sidebar = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -64,6 +70,7 @@ export const Sidebar = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const { sidebarItems } = useGeneralSettings();
|
const { sidebarItems } = useGeneralSettings();
|
||||||
|
const { windowBarStyle } = useWindowSettings();
|
||||||
|
|
||||||
const sidebarItemsWithRoute: SidebarItemType[] = useMemo(() => {
|
const sidebarItemsWithRoute: SidebarItemType[] = useMemo(() => {
|
||||||
if (!sidebarItems) return [];
|
if (!sidebarItems) return [];
|
||||||
@@ -80,9 +87,26 @@ export const Sidebar = () => {
|
|||||||
return items;
|
return items;
|
||||||
}, [sidebarItems, translatedSidebarItemMap]);
|
}, [sidebarItems, translatedSidebarItemMap]);
|
||||||
|
|
||||||
|
const scrollAreaHeight = useMemo(() => {
|
||||||
|
if (showImage) {
|
||||||
|
if (windowBarStyle === Platform.WINDOWS || windowBarStyle === Platform.MACOS) {
|
||||||
|
return `calc(100% - 105px - ${sidebar.leftWidth})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `calc(100% - ${sidebar.leftWidth})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return '100%';
|
||||||
|
}, [showImage, sidebar.leftWidth, windowBarStyle]);
|
||||||
|
|
||||||
|
const isCustomWindowBar =
|
||||||
|
windowBarStyle === Platform.WINDOWS || windowBarStyle === Platform.MACOS;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={styles.container}
|
className={clsx(styles.container, {
|
||||||
|
[styles.customBar]: isCustomWindowBar,
|
||||||
|
})}
|
||||||
id="left-sidebar"
|
id="left-sidebar"
|
||||||
>
|
>
|
||||||
<Group
|
<Group
|
||||||
@@ -95,7 +119,7 @@ export const Sidebar = () => {
|
|||||||
allowDragScroll
|
allowDragScroll
|
||||||
className={styles.scrollArea}
|
className={styles.scrollArea}
|
||||||
style={{
|
style={{
|
||||||
maxHeight: showImage ? `calc(100vh - 90px - ${sidebar.leftWidth})` : '100%',
|
height: scrollAreaHeight,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Accordion
|
<Accordion
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import debounce from 'lodash/debounce';
|
import debounce from 'lodash/debounce';
|
||||||
import { ChangeEvent, useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { MultiSelectWithInvalidData } from '/@/renderer/components/select-with-invalid-data';
|
import { MultiSelectWithInvalidData } from '/@/renderer/components/select-with-invalid-data';
|
||||||
@@ -10,8 +10,8 @@ import { Divider } from '/@/shared/components/divider/divider';
|
|||||||
import { Group } from '/@/shared/components/group/group';
|
import { Group } from '/@/shared/components/group/group';
|
||||||
import { NumberInput } from '/@/shared/components/number-input/number-input';
|
import { NumberInput } from '/@/shared/components/number-input/number-input';
|
||||||
import { Stack } from '/@/shared/components/stack/stack';
|
import { Stack } from '/@/shared/components/stack/stack';
|
||||||
import { Switch } from '/@/shared/components/switch/switch';
|
|
||||||
import { Text } from '/@/shared/components/text/text';
|
import { Text } from '/@/shared/components/text/text';
|
||||||
|
import { YesNoSelect } from '/@/shared/components/yes-no-select/yes-no-select';
|
||||||
import { GenreListSort, LibraryItem, SongListQuery, SortOrder } from '/@/shared/types/domain-types';
|
import { GenreListSort, LibraryItem, SongListQuery, SortOrder } from '/@/shared/types/domain-types';
|
||||||
|
|
||||||
interface JellyfinSongFiltersProps {
|
interface JellyfinSongFiltersProps {
|
||||||
@@ -69,10 +69,10 @@ export const JellyfinSongFilters = ({
|
|||||||
return filter?._custom?.jellyfin?.Tags?.split('|');
|
return filter?._custom?.jellyfin?.Tags?.split('|');
|
||||||
}, [filter?._custom?.jellyfin?.Tags]);
|
}, [filter?._custom?.jellyfin?.Tags]);
|
||||||
|
|
||||||
const toggleFilters = [
|
const yesNoFilters = [
|
||||||
{
|
{
|
||||||
label: t('filter.isFavorited', { postProcess: 'sentenceCase' }),
|
label: t('filter.isFavorited', { postProcess: 'sentenceCase' }),
|
||||||
onChange: (e: ChangeEvent<HTMLInputElement>) => {
|
onChange: (favorite?: boolean) => {
|
||||||
const updatedFilters = setFilter({
|
const updatedFilters = setFilter({
|
||||||
customFilters,
|
customFilters,
|
||||||
data: {
|
data: {
|
||||||
@@ -83,7 +83,7 @@ export const JellyfinSongFilters = ({
|
|||||||
IncludeItemTypes: 'Audio',
|
IncludeItemTypes: 'Audio',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
favorite: e.currentTarget.checked ? true : undefined,
|
favorite,
|
||||||
},
|
},
|
||||||
itemType: LibraryItem.SONG,
|
itemType: LibraryItem.SONG,
|
||||||
key: pageKey,
|
key: pageKey,
|
||||||
@@ -174,15 +174,16 @@ export const JellyfinSongFilters = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack p="0.8rem">
|
<Stack p="0.8rem">
|
||||||
{toggleFilters.map((filter) => (
|
{yesNoFilters.map((filter) => (
|
||||||
<Group
|
<Group
|
||||||
justify="space-between"
|
justify="space-between"
|
||||||
key={`nd-filter-${filter.label}`}
|
key={`nd-filter-${filter.label}`}
|
||||||
>
|
>
|
||||||
<Text>{filter.label}</Text>
|
<Text>{filter.label}</Text>
|
||||||
<Switch
|
<YesNoSelect
|
||||||
checked={filter?.value || false}
|
|
||||||
onChange={filter.onChange}
|
onChange={filter.onChange}
|
||||||
|
size="xs"
|
||||||
|
value={filter.value}
|
||||||
/>
|
/>
|
||||||
</Group>
|
</Group>
|
||||||
))}
|
))}
|
||||||
@@ -218,7 +219,7 @@ export const JellyfinSongFilters = ({
|
|||||||
/>
|
/>
|
||||||
</Group>
|
</Group>
|
||||||
)}
|
)}
|
||||||
{tagsQuery.data?.boolTags?.length && (
|
{tagsQuery.data?.boolTags && tagsQuery.data.boolTags.length > 0 && (
|
||||||
<Group grow>
|
<Group grow>
|
||||||
<MultiSelectWithInvalidData
|
<MultiSelectWithInvalidData
|
||||||
clearable
|
clearable
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import debounce from 'lodash/debounce';
|
import debounce from 'lodash/debounce';
|
||||||
import { ChangeEvent, useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { SelectWithInvalidData } from '/@/renderer/components/select-with-invalid-data';
|
import { SelectWithInvalidData } from '/@/renderer/components/select-with-invalid-data';
|
||||||
@@ -10,8 +10,8 @@ import { Divider } from '/@/shared/components/divider/divider';
|
|||||||
import { Group } from '/@/shared/components/group/group';
|
import { Group } from '/@/shared/components/group/group';
|
||||||
import { NumberInput } from '/@/shared/components/number-input/number-input';
|
import { NumberInput } from '/@/shared/components/number-input/number-input';
|
||||||
import { Stack } from '/@/shared/components/stack/stack';
|
import { Stack } from '/@/shared/components/stack/stack';
|
||||||
import { Switch } from '/@/shared/components/switch/switch';
|
|
||||||
import { Text } from '/@/shared/components/text/text';
|
import { Text } from '/@/shared/components/text/text';
|
||||||
|
import { YesNoSelect } from '/@/shared/components/yes-no-select/yes-no-select';
|
||||||
import { GenreListSort, LibraryItem, SongListQuery, SortOrder } from '/@/shared/types/domain-types';
|
import { GenreListSort, LibraryItem, SongListQuery, SortOrder } from '/@/shared/types/domain-types';
|
||||||
|
|
||||||
interface NavidromeSongFiltersProps {
|
interface NavidromeSongFiltersProps {
|
||||||
@@ -93,12 +93,12 @@ export const NavidromeSongFilters = ({
|
|||||||
const toggleFilters = [
|
const toggleFilters = [
|
||||||
{
|
{
|
||||||
label: t('filter.isFavorited', { postProcess: 'sentenceCase' }),
|
label: t('filter.isFavorited', { postProcess: 'sentenceCase' }),
|
||||||
onChange: (e: ChangeEvent<HTMLInputElement>) => {
|
onChange: (favorite: boolean | undefined) => {
|
||||||
const updatedFilters = setFilter({
|
const updatedFilters = setFilter({
|
||||||
customFilters,
|
customFilters,
|
||||||
data: {
|
data: {
|
||||||
_custom: filter._custom,
|
_custom: filter._custom,
|
||||||
favorite: e.currentTarget.checked ? true : undefined,
|
favorite,
|
||||||
},
|
},
|
||||||
itemType: LibraryItem.SONG,
|
itemType: LibraryItem.SONG,
|
||||||
key: pageKey,
|
key: pageKey,
|
||||||
@@ -137,10 +137,10 @@ export const NavidromeSongFilters = ({
|
|||||||
key={`nd-filter-${filter.label}`}
|
key={`nd-filter-${filter.label}`}
|
||||||
>
|
>
|
||||||
<Text>{filter.label}</Text>
|
<Text>{filter.label}</Text>
|
||||||
<Switch
|
<YesNoSelect
|
||||||
checked={filter?.value || false}
|
|
||||||
onChange={filter.onChange}
|
onChange={filter.onChange}
|
||||||
size="xs"
|
size="xs"
|
||||||
|
value={filter.value}
|
||||||
/>
|
/>
|
||||||
</Group>
|
</Group>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -467,7 +467,7 @@ export const SongListHeaderFilters = ({
|
|||||||
.filter((value) => value !== 'Audio') // Don't account for includeItemTypes: Audio
|
.filter((value) => value !== 'Audio') // Don't account for includeItemTypes: Audio
|
||||||
.some((value) => value !== undefined);
|
.some((value) => value !== undefined);
|
||||||
|
|
||||||
const isGenericFilterApplied = filter?.favorite || filter?.genreIds?.length;
|
const isGenericFilterApplied = filter?.favorite !== undefined || filter?.genreIds?.length;
|
||||||
|
|
||||||
return isNavidromeFilterApplied || isJellyfinFilterApplied || isGenericFilterApplied;
|
return isNavidromeFilterApplied || isJellyfinFilterApplied || isGenericFilterApplied;
|
||||||
}, [
|
}, [
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ export const SubsonicSongFilters = ({
|
|||||||
const updatedFilters = setFilter({
|
const updatedFilters = setFilter({
|
||||||
customFilters,
|
customFilters,
|
||||||
data: {
|
data: {
|
||||||
favorite: e.target.checked,
|
favorite: e.target.checked ? true : undefined,
|
||||||
},
|
},
|
||||||
itemType: LibraryItem.SONG,
|
itemType: LibraryItem.SONG,
|
||||||
key: pageKey,
|
key: pageKey,
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ export const useFastAverageColor = (args: {
|
|||||||
return setBackground(color.rgb);
|
return setBackground(color.rgb);
|
||||||
})
|
})
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
console.log('Error fetching average color', e);
|
console.error('Error fetching average color', e);
|
||||||
idRef.current = id;
|
idRef.current = id;
|
||||||
return setBackground('rgba(0, 0, 0, 0)');
|
return setBackground('rgba(0, 0, 0, 0)');
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,40 +1,57 @@
|
|||||||
|
import isElectron from 'is-electron';
|
||||||
import { debounce } from 'lodash';
|
import { debounce } from 'lodash';
|
||||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
|
|
||||||
import { api } from '/@/renderer/api';
|
import { api } from '/@/renderer/api';
|
||||||
import { useCurrentServer } from '/@/renderer/store';
|
import { useAuthStoreActions, useCurrentServer } from '/@/renderer/store';
|
||||||
import { toast } from '/@/shared/components/toast/toast';
|
import { toast } from '/@/shared/components/toast/toast';
|
||||||
import { SongListSort, SortOrder } from '/@/shared/types/domain-types';
|
import { SongListSort, SortOrder } from '/@/shared/types/domain-types';
|
||||||
import { AuthState, ServerListItem, ServerType } from '/@/shared/types/types';
|
import { AuthState, ServerListItem, ServerType } from '/@/shared/types/types';
|
||||||
|
|
||||||
|
const localSettings = isElectron() ? window.api.localSettings : null;
|
||||||
|
|
||||||
export const useServerAuthenticated = () => {
|
export const useServerAuthenticated = () => {
|
||||||
const priorServerId = useRef<string | undefined>(undefined);
|
const priorServerId = useRef<string | undefined>(undefined);
|
||||||
const server = useCurrentServer();
|
const server = useCurrentServer();
|
||||||
const [ready, setReady] = useState(
|
const [ready, setReady] = useState(
|
||||||
server?.type === ServerType.NAVIDROME ? AuthState.LOADING : AuthState.VALID,
|
server?.type === ServerType.NAVIDROME ? AuthState.VALID : AuthState.VALID,
|
||||||
);
|
);
|
||||||
|
|
||||||
const authenticateNavidrome = useCallback(async (server: ServerListItem) => {
|
const { updateServer } = useAuthStoreActions();
|
||||||
// This trick works because navidrome-api.ts will internally check for authentication
|
|
||||||
// failures and try to log in again (where available). So, all that's necessary is
|
|
||||||
// making one request first
|
|
||||||
try {
|
|
||||||
await api.controller.getSongList({
|
|
||||||
apiClientProps: { server },
|
|
||||||
query: {
|
|
||||||
limit: 1,
|
|
||||||
sortBy: SongListSort.NAME,
|
|
||||||
sortOrder: SortOrder.ASC,
|
|
||||||
startIndex: 0,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
setReady(AuthState.VALID);
|
const authenticateNavidrome = useCallback(
|
||||||
} catch (error) {
|
async (server: ServerListItem) => {
|
||||||
toast.error({ message: (error as Error).message });
|
// This trick works because navidrome-api.ts will internally check for authentication
|
||||||
setReady(AuthState.INVALID);
|
// failures and try to log in again (where available). So, all that's necessary is
|
||||||
}
|
// making one request first
|
||||||
}, []);
|
try {
|
||||||
|
await api.controller.getSongList({
|
||||||
|
apiClientProps: { server },
|
||||||
|
query: {
|
||||||
|
limit: 1,
|
||||||
|
sortBy: SongListSort.NAME,
|
||||||
|
sortOrder: SortOrder.ASC,
|
||||||
|
startIndex: 0,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
setReady(AuthState.VALID);
|
||||||
|
} catch (error) {
|
||||||
|
// Clear server credentials (and saved password).
|
||||||
|
if (server.savePassword && localSettings) {
|
||||||
|
localSettings.passwordRemove(server.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
server.credential = '';
|
||||||
|
updateServer(server.id, server);
|
||||||
|
|
||||||
|
toast.error({ message: (error as Error).message });
|
||||||
|
|
||||||
|
setReady(AuthState.INVALID);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[updateServer],
|
||||||
|
);
|
||||||
|
|
||||||
const debouncedAuth = debounce((server: ServerListItem) => {
|
const debouncedAuth = debounce((server: ServerListItem) => {
|
||||||
authenticateNavidrome(server).catch(console.error);
|
authenticateNavidrome(server).catch(console.error);
|
||||||
|
|||||||
@@ -34,7 +34,10 @@ export function IsUpdatedDialog() {
|
|||||||
>
|
>
|
||||||
<Stack>
|
<Stack>
|
||||||
<Text>{t('common.newVersion', { postProcess: 'sentenceCase', version })}</Text>
|
<Text>{t('common.newVersion', { postProcess: 'sentenceCase', version })}</Text>
|
||||||
<Group wrap="nowrap">
|
<Group
|
||||||
|
justify="flex-end"
|
||||||
|
wrap="nowrap"
|
||||||
|
>
|
||||||
<Button
|
<Button
|
||||||
component="a"
|
component="a"
|
||||||
href={`https://github.com/jeffvli/feishin/releases/tag/v${version}`}
|
href={`https://github.com/jeffvli/feishin/releases/tag/v${version}`}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
.container {
|
.container {
|
||||||
position: relative;
|
position: relative;
|
||||||
grid-area: sidebar;
|
grid-area: sidebar;
|
||||||
|
background: var(--theme-colors-background-alternate);
|
||||||
border-right: 1px solid alpha(var(--theme-colors-border), 0.3);
|
border-right: 1px solid alpha(var(--theme-colors-border), 0.3);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -200,6 +200,7 @@ export interface SettingsState {
|
|||||||
clientId: string;
|
clientId: string;
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
showAsListening: boolean;
|
showAsListening: boolean;
|
||||||
|
showPaused: boolean;
|
||||||
showServerImage: boolean;
|
showServerImage: boolean;
|
||||||
};
|
};
|
||||||
font: {
|
font: {
|
||||||
@@ -266,6 +267,7 @@ export interface SettingsState {
|
|||||||
fontSizeUnsync: number;
|
fontSizeUnsync: number;
|
||||||
gap: number;
|
gap: number;
|
||||||
gapUnsync: number;
|
gapUnsync: number;
|
||||||
|
preferLocalLyrics: boolean;
|
||||||
showMatch: boolean;
|
showMatch: boolean;
|
||||||
showProvider: boolean;
|
showProvider: boolean;
|
||||||
sources: LyricSource[];
|
sources: LyricSource[];
|
||||||
@@ -280,6 +282,7 @@ export interface SettingsState {
|
|||||||
mpvExtraParameters: string[];
|
mpvExtraParameters: string[];
|
||||||
mpvProperties: MpvSettings;
|
mpvProperties: MpvSettings;
|
||||||
muted: boolean;
|
muted: boolean;
|
||||||
|
preservePitch: boolean;
|
||||||
scrobble: {
|
scrobble: {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
scrobbleAtDuration: number;
|
scrobbleAtDuration: number;
|
||||||
@@ -351,6 +354,7 @@ const initialState: SettingsState = {
|
|||||||
clientId: '1165957668758900787',
|
clientId: '1165957668758900787',
|
||||||
enabled: false,
|
enabled: false,
|
||||||
showAsListening: false,
|
showAsListening: false,
|
||||||
|
showPaused: true,
|
||||||
showServerImage: false,
|
showServerImage: false,
|
||||||
},
|
},
|
||||||
font: {
|
font: {
|
||||||
@@ -448,6 +452,7 @@ const initialState: SettingsState = {
|
|||||||
fontSizeUnsync: 24,
|
fontSizeUnsync: 24,
|
||||||
gap: 24,
|
gap: 24,
|
||||||
gapUnsync: 24,
|
gapUnsync: 24,
|
||||||
|
preferLocalLyrics: true,
|
||||||
showMatch: true,
|
showMatch: true,
|
||||||
showProvider: true,
|
showProvider: true,
|
||||||
sources: [LyricSource.NETEASE, LyricSource.LRCLIB],
|
sources: [LyricSource.NETEASE, LyricSource.LRCLIB],
|
||||||
@@ -471,6 +476,7 @@ const initialState: SettingsState = {
|
|||||||
replayGainPreampDB: 0,
|
replayGainPreampDB: 0,
|
||||||
},
|
},
|
||||||
muted: false,
|
muted: false,
|
||||||
|
preservePitch: true,
|
||||||
scrobble: {
|
scrobble: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
scrobbleAtDuration: 240,
|
scrobbleAtDuration: 240,
|
||||||
|
|||||||
@@ -50,7 +50,10 @@ export const useAppTheme = (overrideTheme?: AppTheme) => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (type === FontType.SYSTEM && system) {
|
if (type === FontType.SYSTEM && system) {
|
||||||
const root = document.documentElement;
|
const root = document.documentElement;
|
||||||
root.style.setProperty('--theme-content-font-family', 'dynamic-font');
|
root.style.setProperty(
|
||||||
|
'--theme-content-font-family',
|
||||||
|
'dynamic-font, "Noto Sans JP", sans-serif',
|
||||||
|
);
|
||||||
|
|
||||||
if (!textStyleRef.current) {
|
if (!textStyleRef.current) {
|
||||||
textStyleRef.current = document.createElement('style');
|
textStyleRef.current = document.createElement('style');
|
||||||
@@ -64,7 +67,10 @@ export const useAppTheme = (overrideTheme?: AppTheme) => {
|
|||||||
}`;
|
}`;
|
||||||
} else if (type === FontType.CUSTOM && custom) {
|
} else if (type === FontType.CUSTOM && custom) {
|
||||||
const root = document.documentElement;
|
const root = document.documentElement;
|
||||||
root.style.setProperty('--theme-content-font-family', 'dynamic-font');
|
root.style.setProperty(
|
||||||
|
'--theme-content-font-family',
|
||||||
|
'dynamic-font, "Noto Sans JP", sans-serif',
|
||||||
|
);
|
||||||
|
|
||||||
if (!textStyleRef.current) {
|
if (!textStyleRef.current) {
|
||||||
textStyleRef.current = document.createElement('style');
|
textStyleRef.current = document.createElement('style');
|
||||||
@@ -78,7 +84,10 @@ export const useAppTheme = (overrideTheme?: AppTheme) => {
|
|||||||
}`;
|
}`;
|
||||||
} else {
|
} else {
|
||||||
const root = document.documentElement;
|
const root = document.documentElement;
|
||||||
root.style.setProperty('--theme-content-font-family', builtIn);
|
root.style.setProperty(
|
||||||
|
'--theme-content-font-family',
|
||||||
|
`${builtIn}, "Noto Sans JP", sans-serif`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}, [builtIn, custom, system, type]);
|
}, [builtIn, custom, system, type]);
|
||||||
|
|
||||||
|
|||||||
@@ -53,6 +53,11 @@
|
|||||||
&:focus-visible {
|
&:focus-visible {
|
||||||
background: darken(var(--theme-colors-primary-filled), 10%);
|
background: darken(var(--theme-colors-primary-filled), 10%);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
svg {
|
||||||
|
color: var(--theme-colors-primary-contrast);
|
||||||
|
fill: var(--theme-colors-primary-contrast);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&[data-variant='subtle'] {
|
&[data-variant='subtle'] {
|
||||||
@@ -60,8 +65,15 @@
|
|||||||
background: transparent;
|
background: transparent;
|
||||||
|
|
||||||
&:hover,
|
&:hover,
|
||||||
|
&:active,
|
||||||
&:focus-visible {
|
&:focus-visible {
|
||||||
background: lighten(var(--theme-colors-surface), 10%);
|
@mixin dark {
|
||||||
|
background: lighten(var(--theme-colors-background), 5%);
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin light {
|
||||||
|
background: darken(var(--theme-colors-background), 5%);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -92,7 +92,13 @@
|
|||||||
&:hover,
|
&:hover,
|
||||||
&:active,
|
&:active,
|
||||||
&:focus-visible {
|
&:focus-visible {
|
||||||
background-color: lighten(var(--button-bg), 10%);
|
@mixin dark {
|
||||||
|
background-color: lighten(var(--theme-colors-background), 10%);
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin light {
|
||||||
|
background-color: darken(var(--theme-colors-background), 5%);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,11 +106,11 @@
|
|||||||
border: 1px solid transparent;
|
border: 1px solid transparent;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: darken(var(--button-bg), 5%);
|
background-color: darken(var(--theme-colors-background), 5%);
|
||||||
}
|
}
|
||||||
|
|
||||||
&:focus-visible {
|
&:focus-visible {
|
||||||
background-color: darken(var(--button-bg), 10%);
|
background-color: darken(var(--theme-colors-background), 10%);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -70,6 +70,10 @@
|
|||||||
fill: var(--theme-colors-foreground);
|
fill: var(--theme-colors-foreground);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.fill-contrast {
|
||||||
|
fill: var(--theme-colors-primary-contrast);
|
||||||
|
}
|
||||||
|
|
||||||
.fill-inherit {
|
.fill-inherit {
|
||||||
fill: inherit;
|
fill: inherit;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -224,11 +224,21 @@ export const AppIcon = {
|
|||||||
|
|
||||||
export interface IconProps extends Omit<IconBaseProps, 'color' | 'fill' | 'size'> {
|
export interface IconProps extends Omit<IconBaseProps, 'color' | 'fill' | 'size'> {
|
||||||
animate?: 'pulse' | 'spin';
|
animate?: 'pulse' | 'spin';
|
||||||
color?: 'default' | 'error' | 'info' | 'inherit' | 'muted' | 'primary' | 'success' | 'warn';
|
color?: IconColor;
|
||||||
fill?: 'default' | 'error' | 'info' | 'inherit' | 'muted' | 'primary' | 'success' | 'warn';
|
fill?: IconColor;
|
||||||
icon: keyof typeof AppIcon;
|
icon: keyof typeof AppIcon;
|
||||||
size?: '2xl' | '3xl' | '4xl' | '5xl' | 'lg' | 'md' | 'sm' | 'xl' | 'xs' | number | string;
|
size?: '2xl' | '3xl' | '4xl' | '5xl' | 'lg' | 'md' | 'sm' | 'xl' | 'xs' | number | string;
|
||||||
}
|
}
|
||||||
|
type IconColor =
|
||||||
|
| 'contrast'
|
||||||
|
| 'default'
|
||||||
|
| 'error'
|
||||||
|
| 'info'
|
||||||
|
| 'inherit'
|
||||||
|
| 'muted'
|
||||||
|
| 'primary'
|
||||||
|
| 'success'
|
||||||
|
| 'warn';
|
||||||
|
|
||||||
export const Icon = forwardRef<HTMLDivElement, IconProps>((props, ref) => {
|
export const Icon = forwardRef<HTMLDivElement, IconProps>((props, ref) => {
|
||||||
const { animate, className, color, fill, icon, size = 'md' } = props;
|
const { animate, className, color, fill, icon, size = 'md' } = props;
|
||||||
|
|||||||
@@ -1,16 +1,18 @@
|
|||||||
import type { ImgHTMLAttributes } from 'react';
|
|
||||||
|
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
|
import { motion, MotionConfigProps } from 'motion/react';
|
||||||
|
import { type ImgHTMLAttributes } from 'react';
|
||||||
import { Img } from 'react-image';
|
import { Img } from 'react-image';
|
||||||
|
|
||||||
import styles from './image.module.css';
|
import styles from './image.module.css';
|
||||||
|
|
||||||
|
import { animationProps } from '/@/shared/components/animations/animation-props';
|
||||||
import { Icon } from '/@/shared/components/icon/icon';
|
import { Icon } from '/@/shared/components/icon/icon';
|
||||||
import { Skeleton } from '/@/shared/components/skeleton/skeleton';
|
import { Skeleton } from '/@/shared/components/skeleton/skeleton';
|
||||||
|
|
||||||
interface ImageContainerProps {
|
interface ImageContainerProps extends MotionConfigProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
enableAnimation?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ImageLoaderProps {
|
interface ImageLoaderProps {
|
||||||
@@ -19,6 +21,8 @@ interface ImageLoaderProps {
|
|||||||
|
|
||||||
interface ImageProps extends Omit<ImgHTMLAttributes<HTMLImageElement>, 'src'> {
|
interface ImageProps extends Omit<ImgHTMLAttributes<HTMLImageElement>, 'src'> {
|
||||||
containerClassName?: string;
|
containerClassName?: string;
|
||||||
|
enableAnimation?: boolean;
|
||||||
|
imageContainerProps?: Omit<ImageContainerProps, 'children'>;
|
||||||
includeLoader?: boolean;
|
includeLoader?: boolean;
|
||||||
includeUnloader?: boolean;
|
includeUnloader?: boolean;
|
||||||
src: string | string[] | undefined;
|
src: string | string[] | undefined;
|
||||||
@@ -32,6 +36,8 @@ interface ImageUnloaderProps {
|
|||||||
export function Image({
|
export function Image({
|
||||||
className,
|
className,
|
||||||
containerClassName,
|
containerClassName,
|
||||||
|
enableAnimation,
|
||||||
|
imageContainerProps,
|
||||||
includeLoader = true,
|
includeLoader = true,
|
||||||
includeUnloader = true,
|
includeUnloader = true,
|
||||||
src,
|
src,
|
||||||
@@ -41,7 +47,13 @@ export function Image({
|
|||||||
<Img
|
<Img
|
||||||
className={clsx(styles.image, className)}
|
className={clsx(styles.image, className)}
|
||||||
container={(children) => (
|
container={(children) => (
|
||||||
<ImageContainer className={containerClassName}>{children}</ImageContainer>
|
<ImageContainer
|
||||||
|
className={containerClassName}
|
||||||
|
enableAnimation={enableAnimation}
|
||||||
|
{...imageContainerProps}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</ImageContainer>
|
||||||
)}
|
)}
|
||||||
loader={
|
loader={
|
||||||
includeLoader ? (
|
includeLoader ? (
|
||||||
@@ -50,7 +62,6 @@ export function Image({
|
|||||||
</ImageContainer>
|
</ImageContainer>
|
||||||
) : null
|
) : null
|
||||||
}
|
}
|
||||||
loading="lazy"
|
|
||||||
src={src}
|
src={src}
|
||||||
unloader={
|
unloader={
|
||||||
includeUnloader ? (
|
includeUnloader ? (
|
||||||
@@ -66,8 +77,27 @@ export function Image({
|
|||||||
return <ImageUnloader />;
|
return <ImageUnloader />;
|
||||||
}
|
}
|
||||||
|
|
||||||
function ImageContainer({ children, className }: ImageContainerProps) {
|
function ImageContainer({ children, className, enableAnimation, ...props }: ImageContainerProps) {
|
||||||
return <div className={clsx(styles.imageContainer, className)}>{children}</div>;
|
if (!enableAnimation) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={clsx(styles.imageContainer, className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
className={clsx(styles.imageContainer, className)}
|
||||||
|
{...animationProps.fadeIn}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ImageLoader({ className }: ImageLoaderProps) {
|
function ImageLoader({ className }: ImageLoaderProps) {
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import { Select, SelectProps } from '/@/shared/components/select/select';
|
||||||
|
|
||||||
|
export interface YesNoSelectProps extends Omit<SelectProps, 'data' | 'onChange' | 'value'> {
|
||||||
|
onChange: (e?: boolean) => void;
|
||||||
|
value?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const YesNoSelect = ({ onChange, value, ...props }: YesNoSelectProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Select
|
||||||
|
clearable
|
||||||
|
data={[
|
||||||
|
{
|
||||||
|
label: t('common.no', { postProcess: 'sentenceCase' }),
|
||||||
|
value: 'false',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('common.yes', { postProcess: 'sentenceCase' }),
|
||||||
|
value: 'true',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
onChange={(e) => {
|
||||||
|
onChange(e ? e === 'true' : undefined);
|
||||||
|
}}
|
||||||
|
value={value !== undefined ? value.toString() : null}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -11,7 +11,8 @@
|
|||||||
text-rendering: optimizelegibility;
|
text-rendering: optimizelegibility;
|
||||||
-webkit-tap-highlight-color: rgb(0 0 0 / 0%);
|
-webkit-tap-highlight-color: rgb(0 0 0 / 0%);
|
||||||
text-size-adjust: none;
|
text-size-adjust: none;
|
||||||
outline: none;
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
@@ -122,59 +123,56 @@ button {
|
|||||||
@font-face {
|
@font-face {
|
||||||
font-family: Archivo;
|
font-family: Archivo;
|
||||||
font-weight: 100 1000;
|
font-weight: 100 1000;
|
||||||
src: url('../../renderer/fonts/Archivo-VariableFont_wdth,wght.ttf')
|
src: url('../../../assets/fonts/Archivo-VariableFont_wdth,wght.ttf');
|
||||||
format('truetype-variations');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: Raleway;
|
font-family: Raleway;
|
||||||
font-weight: 100 1000;
|
font-weight: 100 1000;
|
||||||
src: url('../../renderer/fonts/Raleway-VariableFont_wght.ttf') format('truetype-variations');
|
src: url('../../../assets/fonts/Raleway-VariableFont_wght.ttf');
|
||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: Fredoka;
|
font-family: Fredoka;
|
||||||
font-weight: 100 1000;
|
font-weight: 100 1000;
|
||||||
src: url('../../renderer/fonts/Fredoka-VariableFont_wdth,wght.ttf')
|
src: url('../../../assets/fonts/Fredoka-VariableFont_wdth,wght.ttf');
|
||||||
format('truetype-variations');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'League Spartan';
|
font-family: 'League Spartan';
|
||||||
font-weight: 100 1000;
|
font-weight: 100 1000;
|
||||||
src: url('../../renderer/fonts/LeagueSpartan-VariableFont_wght.ttf')
|
src: url('../../../assets/fonts/LeagueSpartan-VariableFont_wght.ttf');
|
||||||
format('truetype-variations');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: Lexend;
|
font-family: Lexend;
|
||||||
font-weight: 100 1000;
|
font-weight: 100 1000;
|
||||||
src: url('../../renderer/fonts/Lexend-VariableFont_wght.ttf') format('truetype-variations');
|
src: url('../../../assets/fonts/Lexend-VariableFont_wght.ttf');
|
||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: Inter;
|
font-family: Inter;
|
||||||
font-weight: 100 1000;
|
font-weight: 100 1000;
|
||||||
src: url('../../renderer/fonts/Inter-VariableFont_slnt,wght.ttf') format('truetype-variations');
|
src: url('../../../assets/fonts/Inter-VariableFont_slnt,wght.ttf');
|
||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: Sora;
|
font-family: Sora;
|
||||||
font-weight: 100 1000;
|
font-weight: 100 1000;
|
||||||
src: url('../../renderer/fonts/Sora-VariableFont_wght.ttf') format('truetype-variations');
|
src: url('../../../assets/fonts/Sora-VariableFont_wght.ttf');
|
||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'Work Sans';
|
font-family: 'Work Sans';
|
||||||
font-weight: 100 1000;
|
font-weight: 100 1000;
|
||||||
src: url('../../renderer/fonts/WorkSans-VariableFont_wght.ttf') format('truetype-variations');
|
src: url('../../../assets/fonts/WorkSans-VariableFont_wght.ttf');
|
||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: Poppins;
|
font-family: Poppins;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
src: url('../../renderer/fonts/Poppins-Regular.ttf') format('truetype');
|
src: url('../../../assets/fonts/Poppins-Regular.ttf');
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -182,7 +180,7 @@ button {
|
|||||||
font-family: Poppins;
|
font-family: Poppins;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
src: url('../../renderer/fonts/Poppins-SemiBold.ttf') format('truetype');
|
src: url('../../../assets/fonts/Poppins-SemiBold.ttf');
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -190,7 +188,7 @@ button {
|
|||||||
font-family: Poppins;
|
font-family: Poppins;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
src: url('../../renderer/fonts/Poppins-Bold.ttf') format('truetype');
|
src: url('../../../assets/fonts/Poppins-Bold.ttf');
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -198,7 +196,7 @@ button {
|
|||||||
font-family: Poppins;
|
font-family: Poppins;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
src: url('../../renderer/fonts/Poppins-ExtraBold.ttf') format('truetype');
|
src: url('../../../assets/fonts/Poppins-ExtraBold.ttf');
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -206,28 +204,21 @@ button {
|
|||||||
font-family: Poppins;
|
font-family: Poppins;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 900;
|
font-weight: 900;
|
||||||
src: url('../../renderer/fonts/Poppins-Black.ttf') format('truetype');
|
src: url('../../../assets/fonts/Poppins-Black.ttf');
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: Raleway;
|
font-family: Raleway;
|
||||||
font-weight: 100 1000;
|
font-weight: 100 1000;
|
||||||
src: url('../../renderer/fonts/Raleway-VariableFont_wght.ttf') format('truetype-variations');
|
src: url('../../../assets/fonts/Raleway-VariableFont_wght.ttf');
|
||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: DroidSerif;
|
font-family: 'Noto Sans JP';
|
||||||
src: url('https://rawgit.com/google/fonts/master/ufl/ubuntumono/UbuntuMono-Italic.ttf')
|
font-weight: 100 900;
|
||||||
format('truetype');
|
src: url('../../../assets/fonts/NotoSansJP-VariableFont_wght.ttf');
|
||||||
unicode-range: U+000-5FF; /* Latin glyphs */
|
unicode-range: U+3000-9FFF, U+FF00-FFEF; /* Japanese characters */
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: DroidSerif;
|
|
||||||
src: url('https://fonts.gstatic.com/ea/notosansjp/v5/NotoSansJP-Regular.woff2')
|
|
||||||
format('truetype');
|
|
||||||
unicode-range: U+3000-9FFF, U+ff??; /* Japanese glyphs */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user