Compare commits

...

18 Commits

Author SHA1 Message Date
jeffvli 93f2573847 Update to v0.9.0 2024-09-10 22:37:48 -07:00
Kendall Garner 03d97c6b1e use unique id for paginated playlist 2024-09-10 22:37:24 -07:00
Hosted Weblate 0b86cb51d3 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (634 of 634 strings)

Co-authored-by: 無情天 <kofzhanganguo@126.com>
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/zh_Hans/
Translation: feishin/Translation
2024-09-10 04:01:31 +02:00
Hosted Weblate ee54b8219b Translated using Weblate (French)
Currently translated at 96.8% (610 of 630 strings)

Co-authored-by: Evan <evan_g@orange.fr>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/fr/
Translation: feishin/Translation
2024-09-10 04:01:31 +02:00
Hosted Weblate 25b593aadd Translated using Weblate (Spanish)
Currently translated at 100.0% (634 of 634 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (632 of 632 strings)

Translated using Weblate (Spanish)

Currently translated at 99.8% (631 of 632 strings)

Co-authored-by: Fordas <fordas15@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/es/
Translation: feishin/Translation
2024-09-10 04:01:31 +02:00
Hosted Weblate 5253e32b67 Translated using Weblate (Czech)
Currently translated at 100.0% (634 of 634 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (632 of 632 strings)

Translated using Weblate (Czech)

Currently translated at 99.6% (630 of 632 strings)

Co-authored-by: Fjuro <fjuro@alius.cz>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/cs/
Translation: feishin/Translation
2024-09-10 04:01:31 +02:00
Hosted Weblate b0b558c90a Translated using Weblate (German)
Currently translated at 85.0% (536 of 630 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Schroti <schrotihd@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/de/
Translation: feishin/Translation
2024-09-10 04:01:31 +02:00
Kendall Garner f11a53c1a4 fix suspense 2024-09-09 19:01:07 -07:00
Kendall Garner e2a05f4204 add track normalization for jellyfin as well 2024-09-09 07:15:26 -07:00
Kendall Garner fcc010eb54 move transcoding placeholder 2024-09-08 22:05:44 -07:00
Kendall Garner 1b41a5a674 enable disabling tray 2024-09-08 20:55:07 -07:00
Kendall Garner 74aa88e082 add web visualizer (#314)
* add web visualizer

* fallback to simple model

* less samples, hopefully more efficient

* Use audiomotion analyzer

- Note: fixed to 4.1.1 because 4.2.0 uses esm which breaks in the current workflow...

* revert publish changes

* r2

* don't massively change package.json

* lazy
2024-09-09 01:25:01 +00:00
Kendall Garner fbac33ceba add shuffle context menu item 2024-09-07 21:31:01 -07:00
Kendall Garner 42ba5a531c use feishin switch instead of default 2024-09-05 18:08:37 -07:00
Kendall Garner 257e1e2cd9 ... 2024-09-05 07:06:37 -07:00
Kendall Garner 3025e84c58 remove height everywhere for jellyfin images 2024-09-04 22:30:50 -07:00
Kendall Garner 4a111d9cf2 don't make artist clickable if no id 2024-09-04 20:01:45 -07:00
Kendall Garner e6bd8deb0c use unique id for places that may have duplicates 2024-09-04 19:34:07 -07:00
35 changed files with 399 additions and 96 deletions
+4 -1
View File
@@ -71,7 +71,9 @@ docker run --name feishin -p 9180:9180 feishin
``` ```
#### Docker Compose #### Docker Compose
To install via Docker Compose use the following snippit. This also works on Portainer. To install via Docker Compose use the following snippit. This also works on Portainer.
``` ```
version: '3' version: '3'
services: services:
@@ -92,7 +94,6 @@ services:
restart: unless-stopped restart: unless-stopped
``` ```
### Configuration ### Configuration
1. Upon startup you will be greeted with a prompt to select the path to your MPV binary. If you do not have MPV installed, you can download it [here](https://mpv.io/installation/) or install it using any package manager supported by your OS. After inputting the path, restart the app. 1. Upon startup you will be greeted with a prompt to select the path to your MPV binary. If you do not have MPV installed, you can download it [here](https://mpv.io/installation/) or install it using any package manager supported by your OS. After inputting the path, restart the app.
@@ -130,6 +131,8 @@ chmod 4755 chrome-sandbox
sudo chown root:root chrome-sandbox sudo chown root:root chrome-sandbox
``` ```
Ubunutu 24.04 specifically introduced breaking changes that affect how namespaces work. Please see https://discourse.ubuntu.com/t/ubuntu-24-04-lts-noble-numbat-release-notes/39890#:~:text=security%20improvements%20 for possible fixes.
## Development ## Development
Built and tested using Node `v16.15.0`. Built and tested using Node `v16.15.0`.
+18 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "feishin", "name": "feishin",
"version": "0.8.1", "version": "0.9.0",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "feishin", "name": "feishin",
"version": "0.8.1", "version": "0.9.0",
"hasInstallScript": true, "hasInstallScript": true,
"license": "GPL-3.0", "license": "GPL-3.0",
"dependencies": { "dependencies": {
@@ -28,6 +28,7 @@
"@tanstack/react-query-persist-client": "^4.32.1", "@tanstack/react-query-persist-client": "^4.32.1",
"@ts-rest/core": "^3.23.0", "@ts-rest/core": "^3.23.0",
"@xhayper/discord-rpc": "^1.0.24", "@xhayper/discord-rpc": "^1.0.24",
"audiomotion-analyzer": "^4.5.0",
"auto-text-size": "^0.2.3", "auto-text-size": "^0.2.3",
"axios": "^1.6.0", "axios": "^1.6.0",
"clsx": "^2.0.0", "clsx": "^2.0.0",
@@ -6740,6 +6741,16 @@
"node": ">=10.12.0" "node": ">=10.12.0"
} }
}, },
"node_modules/audiomotion-analyzer": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/audiomotion-analyzer/-/audiomotion-analyzer-4.5.0.tgz",
"integrity": "sha512-qnmB8TSbrxYkTbFgsQeeym0Z/suQx4c0jFg9Yh5+gaPw6J4AFLdfFpagdnDbtNEsj6K7BntgsC3bkdut5rxozg==",
"license": "AGPL-3.0-or-later",
"funding": {
"type": "Ko-fi",
"url": "https://ko-fi.com/hvianna"
}
},
"node_modules/auto-text-size": { "node_modules/auto-text-size": {
"version": "0.2.3", "version": "0.2.3",
"resolved": "https://registry.npmjs.org/auto-text-size/-/auto-text-size-0.2.3.tgz", "resolved": "https://registry.npmjs.org/auto-text-size/-/auto-text-size-0.2.3.tgz",
@@ -28672,6 +28683,11 @@
"resolved": "https://registry.npmjs.org/atomically/-/atomically-1.7.0.tgz", "resolved": "https://registry.npmjs.org/atomically/-/atomically-1.7.0.tgz",
"integrity": "sha512-Xcz9l0z7y9yQ9rdDaxlmaI4uJHf/T8g9hOEzJcsEqX2SjCj4J20uK7+ldkDHMbpJDK76wF7xEIgxc/vSlsfw5w==" "integrity": "sha512-Xcz9l0z7y9yQ9rdDaxlmaI4uJHf/T8g9hOEzJcsEqX2SjCj4J20uK7+ldkDHMbpJDK76wF7xEIgxc/vSlsfw5w=="
}, },
"audiomotion-analyzer": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/audiomotion-analyzer/-/audiomotion-analyzer-4.5.0.tgz",
"integrity": "sha512-qnmB8TSbrxYkTbFgsQeeym0Z/suQx4c0jFg9Yh5+gaPw6J4AFLdfFpagdnDbtNEsj6K7BntgsC3bkdut5rxozg=="
},
"auto-text-size": { "auto-text-size": {
"version": "0.2.3", "version": "0.2.3",
"resolved": "https://registry.npmjs.org/auto-text-size/-/auto-text-size-0.2.3.tgz", "resolved": "https://registry.npmjs.org/auto-text-size/-/auto-text-size-0.2.3.tgz",
+2 -1
View File
@@ -2,7 +2,7 @@
"name": "feishin", "name": "feishin",
"productName": "Feishin", "productName": "Feishin",
"description": "Feishin music server", "description": "Feishin music server",
"version": "0.8.1", "version": "0.9.0",
"scripts": { "scripts": {
"build": "concurrently \"npm run build:main\" \"npm run build:renderer\" \"npm run build:remote\"", "build": "concurrently \"npm run build:main\" \"npm run build:renderer\" \"npm run build:remote\"",
"build:main": "cross-env NODE_ENV=production TS_NODE_TRANSPILE_ONLY=true webpack --config ./.erb/configs/webpack.config.main.prod.ts", "build:main": "cross-env NODE_ENV=production TS_NODE_TRANSPILE_ONLY=true webpack --config ./.erb/configs/webpack.config.main.prod.ts",
@@ -310,6 +310,7 @@
"@ts-rest/core": "^3.23.0", "@ts-rest/core": "^3.23.0",
"@xhayper/discord-rpc": "^1.0.24", "@xhayper/discord-rpc": "^1.0.24",
"auto-text-size": "^0.2.3", "auto-text-size": "^0.2.3",
"audiomotion-analyzer": "^4.5.0",
"axios": "^1.6.0", "axios": "^1.6.0",
"clsx": "^2.0.0", "clsx": "^2.0.0",
"cmdk": "^0.2.0", "cmdk": "^0.2.0",
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "feishin", "name": "feishin",
"version": "0.8.1", "version": "0.9.0",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "feishin", "name": "feishin",
"version": "0.8.1", "version": "0.9.0",
"hasInstallScript": true, "hasInstallScript": true,
"license": "GPL-3.0", "license": "GPL-3.0",
"dependencies": { "dependencies": {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "feishin", "name": "feishin",
"version": "0.8.1", "version": "0.9.0",
"description": "", "description": "",
"main": "./dist/main/main.js", "main": "./dist/main/main.js",
"author": { "author": {
+7 -3
View File
@@ -11,7 +11,7 @@
"skip_back": "přeskočit dozadu", "skip_back": "přeskočit dozadu",
"favorite": "oblíbené", "favorite": "oblíbené",
"next": "další", "next": "další",
"shuffle": "náhodně", "shuffle": "přehrát náhodně",
"playbackFetchNoResults": "nenalezeny žádné skladby", "playbackFetchNoResults": "nenalezeny žádné skladby",
"playbackFetchInProgress": "načítání skladeb…", "playbackFetchInProgress": "načítání skladeb…",
"addNext": "přidat další", "addNext": "přidat další",
@@ -245,7 +245,10 @@
"playerbarOpenDrawer": "lišta přehrávače jako přepínač celé obrazovky", "playerbarOpenDrawer": "lišta přehrávače jako přepínač celé obrazovky",
"playerbarOpenDrawer_description": "umožňuje kliknutí na lištu přehrávače pro otevření celoobrazovkového přehrávače", "playerbarOpenDrawer_description": "umožňuje kliknutí na lištu přehrávače pro otevření celoobrazovkového přehrávače",
"artistConfiguration": "nastavení stránky umělce alba", "artistConfiguration": "nastavení stránky umělce alba",
"artistConfiguration_description": "nastavit, které položky na stránce umělce alba budou zobrazeny a v jakém pořadí" "artistConfiguration_description": "nastavit, které položky na stránce umělce alba budou zobrazeny a v jakém pořadí",
"playButtonBehavior_optionPlayShuffled": "$t(player.shuffle)",
"trayEnabled": "zobrazit v oznamovací oblasti",
"trayEnabled_description": "zobrazit/skrýt ikonu/nabídku v oznamovací oblasti. pokud je zakázáno, vypne také minimalizaci/ukončení do oznamovací oblasti"
}, },
"action": { "action": {
"editPlaylist": "upravit $t(entity.playlist_one)", "editPlaylist": "upravit $t(entity.playlist_one)",
@@ -572,7 +575,8 @@
"showDetails": "získat informace", "showDetails": "získat informace",
"shareItem": "sdílet položku", "shareItem": "sdílet položku",
"playSimilarSongs": "$t(player.playSimilarSongs)", "playSimilarSongs": "$t(player.playSimilarSongs)",
"download": "stáhnout" "download": "stáhnout",
"playShuffled": "$t(player.shuffle)"
}, },
"home": { "home": {
"mostPlayed": "nejpřehrávanější", "mostPlayed": "nejpřehrávanější",
+18 -5
View File
@@ -66,7 +66,7 @@
"cancel": "Abbrechen", "cancel": "Abbrechen",
"forceRestartRequired": "Neustarten um die Änderungen zu übernehmen... Schließe die Benachrichtigung zum Neustarten", "forceRestartRequired": "Neustarten um die Änderungen zu übernehmen... Schließe die Benachrichtigung zum Neustarten",
"setting": "Einstellungen", "setting": "Einstellungen",
"setting_one": "", "setting_one": "Einstellung",
"setting_other": "Einstellungen", "setting_other": "Einstellungen",
"version": "Version", "version": "Version",
"title": "Titel", "title": "Titel",
@@ -106,7 +106,8 @@
"preview": "Vorschau", "preview": "Vorschau",
"reload": "Neu Laden", "reload": "Neu Laden",
"mbid": "MusicBrainz ID", "mbid": "MusicBrainz ID",
"close": "schliessen" "close": "schliessen",
"share": "Teilen"
}, },
"error": { "error": {
"remotePortWarning": "Starten Sie den Server neu, um den neuen Port anzuwenden", "remotePortWarning": "Starten Sie den Server neu, um den neuen Port anzuwenden",
@@ -218,7 +219,8 @@
"input_optionMatchAny": "Treffer Einige" "input_optionMatchAny": "Treffer Einige"
}, },
"editPlaylist": { "editPlaylist": {
"title": "Bearbeite $t(entity.playlist_one)" "title": "Bearbeite $t(entity.playlist_one)",
"success": "$t(entity.playlist_one) erfolgreich aktualisiert"
}, },
"lyricSearch": { "lyricSearch": {
"title": "Songtext Suche", "title": "Songtext Suche",
@@ -226,7 +228,10 @@
"input_artist": "$t(entity.artist_one)" "input_artist": "$t(entity.artist_one)"
}, },
"shareItem": { "shareItem": {
"description": "Beschreibung" "description": "Beschreibung",
"setExpiration": "Ablaufdatum setzen",
"expireInvalid": "Ablaufdatum muss in der Zukunft liegen",
"allowDownloading": "Herunterladen zulassen"
} }
}, },
"entity": { "entity": {
@@ -260,7 +265,9 @@
"genreWithCount_other": "{{count}} Genres", "genreWithCount_other": "{{count}} Genres",
"trackWithCount_one": "{{count}} Track", "trackWithCount_one": "{{count}} Track",
"trackWithCount_other": "{{count}} Tracks", "trackWithCount_other": "{{count}} Tracks",
"smartPlaylist": "Smart $t(entity.playlist_one)" "smartPlaylist": "Smart $t(entity.playlist_one)",
"play_one": "{{count}} Wiedergabe",
"play_other": "{{count}} Wiedergaben"
}, },
"table": { "table": {
"config": { "config": {
@@ -429,6 +436,12 @@
}, },
"albumList": { "albumList": {
"title": "$t(entity.album_other)" "title": "$t(entity.album_other)"
},
"albumArtistDetail": {
"about": "Über {{artist}}",
"appearsOn": "erscheint auf",
"recentReleases": "Kürzliche Veröffentlichungen",
"viewDiscography": "Diskographie ansehen"
} }
}, },
"player": { "player": {
+5 -1
View File
@@ -333,6 +333,7 @@
"removeFromPlaylist": "$t(action.removeFromPlaylist)", "removeFromPlaylist": "$t(action.removeFromPlaylist)",
"removeFromQueue": "$t(action.removeFromQueue)", "removeFromQueue": "$t(action.removeFromQueue)",
"setRating": "$t(action.setRating)", "setRating": "$t(action.setRating)",
"playShuffled": "$t(player.shuffle)",
"shareItem": "share item", "shareItem": "share item",
"showDetails": "get info" "showDetails": "get info"
}, },
@@ -438,7 +439,7 @@
"repeat_off": "repeat disabled", "repeat_off": "repeat disabled",
"repeat_one": "repeat one", "repeat_one": "repeat one",
"repeat_other": "", "repeat_other": "",
"shuffle": "shuffle", "shuffle": "play shuffled",
"shuffle_off": "shuffle disabled", "shuffle_off": "shuffle disabled",
"skip": "skip", "skip": "skip",
"skip_back": "skip backwards", "skip_back": "skip backwards",
@@ -591,6 +592,7 @@
"playButtonBehavior_optionAddLast": "$t(player.addLast)", "playButtonBehavior_optionAddLast": "$t(player.addLast)",
"playButtonBehavior_optionAddNext": "$t(player.addNext)", "playButtonBehavior_optionAddNext": "$t(player.addNext)",
"playButtonBehavior_optionPlay": "$t(player.play)", "playButtonBehavior_optionPlay": "$t(player.play)",
"playButtonBehavior_optionPlayShuffled": "$t(player.shuffle)",
"playerAlbumArtResolution": "player album art resolution", "playerAlbumArtResolution": "player album art resolution",
"playerAlbumArtResolution_description": "the resolution for the large player's album art preview. larger makes it look more crisp, but may slow loading down. defaults to 0, meaning auto", "playerAlbumArtResolution_description": "the resolution for the large player's album art preview. larger makes it look more crisp, but may slow loading down. defaults to 0, meaning auto",
"playerbarOpenDrawer": "playerbar fullscreen toggle", "playerbarOpenDrawer": "playerbar fullscreen toggle",
@@ -651,6 +653,8 @@
"transcodeBitrate_description": "selects the bitrate to transcode. 0 means let the server pick", "transcodeBitrate_description": "selects the bitrate to transcode. 0 means let the server pick",
"transcodeFormat": "format to transcode", "transcodeFormat": "format to transcode",
"transcodeFormat_description": "selects the format to transcode. leave empty to let the server decide", "transcodeFormat_description": "selects the format to transcode. leave empty to let the server decide",
"trayEnabled": "show tray",
"trayEnabled_description": "show/hide tray icon/menu. if disabled, also disables minimize/exit to tray",
"useSystemTheme": "use system theme", "useSystemTheme": "use system theme",
"useSystemTheme_description": "follow the system-defined light or dark preference", "useSystemTheme_description": "follow the system-defined light or dark preference",
"volumeWheelStep": "volume wheel step", "volumeWheelStep": "volume wheel step",
+7 -3
View File
@@ -11,7 +11,7 @@
"skip_back": "retroceder", "skip_back": "retroceder",
"favorite": "favorito", "favorite": "favorito",
"next": "siguiente", "next": "siguiente",
"shuffle": "mezclar", "shuffle": "Reproducir aleatoriamente",
"playbackFetchNoResults": "ninguna canción encontrada", "playbackFetchNoResults": "ninguna canción encontrada",
"playbackFetchInProgress": "cargando canciones…", "playbackFetchInProgress": "cargando canciones…",
"addNext": "añadir siguiente", "addNext": "añadir siguiente",
@@ -245,7 +245,10 @@
"playerbarOpenDrawer": "Cambiar la barra del reproductor a pantalla completa", "playerbarOpenDrawer": "Cambiar la barra del reproductor a pantalla completa",
"playerbarOpenDrawer_description": "Permitir hacer clic en la barra del reproductor para abrir el reproductor en pantalla completa", "playerbarOpenDrawer_description": "Permitir hacer clic en la barra del reproductor para abrir el reproductor en pantalla completa",
"artistConfiguration": "Configuración de la página del artista del álbum", "artistConfiguration": "Configuración de la página del artista del álbum",
"artistConfiguration_description": "Configurar qué elementos se muestran y en qué orden en la página del artista del álbum" "artistConfiguration_description": "Configurar qué elementos se muestran y en qué orden en la página del artista del álbum",
"playButtonBehavior_optionPlayShuffled": "$t(player.shuffle)",
"trayEnabled": "Mostrar en el área de notificación",
"trayEnabled_description": "mostrar/ocultar el icono/menú del área de notificación. si está deshabilitado, también deshabilita minimizar/salir a la bandeja"
}, },
"action": { "action": {
"editPlaylist": "editar $t(entity.playlist_one)", "editPlaylist": "editar $t(entity.playlist_one)",
@@ -478,7 +481,8 @@
"shareItem": "Compartir elemento", "shareItem": "Compartir elemento",
"showDetails": "Obtener información", "showDetails": "Obtener información",
"playSimilarSongs": "$t(player.playSimilarSongs)", "playSimilarSongs": "$t(player.playSimilarSongs)",
"download": "descargar" "download": "descargar",
"playShuffled": "$t(player.shuffle)"
}, },
"home": { "home": {
"mostPlayed": "más reproducidos", "mostPlayed": "más reproducidos",
+21 -4
View File
@@ -277,7 +277,8 @@
"generalTab": "général", "generalTab": "général",
"hotkeysTab": "raccourcis", "hotkeysTab": "raccourcis",
"windowTab": "fenêtre", "windowTab": "fenêtre",
"playbackTab": "lecteur" "playbackTab": "lecteur",
"advanced": "avancé"
}, },
"globalSearch": { "globalSearch": {
"commands": { "commands": {
@@ -306,7 +307,8 @@
"removeFromQueue": "$t(action.removeFromQueue)", "removeFromQueue": "$t(action.removeFromQueue)",
"shareItem": "partager un élément", "shareItem": "partager un élément",
"playSimilarSongs": "$t(player.playSimilarSongs)", "playSimilarSongs": "$t(player.playSimilarSongs)",
"showDetails": "obtenir des informations" "showDetails": "obtenir des informations",
"download": "télécharger"
}, },
"albumArtistList": { "albumArtistList": {
"title": "$t(entity.albumArtist_other)" "title": "$t(entity.albumArtist_other)"
@@ -530,7 +532,21 @@
"playerAlbumArtResolution_description": "la résolution pour l'aperçu de la pochette d'album agrandie du lecteur. plus grand le rend plus net, mais peut ralentir le chargement. la valeur par défaut est 0 (automatique)", "playerAlbumArtResolution_description": "la résolution pour l'aperçu de la pochette d'album agrandie du lecteur. plus grand le rend plus net, mais peut ralentir le chargement. la valeur par défaut est 0 (automatique)",
"homeConfiguration_description": "configurer quels éléments sont affichés sur la page d'accueil, et dans quel ordre", "homeConfiguration_description": "configurer quels éléments sont affichés sur la page d'accueil, et dans quel ordre",
"startMinimized": "démarrer l'application en mode réduit", "startMinimized": "démarrer l'application en mode réduit",
"genreBehavior_description": "détermine si cliquer sur un genre ouvre par défaut la liste des pistes ou des albums" "genreBehavior_description": "détermine si cliquer sur un genre ouvre par défaut la liste des pistes ou des albums",
"transcode": "activer le transcodage",
"transcode_description": "permet le transcodage vers différents formats",
"transcodeBitrate_description": "sélectionne le débit du transcodage. 0 signifie que le serveur choisit",
"transcodeFormat_description": "sélectionne le format du transcodage. laisser vide pour laisser le serveur décider",
"volumeWidth": "largeur de la barre de volume",
"volumeWidth_description": "la largeur de la barre de volume",
"customCssEnable": "activer le css personnalisé",
"customCssEnable_description": "permet d'écrire du css personnalisé.",
"customCssNotice": "Attention: bien qu'il y ait un certain assainissement (blocage de url() et de content:), l'utilisation de CSS personnalisé peut toujours présenter des risques en modifiant l'interface.",
"customCss": "css personnalisé",
"webAudio": "utiliser l'audio web",
"transcodeBitrate": "débit binaire du transcodage",
"transcodeFormat": "format de transcodage",
"webAudio_description": "utiliser l'audio web. cela permet d'utiliser des fonctions avancées comme le replaygain. désactivez si vous rencontrez d'autres problèmes"
}, },
"form": { "form": {
"deletePlaylist": { "deletePlaylist": {
@@ -574,7 +590,8 @@
"input_optionMatchAny": "correspondre à n'importe quel" "input_optionMatchAny": "correspondre à n'importe quel"
}, },
"editPlaylist": { "editPlaylist": {
"title": "modifier $t(entity.playlist_one)" "title": "modifier $t(entity.playlist_one)",
"publicJellyfinNote": "Jellyfin n'indique pas si une playlist est publique ou non. Si vous souhaitez que cette playlist reste publique, veuillez sélectionner l'entrée suivante"
}, },
"lyricSearch": { "lyricSearch": {
"title": "rechercher parole", "title": "rechercher parole",
+8 -4
View File
@@ -140,7 +140,7 @@
"skip_back": "向后跳过", "skip_back": "向后跳过",
"favorite": "收藏", "favorite": "收藏",
"next": "下一首", "next": "下一首",
"shuffle": "随机", "shuffle": "随机播放",
"playbackFetchNoResults": "未找到歌曲", "playbackFetchNoResults": "未找到歌曲",
"playbackFetchInProgress": "正在加载歌曲…", "playbackFetchInProgress": "正在加载歌曲…",
"addNext": "添加为播放列表下一首", "addNext": "添加为播放列表下一首",
@@ -152,7 +152,7 @@
"unfavorite": "取消收藏", "unfavorite": "取消收藏",
"queue_moveToTop": "将所选项移至底部", "queue_moveToTop": "将所选项移至底部",
"queue_moveToBottom": "将所选项移至顶部", "queue_moveToBottom": "将所选项移至顶部",
"shuffle_off": "随机关闭", "shuffle_off": "禁用随机播放",
"addLast": "添加至播放列表末尾", "addLast": "添加至播放列表末尾",
"mute": "静音", "mute": "静音",
"skip_forward": "向前跳过", "skip_forward": "向前跳过",
@@ -374,7 +374,10 @@
"webAudio_description": "使用 web 音频。这将启用重播增益等高级功能。如果您遇到其他情况,请禁用", "webAudio_description": "使用 web 音频。这将启用重播增益等高级功能。如果您遇到其他情况,请禁用",
"artistConfiguration_description": "配置专辑艺术家页面上显示的项目及其显示顺序", "artistConfiguration_description": "配置专辑艺术家页面上显示的项目及其显示顺序",
"webAudio": "使用 web 音频", "webAudio": "使用 web 音频",
"artistConfiguration": "专辑艺术家页面配置" "artistConfiguration": "专辑艺术家页面配置",
"playButtonBehavior_optionPlayShuffled": "$t(player.shuffle)",
"trayEnabled_description": "显示/隐藏托盘图标/菜单。如果禁用,也会禁用最小化/退出到托盘",
"trayEnabled": "显示托盘"
}, },
"error": { "error": {
"remotePortWarning": "重启服务器使新端口生效", "remotePortWarning": "重启服务器使新端口生效",
@@ -538,7 +541,8 @@
"showDetails": "获取信息", "showDetails": "获取信息",
"shareItem": "分享项目", "shareItem": "分享项目",
"playSimilarSongs": "$t(player.playSimilarSongs)", "playSimilarSongs": "$t(player.playSimilarSongs)",
"download": "下载" "download": "下载",
"playShuffled": "$t(player.shuffle)"
}, },
"trackList": { "trackList": {
"title": "$t(entity.track_other)", "title": "$t(entity.track_other)",
+3 -1
View File
@@ -647,7 +647,9 @@ if (!singleInstance) {
}); });
createWindow(); createWindow();
createTray(); if (store.get('window_enable_tray', true)) {
createTray();
}
app.on('activate', () => { app.on('activate', () => {
// On macOS it's common to re-create a window in the app when the // On macOS it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open. // dock icon is clicked and there are no other windows open.
+16 -11
View File
@@ -53,7 +53,7 @@ const getAlbumArtistCoverArtUrl = (args: {
`${args.baseUrl}/Items` + `${args.baseUrl}/Items` +
`/${args.item.Id}` + `/${args.item.Id}` +
'/Images/Primary' + '/Images/Primary' +
`?width=${size}&` + `?width=${size}` +
'&quality=96' '&quality=96'
); );
}; };
@@ -69,7 +69,7 @@ const getAlbumCoverArtUrl = (args: { baseUrl: string; item: JFAlbum; size: numbe
`${args.baseUrl}/Items` + `${args.baseUrl}/Items` +
`/${args.item.Id}` + `/${args.item.Id}` +
'/Images/Primary' + '/Images/Primary' +
`?width=${size}&height=${size}` + `?width=${size}` +
'&quality=96' '&quality=96'
); );
}; };
@@ -86,7 +86,7 @@ const getSongCoverArtUrl = (args: {
`${args.baseUrl}/Items` + `${args.baseUrl}/Items` +
`/${args.item.Id}` + `/${args.item.Id}` +
'/Images/Primary' + '/Images/Primary' +
`?width=${size}&height=${size}` + `?width=${size}` +
'&quality=96' '&quality=96'
); );
} }
@@ -97,7 +97,7 @@ const getSongCoverArtUrl = (args: {
`${args.baseUrl}/Items` + `${args.baseUrl}/Items` +
`/${args.item?.AlbumId}` + `/${args.item?.AlbumId}` +
'/Images/Primary' + '/Images/Primary' +
`?width=${size}&height=${size}` + `?width=${size}` +
'&quality=96' '&quality=96'
); );
} }
@@ -116,7 +116,7 @@ const getPlaylistCoverArtUrl = (args: { baseUrl: string; item: JFPlaylist; size:
`${args.baseUrl}/Items` + `${args.baseUrl}/Items` +
`/${args.item.Id}` + `/${args.item.Id}` +
'/Images/Primary' + '/Images/Primary' +
`?width=${size}&height=${size}` + `?width=${size}` +
'&quality=96' '&quality=96'
); );
}; };
@@ -153,11 +153,16 @@ const normalizeSong = (
discNumber: (item.ParentIndexNumber && item.ParentIndexNumber) || 1, discNumber: (item.ParentIndexNumber && item.ParentIndexNumber) || 1,
discSubtitle: null, discSubtitle: null,
duration: item.RunTimeTicks / 10000, duration: item.RunTimeTicks / 10000,
gain: item.LUFS gain:
? { item.NormalizationGain !== undefined
track: -18 - item.LUFS, ? {
} track: item.NormalizationGain,
: null, }
: item.LUFS
? {
track: -18 - item.LUFS,
}
: null,
genres: item.GenreItems?.map((entry) => ({ genres: item.GenreItems?.map((entry) => ({
id: entry.Id, id: entry.Id,
imageUrl: null, imageUrl: null,
@@ -388,7 +393,7 @@ const getGenreCoverArtUrl = (args: {
`${args.baseUrl}/Items` + `${args.baseUrl}/Items` +
`/${args.item.Id}` + `/${args.item.Id}` +
'/Images/Primary' + '/Images/Primary' +
`?width=${size}&height=${size}` + `?width=${size}` +
'&quality=96' '&quality=96'
); );
}; };
@@ -413,6 +413,7 @@ const song = z.object({
MediaSources: z.array(mediaSources), MediaSources: z.array(mediaSources),
MediaType: z.string(), MediaType: z.string(),
Name: z.string(), Name: z.string(),
NormalizationGain: z.number().optional(),
ParentIndexNumber: z.number(), ParentIndexNumber: z.number(),
PlaylistItemId: z.string().optional(), PlaylistItemId: z.string().optional(),
PremiereDate: z.string().optional(), PremiereDate: z.string().optional(),
+12 -3
View File
@@ -1,4 +1,4 @@
import { useEffect, useMemo, useRef } from 'react'; import { useEffect, useMemo, useState, useRef } from 'react';
import { ClientSideRowModelModule } from '@ag-grid-community/client-side-row-model'; import { ClientSideRowModelModule } from '@ag-grid-community/client-side-row-model';
import { ModuleRegistry } from '@ag-grid-community/core'; import { ModuleRegistry } from '@ag-grid-community/core';
import { InfiniteRowModelModule } from '@ag-grid-community/infinite-row-model'; import { InfiniteRowModelModule } from '@ag-grid-community/infinite-row-model';
@@ -21,8 +21,9 @@ import { useHandlePlayQueueAdd } from '/@/renderer/features/player/hooks/use-han
import { PlayQueueHandlerContext } from '/@/renderer/features/player'; import { PlayQueueHandlerContext } from '/@/renderer/features/player';
import { getMpvProperties } from '/@/renderer/features/settings/components/playback/mpv-settings'; import { getMpvProperties } from '/@/renderer/features/settings/components/playback/mpv-settings';
import { PlayerState, useCssSettings, usePlayerStore, useQueueControls } from '/@/renderer/store'; import { PlayerState, useCssSettings, usePlayerStore, useQueueControls } from '/@/renderer/store';
import { FontType, PlaybackType, PlayerStatus } from '/@/renderer/types'; import { FontType, PlaybackType, PlayerStatus, WebAudio } from '/@/renderer/types';
import '@ag-grid-community/styles/ag-grid.css'; import '@ag-grid-community/styles/ag-grid.css';
import { WebAudioContext } from '/@/renderer/features/player/context/webaudio-context';
import { useDiscordRpc } from '/@/renderer/features/discord-rpc/use-discord-rpc'; import { useDiscordRpc } from '/@/renderer/features/discord-rpc/use-discord-rpc';
import i18n from '/@/i18n/i18n'; import i18n from '/@/i18n/i18n';
import { useServerVersion } from '/@/renderer/hooks/use-server-version'; import { useServerVersion } from '/@/renderer/hooks/use-server-version';
@@ -91,6 +92,8 @@ export const App = () => {
} }
}, [builtIn, custom, system, type]); }, [builtIn, custom, system, type]);
const [webAudio, setWebAudio] = useState<WebAudio>();
useEffect(() => { useEffect(() => {
if (enabled && content) { if (enabled && content) {
// Yes, CSS is sanitized here as well. Prevent a suer from changing the // Yes, CSS is sanitized here as well. Prevent a suer from changing the
@@ -125,6 +128,10 @@ export const App = () => {
return { handlePlayQueueAdd }; return { handlePlayQueueAdd };
}, [handlePlayQueueAdd]); }, [handlePlayQueueAdd]);
const webAudioProvider = useMemo(() => {
return { setWebAudio, webAudio };
}, [webAudio]);
// Start the mpv instance on startup // Start the mpv instance on startup
useEffect(() => { useEffect(() => {
const initializeMpv = async () => { const initializeMpv = async () => {
@@ -278,7 +285,9 @@ export const App = () => {
> >
<PlayQueueHandlerContext.Provider value={providerValue}> <PlayQueueHandlerContext.Provider value={providerValue}>
<ContextMenuProvider> <ContextMenuProvider>
<AppRouter /> <WebAudioContext.Provider value={webAudioProvider}>
<AppRouter />
</WebAudioContext.Provider>{' '}
</ContextMenuProvider> </ContextMenuProvider>
</PlayQueueHandlerContext.Provider> </PlayQueueHandlerContext.Provider>
<IsUpdatedDialog /> <IsUpdatedDialog />
@@ -18,6 +18,7 @@ import {
import { useSettingsStore, useSettingsStoreActions } from '/@/renderer/store/settings.store'; import { useSettingsStore, useSettingsStoreActions } from '/@/renderer/store/settings.store';
import type { CrossfadeStyle } from '/@/renderer/types'; import type { CrossfadeStyle } from '/@/renderer/types';
import { PlaybackStyle, PlayerStatus } from '/@/renderer/types'; import { PlaybackStyle, PlayerStatus } from '/@/renderer/types';
import { useWebAudio } from '/@/renderer/features/player/hooks/use-webaudio';
import { getServerById, TranscodingConfig, usePlaybackSettings, useSpeed } from '/@/renderer/store'; import { getServerById, TranscodingConfig, usePlaybackSettings, useSpeed } from '/@/renderer/store';
import { toast } from '/@/renderer/components/toast'; import { toast } from '/@/renderer/components/toast';
import { api } from '/@/renderer/api'; import { api } from '/@/renderer/api';
@@ -44,11 +45,6 @@ const getDuration = (ref: any) => {
return ref.current?.player?.player?.player?.duration; return ref.current?.player?.player?.player?.duration;
}; };
type WebAudio = {
context: AudioContext;
gain: GainNode;
};
// Credits: https://gist.github.com/novwhisky/8a1a0168b94f3b6abfaa?permalink_comment_id=1551393#gistcomment-1551393 // Credits: https://gist.github.com/novwhisky/8a1a0168b94f3b6abfaa?permalink_comment_id=1551393#gistcomment-1551393
// This is used so that the player will always have an <audio> element. This means that // This is used so that the player will always have an <audio> element. This means that
// player1Source and player2Source are connected BEFORE the user presses play for // player1Source and player2Source are connected BEFORE the user presses play for
@@ -116,7 +112,7 @@ export const AudioPlayer = forwardRef(
const [isTransitioning, setIsTransitioning] = useState(false); const [isTransitioning, setIsTransitioning] = useState(false);
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 useWebAudio = useSettingsStore((state) => state.playback.webAudio); const shouldUseWebAudio = useSettingsStore((state) => state.playback.webAudio);
const { resetSampleRate } = useSettingsStoreActions(); const { resetSampleRate } = useSettingsStoreActions();
const playbackSpeed = useSpeed(); const playbackSpeed = useSpeed();
const { transcode } = usePlaybackSettings(); const { transcode } = usePlaybackSettings();
@@ -124,7 +120,7 @@ export const AudioPlayer = forwardRef(
const stream1 = useSongUrl(transcode, currentPlayer === 1, player1); const stream1 = useSongUrl(transcode, currentPlayer === 1, player1);
const stream2 = useSongUrl(transcode, currentPlayer === 2, player2); const stream2 = useSongUrl(transcode, currentPlayer === 2, player2);
const [webAudio, setWebAudio] = useState<WebAudio | null>(null); const { webAudio, setWebAudio } = useWebAudio();
const [player1Source, setPlayer1Source] = useState<MediaElementAudioSourceNode | null>( const [player1Source, setPlayer1Source] = useState<MediaElementAudioSourceNode | null>(
null, null,
); );
@@ -181,7 +177,7 @@ export const AudioPlayer = forwardRef(
); );
useEffect(() => { useEffect(() => {
if (useWebAudio && 'AudioContext' in window) { if (shouldUseWebAudio && 'AudioContext' in window) {
let context: AudioContext; let context: AudioContext;
try { try {
@@ -200,7 +196,7 @@ export const AudioPlayer = forwardRef(
const gain = context.createGain(); const gain = context.createGain();
gain.connect(context.destination); gain.connect(context.destination);
setWebAudio({ context, gain }); setWebAudio!({ context, gain });
return () => { return () => {
return context.close(); return context.close();
@@ -568,7 +568,7 @@ export const AlbumArtistDetailContent = ({ background }: AlbumArtistDetailConten
suppressRowDrag suppressRowDrag
columnDefs={topSongsColumnDefs} columnDefs={topSongsColumnDefs}
enableCellChangeFlash={false} enableCellChangeFlash={false}
getRowId={(data) => data.data.id} getRowId={(data) => data.data.uniqueId}
rowData={topSongs} rowData={topSongs}
rowHeight={60} rowHeight={60}
rowSelection="multiple" rowSelection="multiple"
@@ -66,7 +66,7 @@ export const AlbumArtistDetailTopSongsListContent = ({
ref={tableRef} ref={tableRef}
shouldUpdateSong shouldUpdateSong
{...tableProps} {...tableProps}
getRowId={(data) => data.data.id} getRowId={(data) => data.data.uniqueId}
rowClassRules={rowClassRules} rowClassRules={rowClassRules}
rowData={data} rowData={data}
rowModelType="clientSide" rowModelType="clientSide"
@@ -18,6 +18,7 @@ export const SONG_CONTEXT_MENU_ITEMS: SetContextMenuItems = [
{ id: 'play' }, { id: 'play' },
{ id: 'playLast' }, { id: 'playLast' },
{ id: 'playNext' }, { id: 'playNext' },
{ id: 'playShuffled' },
{ divider: true, id: 'playSimilarSongs' }, { divider: true, id: 'playSimilarSongs' },
{ divider: true, id: 'addToPlaylist' }, { divider: true, id: 'addToPlaylist' },
{ id: 'addToFavorites' }, { id: 'addToFavorites' },
@@ -31,7 +32,8 @@ export const SONG_CONTEXT_MENU_ITEMS: SetContextMenuItems = [
export const SONG_ALBUM_PAGE: SetContextMenuItems = [ export const SONG_ALBUM_PAGE: SetContextMenuItems = [
{ id: 'play' }, { id: 'play' },
{ id: 'playLast' }, { id: 'playLast' },
{ divider: true, id: 'playNext' }, { id: 'playNext' },
{ divider: true, id: 'playShuffled' },
{ divider: true, id: 'addToPlaylist' }, { divider: true, id: 'addToPlaylist' },
]; ];
@@ -39,6 +41,7 @@ export const PLAYLIST_SONG_CONTEXT_MENU_ITEMS: SetContextMenuItems = [
{ id: 'play' }, { id: 'play' },
{ id: 'playLast' }, { id: 'playLast' },
{ id: 'playNext' }, { id: 'playNext' },
{ id: 'playShuffled' },
{ divider: true, id: 'playSimilarSongs' }, { divider: true, id: 'playSimilarSongs' },
{ id: 'addToPlaylist' }, { id: 'addToPlaylist' },
{ divider: true, id: 'removeFromPlaylist' }, { divider: true, id: 'removeFromPlaylist' },
@@ -54,6 +57,7 @@ export const SMART_PLAYLIST_SONG_CONTEXT_MENU_ITEMS: SetContextMenuItems = [
{ id: 'play' }, { id: 'play' },
{ id: 'playLast' }, { id: 'playLast' },
{ id: 'playNext' }, { id: 'playNext' },
{ divider: true, id: 'playShuffled' },
{ divider: true, id: 'playSimilarSongs' }, { divider: true, id: 'playSimilarSongs' },
{ divider: true, id: 'addToPlaylist' }, { divider: true, id: 'addToPlaylist' },
{ id: 'addToFavorites' }, { id: 'addToFavorites' },
@@ -67,7 +71,8 @@ export const SMART_PLAYLIST_SONG_CONTEXT_MENU_ITEMS: SetContextMenuItems = [
export const ALBUM_CONTEXT_MENU_ITEMS: SetContextMenuItems = [ export const ALBUM_CONTEXT_MENU_ITEMS: SetContextMenuItems = [
{ id: 'play' }, { id: 'play' },
{ id: 'playLast' }, { id: 'playLast' },
{ divider: true, id: 'playNext' }, { id: 'playNext' },
{ divider: true, id: 'playShuffled' },
{ divider: true, id: 'addToPlaylist' }, { divider: true, id: 'addToPlaylist' },
{ id: 'addToFavorites' }, { id: 'addToFavorites' },
{ id: 'removeFromFavorites' }, { id: 'removeFromFavorites' },
@@ -79,14 +84,16 @@ export const ALBUM_CONTEXT_MENU_ITEMS: SetContextMenuItems = [
export const GENRE_CONTEXT_MENU_ITEMS: SetContextMenuItems = [ export const GENRE_CONTEXT_MENU_ITEMS: SetContextMenuItems = [
{ id: 'play' }, { id: 'play' },
{ id: 'playLast' }, { id: 'playLast' },
{ divider: true, id: 'playNext' }, { id: 'playNext' },
{ divider: true, id: 'playShuffled' },
{ divider: true, id: 'addToPlaylist' }, { divider: true, id: 'addToPlaylist' },
]; ];
export const ARTIST_CONTEXT_MENU_ITEMS: SetContextMenuItems = [ export const ARTIST_CONTEXT_MENU_ITEMS: SetContextMenuItems = [
{ id: 'play' }, { id: 'play' },
{ id: 'playLast' }, { id: 'playLast' },
{ divider: true, id: 'playNext' }, { id: 'playNext' },
{ divider: true, id: 'playShuffled' },
{ divider: true, id: 'addToPlaylist' }, { divider: true, id: 'addToPlaylist' },
{ id: 'addToFavorites' }, { id: 'addToFavorites' },
{ divider: true, id: 'removeFromFavorites' }, { divider: true, id: 'removeFromFavorites' },
@@ -98,7 +105,8 @@ export const ARTIST_CONTEXT_MENU_ITEMS: SetContextMenuItems = [
export const PLAYLIST_CONTEXT_MENU_ITEMS: SetContextMenuItems = [ export const PLAYLIST_CONTEXT_MENU_ITEMS: SetContextMenuItems = [
{ id: 'play' }, { id: 'play' },
{ id: 'playLast' }, { id: 'playLast' },
{ divider: true, id: 'playNext' }, { id: 'playNext' },
{ divider: true, id: 'playShuffled' },
{ divider: true, id: 'shareItem' }, { divider: true, id: 'shareItem' },
{ id: 'deletePlaylist' }, { id: 'deletePlaylist' },
]; ];
@@ -31,6 +31,7 @@ import {
RiInformationFill, RiInformationFill,
RiRadio2Fill, RiRadio2Fill,
RiDownload2Line, RiDownload2Line,
RiShuffleFill,
} from 'react-icons/ri'; } from 'react-icons/ri';
import { AnyLibraryItems, LibraryItem, ServerType, AnyLibraryItem } from '/@/renderer/api/types'; import { AnyLibraryItems, LibraryItem, ServerType, AnyLibraryItem } from '/@/renderer/api/types';
import { import {
@@ -774,6 +775,12 @@ export const ContextMenuProvider = ({ children }: ContextMenuProviderProps) => {
leftIcon: <RiAddCircleFill size="1.1rem" />, leftIcon: <RiAddCircleFill size="1.1rem" />,
onClick: () => handlePlay(Play.NEXT), onClick: () => handlePlay(Play.NEXT),
}, },
playShuffled: {
id: 'playShuffled',
label: t('page.contextMenu.playShuffled', { postProcess: 'sentenceCase' }),
leftIcon: <RiShuffleFill size="1.1rem" />,
onClick: () => handlePlay(Play.SHUFFLE),
},
playSimilarSongs: { playSimilarSongs: {
id: 'playSimilarSongs', id: 'playSimilarSongs',
label: t('page.contextMenu.playSimilarSongs', { postProcess: 'sentenceCase' }), label: t('page.contextMenu.playSimilarSongs', { postProcess: 'sentenceCase' }),
@@ -23,6 +23,7 @@ export type ContextMenuItemType =
| 'play' | 'play'
| 'playLast' | 'playLast'
| 'playNext' | 'playNext'
| 'playShuffled'
| 'addToPlaylist' | 'addToPlaylist'
| 'removeFromPlaylist' | 'removeFromPlaylist'
| 'addToFavorites' | 'addToFavorites'
@@ -45,6 +46,7 @@ export const CONFIGURABLE_CONTEXT_MENU_ITEMS: ContextMenuItemType[] = [
'play', 'play',
'playLast', 'playLast',
'playNext', 'playNext',
'playShuffled',
'playSimilarSongs', 'playSimilarSongs',
'addToPlaylist', 'addToPlaylist',
'removeFromPlaylist', 'removeFromPlaylist',
@@ -11,8 +11,17 @@ import {
useFullScreenPlayerStoreActions, useFullScreenPlayerStoreActions,
} from '/@/renderer/store/full-screen-player.store'; } from '/@/renderer/store/full-screen-player.store';
import { Lyrics } from '/@/renderer/features/lyrics/lyrics'; import { Lyrics } from '/@/renderer/features/lyrics/lyrics';
import { lazy, Suspense, useMemo } from 'react';
import { usePlaybackSettings } from '/@/renderer/store';
import { PlaybackType } from '/@/renderer/types';
import { FullScreenSimilarSongs } from '/@/renderer/features/player/components/full-screen-similar-songs'; import { FullScreenSimilarSongs } from '/@/renderer/features/player/components/full-screen-similar-songs';
const Visualizer = lazy(() =>
import('/@/renderer/features/player/components/visualizer').then((module) => ({
default: module.Visualizer,
})),
);
const QueueContainer = styled.div` const QueueContainer = styled.div`
position: relative; position: relative;
display: flex; display: flex;
@@ -61,27 +70,41 @@ export const FullScreenPlayerQueue = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const { activeTab, opacity } = useFullScreenPlayerStore(); const { activeTab, opacity } = useFullScreenPlayerStore();
const { setStore } = useFullScreenPlayerStoreActions(); const { setStore } = useFullScreenPlayerStoreActions();
const { type, webAudio } = usePlaybackSettings();
const headerItems = [ const headerItems = useMemo(() => {
{ const items = [
active: activeTab === 'queue', {
icon: <RiFileMusicLine size="1.5rem" />, active: activeTab === 'queue',
label: t('page.fullscreenPlayer.upNext'), icon: <RiFileMusicLine size="1.5rem" />,
onClick: () => setStore({ activeTab: 'queue' }), label: t('page.fullscreenPlayer.upNext'),
}, onClick: () => setStore({ activeTab: 'queue' }),
{ },
active: activeTab === 'related', {
icon: <HiOutlineQueueList size="1.5rem" />, active: activeTab === 'related',
label: t('page.fullscreenPlayer.related'), icon: <HiOutlineQueueList size="1.5rem" />,
onClick: () => setStore({ activeTab: 'related' }), label: t('page.fullscreenPlayer.related'),
}, onClick: () => setStore({ activeTab: 'related' }),
{ },
active: activeTab === 'lyrics', {
icon: <RiFileTextLine size="1.5rem" />, active: activeTab === 'lyrics',
label: t('page.fullscreenPlayer.lyrics'), icon: <RiFileTextLine size="1.5rem" />,
onClick: () => setStore({ activeTab: 'lyrics' }), label: t('page.fullscreenPlayer.lyrics'),
}, onClick: () => setStore({ activeTab: 'lyrics' }),
]; },
];
if (type === PlaybackType.WEB && webAudio) {
items.push({
active: activeTab === 'visualizer',
icon: <RiFileTextLine size="1.5rem" />,
label: 'Visualizer',
onClick: () => setStore({ activeTab: 'visualizer' }),
});
}
return items;
}, [activeTab, setStore, t, type, webAudio]);
return ( return (
<GridContainer <GridContainer
@@ -91,6 +114,7 @@ export const FullScreenPlayerQueue = () => {
<Group <Group
grow grow
align="center" align="center"
className="full-screen-player-queue-header"
position="center" position="center"
> >
{headerItems.map((item) => ( {headerItems.map((item) => (
@@ -127,6 +151,10 @@ export const FullScreenPlayerQueue = () => {
</QueueContainer> </QueueContainer>
) : activeTab === 'lyrics' ? ( ) : activeTab === 'lyrics' ? (
<Lyrics /> <Lyrics />
) : activeTab === 'visualizer' && type === PlaybackType.WEB && webAudio ? (
<Suspense fallback={<></>}>
<Visualizer />
</Suspense>
) : null} ) : null}
</GridContainer> </GridContainer>
); );
@@ -244,8 +244,8 @@ export const LeftControls = () => {
<React.Fragment key={`bar-${artist.id}`}> <React.Fragment key={`bar-${artist.id}`}>
{index > 0 && <Separator />} {index > 0 && <Separator />}
<Text <Text
$link $link={artist.id !== ''}
component={Link} component={artist.id ? Link : undefined}
overflow="hidden" overflow="hidden"
size="md" size="md"
to={ to={
@@ -253,7 +253,7 @@ export const LeftControls = () => {
? generatePath(AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL, { ? generatePath(AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL, {
albumArtistId: artist.id, albumArtistId: artist.id,
}) })
: '' : undefined
} }
weight={500} weight={500}
> >
@@ -0,0 +1,72 @@
import { createRef, useCallback, useEffect, useState } from 'react';
import { useWebAudio } from '/@/renderer/features/player/hooks/use-webaudio';
import AudioMotionAnalyzer from 'audiomotion-analyzer';
import styled from 'styled-components';
import { useSettingsStore } from '/@/renderer/store';
const StyledContainer = styled.div`
margin: auto;
max-width: 100%;
canvas {
margin: auto;
width: 100%;
}
`;
export const Visualizer = () => {
const { webAudio } = useWebAudio();
const canvasRef = createRef<HTMLDivElement>();
const accent = useSettingsStore((store) => store.general.accent);
const [motion, setMotion] = useState<AudioMotionAnalyzer>();
const [length, setLength] = useState(500);
useEffect(() => {
const { context, gain } = webAudio || {};
if (gain && context && canvasRef.current && !motion) {
const audioMotion = new AudioMotionAnalyzer(canvasRef.current, {
ansiBands: true,
audioCtx: context,
connectSpeakers: false,
gradient: 'prism',
mode: 4,
showPeaks: false,
smoothing: 0.8,
});
setMotion(audioMotion);
audioMotion.connectInput(gain);
}
return () => {};
}, [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 (
<StyledContainer
ref={canvasRef}
style={{ height: length, width: length }}
/>
);
};
@@ -0,0 +1,7 @@
import { createContext } from 'react';
import { WebAudio } from '/@/renderer/types';
export const WebAudioContext = createContext<{
setWebAudio?: (audio: WebAudio) => void;
webAudio?: WebAudio;
}>({});
@@ -0,0 +1,7 @@
import { useContext } from 'react';
import { WebAudioContext } from '/@/renderer/features/player/context/webaudio-context';
export const useWebAudio = () => {
const { webAudio, setWebAudio } = useContext(WebAudioContext);
return { setWebAudio, webAudio };
};
@@ -222,7 +222,7 @@ export const PlaylistDetailContent = ({ tableRef }: PlaylistDetailContentProps)
suppressLoadingOverlay suppressLoadingOverlay
suppressRowDrag suppressRowDrag
columnDefs={columnDefs} columnDefs={columnDefs}
getRowId={(data) => `${data.data.id}-${data.data.pageIndex}`} getRowId={(data) => `${data.data.uniqueId}-${data.data.pageIndex}`}
rowClassRules={rowClassRules} rowClassRules={rowClassRules}
rowData={playlistSongData} rowData={playlistSongData}
rowHeight={60} rowHeight={60}
@@ -215,6 +215,13 @@ export const ControlSettings = () => {
}), }),
value: Play.LAST, value: Play.LAST,
}, },
{
label: t('setting.playButtonBehavior', {
context: 'optionPlayShuffled',
postProcess: 'titleCase',
}),
value: Play.SHUFFLE,
},
]} ]}
defaultValue={settings.playButtonBehavior} defaultValue={settings.playButtonBehavior}
onChange={(e) => onChange={(e) =>
@@ -1,7 +1,7 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { SelectItem, Switch } from '@mantine/core'; import { SelectItem } from '@mantine/core';
import isElectron from 'is-electron'; import isElectron from 'is-electron';
import { Select, Slider, toast } from '/@/renderer/components'; import { Select, Slider, Switch, toast } from '/@/renderer/components';
import { import {
SettingsSection, SettingsSection,
SettingOption, SettingOption,
@@ -36,7 +36,6 @@ export const TranscodeSettings = () => {
aria-label="Transcode bitrate" aria-label="Transcode bitrate"
defaultValue={transcode.bitrate} defaultValue={transcode.bitrate}
min={0} min={0}
placeholder="mp3, opus"
w={100} w={100}
onBlur={(e) => { onBlur={(e) => {
setTranscodingConfig({ setTranscodingConfig({
@@ -61,6 +60,7 @@ export const TranscodeSettings = () => {
<TextInput <TextInput
aria-label="transcoding format" aria-label="transcoding format"
defaultValue={transcode.format} defaultValue={transcode.format}
placeholder="mp3, opus"
width={100} width={100}
onBlur={(e) => { onBlur={(e) => {
setTranscodingConfig({ setTranscodingConfig({
@@ -81,11 +81,55 @@ export const WindowSettings = () => {
isHidden: !isElectron(), isHidden: !isElectron(),
title: t('setting.windowBarStyle', { postProcess: 'sentenceCase' }), title: t('setting.windowBarStyle', { postProcess: 'sentenceCase' }),
}, },
{
control: (
<Switch
aria-label="toggle hiding tray"
defaultChecked={settings.tray}
disabled={!isElectron()}
onChange={(e) => {
if (!e) return;
localSettings?.set('window_enable_tray', e.currentTarget.checked);
if (e.currentTarget.checked) {
setSettings({
window: {
...settings,
tray: true,
},
});
} else {
localSettings?.set('window_start_minimized', false);
localSettings?.set('window_exit_to_tray', false);
localSettings?.set('window_minimize_to_tray', false);
setSettings({
window: {
...settings,
exitToTray: false,
minimizeToTray: false,
startMinimized: false,
tray: false,
},
});
}
}}
/>
),
description: t('setting.trayEnabled', {
context: 'description',
postProcess: 'sentenceCase',
}),
isHidden: !isElectron(),
note: t('common.restartRequired', {
postProcess: 'sentenceCase',
}),
title: t('setting.trayEnabled', { postProcess: 'sentenceCase' }),
},
{ {
control: ( control: (
<Switch <Switch
aria-label="Toggle minimize to tray" aria-label="Toggle minimize to tray"
defaultChecked={settings.minimizeToTray} defaultChecked={settings.tray}
disabled={!isElectron()} disabled={!isElectron()}
onChange={(e) => { onChange={(e) => {
if (!e) return; if (!e) return;
@@ -103,7 +147,7 @@ export const WindowSettings = () => {
context: 'description', context: 'description',
postProcess: 'sentenceCase', postProcess: 'sentenceCase',
}), }),
isHidden: !isElectron(), isHidden: !isElectron() || !settings.tray,
title: t('setting.minimizeToTray', { postProcess: 'sentenceCase' }), title: t('setting.minimizeToTray', { postProcess: 'sentenceCase' }),
}, },
{ {
@@ -128,7 +172,7 @@ export const WindowSettings = () => {
context: 'description', context: 'description',
postProcess: 'sentenceCase', postProcess: 'sentenceCase',
}), }),
isHidden: !isElectron(), isHidden: !isElectron() || !settings.tray,
title: t('setting.exitToTray', { postProcess: 'sentenceCase' }), title: t('setting.exitToTray', { postProcess: 'sentenceCase' }),
}, },
{ {
@@ -153,7 +197,7 @@ export const WindowSettings = () => {
context: 'description', context: 'description',
postProcess: 'sentenceCase', postProcess: 'sentenceCase',
}), }),
isHidden: !isElectron(), isHidden: !isElectron() || !settings.tray,
title: t('setting.startMinimized', { postProcess: 'sentenceCase' }), title: t('setting.startMinimized', { postProcess: 'sentenceCase' }),
}, },
]; ];
@@ -70,7 +70,7 @@ export const SimilarSongsList = ({ count, fullScreen, song }: SimilarSongsListPr
song, song,
}} }}
deselectOnClickOutside={fullScreen} deselectOnClickOutside={fullScreen}
getRowId={(data) => data.data.id} getRowId={(data) => data.data.uniqueId}
rowBuffer={50} rowBuffer={50}
rowData={songQuery.data ?? []} rowData={songQuery.data ?? []}
rowHeight={tableConfig.rowHeight || 40} rowHeight={tableConfig.rowHeight || 40}
+40 -6
View File
@@ -107,18 +107,53 @@ export const usePlayerStore = create<PlayerSlice>()(
actions: { actions: {
addToQueue: (args) => { addToQueue: (args) => {
const { initialIndex, playType, songs } = args; const { initialIndex, playType, songs } = args;
const { shuffledIndex } = get().current;
const shuffledQueue = get().queue.shuffled;
const songsToAddToQueue = map(songs, (song) => ({ const songsToAddToQueue = map(songs, (song) => ({
...song, ...song,
uniqueId: nanoid(), uniqueId: nanoid(),
})); }));
const queue = get().queue.default;
// If the queue is empty, next/last should behave the same as now // If the queue is empty, next/last should behave the same as now
if (playType === Play.NOW || queue.length === 0) { if (playType === Play.SHUFFLE) {
const songs = shuffle(songsToAddToQueue);
const initialSong = songs[0];
if (get().shuffle === PlayerShuffle.TRACK) {
const shuffledIds = [
initialSong.uniqueId,
...shuffle(songs.slice(1).map((song) => song.uniqueId)),
];
set((state) => {
state.queue.default = songs;
state.queue.shuffled = shuffledIds;
state.current.time = 0;
state.current.player = 1;
state.current.index = 0;
state.current.shuffledIndex = 0;
state.current.song = initialSong;
});
} else {
set((state) => {
state.queue.default = songs;
state.queue.shuffled = [];
state.current.time = 0;
state.current.player = 1;
state.current.index = 0;
state.current.shuffledIndex = 0;
state.current.song = initialSong;
});
}
return get().actions.getPlayerData();
}
const shuffledQueue = get().queue.shuffled;
const queue = get().queue.default;
const { shuffledIndex } = get().current;
if (playType === Play.NOW || queue.length === 0) {
const index = initialIndex || 0;
if (get().shuffle === PlayerShuffle.TRACK) { if (get().shuffle === PlayerShuffle.TRACK) {
const index = initialIndex || 0;
const initialSong = songsToAddToQueue[index]; const initialSong = songsToAddToQueue[index];
const queueCopy = [...songsToAddToQueue]; const queueCopy = [...songsToAddToQueue];
@@ -145,7 +180,6 @@ export const usePlayerStore = create<PlayerSlice>()(
state.current.song = initialSong; state.current.song = initialSong;
}); });
} else { } else {
const index = initialIndex || 0;
set((state) => { set((state) => {
state.queue.default = songsToAddToQueue; state.queue.default = songsToAddToQueue;
state.current.time = 0; state.current.time = 0;
+2
View File
@@ -313,6 +313,7 @@ export interface SettingsState {
exitToTray: boolean; exitToTray: boolean;
minimizeToTray: boolean; minimizeToTray: boolean;
startMinimized: boolean; startMinimized: boolean;
tray: boolean;
windowBarStyle: Platform; windowBarStyle: Platform;
}; };
} }
@@ -647,6 +648,7 @@ const initialState: SettingsState = {
exitToTray: false, exitToTray: false,
minimizeToTray: false, minimizeToTray: false,
startMinimized: false, startMinimized: false,
tray: true,
windowBarStyle: platformDefaultWindowBarStyle, windowBarStyle: platformDefaultWindowBarStyle,
}, },
}; };
+6
View File
@@ -109,6 +109,7 @@ export enum Play {
LAST = 'last', LAST = 'last',
NEXT = 'next', NEXT = 'next',
NOW = 'now', NOW = 'now',
SHUFFLE = 'shuffle',
} }
export enum CrossfadeStyle { export enum CrossfadeStyle {
@@ -234,3 +235,8 @@ export enum AuthState {
LOADING = 'loading', LOADING = 'loading',
VALID = 'valid', VALID = 'valid',
} }
export type WebAudio = {
context: AudioContext;
gain: GainNode;
};