Compare commits

...

7 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
3 changed files with 88 additions and 110 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "feishin", "name": "feishin",
"version": "0.21.1", "version": "0.21.2",
"description": "A modern self-hosted music player.", "description": "A modern self-hosted music player.",
"keywords": [ "keywords": [
"subsonic", "subsonic",
@@ -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,
@@ -66,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],
@@ -237,110 +267,57 @@ export const CenterControls = ({ playersRef }: CenterControlsProps) => {
/> />
</div> </div>
</div> </div>
<PlayerSeekSlider <div className={styles.sliderContainer}>
handleSeekSlider={handleSeekSlider} <div className={styles.sliderValueWrapper}>
player1={player1} <Text
player2={player2} 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 = ({
handleSeekSlider,
player1,
player2,
}: {
handleSeekSlider: (e: any | number) => void;
player1: any;
player2: any;
}) => {
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>
);
};
@@ -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' }),
}, },