Compare commits

...

12 Commits

Author SHA1 Message Date
jeffvli b16e57710b hide mediasession setting for non-desktop 2025-10-13 12:06:03 -07:00
jeffvli 931e96b9d1 fix media session setting toggle for web 2025-10-13 12:00:42 -07:00
jeffvli c27b86d2b2 fix media session settings error on web 2025-10-13 11:49:34 -07:00
jeffvli b3cf73836d update to v0.21.2 2025-10-13 11:47:02 -07:00
jeffvli 1b15c73db0 fix scrobble time race condition
- revert playerbar slider refactor
- re-implement mediasession handler
2025-10-13 11:44:42 -07:00
jeffvli 4e53030e8d Revert "refactor playerbar slider to separate component"
This reverts commit 309b49b46e.
2025-10-13 11:38:26 -07:00
jeffvli 22b798812e Revert "fix playback controls being called multiple times on media key input"
This reverts commit 1b8661d566.
2025-10-13 11:38:19 -07:00
jeffvli 1f7d510110 update to v0.21.1 2025-10-13 04:48:10 -07:00
Hosted Weblate 44fae06143 Translated using Weblate (Chinese (Traditional Han script))
Currently translated at 99.8% (715 of 716 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: linger <linger0517@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/zh_Hant/
Translation: feishin/Translation
2025-10-13 11:33:21 +00:00
Hosted Weblate 1e24e12a55 Translated using Weblate (Catalan)
Currently translated at 100.0% (716 of 716 strings)

Co-authored-by: Ondo <SparkyOndo@proton.me>
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/ca/
Translation: feishin/Translation
2025-10-13 11:33:20 +00:00
Hosted Weblate 358bdec2b6 Translated using Weblate (Basque)
Currently translated at 81.9% (587 of 716 strings)

Translated using Weblate (Basque)

Currently translated at 73.1% (524 of 716 strings)

Co-authored-by: Aitor Astorga <a.astorga.sdv@protonmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/eu/
Translation: feishin/Translation
2025-10-13 11:33:20 +00:00
jeffvli 1b8661d566 fix playback controls being called multiple times on media key input 2025-10-13 04:33:11 -07:00
7 changed files with 244 additions and 142 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "feishin", "name": "feishin",
"version": "0.21.0", "version": "0.21.2",
"description": "A modern self-hosted music player.", "description": "A modern self-hosted music player.",
"keywords": [ "keywords": [
"subsonic", "subsonic",
+12 -2
View File
@@ -483,7 +483,7 @@
"discordRichPresence": "estat d'activitat de {{discord}}", "discordRichPresence": "estat d'activitat de {{discord}}",
"discordRichPresence_description": "activa l'estat de reproducció a l'activitat de {{discord}}. Les tecles d'imatge són: {{icon}}, {{playing}} i {{paused}}", "discordRichPresence_description": "activa l'estat de reproducció a l'activitat de {{discord}}. Les tecles d'imatge són: {{icon}}, {{playing}} i {{paused}}",
"discordServeImage": "serveix imatges de {{discord}} des del servidor", "discordServeImage": "serveix imatges de {{discord}} des del servidor",
"discordServeImage_description": "comparteix la caràtula per l'estat d'activitat de {{discord}} des del servidor; només disponible per jellyfin i navidrome", "discordServeImage_description": "comparteix la caràtula per l'estat d'activitat de {{discord}} des del servidor; només disponible per jellyfin i navidrome. {{discord}} fa ser un bot per trobar les imatges, de manera que el vostre servidor ha de ser visible per l'internet públic.",
"discordUpdateInterval": "interval d'actualització de l'estat d'activitat de {{discord}}", "discordUpdateInterval": "interval d'actualització de l'estat d'activitat de {{discord}}",
"doubleClickBehavior": "posa en cua totes les pistes cercades en fer doble clic", "doubleClickBehavior": "posa en cua totes les pistes cercades en fer doble clic",
"doubleClickBehavior_description": "si està actiu, totes les pistes coincidents en una cerca de pistes es posaran a la cua. altrament, només la que seleccioneu s'afegirà a la cua", "doubleClickBehavior_description": "si està actiu, totes les pistes coincidents en una cerca de pistes es posaran a la cua. altrament, només la que seleccioneu s'afegirà a la cua",
@@ -653,7 +653,17 @@
"discordLinkType": "enllaços d'estat de {{discord}}", "discordLinkType": "enllaços d'estat de {{discord}}",
"discordLinkType_description": "afegeix enllaços externs a {{lastfm}} o {{musicbrainz}} als camps de cançó i artista a l'estat d'activitat de {{discord}}. {{musicbrainz}} és el més precís, però requereix etiquetes i no proporciona enllaços d'artista, mentre que {{lastfm}} hauria de propocionar un enllaç sempre. no fa sol·licituds de xarxa addicionals", "discordLinkType_description": "afegeix enllaços externs a {{lastfm}} o {{musicbrainz}} als camps de cançó i artista a l'estat d'activitat de {{discord}}. {{musicbrainz}} és el més precís, però requereix etiquetes i no proporciona enllaços d'artista, mentre que {{lastfm}} hauria de propocionar un enllaç sempre. no fa sol·licituds de xarxa addicionals",
"discordLinkType_none": "$t(common.none)", "discordLinkType_none": "$t(common.none)",
"discordLinkType_mbz_lastfm": "{{musicbrainz}} amb {{lastfm}} com a alternativa" "discordLinkType_mbz_lastfm": "{{musicbrainz}} amb {{lastfm}} com a alternativa",
"artistBackground": "imatge de fons de l'artista",
"artistBackground_description": "afegeix una imatge de fons per les pàgines d'artista amb l'art de l'artista",
"artistBackgroundBlur": "mida del desenfocament de la imatge de fons de l'artista",
"artistBackgroundBlur_description": "ajusta la quantitat de desenfocament aplicat a la imatge de fons de l'artista",
"releaseChannel_optionLatest": "estable",
"releaseChannel_optionBeta": "beta",
"releaseChannel": "canal de versions",
"releaseChannel_description": "tria entre versions estables i versions beta per les actualitzacions automàtiques",
"mediaSession": "activa Media Session",
"mediaSession_description": "Activa la integració amb Windows Media Session per mostrar els controls multimèdia i les metadades a l'indicador de volum del sistema i la pantalla de bloqueig (només per Windows)"
}, },
"table": { "table": {
"column": { "column": {
+76 -10
View File
@@ -116,7 +116,8 @@
"saveAs": "gorde honela", "saveAs": "gorde honela",
"trackGain": "pista irabazpena", "trackGain": "pista irabazpena",
"comingSoon": "laster…", "comingSoon": "laster…",
"trackPeak": "pistaren gailurra" "trackPeak": "pistaren gailurra",
"albumPeak": "albumaren gailurra"
}, },
"player": { "player": {
"repeat": "errepikatu", "repeat": "errepikatu",
@@ -161,7 +162,10 @@
}, },
"general": { "general": {
"gap": "$t(common.gap)", "gap": "$t(common.gap)",
"size": "$t(common.size)" "size": "$t(common.size)",
"tableColumns": "taula zutabeak",
"itemSize": "elementuaren tamaina (px)",
"followCurrentSong": "jarraitu uneko abestia"
}, },
"label": { "label": {
"actions": "$t(common.action_other)", "actions": "$t(common.action_other)",
@@ -183,7 +187,13 @@
"size": "$t(common.size)", "size": "$t(common.size)",
"songCount": "$t(entity.track_other)", "songCount": "$t(entity.track_other)",
"title": "$t(common.title)", "title": "$t(common.title)",
"year": "$t(common.year)" "year": "$t(common.year)",
"titleCombined": "$t(common.title) (batuta)",
"releaseDate": "argitalpen data",
"playCount": "erreprodukzio kopurua",
"lastPlayed": "azken aldiz entzundakoa",
"discNumber": "disko zenbakia",
"dateAdded": "gehitze data"
} }
}, },
"column": { "column": {
@@ -206,7 +216,11 @@
"trackNumber": "pista", "trackNumber": "pista",
"bpm": "bpm", "bpm": "bpm",
"comment": "iruzkina", "comment": "iruzkina",
"playCount": "erreprodukzioak" "playCount": "erreprodukzioak",
"releaseDate": "argitalpen data",
"lastPlayed": "azken aldiz entzundakoa",
"dateAdded": "gehitutako data",
"albumArtist": "albumeko artista"
} }
}, },
"entity": { "entity": {
@@ -397,7 +411,7 @@
"discordRichPresence": "{{discord}} jarduera-egoera", "discordRichPresence": "{{discord}} jarduera-egoera",
"discordRichPresence_description": "gaitu erreprodukzioa egoera {{discord}}-en jarduera-egoeran. Irudi gakoak hauek dira: {{icon}}, {{playing}}, eta {{paused}}", "discordRichPresence_description": "gaitu erreprodukzioa egoera {{discord}}-en jarduera-egoeran. Irudi gakoak hauek dira: {{icon}}, {{playing}}, eta {{paused}}",
"discordServeImage": "zerbitzatu {{discord}} irudiak zerbitzaritik", "discordServeImage": "zerbitzatu {{discord}} irudiak zerbitzaritik",
"discordServeImage_description": "partekatu {{discord}} jarduera-egoerarentzako azala artea zerbitzaritik bertatik, Jellyfin eta Navidrome-rentzat bakarrik eskuragarri", "discordServeImage_description": "partekatu {{discord}} jarduera-egoerarentzako azala artea zerbitzaritik bertatik, Jellyfin eta Navidrome-rentzat bakarrik eskuragarri. {{discord}}-(e)k bot bat erabiltzen du irudiak eskuratzeko, beraz, zure zerbitzaria internet publikotik eskuragarri egon behar da.",
"discordUpdateInterval": "{{discord}} jarduera-egoera eguneraketa tartea", "discordUpdateInterval": "{{discord}} jarduera-egoera eguneraketa tartea",
"discordLinkType_none": "$t(common.none)", "discordLinkType_none": "$t(common.none)",
"albumBackground": "albumaren atzeko planoaren irudia", "albumBackground": "albumaren atzeko planoaren irudia",
@@ -413,7 +427,55 @@
"discordDisplayType_songname": "abesti izena", "discordDisplayType_songname": "abesti izena",
"discordDisplayType_artistname": "artista izena(k)", "discordDisplayType_artistname": "artista izena(k)",
"fontType_optionBuiltIn": "barneko letra-tipoa", "fontType_optionBuiltIn": "barneko letra-tipoa",
"hotkey_globalSearch": "bilaketa globala" "hotkey_globalSearch": "bilaketa globala",
"albumBackgroundBlur_description": "albumaren atzeko planoaren irudiari aplikatzen zaion lausotze-kopurua doitzen du",
"artistBackground": "artistaren atzeko planoaren irudia",
"artistBackgroundBlur": "artistaren atzeko planoko irudiaren lausotze-tamaina",
"artistBackgroundBlur_description": "artistaren atzeko planoaren irudiari aplikatzen zaion lausotze-kopurua doitzen du",
"artistConfiguration": "albumaren artistaren konfigurazio orria",
"artistConfiguration_description": "konfiguratu zein elementu erakusten diren eta zein ordenatan albumaren artistaren orrian",
"audioExclusiveMode": "audio esklusiboko modua",
"releaseChannel_optionLatest": "egonkorra",
"releaseChannel_optionBeta": "beta",
"releaseChannel": "argitalpen kanala",
"releaseChannel_description": "aukeratu argitalpen egonkorren edo beta artean eguneratze automatikoak lortzeko",
"discordUpdateInterval_description": "eguneratze bakoitzaren arteko denbora segundotan (gutxienez 15 segundo)",
"discordDisplayType": "{{discord}} jarduera-pantailaren mota",
"discordDisplayType_description": "zure egoeran entzuten ari zarena aldatzen du",
"discordLinkType": "{{discord}} egoera estekak",
"fontType_description": "barneko letra-tipoa Feishinek eskaintzen dituen letra-tipoetako bat aukeratzen du. sistemaren letra-tipoa zure sistema eragileak eskaintzen duen edozein letra-tipo hautatzeko aukera ematen dizu. pertsonalizatua zure letra-tipoa eskaintzeko aukera ematen dizu",
"genreBehavior": "genero orriaren portaera lehenetsia",
"homeConfiguration_description": "konfiguratu zein elementu erakusten diren hasierako orrian eta zein ordenatan",
"homeFeature": "etxeko karrusela nabarmendua",
"homeFeature_description": "hasierako orrian karrusel nabarmen handia erakutsi behar den ala ez kontrolatzen du",
"hotkey_localSearch": "orrian bilatu",
"hotkey_rate0": "garbitu balorazioa",
"hotkey_rate1": "1 izarretako balorazioa",
"hotkey_rate2": "2 izarretako balorazioa",
"hotkey_rate3": "3 izarretako balorazioa",
"hotkey_rate4": "4 izarretako balorazioa",
"hotkey_rate5": "5 izarretako balorazioa",
"zoom_description": "aplikazioaren zoom ehunekoa ezartzen du",
"zoom": "zoom ehunekoa",
"windowBarStyle_description": "aukeratu leiho-barraren estiloa",
"windowBarStyle": "leiho-barra estiloa",
"webAudio": "erabili web audioa",
"useSystemTheme_description": "jarraitu sistemak definitutako argi edo iluntasun lehentasuna",
"useSystemTheme": "erabili sistemaren gaia",
"translationTargetLanguage_description": "itzulpenerako helburu-hizkuntza",
"translationTargetLanguage": "itzulpenerako helburu-hizkuntza",
"translationApiKey": "itzulpen api gakoa",
"translationApiProvider_description": "itzulpenerako api hornitzailea",
"translationApiProvider": "itzulpen api hornitzailea",
"mediaSession": "gaitu multimedia saioa",
"themeLight_description": "aplikaziorako erabiliko den gaia argia ezartzen du",
"themeLight": "gaia (argia)",
"themeDark_description": "aplikaziorako erabiliko den gai iluna ezartzen du",
"themeDark": "gaia (iluna)",
"theme_description": "aplikaziorako erabiliko den gaia ezartzen du",
"externalLinks": "kanpoko estekak erakutsi",
"externalLinks_description": "kanpoko estekak (Last.fm, MusicBrainz) artista/album orrietan erakustea gaitzen du",
"exitToTray": "irten erretilura"
}, },
"form": { "form": {
"addServer": { "addServer": {
@@ -426,7 +488,8 @@
"title": "zerbitzaria gehitu", "title": "zerbitzaria gehitu",
"ignoreCors": "alde batera utzi cors $t(common.restartRequired)", "ignoreCors": "alde batera utzi cors $t(common.restartRequired)",
"ignoreSsl": "alde batera utzi ssl $t(common.restartRequired)", "ignoreSsl": "alde batera utzi ssl $t(common.restartRequired)",
"input_legacyAuthentication": "gaitu zaharkitutako autentifikazioa" "input_legacyAuthentication": "gaitu zaharkitutako autentifikazioa",
"success": "zerbitzaria behar bezala gehitu da"
}, },
"addToPlaylist": { "addToPlaylist": {
"input_playlists": "$t(entity.playlist_other)", "input_playlists": "$t(entity.playlist_other)",
@@ -462,7 +525,8 @@
}, },
"editPlaylist": { "editPlaylist": {
"success": "$t(entity.playlist_one) behar bezala eguneratu da", "success": "$t(entity.playlist_one) behar bezala eguneratu da",
"title": "$t(entity.playlist_one) editatu" "title": "$t(entity.playlist_one) editatu",
"publicJellyfinNote": "Arrazoiren batengatik, Jellyfin ez du erakusten erreprodukzio-zerrendak publikoak diren edo ez. Hau publiko izaten jarraitzea nahi baduzu, hautatu sarrera hau"
}, },
"queryEditor": { "queryEditor": {
"title": "kontsulta editorea", "title": "kontsulta editorea",
@@ -504,7 +568,8 @@
"privateModeOff": "itzali modu pribatua", "privateModeOff": "itzali modu pribatua",
"privateModeOn": "aktibatu modu pribatua", "privateModeOn": "aktibatu modu pribatua",
"selectServer": "aukeratu zerbitzaria", "selectServer": "aukeratu zerbitzaria",
"version": "bertsioa {{version}}" "version": "bertsioa {{version}}",
"openBrowserDevtools": "ireki nabigatzailearen garapen tresnak"
}, },
"manageServers": { "manageServers": {
"url": "URLa", "url": "URLa",
@@ -552,7 +617,8 @@
"dynamicImageBlur": "irudiaren lausotze tamaina", "dynamicImageBlur": "irudiaren lausotze tamaina",
"lyricAlignment": "letraren lerrokatzea", "lyricAlignment": "letraren lerrokatzea",
"showLyricMatch": "erakutsi letren bat-etortzea", "showLyricMatch": "erakutsi letren bat-etortzea",
"showLyricProvider": "erakutsi letra hornitzailea" "showLyricProvider": "erakutsi letra hornitzailea",
"lyricOffset": "letra-desplazamendua (ms)"
}, },
"lyrics": "letrak", "lyrics": "letrak",
"related": "erlazionatuta", "related": "erlazionatuta",
+38 -6
View File
@@ -142,7 +142,9 @@
"playSimilarSongs": "$t(player.playSimilarSongs)", "playSimilarSongs": "$t(player.playSimilarSongs)",
"playShuffled": "$t(player.shuffle)", "playShuffled": "$t(player.shuffle)",
"shareItem": "分享項目", "shareItem": "分享項目",
"showDetails": "取得資訊" "showDetails": "取得資訊",
"goToAlbum": "前往 $t(entity.album_one)",
"goToAlbumArtist": "前往 $t(entity.albumArtist_one)"
}, },
"globalSearch": { "globalSearch": {
"title": "指令", "title": "指令",
@@ -157,7 +159,8 @@
"recentlyPlayed": "最近播放", "recentlyPlayed": "最近播放",
"title": "$t(common.home)", "title": "$t(common.home)",
"mostPlayed": "最多播放", "mostPlayed": "最多播放",
"newlyAdded": "最近新增的發行" "newlyAdded": "最近新增的發行",
"recentlyReleased": "最近發佈"
}, },
"appMenu": { "appMenu": {
"openBrowserDevtools": "打開瀏覽器開發者工具", "openBrowserDevtools": "打開瀏覽器開發者工具",
@@ -169,7 +172,9 @@
"selectServer": "選擇伺服器", "selectServer": "選擇伺服器",
"settings": "$t(common.setting_other)", "settings": "$t(common.setting_other)",
"version": "版本 {{version}}", "version": "版本 {{version}}",
"manageServers": "管理伺服器" "manageServers": "管理伺服器",
"privateModeOff": "關閉私人模式",
"privateModeOn": "開啟私人模式"
}, },
"fullscreenPlayer": { "fullscreenPlayer": {
"config": { "config": {
@@ -489,7 +494,7 @@
"discordListening": "將狀態設為\"正在聽\"", "discordListening": "將狀態設為\"正在聽\"",
"discordListening_description": "將狀態顯示為\"正在聽\"而不是\"正在玩\"", "discordListening_description": "將狀態顯示為\"正在聽\"而不是\"正在玩\"",
"discordServeImage": "從伺服器提供{{discord}}圖片", "discordServeImage": "從伺服器提供{{discord}}圖片",
"discordServeImage_description": "從伺服器本身 {{discord}} rich presence 分享封面圖片,只在jellyfin和navidrome可用", "discordServeImage_description": "從伺服器本身分享 {{discord}} Rich Presence封面圖片,僅支援 Jellyfin 與 Navidrome。{{discord}} 會透過機器人擷取圖片,因此您的伺服器必須能從公開網路連線。",
"doubleClickBehavior": "雙擊時將所有搜尋到的曲目加入佇列", "doubleClickBehavior": "雙擊時將所有搜尋到的曲目加入佇列",
"doubleClickBehavior_description": "如果為 true,則歌曲搜尋中所有符合的歌曲都會被加入佇列。否則,只有被點擊的歌曲才會被加入佇列", "doubleClickBehavior_description": "如果為 true,則歌曲搜尋中所有符合的歌曲都會被加入佇列。否則,只有被點擊的歌曲才會被加入佇列",
"externalLinks": "顯示外部連結", "externalLinks": "顯示外部連結",
@@ -544,7 +549,27 @@
"webAudio": "使用網頁音訊", "webAudio": "使用網頁音訊",
"webAudio_description": "使用網頁音訊。這將啟用重播增益等進階功能。如果您遇到其他問題,請停用", "webAudio_description": "使用網頁音訊。這將啟用重播增益等進階功能。如果您遇到其他問題,請停用",
"preservePitch": "保持音高", "preservePitch": "保持音高",
"preservePitch_description": "修改播放速度時保留音調" "preservePitch_description": "修改播放速度時保留音調",
"artistBackground": "藝人背景圖片",
"artistBackground_description": "為藝人頁面新增含藝人圖片的背景圖像",
"artistBackgroundBlur": "藝人背景圖片模糊程度",
"artistBackgroundBlur_description": "調整套用至藝人背景圖片的模糊程度",
"releaseChannel_optionLatest": "穩定版",
"releaseChannel_optionBeta": "測試版",
"releaseChannel_description": "選擇自動更新時要使用穩定版本或是測試版本",
"discordDisplayType": "{{discord}} presence 顯示類型",
"discordDisplayType_description": "變更您在狀態中正在聆聽的內容",
"discordDisplayType_songname": "歌曲名稱",
"discordDisplayType_artistname": "藝人名稱",
"discordLinkType": "{{discord}} presence 連結",
"discordLinkType_description": "在 {{discord}} Rich Presence中,為歌曲和藝人欄位新增 {{lastfm}} 或 {{musicbrainz}} 的外部連結。{{musicbrainz}} 的準確度最高,但需要標籤且不提供藝人連結;而 {{lastfm}} 通常都能提供連結。此功能不會產生額外的網路請求",
"discordLinkType_none": "$t(common.none)",
"discordLinkType_mbz_lastfm": "{{musicbrainz}} 並以 {{lastfm}} 備用",
"hotkey_navigateHome": "導航至首頁",
"preventSleepOnPlayback": "防止播放時進入睡眠狀態",
"preventSleepOnPlayback_description": "在音樂播放時防止螢幕進入睡眠狀態",
"mediaSession": "啟用Media Session",
"mediaSession_description": "啟用 Windows Media Session 整合功能,於系統音量Overlay和鎖定畫面中顯示媒體資料與控制面板(僅限 Windows)"
}, },
"table": { "table": {
"config": { "config": {
@@ -723,7 +748,9 @@
"title": "新增伺服器", "title": "新增伺服器",
"error_savePassword": "儲存密碼時出現錯誤", "error_savePassword": "儲存密碼時出現錯誤",
"ignoreCors": "忽略 cors $t(common.restartRequired)", "ignoreCors": "忽略 cors $t(common.restartRequired)",
"ignoreSsl": "忽略 ssl $t(common.restartRequired)" "ignoreSsl": "忽略 ssl $t(common.restartRequired)",
"input_preferInstantMix": "偏好即時混音",
"input_preferInstantMixDescription": "僅使用即時混音功能來獲取相似歌曲。若您擁有能修改此行為的外掛,此功能將相當實用"
}, },
"addToPlaylist": { "addToPlaylist": {
"input_playlists": "$t(entity.playlist_other)", "input_playlists": "$t(entity.playlist_other)",
@@ -770,6 +797,11 @@
"success": "分享連結已複製到剪貼簿(或點擊此處開啟)", "success": "分享連結已複製到剪貼簿(或點擊此處開啟)",
"expireInvalid": "到期日必須是未來", "expireInvalid": "到期日必須是未來",
"createFailed": "無法建立分享(分享是否啟用?)" "createFailed": "無法建立分享(分享是否啟用?)"
},
"privateMode": {
"enabled": "已啟用私人模式,播放狀態將對外部整合隱藏",
"disabled": "已停用私人模式,播放狀態現對已啟用的外部整合可見",
"title": "私人模式"
} }
} }
} }
@@ -39,17 +39,21 @@ interface CenterControlsProps {
export const CenterControls = ({ playersRef }: CenterControlsProps) => { export const CenterControls = ({ playersRef }: CenterControlsProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const [isSeeking, setIsSeeking] = useState(false);
const currentSong = useCurrentSong(); const currentSong = useCurrentSong();
const skip = useSettingsStore((state) => state.general.skipButtons); const skip = useSettingsStore((state) => state.general.skipButtons);
const buttonSize = useSettingsStore((state) => state.general.buttonSize); const buttonSize = useSettingsStore((state) => state.general.buttonSize);
const playbackType = usePlaybackType();
const player1 = playersRef?.current?.player1; const player1 = playersRef?.current?.player1;
const player2 = playersRef?.current?.player2; const player2 = playersRef?.current?.player2;
const status = useCurrentStatus(); const status = useCurrentStatus();
const player = useCurrentPlayer();
const setCurrentTime = useSetCurrentTime();
const repeat = useRepeatStatus(); const repeat = useRepeatStatus();
const shuffle = useShuffleStatus(); const shuffle = useShuffleStatus();
const { bindings } = useHotkeySettings(); const { bindings } = useHotkeySettings();
const { showTimeRemaining } = useAppStore();
const { setShowTimeRemaining } = useAppStoreActions();
const { const {
handleNextTrack, handleNextTrack,
@@ -57,6 +61,7 @@ export const CenterControls = ({ playersRef }: CenterControlsProps) => {
handlePlay, handlePlay,
handlePlayPause, handlePlayPause,
handlePrevTrack, handlePrevTrack,
handleSeekSlider,
handleSkipBackward, handleSkipBackward,
handleSkipForward, handleSkipForward,
handleStop, handleStop,
@@ -65,6 +70,32 @@ export const CenterControls = ({ playersRef }: CenterControlsProps) => {
} = useCenterControls({ playersRef }); } = useCenterControls({ playersRef });
const handlePlayQueueAdd = usePlayQueueAdd(); const handlePlayQueueAdd = usePlayQueueAdd();
const songDuration = currentSong?.duration ? currentSong.duration / 1000 : 0;
const currentTime = useCurrentTime();
const currentPlayerRef = player === 1 ? player1 : player2;
const formattedDuration = formatDuration(songDuration * 1000 || 0);
const formattedTimeRemaining = formatDuration((currentTime - songDuration) * 1000 || 0);
const formattedTime = formatDuration(currentTime * 1000 || 0);
useEffect(() => {
let interval: ReturnType<typeof setInterval>;
if (status === PlayerStatus.PLAYING && !isSeeking) {
if (!isElectron() || playbackType === PlaybackType.WEB) {
// Update twice a second for slightly better performance
interval = setInterval(() => {
if (currentPlayerRef) {
setCurrentTime(currentPlayerRef.getCurrentTime());
}
}, 500);
}
}
return () => clearInterval(interval);
}, [currentPlayerRef, isSeeking, setCurrentTime, playbackType, status]);
const [seekValue, setSeekValue] = useState(0);
useHotkeys([ useHotkeys([
[bindings.playPause.isGlobal ? '' : bindings.playPause.hotkey, handlePlayPause], [bindings.playPause.isGlobal ? '' : bindings.playPause.hotkey, handlePlayPause],
[bindings.play.isGlobal ? '' : bindings.play.hotkey, handlePlay], [bindings.play.isGlobal ? '' : bindings.play.hotkey, handlePlay],
@@ -84,7 +115,16 @@ export const CenterControls = ({ playersRef }: CenterControlsProps) => {
], ],
]); ]);
useMediaSession(playersRef); useMediaSession({
handleNextTrack,
handlePause,
handlePlay,
handlePrevTrack,
handleSeekSlider,
handleSkipBackward,
handleSkipForward,
handleStop,
});
return ( return (
<> <>
@@ -227,58 +267,6 @@ export const CenterControls = ({ playersRef }: CenterControlsProps) => {
/> />
</div> </div>
</div> </div>
<PlayerSeekSlider player1={player1} player2={player2} playersRef={playersRef} />
</>
);
};
const PlayerSeekSlider = ({
player1,
player2,
playersRef,
}: {
player1: any;
player2: any;
playersRef: any;
}) => {
const { handleSeekSlider } = useCenterControls({ playersRef });
const player = useCurrentPlayer();
const playbackType = usePlaybackType();
const setCurrentTime = useSetCurrentTime();
const status = useCurrentStatus();
const currentSong = useCurrentSong();
const songDuration = currentSong?.duration ? currentSong.duration / 1000 : 0;
const currentTime = useCurrentTime();
const currentPlayerRef = player === 1 ? player1 : player2;
const [isSeeking, setIsSeeking] = useState(false);
useEffect(() => {
let interval: ReturnType<typeof setInterval>;
if (status === PlayerStatus.PLAYING && !isSeeking) {
if (!isElectron() || playbackType === PlaybackType.WEB) {
// Update twice a second for slightly better performance
interval = setInterval(() => {
if (currentPlayerRef) {
setCurrentTime(currentPlayerRef.getCurrentTime());
}
}, 500);
}
}
return () => clearInterval(interval);
}, [currentPlayerRef, isSeeking, setCurrentTime, playbackType, status]);
const { showTimeRemaining } = useAppStore();
const { setShowTimeRemaining } = useAppStoreActions();
const formattedDuration = formatDuration(songDuration * 1000 || 0);
const formattedTimeRemaining = formatDuration((currentTime - songDuration) * 1000 || 0);
const formattedTime = formatDuration(currentTime * 1000 || 0);
const [seekValue, setSeekValue] = useState(0);
return (
<div className={styles.sliderContainer}> <div className={styles.sliderContainer}>
<div className={styles.sliderValueWrapper}> <div className={styles.sliderValueWrapper}>
<Text <Text
@@ -330,5 +318,6 @@ const PlayerSeekSlider = ({
</Text> </Text>
</div> </div>
</div> </div>
</>
); );
}; };
@@ -1,6 +1,5 @@
import { useEffect } from 'react'; import { useEffect } from 'react';
import { useCenterControls } from '/@/renderer/features/player/hooks/use-center-controls';
import { import {
useCurrentSong, useCurrentSong,
useCurrentStatus, useCurrentStatus,
@@ -9,14 +8,7 @@ import {
} from '/@/renderer/store'; } from '/@/renderer/store';
import { PlayerStatus } from '/@/shared/types/types'; import { PlayerStatus } from '/@/shared/types/types';
export const useMediaSession = (playersRef: { player1: any; player2: any }) => { export const useMediaSession = ({
const { mediaSession: mediaSessionEnabled } = usePlaybackSettings();
const playerStatus = useCurrentStatus();
const currentSong = useCurrentSong();
const mediaSession = navigator.mediaSession;
const skip = useSettingsStore((state) => state.general.skipButtons);
const {
handleNextTrack, handleNextTrack,
handlePause, handlePause,
handlePlay, handlePlay,
@@ -25,9 +17,21 @@ export const useMediaSession = (playersRef: { player1: any; player2: any }) => {
handleSkipBackward, handleSkipBackward,
handleSkipForward, handleSkipForward,
handleStop, handleStop,
} = useCenterControls({ }: {
playersRef, handleNextTrack: () => void;
}); handlePause: () => void;
handlePlay: () => void;
handlePrevTrack: () => void;
handleSeekSlider: (e: any | number) => void;
handleSkipBackward: (seconds: number) => void;
handleSkipForward: (seconds: number) => void;
handleStop: () => void;
}) => {
const { mediaSession: mediaSessionEnabled } = usePlaybackSettings();
const playerStatus = useCurrentStatus();
const currentSong = useCurrentSong();
const mediaSession = navigator.mediaSession;
const skip = useSettingsStore((state) => state.general.skipButtons);
useEffect(() => { useEffect(() => {
if (!mediaSessionEnabled || !mediaSession) { if (!mediaSessionEnabled || !mediaSession) {
@@ -8,8 +8,9 @@ import {
import { usePlaybackSettings, useSettingsStoreActions } from '/@/renderer/store/settings.store'; import { usePlaybackSettings, useSettingsStoreActions } from '/@/renderer/store/settings.store';
import { Switch } from '/@/shared/components/switch/switch'; import { Switch } from '/@/shared/components/switch/switch';
const isWindows = window.api.utils.isWindows(); const isWindows = isElectron() ? window.api.utils.isWindows() : null;
const isDesktop = isElectron(); const isDesktop = isElectron();
const ipc = isElectron() ? window.api.ipc : null;
export const MediaSessionSettings = () => { export const MediaSessionSettings = () => {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -19,7 +20,7 @@ export const MediaSessionSettings = () => {
function handleMediaSessionChange() { function handleMediaSessionChange() {
const current = mediaSession; const current = mediaSession;
toggleMediaSession(); toggleMediaSession();
window.api.ipc.send('settings-set', { property: 'mediaSession', value: !current }); ipc?.send('settings-set', { property: 'mediaSession', value: !current });
} }
const mediaSessionOptions: SettingOption[] = [ const mediaSessionOptions: SettingOption[] = [
@@ -35,7 +36,7 @@ export const MediaSessionSettings = () => {
context: 'description', context: 'description',
postProcess: 'sentenceCase', postProcess: 'sentenceCase',
}), }),
isHidden: isDesktop && !isWindows, isHidden: !isWindows || !isDesktop,
note: t('common.restartRequired', { postProcess: 'sentenceCase' }), note: t('common.restartRequired', { postProcess: 'sentenceCase' }),
title: t('setting.mediaSession', { postProcess: 'sentenceCase' }), title: t('setting.mediaSession', { postProcess: 'sentenceCase' }),
}, },