mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-10 04:30:25 +02:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b16e57710b | |||
| 931e96b9d1 | |||
| c27b86d2b2 | |||
| b3cf73836d | |||
| 1b15c73db0 | |||
| 4e53030e8d | |||
| 22b798812e |
+1
-1
@@ -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' }),
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user