mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-07 04:20:12 +02:00
Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 93f2573847 | |||
| 03d97c6b1e | |||
| 0b86cb51d3 | |||
| ee54b8219b | |||
| 25b593aadd | |||
| 5253e32b67 | |||
| b0b558c90a | |||
| f11a53c1a4 | |||
| e2a05f4204 | |||
| fcc010eb54 | |||
| 1b41a5a674 | |||
| 74aa88e082 | |||
| fbac33ceba | |||
| 42ba5a531c | |||
| 257e1e2cd9 | |||
| 3025e84c58 | |||
| 4a111d9cf2 | |||
| e6bd8deb0c |
@@ -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`.
|
||||||
|
|||||||
Generated
+18
-2
@@ -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
@@ -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",
|
||||||
|
|||||||
Generated
+2
-2
@@ -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,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": {
|
||||||
|
|||||||
@@ -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ší",
|
||||||
|
|||||||
@@ -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": {
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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
@@ -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.
|
||||||
|
|||||||
@@ -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
@@ -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"
|
||||||
|
|||||||
+1
-1
@@ -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}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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;
|
||||||
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user