mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-08 13:00:13 +02:00
Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b16e57710b | |||
| 931e96b9d1 | |||
| c27b86d2b2 | |||
| b3cf73836d | |||
| 1b15c73db0 | |||
| 4e53030e8d | |||
| 22b798812e | |||
| 1f7d510110 | |||
| 44fae06143 | |||
| 1e24e12a55 | |||
| 358bdec2b6 | |||
| 1b8661d566 |
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "feishin",
|
||||
"version": "0.21.0",
|
||||
"version": "0.21.2",
|
||||
"description": "A modern self-hosted music player.",
|
||||
"keywords": [
|
||||
"subsonic",
|
||||
|
||||
@@ -483,7 +483,7 @@
|
||||
"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}}",
|
||||
"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}}",
|
||||
"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",
|
||||
@@ -653,7 +653,17 @@
|
||||
"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_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": {
|
||||
"column": {
|
||||
|
||||
+76
-10
@@ -116,7 +116,8 @@
|
||||
"saveAs": "gorde honela",
|
||||
"trackGain": "pista irabazpena",
|
||||
"comingSoon": "laster…",
|
||||
"trackPeak": "pistaren gailurra"
|
||||
"trackPeak": "pistaren gailurra",
|
||||
"albumPeak": "albumaren gailurra"
|
||||
},
|
||||
"player": {
|
||||
"repeat": "errepikatu",
|
||||
@@ -161,7 +162,10 @@
|
||||
},
|
||||
"general": {
|
||||
"gap": "$t(common.gap)",
|
||||
"size": "$t(common.size)"
|
||||
"size": "$t(common.size)",
|
||||
"tableColumns": "taula zutabeak",
|
||||
"itemSize": "elementuaren tamaina (px)",
|
||||
"followCurrentSong": "jarraitu uneko abestia"
|
||||
},
|
||||
"label": {
|
||||
"actions": "$t(common.action_other)",
|
||||
@@ -183,7 +187,13 @@
|
||||
"size": "$t(common.size)",
|
||||
"songCount": "$t(entity.track_other)",
|
||||
"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": {
|
||||
@@ -206,7 +216,11 @@
|
||||
"trackNumber": "pista",
|
||||
"bpm": "bpm",
|
||||
"comment": "iruzkina",
|
||||
"playCount": "erreprodukzioak"
|
||||
"playCount": "erreprodukzioak",
|
||||
"releaseDate": "argitalpen data",
|
||||
"lastPlayed": "azken aldiz entzundakoa",
|
||||
"dateAdded": "gehitutako data",
|
||||
"albumArtist": "albumeko artista"
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
@@ -397,7 +411,7 @@
|
||||
"discordRichPresence": "{{discord}} jarduera-egoera",
|
||||
"discordRichPresence_description": "gaitu erreprodukzioa egoera {{discord}}-en jarduera-egoeran. Irudi gakoak hauek dira: {{icon}}, {{playing}}, eta {{paused}}",
|
||||
"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",
|
||||
"discordLinkType_none": "$t(common.none)",
|
||||
"albumBackground": "albumaren atzeko planoaren irudia",
|
||||
@@ -413,7 +427,55 @@
|
||||
"discordDisplayType_songname": "abesti izena",
|
||||
"discordDisplayType_artistname": "artista izena(k)",
|
||||
"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": {
|
||||
"addServer": {
|
||||
@@ -426,7 +488,8 @@
|
||||
"title": "zerbitzaria gehitu",
|
||||
"ignoreCors": "alde batera utzi cors $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": {
|
||||
"input_playlists": "$t(entity.playlist_other)",
|
||||
@@ -462,7 +525,8 @@
|
||||
},
|
||||
"editPlaylist": {
|
||||
"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": {
|
||||
"title": "kontsulta editorea",
|
||||
@@ -504,7 +568,8 @@
|
||||
"privateModeOff": "itzali modu pribatua",
|
||||
"privateModeOn": "aktibatu modu pribatua",
|
||||
"selectServer": "aukeratu zerbitzaria",
|
||||
"version": "bertsioa {{version}}"
|
||||
"version": "bertsioa {{version}}",
|
||||
"openBrowserDevtools": "ireki nabigatzailearen garapen tresnak"
|
||||
},
|
||||
"manageServers": {
|
||||
"url": "URLa",
|
||||
@@ -552,7 +617,8 @@
|
||||
"dynamicImageBlur": "irudiaren lausotze tamaina",
|
||||
"lyricAlignment": "letraren lerrokatzea",
|
||||
"showLyricMatch": "erakutsi letren bat-etortzea",
|
||||
"showLyricProvider": "erakutsi letra hornitzailea"
|
||||
"showLyricProvider": "erakutsi letra hornitzailea",
|
||||
"lyricOffset": "letra-desplazamendua (ms)"
|
||||
},
|
||||
"lyrics": "letrak",
|
||||
"related": "erlazionatuta",
|
||||
|
||||
@@ -142,7 +142,9 @@
|
||||
"playSimilarSongs": "$t(player.playSimilarSongs)",
|
||||
"playShuffled": "$t(player.shuffle)",
|
||||
"shareItem": "分享項目",
|
||||
"showDetails": "取得資訊"
|
||||
"showDetails": "取得資訊",
|
||||
"goToAlbum": "前往 $t(entity.album_one)",
|
||||
"goToAlbumArtist": "前往 $t(entity.albumArtist_one)"
|
||||
},
|
||||
"globalSearch": {
|
||||
"title": "指令",
|
||||
@@ -157,7 +159,8 @@
|
||||
"recentlyPlayed": "最近播放",
|
||||
"title": "$t(common.home)",
|
||||
"mostPlayed": "最多播放",
|
||||
"newlyAdded": "最近新增的發行"
|
||||
"newlyAdded": "最近新增的發行",
|
||||
"recentlyReleased": "最近發佈"
|
||||
},
|
||||
"appMenu": {
|
||||
"openBrowserDevtools": "打開瀏覽器開發者工具",
|
||||
@@ -169,7 +172,9 @@
|
||||
"selectServer": "選擇伺服器",
|
||||
"settings": "$t(common.setting_other)",
|
||||
"version": "版本 {{version}}",
|
||||
"manageServers": "管理伺服器"
|
||||
"manageServers": "管理伺服器",
|
||||
"privateModeOff": "關閉私人模式",
|
||||
"privateModeOn": "開啟私人模式"
|
||||
},
|
||||
"fullscreenPlayer": {
|
||||
"config": {
|
||||
@@ -489,7 +494,7 @@
|
||||
"discordListening": "將狀態設為\"正在聽\"",
|
||||
"discordListening_description": "將狀態顯示為\"正在聽\"而不是\"正在玩\"",
|
||||
"discordServeImage": "從伺服器提供{{discord}}圖片",
|
||||
"discordServeImage_description": "從伺服器本身為 {{discord}} rich presence 分享封面圖片,只在jellyfin和navidrome可用",
|
||||
"discordServeImage_description": "從伺服器本身分享 {{discord}} Rich Presence的封面圖片,僅支援 Jellyfin 與 Navidrome。{{discord}} 會透過機器人擷取圖片,因此您的伺服器必須能從公開網路連線。",
|
||||
"doubleClickBehavior": "雙擊時將所有搜尋到的曲目加入佇列",
|
||||
"doubleClickBehavior_description": "如果為 true,則歌曲搜尋中所有符合的歌曲都會被加入佇列。否則,只有被點擊的歌曲才會被加入佇列",
|
||||
"externalLinks": "顯示外部連結",
|
||||
@@ -544,7 +549,27 @@
|
||||
"webAudio": "使用網頁音訊",
|
||||
"webAudio_description": "使用網頁音訊。這將啟用重播增益等進階功能。如果您遇到其他問題,請停用",
|
||||
"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": {
|
||||
"config": {
|
||||
@@ -723,7 +748,9 @@
|
||||
"title": "新增伺服器",
|
||||
"error_savePassword": "儲存密碼時出現錯誤",
|
||||
"ignoreCors": "忽略 cors $t(common.restartRequired)",
|
||||
"ignoreSsl": "忽略 ssl $t(common.restartRequired)"
|
||||
"ignoreSsl": "忽略 ssl $t(common.restartRequired)",
|
||||
"input_preferInstantMix": "偏好即時混音",
|
||||
"input_preferInstantMixDescription": "僅使用即時混音功能來獲取相似歌曲。若您擁有能修改此行為的外掛,此功能將相當實用"
|
||||
},
|
||||
"addToPlaylist": {
|
||||
"input_playlists": "$t(entity.playlist_other)",
|
||||
@@ -770,6 +797,11 @@
|
||||
"success": "分享連結已複製到剪貼簿(或點擊此處開啟)",
|
||||
"expireInvalid": "到期日必須是未來",
|
||||
"createFailed": "無法建立分享(分享是否啟用?)"
|
||||
},
|
||||
"privateMode": {
|
||||
"enabled": "已啟用私人模式,播放狀態將對外部整合隱藏",
|
||||
"disabled": "已停用私人模式,播放狀態現對已啟用的外部整合可見",
|
||||
"title": "私人模式"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,17 +39,21 @@ interface CenterControlsProps {
|
||||
export const CenterControls = ({ playersRef }: CenterControlsProps) => {
|
||||
const { t } = useTranslation();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const [isSeeking, setIsSeeking] = useState(false);
|
||||
const currentSong = useCurrentSong();
|
||||
const skip = useSettingsStore((state) => state.general.skipButtons);
|
||||
const buttonSize = useSettingsStore((state) => state.general.buttonSize);
|
||||
|
||||
const playbackType = usePlaybackType();
|
||||
const player1 = playersRef?.current?.player1;
|
||||
const player2 = playersRef?.current?.player2;
|
||||
const status = useCurrentStatus();
|
||||
const player = useCurrentPlayer();
|
||||
const setCurrentTime = useSetCurrentTime();
|
||||
const repeat = useRepeatStatus();
|
||||
const shuffle = useShuffleStatus();
|
||||
const { bindings } = useHotkeySettings();
|
||||
const { showTimeRemaining } = useAppStore();
|
||||
const { setShowTimeRemaining } = useAppStoreActions();
|
||||
|
||||
const {
|
||||
handleNextTrack,
|
||||
@@ -57,6 +61,7 @@ export const CenterControls = ({ playersRef }: CenterControlsProps) => {
|
||||
handlePlay,
|
||||
handlePlayPause,
|
||||
handlePrevTrack,
|
||||
handleSeekSlider,
|
||||
handleSkipBackward,
|
||||
handleSkipForward,
|
||||
handleStop,
|
||||
@@ -65,6 +70,32 @@ export const CenterControls = ({ playersRef }: CenterControlsProps) => {
|
||||
} = useCenterControls({ playersRef });
|
||||
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([
|
||||
[bindings.playPause.isGlobal ? '' : bindings.playPause.hotkey, handlePlayPause],
|
||||
[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 (
|
||||
<>
|
||||
@@ -227,108 +267,57 @@ export const CenterControls = ({ playersRef }: CenterControlsProps) => {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<PlayerSeekSlider player1={player1} player2={player2} playersRef={playersRef} />
|
||||
<div className={styles.sliderContainer}>
|
||||
<div className={styles.sliderValueWrapper}>
|
||||
<Text
|
||||
className={PlaybackSelectors.elapsedTime}
|
||||
fw={600}
|
||||
isMuted
|
||||
isNoSelect
|
||||
size="xs"
|
||||
style={{ userSelect: 'none' }}
|
||||
>
|
||||
{formattedTime}
|
||||
</Text>
|
||||
</div>
|
||||
<div className={styles.sliderWrapper}>
|
||||
<PlayerbarSlider
|
||||
label={(value) => formatDuration(value * 1000)}
|
||||
max={songDuration}
|
||||
min={0}
|
||||
onChange={(e) => {
|
||||
setIsSeeking(true);
|
||||
setSeekValue(e);
|
||||
}}
|
||||
onChangeEnd={(e) => {
|
||||
// There is a timing bug in Mantine in which the onChangeEnd
|
||||
// event fires before onChange. Add a small delay to force
|
||||
// onChangeEnd to happen after onCHange
|
||||
setTimeout(() => {
|
||||
handleSeekSlider(e);
|
||||
setIsSeeking(false);
|
||||
}, 50);
|
||||
}}
|
||||
size={6}
|
||||
value={!isSeeking ? currentTime : seekValue}
|
||||
w="100%"
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.sliderValueWrapper}>
|
||||
<Text
|
||||
className={PlaybackSelectors.totalDuration}
|
||||
fw={600}
|
||||
isMuted
|
||||
isNoSelect
|
||||
onClick={() => setShowTimeRemaining(!showTimeRemaining)}
|
||||
role="button"
|
||||
size="xs"
|
||||
style={{ cursor: 'pointer', userSelect: 'none' }}
|
||||
>
|
||||
{showTimeRemaining ? formattedTimeRemaining : formattedDuration}
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
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.sliderValueWrapper}>
|
||||
<Text
|
||||
className={PlaybackSelectors.elapsedTime}
|
||||
fw={600}
|
||||
isMuted
|
||||
isNoSelect
|
||||
size="xs"
|
||||
style={{ userSelect: 'none' }}
|
||||
>
|
||||
{formattedTime}
|
||||
</Text>
|
||||
</div>
|
||||
<div className={styles.sliderWrapper}>
|
||||
<PlayerbarSlider
|
||||
label={(value) => formatDuration(value * 1000)}
|
||||
max={songDuration}
|
||||
min={0}
|
||||
onChange={(e) => {
|
||||
setIsSeeking(true);
|
||||
setSeekValue(e);
|
||||
}}
|
||||
onChangeEnd={(e) => {
|
||||
// There is a timing bug in Mantine in which the onChangeEnd
|
||||
// event fires before onChange. Add a small delay to force
|
||||
// onChangeEnd to happen after onCHange
|
||||
setTimeout(() => {
|
||||
handleSeekSlider(e);
|
||||
setIsSeeking(false);
|
||||
}, 50);
|
||||
}}
|
||||
size={6}
|
||||
value={!isSeeking ? currentTime : seekValue}
|
||||
w="100%"
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.sliderValueWrapper}>
|
||||
<Text
|
||||
className={PlaybackSelectors.totalDuration}
|
||||
fw={600}
|
||||
isMuted
|
||||
isNoSelect
|
||||
onClick={() => setShowTimeRemaining(!showTimeRemaining)}
|
||||
role="button"
|
||||
size="xs"
|
||||
style={{ cursor: 'pointer', userSelect: 'none' }}
|
||||
>
|
||||
{showTimeRemaining ? formattedTimeRemaining : formattedDuration}
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import { useCenterControls } from '/@/renderer/features/player/hooks/use-center-controls';
|
||||
import {
|
||||
useCurrentSong,
|
||||
useCurrentStatus,
|
||||
@@ -9,26 +8,31 @@ import {
|
||||
} from '/@/renderer/store';
|
||||
import { PlayerStatus } from '/@/shared/types/types';
|
||||
|
||||
export const useMediaSession = (playersRef: { player1: any; player2: any }) => {
|
||||
export const useMediaSession = ({
|
||||
handleNextTrack,
|
||||
handlePause,
|
||||
handlePlay,
|
||||
handlePrevTrack,
|
||||
handleSeekSlider,
|
||||
handleSkipBackward,
|
||||
handleSkipForward,
|
||||
handleStop,
|
||||
}: {
|
||||
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);
|
||||
|
||||
const {
|
||||
handleNextTrack,
|
||||
handlePause,
|
||||
handlePlay,
|
||||
handlePrevTrack,
|
||||
handleSeekSlider,
|
||||
handleSkipBackward,
|
||||
handleSkipForward,
|
||||
handleStop,
|
||||
} = useCenterControls({
|
||||
playersRef,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!mediaSessionEnabled || !mediaSession) {
|
||||
return;
|
||||
|
||||
@@ -8,8 +8,9 @@ import {
|
||||
import { usePlaybackSettings, useSettingsStoreActions } from '/@/renderer/store/settings.store';
|
||||
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 ipc = isElectron() ? window.api.ipc : null;
|
||||
|
||||
export const MediaSessionSettings = () => {
|
||||
const { t } = useTranslation();
|
||||
@@ -19,7 +20,7 @@ export const MediaSessionSettings = () => {
|
||||
function handleMediaSessionChange() {
|
||||
const current = mediaSession;
|
||||
toggleMediaSession();
|
||||
window.api.ipc.send('settings-set', { property: 'mediaSession', value: !current });
|
||||
ipc?.send('settings-set', { property: 'mediaSession', value: !current });
|
||||
}
|
||||
|
||||
const mediaSessionOptions: SettingOption[] = [
|
||||
@@ -35,7 +36,7 @@ export const MediaSessionSettings = () => {
|
||||
context: 'description',
|
||||
postProcess: 'sentenceCase',
|
||||
}),
|
||||
isHidden: isDesktop && !isWindows,
|
||||
isHidden: !isWindows || !isDesktop,
|
||||
note: t('common.restartRequired', { postProcess: 'sentenceCase' }),
|
||||
title: t('setting.mediaSession', { postProcess: 'sentenceCase' }),
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user