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
|
||||
|
||||
To install via Docker Compose use the following snippit. This also works on Portainer.
|
||||
|
||||
```
|
||||
version: '3'
|
||||
services:
|
||||
@@ -92,7 +94,6 @@ services:
|
||||
restart: unless-stopped
|
||||
```
|
||||
|
||||
|
||||
### 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.
|
||||
@@ -130,6 +131,8 @@ chmod 4755 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
|
||||
|
||||
Built and tested using Node `v16.15.0`.
|
||||
|
||||
Generated
+18
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "feishin",
|
||||
"version": "0.8.1",
|
||||
"version": "0.9.0",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "feishin",
|
||||
"version": "0.8.1",
|
||||
"version": "0.9.0",
|
||||
"hasInstallScript": true,
|
||||
"license": "GPL-3.0",
|
||||
"dependencies": {
|
||||
@@ -28,6 +28,7 @@
|
||||
"@tanstack/react-query-persist-client": "^4.32.1",
|
||||
"@ts-rest/core": "^3.23.0",
|
||||
"@xhayper/discord-rpc": "^1.0.24",
|
||||
"audiomotion-analyzer": "^4.5.0",
|
||||
"auto-text-size": "^0.2.3",
|
||||
"axios": "^1.6.0",
|
||||
"clsx": "^2.0.0",
|
||||
@@ -6740,6 +6741,16 @@
|
||||
"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": {
|
||||
"version": "0.2.3",
|
||||
"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",
|
||||
"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": {
|
||||
"version": "0.2.3",
|
||||
"resolved": "https://registry.npmjs.org/auto-text-size/-/auto-text-size-0.2.3.tgz",
|
||||
|
||||
+2
-1
@@ -2,7 +2,7 @@
|
||||
"name": "feishin",
|
||||
"productName": "Feishin",
|
||||
"description": "Feishin music server",
|
||||
"version": "0.8.1",
|
||||
"version": "0.9.0",
|
||||
"scripts": {
|
||||
"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",
|
||||
@@ -310,6 +310,7 @@
|
||||
"@ts-rest/core": "^3.23.0",
|
||||
"@xhayper/discord-rpc": "^1.0.24",
|
||||
"auto-text-size": "^0.2.3",
|
||||
"audiomotion-analyzer": "^4.5.0",
|
||||
"axios": "^1.6.0",
|
||||
"clsx": "^2.0.0",
|
||||
"cmdk": "^0.2.0",
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "feishin",
|
||||
"version": "0.8.1",
|
||||
"version": "0.9.0",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "feishin",
|
||||
"version": "0.8.1",
|
||||
"version": "0.9.0",
|
||||
"hasInstallScript": true,
|
||||
"license": "GPL-3.0",
|
||||
"dependencies": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "feishin",
|
||||
"version": "0.8.1",
|
||||
"version": "0.9.0",
|
||||
"description": "",
|
||||
"main": "./dist/main/main.js",
|
||||
"author": {
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
"skip_back": "přeskočit dozadu",
|
||||
"favorite": "oblíbené",
|
||||
"next": "další",
|
||||
"shuffle": "náhodně",
|
||||
"shuffle": "přehrát náhodně",
|
||||
"playbackFetchNoResults": "nenalezeny žádné skladby",
|
||||
"playbackFetchInProgress": "načítání skladeb…",
|
||||
"addNext": "přidat další",
|
||||
@@ -245,7 +245,10 @@
|
||||
"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",
|
||||
"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": {
|
||||
"editPlaylist": "upravit $t(entity.playlist_one)",
|
||||
@@ -572,7 +575,8 @@
|
||||
"showDetails": "získat informace",
|
||||
"shareItem": "sdílet položku",
|
||||
"playSimilarSongs": "$t(player.playSimilarSongs)",
|
||||
"download": "stáhnout"
|
||||
"download": "stáhnout",
|
||||
"playShuffled": "$t(player.shuffle)"
|
||||
},
|
||||
"home": {
|
||||
"mostPlayed": "nejpřehrávanější",
|
||||
|
||||
@@ -66,7 +66,7 @@
|
||||
"cancel": "Abbrechen",
|
||||
"forceRestartRequired": "Neustarten um die Änderungen zu übernehmen... Schließe die Benachrichtigung zum Neustarten",
|
||||
"setting": "Einstellungen",
|
||||
"setting_one": "",
|
||||
"setting_one": "Einstellung",
|
||||
"setting_other": "Einstellungen",
|
||||
"version": "Version",
|
||||
"title": "Titel",
|
||||
@@ -106,7 +106,8 @@
|
||||
"preview": "Vorschau",
|
||||
"reload": "Neu Laden",
|
||||
"mbid": "MusicBrainz ID",
|
||||
"close": "schliessen"
|
||||
"close": "schliessen",
|
||||
"share": "Teilen"
|
||||
},
|
||||
"error": {
|
||||
"remotePortWarning": "Starten Sie den Server neu, um den neuen Port anzuwenden",
|
||||
@@ -218,7 +219,8 @@
|
||||
"input_optionMatchAny": "Treffer Einige"
|
||||
},
|
||||
"editPlaylist": {
|
||||
"title": "Bearbeite $t(entity.playlist_one)"
|
||||
"title": "Bearbeite $t(entity.playlist_one)",
|
||||
"success": "$t(entity.playlist_one) erfolgreich aktualisiert"
|
||||
},
|
||||
"lyricSearch": {
|
||||
"title": "Songtext Suche",
|
||||
@@ -226,7 +228,10 @@
|
||||
"input_artist": "$t(entity.artist_one)"
|
||||
},
|
||||
"shareItem": {
|
||||
"description": "Beschreibung"
|
||||
"description": "Beschreibung",
|
||||
"setExpiration": "Ablaufdatum setzen",
|
||||
"expireInvalid": "Ablaufdatum muss in der Zukunft liegen",
|
||||
"allowDownloading": "Herunterladen zulassen"
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
@@ -260,7 +265,9 @@
|
||||
"genreWithCount_other": "{{count}} Genres",
|
||||
"trackWithCount_one": "{{count}} Track",
|
||||
"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": {
|
||||
"config": {
|
||||
@@ -429,6 +436,12 @@
|
||||
},
|
||||
"albumList": {
|
||||
"title": "$t(entity.album_other)"
|
||||
},
|
||||
"albumArtistDetail": {
|
||||
"about": "Über {{artist}}",
|
||||
"appearsOn": "erscheint auf",
|
||||
"recentReleases": "Kürzliche Veröffentlichungen",
|
||||
"viewDiscography": "Diskographie ansehen"
|
||||
}
|
||||
},
|
||||
"player": {
|
||||
|
||||
@@ -333,6 +333,7 @@
|
||||
"removeFromPlaylist": "$t(action.removeFromPlaylist)",
|
||||
"removeFromQueue": "$t(action.removeFromQueue)",
|
||||
"setRating": "$t(action.setRating)",
|
||||
"playShuffled": "$t(player.shuffle)",
|
||||
"shareItem": "share item",
|
||||
"showDetails": "get info"
|
||||
},
|
||||
@@ -438,7 +439,7 @@
|
||||
"repeat_off": "repeat disabled",
|
||||
"repeat_one": "repeat one",
|
||||
"repeat_other": "",
|
||||
"shuffle": "shuffle",
|
||||
"shuffle": "play shuffled",
|
||||
"shuffle_off": "shuffle disabled",
|
||||
"skip": "skip",
|
||||
"skip_back": "skip backwards",
|
||||
@@ -591,6 +592,7 @@
|
||||
"playButtonBehavior_optionAddLast": "$t(player.addLast)",
|
||||
"playButtonBehavior_optionAddNext": "$t(player.addNext)",
|
||||
"playButtonBehavior_optionPlay": "$t(player.play)",
|
||||
"playButtonBehavior_optionPlayShuffled": "$t(player.shuffle)",
|
||||
"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",
|
||||
"playerbarOpenDrawer": "playerbar fullscreen toggle",
|
||||
@@ -651,6 +653,8 @@
|
||||
"transcodeBitrate_description": "selects the bitrate to transcode. 0 means let the server pick",
|
||||
"transcodeFormat": "format to transcode",
|
||||
"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_description": "follow the system-defined light or dark preference",
|
||||
"volumeWheelStep": "volume wheel step",
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
"skip_back": "retroceder",
|
||||
"favorite": "favorito",
|
||||
"next": "siguiente",
|
||||
"shuffle": "mezclar",
|
||||
"shuffle": "Reproducir aleatoriamente",
|
||||
"playbackFetchNoResults": "ninguna canción encontrada",
|
||||
"playbackFetchInProgress": "cargando canciones…",
|
||||
"addNext": "añadir siguiente",
|
||||
@@ -245,7 +245,10 @@
|
||||
"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",
|
||||
"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": {
|
||||
"editPlaylist": "editar $t(entity.playlist_one)",
|
||||
@@ -478,7 +481,8 @@
|
||||
"shareItem": "Compartir elemento",
|
||||
"showDetails": "Obtener información",
|
||||
"playSimilarSongs": "$t(player.playSimilarSongs)",
|
||||
"download": "descargar"
|
||||
"download": "descargar",
|
||||
"playShuffled": "$t(player.shuffle)"
|
||||
},
|
||||
"home": {
|
||||
"mostPlayed": "más reproducidos",
|
||||
|
||||
@@ -277,7 +277,8 @@
|
||||
"generalTab": "général",
|
||||
"hotkeysTab": "raccourcis",
|
||||
"windowTab": "fenêtre",
|
||||
"playbackTab": "lecteur"
|
||||
"playbackTab": "lecteur",
|
||||
"advanced": "avancé"
|
||||
},
|
||||
"globalSearch": {
|
||||
"commands": {
|
||||
@@ -306,7 +307,8 @@
|
||||
"removeFromQueue": "$t(action.removeFromQueue)",
|
||||
"shareItem": "partager un élément",
|
||||
"playSimilarSongs": "$t(player.playSimilarSongs)",
|
||||
"showDetails": "obtenir des informations"
|
||||
"showDetails": "obtenir des informations",
|
||||
"download": "télécharger"
|
||||
},
|
||||
"albumArtistList": {
|
||||
"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)",
|
||||
"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",
|
||||
"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": {
|
||||
"deletePlaylist": {
|
||||
@@ -574,7 +590,8 @@
|
||||
"input_optionMatchAny": "correspondre à n'importe quel"
|
||||
},
|
||||
"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": {
|
||||
"title": "rechercher parole",
|
||||
|
||||
@@ -140,7 +140,7 @@
|
||||
"skip_back": "向后跳过",
|
||||
"favorite": "收藏",
|
||||
"next": "下一首",
|
||||
"shuffle": "随机",
|
||||
"shuffle": "随机播放",
|
||||
"playbackFetchNoResults": "未找到歌曲",
|
||||
"playbackFetchInProgress": "正在加载歌曲…",
|
||||
"addNext": "添加为播放列表下一首",
|
||||
@@ -152,7 +152,7 @@
|
||||
"unfavorite": "取消收藏",
|
||||
"queue_moveToTop": "将所选项移至底部",
|
||||
"queue_moveToBottom": "将所选项移至顶部",
|
||||
"shuffle_off": "随机关闭",
|
||||
"shuffle_off": "禁用随机播放",
|
||||
"addLast": "添加至播放列表末尾",
|
||||
"mute": "静音",
|
||||
"skip_forward": "向前跳过",
|
||||
@@ -374,7 +374,10 @@
|
||||
"webAudio_description": "使用 web 音频。这将启用重播增益等高级功能。如果您遇到其他情况,请禁用",
|
||||
"artistConfiguration_description": "配置专辑艺术家页面上显示的项目及其显示顺序",
|
||||
"webAudio": "使用 web 音频",
|
||||
"artistConfiguration": "专辑艺术家页面配置"
|
||||
"artistConfiguration": "专辑艺术家页面配置",
|
||||
"playButtonBehavior_optionPlayShuffled": "$t(player.shuffle)",
|
||||
"trayEnabled_description": "显示/隐藏托盘图标/菜单。如果禁用,也会禁用最小化/退出到托盘",
|
||||
"trayEnabled": "显示托盘"
|
||||
},
|
||||
"error": {
|
||||
"remotePortWarning": "重启服务器使新端口生效",
|
||||
@@ -538,7 +541,8 @@
|
||||
"showDetails": "获取信息",
|
||||
"shareItem": "分享项目",
|
||||
"playSimilarSongs": "$t(player.playSimilarSongs)",
|
||||
"download": "下载"
|
||||
"download": "下载",
|
||||
"playShuffled": "$t(player.shuffle)"
|
||||
},
|
||||
"trackList": {
|
||||
"title": "$t(entity.track_other)",
|
||||
|
||||
+3
-1
@@ -647,7 +647,9 @@ if (!singleInstance) {
|
||||
});
|
||||
|
||||
createWindow();
|
||||
createTray();
|
||||
if (store.get('window_enable_tray', true)) {
|
||||
createTray();
|
||||
}
|
||||
app.on('activate', () => {
|
||||
// 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.
|
||||
|
||||
@@ -53,7 +53,7 @@ const getAlbumArtistCoverArtUrl = (args: {
|
||||
`${args.baseUrl}/Items` +
|
||||
`/${args.item.Id}` +
|
||||
'/Images/Primary' +
|
||||
`?width=${size}&` +
|
||||
`?width=${size}` +
|
||||
'&quality=96'
|
||||
);
|
||||
};
|
||||
@@ -69,7 +69,7 @@ const getAlbumCoverArtUrl = (args: { baseUrl: string; item: JFAlbum; size: numbe
|
||||
`${args.baseUrl}/Items` +
|
||||
`/${args.item.Id}` +
|
||||
'/Images/Primary' +
|
||||
`?width=${size}&height=${size}` +
|
||||
`?width=${size}` +
|
||||
'&quality=96'
|
||||
);
|
||||
};
|
||||
@@ -86,7 +86,7 @@ const getSongCoverArtUrl = (args: {
|
||||
`${args.baseUrl}/Items` +
|
||||
`/${args.item.Id}` +
|
||||
'/Images/Primary' +
|
||||
`?width=${size}&height=${size}` +
|
||||
`?width=${size}` +
|
||||
'&quality=96'
|
||||
);
|
||||
}
|
||||
@@ -97,7 +97,7 @@ const getSongCoverArtUrl = (args: {
|
||||
`${args.baseUrl}/Items` +
|
||||
`/${args.item?.AlbumId}` +
|
||||
'/Images/Primary' +
|
||||
`?width=${size}&height=${size}` +
|
||||
`?width=${size}` +
|
||||
'&quality=96'
|
||||
);
|
||||
}
|
||||
@@ -116,7 +116,7 @@ const getPlaylistCoverArtUrl = (args: { baseUrl: string; item: JFPlaylist; size:
|
||||
`${args.baseUrl}/Items` +
|
||||
`/${args.item.Id}` +
|
||||
'/Images/Primary' +
|
||||
`?width=${size}&height=${size}` +
|
||||
`?width=${size}` +
|
||||
'&quality=96'
|
||||
);
|
||||
};
|
||||
@@ -153,11 +153,16 @@ const normalizeSong = (
|
||||
discNumber: (item.ParentIndexNumber && item.ParentIndexNumber) || 1,
|
||||
discSubtitle: null,
|
||||
duration: item.RunTimeTicks / 10000,
|
||||
gain: item.LUFS
|
||||
? {
|
||||
track: -18 - item.LUFS,
|
||||
}
|
||||
: null,
|
||||
gain:
|
||||
item.NormalizationGain !== undefined
|
||||
? {
|
||||
track: item.NormalizationGain,
|
||||
}
|
||||
: item.LUFS
|
||||
? {
|
||||
track: -18 - item.LUFS,
|
||||
}
|
||||
: null,
|
||||
genres: item.GenreItems?.map((entry) => ({
|
||||
id: entry.Id,
|
||||
imageUrl: null,
|
||||
@@ -388,7 +393,7 @@ const getGenreCoverArtUrl = (args: {
|
||||
`${args.baseUrl}/Items` +
|
||||
`/${args.item.Id}` +
|
||||
'/Images/Primary' +
|
||||
`?width=${size}&height=${size}` +
|
||||
`?width=${size}` +
|
||||
'&quality=96'
|
||||
);
|
||||
};
|
||||
|
||||
@@ -413,6 +413,7 @@ const song = z.object({
|
||||
MediaSources: z.array(mediaSources),
|
||||
MediaType: z.string(),
|
||||
Name: z.string(),
|
||||
NormalizationGain: z.number().optional(),
|
||||
ParentIndexNumber: z.number(),
|
||||
PlaylistItemId: 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 { ModuleRegistry } from '@ag-grid-community/core';
|
||||
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 { getMpvProperties } from '/@/renderer/features/settings/components/playback/mpv-settings';
|
||||
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 { WebAudioContext } from '/@/renderer/features/player/context/webaudio-context';
|
||||
import { useDiscordRpc } from '/@/renderer/features/discord-rpc/use-discord-rpc';
|
||||
import i18n from '/@/i18n/i18n';
|
||||
import { useServerVersion } from '/@/renderer/hooks/use-server-version';
|
||||
@@ -91,6 +92,8 @@ export const App = () => {
|
||||
}
|
||||
}, [builtIn, custom, system, type]);
|
||||
|
||||
const [webAudio, setWebAudio] = useState<WebAudio>();
|
||||
|
||||
useEffect(() => {
|
||||
if (enabled && content) {
|
||||
// Yes, CSS is sanitized here as well. Prevent a suer from changing the
|
||||
@@ -125,6 +128,10 @@ export const App = () => {
|
||||
return { handlePlayQueueAdd };
|
||||
}, [handlePlayQueueAdd]);
|
||||
|
||||
const webAudioProvider = useMemo(() => {
|
||||
return { setWebAudio, webAudio };
|
||||
}, [webAudio]);
|
||||
|
||||
// Start the mpv instance on startup
|
||||
useEffect(() => {
|
||||
const initializeMpv = async () => {
|
||||
@@ -278,7 +285,9 @@ export const App = () => {
|
||||
>
|
||||
<PlayQueueHandlerContext.Provider value={providerValue}>
|
||||
<ContextMenuProvider>
|
||||
<AppRouter />
|
||||
<WebAudioContext.Provider value={webAudioProvider}>
|
||||
<AppRouter />
|
||||
</WebAudioContext.Provider>{' '}
|
||||
</ContextMenuProvider>
|
||||
</PlayQueueHandlerContext.Provider>
|
||||
<IsUpdatedDialog />
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
import { useSettingsStore, useSettingsStoreActions } from '/@/renderer/store/settings.store';
|
||||
import type { CrossfadeStyle } 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 { toast } from '/@/renderer/components/toast';
|
||||
import { api } from '/@/renderer/api';
|
||||
@@ -44,11 +45,6 @@ const getDuration = (ref: any) => {
|
||||
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
|
||||
// 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
|
||||
@@ -116,7 +112,7 @@ export const AudioPlayer = forwardRef(
|
||||
const [isTransitioning, setIsTransitioning] = useState(false);
|
||||
const audioDeviceId = useSettingsStore((state) => state.playback.audioDeviceId);
|
||||
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 playbackSpeed = useSpeed();
|
||||
const { transcode } = usePlaybackSettings();
|
||||
@@ -124,7 +120,7 @@ export const AudioPlayer = forwardRef(
|
||||
const stream1 = useSongUrl(transcode, currentPlayer === 1, player1);
|
||||
const stream2 = useSongUrl(transcode, currentPlayer === 2, player2);
|
||||
|
||||
const [webAudio, setWebAudio] = useState<WebAudio | null>(null);
|
||||
const { webAudio, setWebAudio } = useWebAudio();
|
||||
const [player1Source, setPlayer1Source] = useState<MediaElementAudioSourceNode | null>(
|
||||
null,
|
||||
);
|
||||
@@ -181,7 +177,7 @@ export const AudioPlayer = forwardRef(
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (useWebAudio && 'AudioContext' in window) {
|
||||
if (shouldUseWebAudio && 'AudioContext' in window) {
|
||||
let context: AudioContext;
|
||||
|
||||
try {
|
||||
@@ -200,7 +196,7 @@ export const AudioPlayer = forwardRef(
|
||||
const gain = context.createGain();
|
||||
gain.connect(context.destination);
|
||||
|
||||
setWebAudio({ context, gain });
|
||||
setWebAudio!({ context, gain });
|
||||
|
||||
return () => {
|
||||
return context.close();
|
||||
|
||||
@@ -568,7 +568,7 @@ export const AlbumArtistDetailContent = ({ background }: AlbumArtistDetailConten
|
||||
suppressRowDrag
|
||||
columnDefs={topSongsColumnDefs}
|
||||
enableCellChangeFlash={false}
|
||||
getRowId={(data) => data.data.id}
|
||||
getRowId={(data) => data.data.uniqueId}
|
||||
rowData={topSongs}
|
||||
rowHeight={60}
|
||||
rowSelection="multiple"
|
||||
|
||||
+1
-1
@@ -66,7 +66,7 @@ export const AlbumArtistDetailTopSongsListContent = ({
|
||||
ref={tableRef}
|
||||
shouldUpdateSong
|
||||
{...tableProps}
|
||||
getRowId={(data) => data.data.id}
|
||||
getRowId={(data) => data.data.uniqueId}
|
||||
rowClassRules={rowClassRules}
|
||||
rowData={data}
|
||||
rowModelType="clientSide"
|
||||
|
||||
@@ -18,6 +18,7 @@ export const SONG_CONTEXT_MENU_ITEMS: SetContextMenuItems = [
|
||||
{ id: 'play' },
|
||||
{ id: 'playLast' },
|
||||
{ id: 'playNext' },
|
||||
{ id: 'playShuffled' },
|
||||
{ divider: true, id: 'playSimilarSongs' },
|
||||
{ divider: true, id: 'addToPlaylist' },
|
||||
{ id: 'addToFavorites' },
|
||||
@@ -31,7 +32,8 @@ export const SONG_CONTEXT_MENU_ITEMS: SetContextMenuItems = [
|
||||
export const SONG_ALBUM_PAGE: SetContextMenuItems = [
|
||||
{ id: 'play' },
|
||||
{ id: 'playLast' },
|
||||
{ divider: true, id: 'playNext' },
|
||||
{ id: 'playNext' },
|
||||
{ divider: true, id: 'playShuffled' },
|
||||
{ divider: true, id: 'addToPlaylist' },
|
||||
];
|
||||
|
||||
@@ -39,6 +41,7 @@ export const PLAYLIST_SONG_CONTEXT_MENU_ITEMS: SetContextMenuItems = [
|
||||
{ id: 'play' },
|
||||
{ id: 'playLast' },
|
||||
{ id: 'playNext' },
|
||||
{ id: 'playShuffled' },
|
||||
{ divider: true, id: 'playSimilarSongs' },
|
||||
{ id: 'addToPlaylist' },
|
||||
{ divider: true, id: 'removeFromPlaylist' },
|
||||
@@ -54,6 +57,7 @@ export const SMART_PLAYLIST_SONG_CONTEXT_MENU_ITEMS: SetContextMenuItems = [
|
||||
{ id: 'play' },
|
||||
{ id: 'playLast' },
|
||||
{ id: 'playNext' },
|
||||
{ divider: true, id: 'playShuffled' },
|
||||
{ divider: true, id: 'playSimilarSongs' },
|
||||
{ divider: true, id: 'addToPlaylist' },
|
||||
{ id: 'addToFavorites' },
|
||||
@@ -67,7 +71,8 @@ export const SMART_PLAYLIST_SONG_CONTEXT_MENU_ITEMS: SetContextMenuItems = [
|
||||
export const ALBUM_CONTEXT_MENU_ITEMS: SetContextMenuItems = [
|
||||
{ id: 'play' },
|
||||
{ id: 'playLast' },
|
||||
{ divider: true, id: 'playNext' },
|
||||
{ id: 'playNext' },
|
||||
{ divider: true, id: 'playShuffled' },
|
||||
{ divider: true, id: 'addToPlaylist' },
|
||||
{ id: 'addToFavorites' },
|
||||
{ id: 'removeFromFavorites' },
|
||||
@@ -79,14 +84,16 @@ export const ALBUM_CONTEXT_MENU_ITEMS: SetContextMenuItems = [
|
||||
export const GENRE_CONTEXT_MENU_ITEMS: SetContextMenuItems = [
|
||||
{ id: 'play' },
|
||||
{ id: 'playLast' },
|
||||
{ divider: true, id: 'playNext' },
|
||||
{ id: 'playNext' },
|
||||
{ divider: true, id: 'playShuffled' },
|
||||
{ divider: true, id: 'addToPlaylist' },
|
||||
];
|
||||
|
||||
export const ARTIST_CONTEXT_MENU_ITEMS: SetContextMenuItems = [
|
||||
{ id: 'play' },
|
||||
{ id: 'playLast' },
|
||||
{ divider: true, id: 'playNext' },
|
||||
{ id: 'playNext' },
|
||||
{ divider: true, id: 'playShuffled' },
|
||||
{ divider: true, id: 'addToPlaylist' },
|
||||
{ id: 'addToFavorites' },
|
||||
{ divider: true, id: 'removeFromFavorites' },
|
||||
@@ -98,7 +105,8 @@ export const ARTIST_CONTEXT_MENU_ITEMS: SetContextMenuItems = [
|
||||
export const PLAYLIST_CONTEXT_MENU_ITEMS: SetContextMenuItems = [
|
||||
{ id: 'play' },
|
||||
{ id: 'playLast' },
|
||||
{ divider: true, id: 'playNext' },
|
||||
{ id: 'playNext' },
|
||||
{ divider: true, id: 'playShuffled' },
|
||||
{ divider: true, id: 'shareItem' },
|
||||
{ id: 'deletePlaylist' },
|
||||
];
|
||||
|
||||
@@ -31,6 +31,7 @@ import {
|
||||
RiInformationFill,
|
||||
RiRadio2Fill,
|
||||
RiDownload2Line,
|
||||
RiShuffleFill,
|
||||
} from 'react-icons/ri';
|
||||
import { AnyLibraryItems, LibraryItem, ServerType, AnyLibraryItem } from '/@/renderer/api/types';
|
||||
import {
|
||||
@@ -774,6 +775,12 @@ export const ContextMenuProvider = ({ children }: ContextMenuProviderProps) => {
|
||||
leftIcon: <RiAddCircleFill size="1.1rem" />,
|
||||
onClick: () => handlePlay(Play.NEXT),
|
||||
},
|
||||
playShuffled: {
|
||||
id: 'playShuffled',
|
||||
label: t('page.contextMenu.playShuffled', { postProcess: 'sentenceCase' }),
|
||||
leftIcon: <RiShuffleFill size="1.1rem" />,
|
||||
onClick: () => handlePlay(Play.SHUFFLE),
|
||||
},
|
||||
playSimilarSongs: {
|
||||
id: 'playSimilarSongs',
|
||||
label: t('page.contextMenu.playSimilarSongs', { postProcess: 'sentenceCase' }),
|
||||
|
||||
@@ -23,6 +23,7 @@ export type ContextMenuItemType =
|
||||
| 'play'
|
||||
| 'playLast'
|
||||
| 'playNext'
|
||||
| 'playShuffled'
|
||||
| 'addToPlaylist'
|
||||
| 'removeFromPlaylist'
|
||||
| 'addToFavorites'
|
||||
@@ -45,6 +46,7 @@ export const CONFIGURABLE_CONTEXT_MENU_ITEMS: ContextMenuItemType[] = [
|
||||
'play',
|
||||
'playLast',
|
||||
'playNext',
|
||||
'playShuffled',
|
||||
'playSimilarSongs',
|
||||
'addToPlaylist',
|
||||
'removeFromPlaylist',
|
||||
|
||||
@@ -11,8 +11,17 @@ import {
|
||||
useFullScreenPlayerStoreActions,
|
||||
} from '/@/renderer/store/full-screen-player.store';
|
||||
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';
|
||||
|
||||
const Visualizer = lazy(() =>
|
||||
import('/@/renderer/features/player/components/visualizer').then((module) => ({
|
||||
default: module.Visualizer,
|
||||
})),
|
||||
);
|
||||
|
||||
const QueueContainer = styled.div`
|
||||
position: relative;
|
||||
display: flex;
|
||||
@@ -61,27 +70,41 @@ export const FullScreenPlayerQueue = () => {
|
||||
const { t } = useTranslation();
|
||||
const { activeTab, opacity } = useFullScreenPlayerStore();
|
||||
const { setStore } = useFullScreenPlayerStoreActions();
|
||||
const { type, webAudio } = usePlaybackSettings();
|
||||
|
||||
const headerItems = [
|
||||
{
|
||||
active: activeTab === 'queue',
|
||||
icon: <RiFileMusicLine size="1.5rem" />,
|
||||
label: t('page.fullscreenPlayer.upNext'),
|
||||
onClick: () => setStore({ activeTab: 'queue' }),
|
||||
},
|
||||
{
|
||||
active: activeTab === 'related',
|
||||
icon: <HiOutlineQueueList size="1.5rem" />,
|
||||
label: t('page.fullscreenPlayer.related'),
|
||||
onClick: () => setStore({ activeTab: 'related' }),
|
||||
},
|
||||
{
|
||||
active: activeTab === 'lyrics',
|
||||
icon: <RiFileTextLine size="1.5rem" />,
|
||||
label: t('page.fullscreenPlayer.lyrics'),
|
||||
onClick: () => setStore({ activeTab: 'lyrics' }),
|
||||
},
|
||||
];
|
||||
const headerItems = useMemo(() => {
|
||||
const items = [
|
||||
{
|
||||
active: activeTab === 'queue',
|
||||
icon: <RiFileMusicLine size="1.5rem" />,
|
||||
label: t('page.fullscreenPlayer.upNext'),
|
||||
onClick: () => setStore({ activeTab: 'queue' }),
|
||||
},
|
||||
{
|
||||
active: activeTab === 'related',
|
||||
icon: <HiOutlineQueueList size="1.5rem" />,
|
||||
label: t('page.fullscreenPlayer.related'),
|
||||
onClick: () => setStore({ activeTab: 'related' }),
|
||||
},
|
||||
{
|
||||
active: activeTab === 'lyrics',
|
||||
icon: <RiFileTextLine size="1.5rem" />,
|
||||
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 (
|
||||
<GridContainer
|
||||
@@ -91,6 +114,7 @@ export const FullScreenPlayerQueue = () => {
|
||||
<Group
|
||||
grow
|
||||
align="center"
|
||||
className="full-screen-player-queue-header"
|
||||
position="center"
|
||||
>
|
||||
{headerItems.map((item) => (
|
||||
@@ -127,6 +151,10 @@ export const FullScreenPlayerQueue = () => {
|
||||
</QueueContainer>
|
||||
) : activeTab === 'lyrics' ? (
|
||||
<Lyrics />
|
||||
) : activeTab === 'visualizer' && type === PlaybackType.WEB && webAudio ? (
|
||||
<Suspense fallback={<></>}>
|
||||
<Visualizer />
|
||||
</Suspense>
|
||||
) : null}
|
||||
</GridContainer>
|
||||
);
|
||||
|
||||
@@ -244,8 +244,8 @@ export const LeftControls = () => {
|
||||
<React.Fragment key={`bar-${artist.id}`}>
|
||||
{index > 0 && <Separator />}
|
||||
<Text
|
||||
$link
|
||||
component={Link}
|
||||
$link={artist.id !== ''}
|
||||
component={artist.id ? Link : undefined}
|
||||
overflow="hidden"
|
||||
size="md"
|
||||
to={
|
||||
@@ -253,7 +253,7 @@ export const LeftControls = () => {
|
||||
? generatePath(AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL, {
|
||||
albumArtistId: artist.id,
|
||||
})
|
||||
: ''
|
||||
: undefined
|
||||
}
|
||||
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
|
||||
suppressRowDrag
|
||||
columnDefs={columnDefs}
|
||||
getRowId={(data) => `${data.data.id}-${data.data.pageIndex}`}
|
||||
getRowId={(data) => `${data.data.uniqueId}-${data.data.pageIndex}`}
|
||||
rowClassRules={rowClassRules}
|
||||
rowData={playlistSongData}
|
||||
rowHeight={60}
|
||||
|
||||
@@ -215,6 +215,13 @@ export const ControlSettings = () => {
|
||||
}),
|
||||
value: Play.LAST,
|
||||
},
|
||||
{
|
||||
label: t('setting.playButtonBehavior', {
|
||||
context: 'optionPlayShuffled',
|
||||
postProcess: 'titleCase',
|
||||
}),
|
||||
value: Play.SHUFFLE,
|
||||
},
|
||||
]}
|
||||
defaultValue={settings.playButtonBehavior}
|
||||
onChange={(e) =>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { SelectItem, Switch } from '@mantine/core';
|
||||
import { SelectItem } from '@mantine/core';
|
||||
import isElectron from 'is-electron';
|
||||
import { Select, Slider, toast } from '/@/renderer/components';
|
||||
import { Select, Slider, Switch, toast } from '/@/renderer/components';
|
||||
import {
|
||||
SettingsSection,
|
||||
SettingOption,
|
||||
|
||||
@@ -36,7 +36,6 @@ export const TranscodeSettings = () => {
|
||||
aria-label="Transcode bitrate"
|
||||
defaultValue={transcode.bitrate}
|
||||
min={0}
|
||||
placeholder="mp3, opus"
|
||||
w={100}
|
||||
onBlur={(e) => {
|
||||
setTranscodingConfig({
|
||||
@@ -61,6 +60,7 @@ export const TranscodeSettings = () => {
|
||||
<TextInput
|
||||
aria-label="transcoding format"
|
||||
defaultValue={transcode.format}
|
||||
placeholder="mp3, opus"
|
||||
width={100}
|
||||
onBlur={(e) => {
|
||||
setTranscodingConfig({
|
||||
|
||||
@@ -81,11 +81,55 @@ export const WindowSettings = () => {
|
||||
isHidden: !isElectron(),
|
||||
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: (
|
||||
<Switch
|
||||
aria-label="Toggle minimize to tray"
|
||||
defaultChecked={settings.minimizeToTray}
|
||||
defaultChecked={settings.tray}
|
||||
disabled={!isElectron()}
|
||||
onChange={(e) => {
|
||||
if (!e) return;
|
||||
@@ -103,7 +147,7 @@ export const WindowSettings = () => {
|
||||
context: 'description',
|
||||
postProcess: 'sentenceCase',
|
||||
}),
|
||||
isHidden: !isElectron(),
|
||||
isHidden: !isElectron() || !settings.tray,
|
||||
title: t('setting.minimizeToTray', { postProcess: 'sentenceCase' }),
|
||||
},
|
||||
{
|
||||
@@ -128,7 +172,7 @@ export const WindowSettings = () => {
|
||||
context: 'description',
|
||||
postProcess: 'sentenceCase',
|
||||
}),
|
||||
isHidden: !isElectron(),
|
||||
isHidden: !isElectron() || !settings.tray,
|
||||
title: t('setting.exitToTray', { postProcess: 'sentenceCase' }),
|
||||
},
|
||||
{
|
||||
@@ -153,7 +197,7 @@ export const WindowSettings = () => {
|
||||
context: 'description',
|
||||
postProcess: 'sentenceCase',
|
||||
}),
|
||||
isHidden: !isElectron(),
|
||||
isHidden: !isElectron() || !settings.tray,
|
||||
title: t('setting.startMinimized', { postProcess: 'sentenceCase' }),
|
||||
},
|
||||
];
|
||||
|
||||
@@ -70,7 +70,7 @@ export const SimilarSongsList = ({ count, fullScreen, song }: SimilarSongsListPr
|
||||
song,
|
||||
}}
|
||||
deselectOnClickOutside={fullScreen}
|
||||
getRowId={(data) => data.data.id}
|
||||
getRowId={(data) => data.data.uniqueId}
|
||||
rowBuffer={50}
|
||||
rowData={songQuery.data ?? []}
|
||||
rowHeight={tableConfig.rowHeight || 40}
|
||||
|
||||
@@ -107,18 +107,53 @@ export const usePlayerStore = create<PlayerSlice>()(
|
||||
actions: {
|
||||
addToQueue: (args) => {
|
||||
const { initialIndex, playType, songs } = args;
|
||||
const { shuffledIndex } = get().current;
|
||||
const shuffledQueue = get().queue.shuffled;
|
||||
const songsToAddToQueue = map(songs, (song) => ({
|
||||
...song,
|
||||
uniqueId: nanoid(),
|
||||
}));
|
||||
const queue = get().queue.default;
|
||||
|
||||
// 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) {
|
||||
const index = initialIndex || 0;
|
||||
const initialSong = songsToAddToQueue[index];
|
||||
const queueCopy = [...songsToAddToQueue];
|
||||
|
||||
@@ -145,7 +180,6 @@ export const usePlayerStore = create<PlayerSlice>()(
|
||||
state.current.song = initialSong;
|
||||
});
|
||||
} else {
|
||||
const index = initialIndex || 0;
|
||||
set((state) => {
|
||||
state.queue.default = songsToAddToQueue;
|
||||
state.current.time = 0;
|
||||
|
||||
@@ -313,6 +313,7 @@ export interface SettingsState {
|
||||
exitToTray: boolean;
|
||||
minimizeToTray: boolean;
|
||||
startMinimized: boolean;
|
||||
tray: boolean;
|
||||
windowBarStyle: Platform;
|
||||
};
|
||||
}
|
||||
@@ -647,6 +648,7 @@ const initialState: SettingsState = {
|
||||
exitToTray: false,
|
||||
minimizeToTray: false,
|
||||
startMinimized: false,
|
||||
tray: true,
|
||||
windowBarStyle: platformDefaultWindowBarStyle,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -109,6 +109,7 @@ export enum Play {
|
||||
LAST = 'last',
|
||||
NEXT = 'next',
|
||||
NOW = 'now',
|
||||
SHUFFLE = 'shuffle',
|
||||
}
|
||||
|
||||
export enum CrossfadeStyle {
|
||||
@@ -234,3 +235,8 @@ export enum AuthState {
|
||||
LOADING = 'loading',
|
||||
VALID = 'valid',
|
||||
}
|
||||
|
||||
export type WebAudio = {
|
||||
context: AudioContext;
|
||||
gain: GainNode;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user