diff --git a/src/main/features/core/player/index.ts b/src/main/features/core/player/index.ts
index c70e5cdeb..e9f80c290 100644
--- a/src/main/features/core/player/index.ts
+++ b/src/main/features/core/player/index.ts
@@ -32,7 +32,7 @@ mpv.on('status', (status: any) => {
});
// Automatically updates the play button when the player is playing
-mpv.on('started', () => {
+mpv.on('resumed', () => {
getMainWindow()?.webContents.send('renderer-player-play');
});
diff --git a/src/main/preload.ts b/src/main/preload.ts
index b4777332f..bf2987e66 100644
--- a/src/main/preload.ts
+++ b/src/main/preload.ts
@@ -87,6 +87,9 @@ contextBridge.exposeInMainWorld('electron', {
SETTINGS_SET(data: { property: string; value: any }) {
ipcRenderer.send('settings-set', data);
},
+ removeAllListeners(value: string) {
+ ipcRenderer.removeAllListeners(value);
+ },
windowClose() {
ipcRenderer.send('window-close');
},
diff --git a/src/renderer/components/audio-player/utils/list-handlers.ts b/src/renderer/components/audio-player/utils/list-handlers.ts
index 0aaa28686..06daae9ea 100644
--- a/src/renderer/components/audio-player/utils/list-handlers.ts
+++ b/src/renderer/components/audio-player/utils/list-handlers.ts
@@ -1,6 +1,6 @@
/* eslint-disable no-nested-ternary */
import { Dispatch } from 'react';
-import { CrossfadeStyle } from '../../../../types';
+import { CrossfadeStyle } from '@/renderer/types';
export const gaplessHandler = (args: {
currentTime: number;
diff --git a/src/renderer/features/player/components/center-controls.tsx b/src/renderer/features/player/components/center-controls.tsx
index 0d1871822..c2a77c830 100644
--- a/src/renderer/features/player/components/center-controls.tsx
+++ b/src/renderer/features/player/components/center-controls.tsx
@@ -5,13 +5,16 @@ import { IoIosPause } from 'react-icons/io';
import {
RiPlayFill,
RiRepeat2Fill,
+ RiRewindFill,
RiShuffleFill,
RiSkipBackFill,
RiSkipForwardFill,
+ RiSpeedFill,
} from 'react-icons/ri';
import styled from 'styled-components';
import { Text } from '@/renderer/components';
import { usePlayerStore } from '@/renderer/store';
+import { useSettingsStore } from '@/renderer/store/settings.store';
import { Font } from '@/renderer/styles';
import { PlaybackType, PlayerStatus } from '@/renderer/types';
import { useCenterControls } from '../hooks/use-center-controls';
@@ -56,10 +59,11 @@ const SliderWrapper = styled.div`
export const CenterControls = ({ playersRef }: CenterControlsProps) => {
const [isSeeking, setIsSeeking] = useState(false);
const playerData = usePlayerStore((state) => state.getPlayerData());
+ const skip = useSettingsStore((state) => state.player.skipButtons);
+ const playerType = useSettingsStore((state) => state.player.type);
const player1 = playersRef?.current?.player1?.player;
const player2 = playersRef?.current?.player2?.player;
const { status, player } = usePlayerStore((state) => state.current);
- const settings = usePlayerStore((state) => state.settings);
const setCurrentTime = usePlayerStore((state) => state.setCurrentTime);
const {
@@ -67,6 +71,8 @@ export const CenterControls = ({ playersRef }: CenterControlsProps) => {
handlePlayPause,
handlePrevTrack,
handleSeekSlider,
+ handleSkipBackward,
+ handleSkipForward,
} = useCenterControls({ playersRef });
const currentTime = usePlayerStore((state) => state.current.time);
@@ -78,7 +84,7 @@ export const CenterControls = ({ playersRef }: CenterControlsProps) => {
let interval: any;
if (status === PlayerStatus.PLAYING && !isSeeking) {
- if (!isElectron() || settings.type === PlaybackType.WEB) {
+ if (!isElectron() || playerType === PlaybackType.WEB) {
interval = setInterval(() => {
setCurrentTime(currentPlayerRef.getCurrentTime());
}, 1000);
@@ -88,7 +94,7 @@ export const CenterControls = ({ playersRef }: CenterControlsProps) => {
}
return () => clearInterval(interval);
- }, [currentPlayerRef, isSeeking, setCurrentTime, settings.type, status]);
+ }, [currentPlayerRef, isSeeking, setCurrentTime, playerType, status]);
return (
<>
@@ -106,6 +112,17 @@ export const CenterControls = ({ playersRef }: CenterControlsProps) => {
variant="secondary"
onClick={handlePrevTrack}
/>
+ {skip?.enabled && (
+ }
+ tooltip={{
+ label: `Skip backwards ${skip?.skipBackwardSeconds} seconds`,
+ openDelay: 500,
+ }}
+ variant="secondary"
+ onClick={() => handleSkipBackward(skip?.skipBackwardSeconds)}
+ />
+ )}
{
variant="main"
onClick={handlePlayPause}
/>
+ {skip?.enabled && (
+ }
+ tooltip={{
+ label: `Skip forwards ${skip?.skipBackwardSeconds} seconds`,
+ openDelay: 500,
+ }}
+ variant="secondary"
+ onClick={() => handleSkipForward(skip?.skipForwardSeconds)}
+ />
+ )}
}
tooltip={{ label: 'Next track', openDelay: 500 }}
diff --git a/src/renderer/features/player/components/playerbar.tsx b/src/renderer/features/player/components/playerbar.tsx
index 4045fe416..b73393a06 100644
--- a/src/renderer/features/player/components/playerbar.tsx
+++ b/src/renderer/features/player/components/playerbar.tsx
@@ -1,6 +1,6 @@
import { useRef } from 'react';
-import isElectron from 'is-electron';
import styled from 'styled-components';
+import { useSettingsStore } from '@/renderer/store/settings.store';
import { PlaybackType } from '@/renderer/types';
import { AudioPlayer } from '../../../components';
import { usePlayerStore } from '../../../store';
@@ -45,8 +45,8 @@ const CenterGridItem = styled.div`
export const Playerbar = () => {
const playersRef = useRef();
- const settings = usePlayerStore((state) => state.settings);
- const volume = usePlayerStore((state) => state.settings.volume);
+ const settings = useSettingsStore((state) => state.player);
+ const volume = usePlayerStore((state) => state.volume);
const player1 = usePlayerStore((state) => state.player1());
const player2 = usePlayerStore((state) => state.player2());
const status = usePlayerStore((state) => state.current.status);
@@ -66,7 +66,7 @@ export const Playerbar = () => {
- {(!isElectron() || settings.type === PlaybackType.WEB) && (
+ {settings.type === PlaybackType.WEB && (
{
crossfadeStyle={settings.crossfadeStyle}
currentPlayer={player}
muted={settings.muted}
+ playbackStyle={settings.style}
player1={player1}
player2={player2}
status={status}
diff --git a/src/renderer/features/player/components/right-controls.tsx b/src/renderer/features/player/components/right-controls.tsx
index 1951353ba..a8f4ea18e 100644
--- a/src/renderer/features/player/components/right-controls.tsx
+++ b/src/renderer/features/player/components/right-controls.tsx
@@ -36,8 +36,8 @@ const MetadataStack = styled.div`
`;
export const RightControls = () => {
- const volume = usePlayerStore((state) => state.settings.volume);
- const muted = usePlayerStore((state) => state.settings.muted);
+ const volume = usePlayerStore((state) => state.volume);
+ const muted = usePlayerStore((state) => state.muted);
const setSidebar = useAppStore((state) => state.setSidebar);
const isQueueExpanded = useAppStore((state) => state.sidebar.rightExpanded);
const { handleVolumeSlider, handleVolumeSliderState, handleMute } =
diff --git a/src/renderer/features/player/hooks/use-center-controls.ts b/src/renderer/features/player/hooks/use-center-controls.ts
index 04ae05429..f6744c9f3 100644
--- a/src/renderer/features/player/hooks/use-center-controls.ts
+++ b/src/renderer/features/player/hooks/use-center-controls.ts
@@ -1,12 +1,16 @@
-import { useCallback } from 'react';
+import { useCallback, useEffect } from 'react';
+import isElectron from 'is-electron';
import { PlaybackType, PlayerStatus } from '@/renderer/types';
import { usePlayerStore } from '../../../store';
-import { mpvPlayer } from '../utils/mpvPlayer';
+import { useSettingsStore } from '../../../store/settings.store';
+import { mpvPlayer } from '../utils/mpv-player';
+
+const ipc = isElectron() ? window.electron.ipcRenderer : null;
export const useCenterControls = (args: { playersRef: any }) => {
const { playersRef } = args;
- const settings = usePlayerStore((state) => state.settings);
+ const settings = useSettingsStore((state) => state.player);
const play = usePlayerStore((state) => state.play);
const pause = usePlayerStore((state) => state.pause);
const prev = usePlayerStore((state) => state.prev);
@@ -41,26 +45,28 @@ export const useCenterControls = (args: { playersRef: any }) => {
resetPlayers();
};
+ const isMpvPlayer = isElectron() && settings.type === PlaybackType.LOCAL;
+
const handlePlay = useCallback(() => {
- if (settings.type === PlaybackType.LOCAL) {
+ if (isMpvPlayer) {
mpvPlayer.play();
} else {
currentPlayerRef.getInternalPlayer().play();
}
play();
- }, [currentPlayerRef, play, settings]);
+ }, [currentPlayerRef, isMpvPlayer, play]);
const handlePause = useCallback(() => {
- if (settings.type === PlaybackType.LOCAL) {
+ if (isMpvPlayer) {
mpvPlayer.pause();
}
pause();
- }, [pause, settings]);
+ }, [isMpvPlayer, pause]);
const handleStop = () => {
- if (settings.type === PlaybackType.LOCAL) {
+ if (isMpvPlayer) {
mpvPlayer.stop();
} else {
stopPlayback();
@@ -73,7 +79,7 @@ export const useCenterControls = (args: { playersRef: any }) => {
const handleNextTrack = useCallback(() => {
const playerData = next();
- if (settings.type === PlaybackType.LOCAL) {
+ if (isMpvPlayer) {
mpvPlayer.setQueue(playerData);
mpvPlayer.next();
} else {
@@ -81,12 +87,12 @@ export const useCenterControls = (args: { playersRef: any }) => {
}
setCurrentTime(0);
- }, [next, resetPlayers, setCurrentTime, settings]);
+ }, [isMpvPlayer, next, resetPlayers, setCurrentTime]);
const handlePrevTrack = useCallback(() => {
const playerData = prev();
- if (settings.type === PlaybackType.LOCAL) {
+ if (isMpvPlayer) {
mpvPlayer.setQueue(playerData);
mpvPlayer.previous();
} else {
@@ -94,7 +100,7 @@ export const useCenterControls = (args: { playersRef: any }) => {
}
setCurrentTime(0);
- }, [prev, resetPlayers, setCurrentTime, settings]);
+ }, [isMpvPlayer, prev, resetPlayers, setCurrentTime]);
const handlePlayPause = useCallback(() => {
if (queue) {
@@ -108,30 +114,26 @@ export const useCenterControls = (args: { playersRef: any }) => {
return null;
}, [handlePause, handlePlay, playerStatus, queue]);
- const handleSkipBackward = () => {
- const skipBackwardSec = 5;
-
- if (settings.type === PlaybackType.LOCAL) {
- const newTime = currentTime - skipBackwardSec;
- mpvPlayer.seek(-skipBackwardSec);
+ const handleSkipBackward = (seconds: number) => {
+ if (isMpvPlayer) {
+ const newTime = currentTime - seconds;
+ mpvPlayer.seek(-seconds);
setCurrentTime(newTime < 0 ? 0 : newTime);
} else {
- const newTime = currentPlayerRef.getCurrentTime() - skipBackwardSec;
+ const newTime = currentPlayerRef?.getCurrentTime() - seconds;
resetNextPlayer();
setCurrentTime(newTime);
currentPlayerRef.seekTo(newTime);
}
};
- const handleSkipForward = () => {
- const skipForwardSec = 5;
-
- if (settings.type === PlaybackType.LOCAL) {
- const newTime = currentTime + skipForwardSec;
- mpvPlayer.seek(skipForwardSec);
+ const handleSkipForward = (seconds: number) => {
+ if (isMpvPlayer) {
+ const newTime = currentTime + seconds;
+ mpvPlayer.seek(seconds);
setCurrentTime(newTime);
} else {
- const checkNewTime = currentPlayerRef.getCurrentTime() + skipForwardSec;
+ const checkNewTime = currentPlayerRef?.getCurrentTime() + seconds;
const songDuration = currentPlayerRef.player.player.duration;
const newTime =
@@ -147,26 +149,67 @@ export const useCenterControls = (args: { playersRef: any }) => {
(e: number | any) => {
setCurrentTime(e);
- if (settings.type === PlaybackType.LOCAL) {
+ if (isMpvPlayer) {
mpvPlayer.seekTo(e);
} else {
currentPlayerRef.seekTo(e);
}
},
- [currentPlayerRef, setCurrentTime, settings]
+ [currentPlayerRef, isMpvPlayer, setCurrentTime]
);
- // const handleVolumeSlider = useCallback(
- // (e: number | any) => {
- // // dispatch(setVolume(e));
- // if (settings.type === PlaybackType.Local) {
- // // playerApi.volume(currentTime, e);
- // }
+ useEffect(() => {
+ ipc?.RENDERER_PLAYER_PLAY_PAUSE(() => {
+ const { status } = usePlayerStore.getState().current;
+ if (status === PlayerStatus.PAUSED) {
+ play();
- // setSettings({ volume: (e / 100) ** 2 });
- // },
- // [currentTime, setSettings, settings]
- // );
+ if (isMpvPlayer) {
+ mpvPlayer.play();
+ }
+ } else {
+ pause();
+ if (isMpvPlayer) {
+ mpvPlayer.pause();
+ }
+ }
+ });
+
+ ipc?.RENDERER_PLAYER_NEXT(() => {
+ const playerData = next();
+
+ if (isMpvPlayer) {
+ mpvPlayer.setQueue(playerData);
+ mpvPlayer.next();
+ }
+ });
+
+ ipc?.RENDERER_PLAYER_PREVIOUS(() => {
+ const playerData = prev();
+ if (isMpvPlayer) {
+ mpvPlayer.setQueue(playerData);
+ mpvPlayer.previous();
+ }
+ });
+
+ ipc?.RENDERER_PLAYER_PLAY(() => play());
+
+ ipc?.RENDERER_PLAYER_PAUSE(() => pause());
+
+ ipc?.RENDERER_PLAYER_STOP(() => pause());
+
+ ipc?.RENDERER_PLAYER_CURRENT_TIME((_event, time) => setCurrentTime(time));
+
+ return () => {
+ ipc?.removeAllListeners('renderer-player-play-pause');
+ ipc?.removeAllListeners('renderer-player-next');
+ ipc?.removeAllListeners('renderer-player-previous');
+ ipc?.removeAllListeners('renderer-player-play');
+ ipc?.removeAllListeners('renderer-player-pause');
+ ipc?.removeAllListeners('renderer-player-stop');
+ ipc?.removeAllListeners('renderer-player-current-time');
+ };
+ }, [isMpvPlayer, next, pause, play, prev, setCurrentTime]);
return {
handleNextTrack,
diff --git a/src/renderer/features/player/hooks/use-playqueue-handler.ts b/src/renderer/features/player/hooks/use-playqueue-handler.ts
index a559fe8d3..4ffe49c66 100644
--- a/src/renderer/features/player/hooks/use-playqueue-handler.ts
+++ b/src/renderer/features/player/hooks/use-playqueue-handler.ts
@@ -3,14 +3,22 @@ import { api } from '@/renderer/api';
import { queryKeys } from '@/renderer/api/query-keys';
import { useServerCredential } from '@/renderer/features/shared';
import { useAuthStore, usePlayerStore } from '@/renderer/store';
-import { LibraryItem, Play, PlayQueueAddOptions } from '@/renderer/types';
+import { useSettingsStore } from '@/renderer/store/settings.store';
+import {
+ LibraryItem,
+ Play,
+ PlaybackType,
+ PlayQueueAddOptions,
+} from '@/renderer/types';
import { mpvPlayer } from '../utils/mpv-player';
export const usePlayQueueHandler = () => {
const queryClient = useQueryClient();
const serverId = useAuthStore((state) => state.currentServer?.id) || '';
const { serverToken, isImageTokenRequired } = useServerCredential();
+ const play = usePlayerStore((state) => state.play);
const addToQueue = usePlayerStore((state) => state.addToQueue);
+ const playerType = useSettingsStore((state) => state.player.type);
const handlePlayQueueAdd = async (options: PlayQueueAddOptions) => {
if (options.byData) {
@@ -55,10 +63,18 @@ export const usePlayQueueHandler = () => {
const playerData = addToQueue(songs, options.play);
if (options.play === Play.NEXT || options.play === Play.LAST) {
- mpvPlayer.setQueueNext(playerData);
- } else {
- mpvPlayer.setQueue(playerData);
- mpvPlayer.play();
+ if (playerType === PlaybackType.LOCAL) {
+ mpvPlayer.setQueueNext(playerData);
+ }
+ }
+
+ if (options.play === Play.NOW) {
+ if (playerType === PlaybackType.LOCAL) {
+ mpvPlayer.setQueue(playerData);
+ mpvPlayer.play();
+ }
+
+ play();
}
}
};
diff --git a/src/renderer/features/player/hooks/use-right-controls.ts b/src/renderer/features/player/hooks/use-right-controls.ts
index 47c43fcdc..d20cac422 100644
--- a/src/renderer/features/player/hooks/use-right-controls.ts
+++ b/src/renderer/features/player/hooks/use-right-controls.ts
@@ -1,11 +1,12 @@
import { useEffect } from 'react';
import { usePlayerStore } from '../../../store';
-import { mpvPlayer } from '../utils/mpvPlayer';
+import { mpvPlayer } from '../utils/mpv-player';
export const useRightControls = () => {
- const setSettings = usePlayerStore((state) => state.setSettings);
- const volume = usePlayerStore((state) => state.settings.volume);
- const muted = usePlayerStore((state) => state.settings.muted);
+ const setVolume = usePlayerStore((state) => state.setVolume);
+ const volume = usePlayerStore((state) => state.volume);
+ const muted = usePlayerStore((state) => state.muted);
+ const setMuted = usePlayerStore((state) => state.setMuted);
// Ensure that the mpv player volume is set on startup
useEffect(() => {
@@ -19,14 +20,15 @@ export const useRightControls = () => {
const handleVolumeSlider = (e: number) => {
mpvPlayer.volume(e);
+ setVolume(e);
};
const handleVolumeSliderState = (e: number) => {
- setSettings({ volume: e });
+ setVolume(e);
};
const handleMute = () => {
- setSettings({ muted: !muted });
+ setMuted(!muted);
mpvPlayer.mute();
};
diff --git a/src/renderer/features/player/utils/mpvPlayer.ts b/src/renderer/features/player/utils/mpv-player.ts
similarity index 80%
rename from src/renderer/features/player/utils/mpvPlayer.ts
rename to src/renderer/features/player/utils/mpv-player.ts
index 89a8ea8c7..245eb9769 100644
--- a/src/renderer/features/player/utils/mpvPlayer.ts
+++ b/src/renderer/features/player/utils/mpv-player.ts
@@ -34,20 +34,7 @@ const volume = (value: number) => ipc?.PLAYER_VOLUME(value);
const mute = () => ipc?.PLAYER_MUTE();
-const {
- setCurrentTime,
- play: setPlay,
- pause: setPause,
- autoNext,
-} = usePlayerStore.getState();
-
-ipc?.RENDERER_PLAYER_PLAY(() => setPlay());
-
-ipc?.RENDERER_PLAYER_PAUSE(() => setPause());
-
-ipc?.RENDERER_PLAYER_STOP(() => setPause());
-
-ipc?.RENDERER_PLAYER_CURRENT_TIME((_event, time) => setCurrentTime(time));
+const { autoNext } = usePlayerStore.getState();
ipc?.RENDERER_PLAYER_AUTO_NEXT(() => {
const playerData = autoNext();
diff --git a/src/renderer/features/settings/components/general-tab.tsx b/src/renderer/features/settings/components/general-tab.tsx
new file mode 100644
index 000000000..9d43c2abe
--- /dev/null
+++ b/src/renderer/features/settings/components/general-tab.tsx
@@ -0,0 +1,37 @@
+import { Select, Stack } from '@mantine/core';
+import { SettingsOptions } from '@/renderer/features/settings/components/settings-option';
+
+export const GeneralTab = () => {
+ const options = [
+ {
+ control: ,
+ description: 'Primary application language ',
+ title: 'Language',
+ },
+ {
+ control: ,
+ description: 'Theme for the application',
+ title: 'Theme',
+ },
+ {
+ control: ,
+ description: 'Font for the application',
+ title: 'Font',
+ },
+ {
+ control: (
+
+ ),
+ description: 'Font for the application',
+ title: 'Titlebar style',
+ },
+ ];
+
+ return (
+
+ {options.map((option) => (
+
+ ))}
+
+ );
+};
diff --git a/src/renderer/features/settings/components/playback-tab.tsx b/src/renderer/features/settings/components/playback-tab.tsx
new file mode 100644
index 000000000..003358436
--- /dev/null
+++ b/src/renderer/features/settings/components/playback-tab.tsx
@@ -0,0 +1,284 @@
+import { useEffect, useState } from 'react';
+import { Divider, Group, SelectItem, Stack } from '@mantine/core';
+import isElectron from 'is-electron';
+import {
+ NumberInput,
+ SegmentedControl,
+ Select,
+ Slider,
+ Switch,
+ toast,
+ Tooltip,
+} from '@/renderer/components';
+import { mpvPlayer } from '@/renderer/features/player/utils/mpv-player';
+import { SettingsOptions } from '@/renderer/features/settings/components/settings-option';
+import { usePlayerStore } from '@/renderer/store';
+import { useSettingsStore } from '@/renderer/store/settings.store';
+import {
+ Play,
+ PlaybackStyle,
+ PlaybackType,
+ PlayerStatus,
+} from '@/renderer/types';
+
+const getAudioDevice = async () => {
+ const devices = await navigator.mediaDevices.enumerateDevices();
+ return (devices || []).filter(
+ (dev: MediaDeviceInfo) => dev.kind === 'audiooutput'
+ );
+};
+
+const ipc = isElectron() ? window.electron.ipcRenderer : null;
+
+const set = (property: string, value: any) => {
+ ipc?.SETTINGS_SET({ property, value });
+};
+
+export const PlaybackTab = () => {
+ const settings = useSettingsStore((state) => state.player);
+ const update = useSettingsStore((state) => state.setSettings);
+ const status = usePlayerStore((state) => state.current.status);
+ const [audioDevices, setAudioDevices] = useState([]);
+
+ useEffect(() => {
+ const getAudioDevices = () => {
+ getAudioDevice()
+ .then((dev) =>
+ setAudioDevices(
+ dev.map((d) => ({ label: d.label, value: d.deviceId }))
+ )
+ )
+ .catch(() => toast.error({ message: 'Error fetching audio devices' }));
+ };
+
+ getAudioDevices();
+ }, []);
+
+ const playerOptions = [
+ {
+ control: (
+ {
+ update({ player: { ...settings, type: e as PlaybackType } });
+ if (isElectron() && e === PlaybackType.LOCAL) {
+ const queueData = usePlayerStore.getState().getPlayerData();
+ mpvPlayer.setQueue(queueData);
+ }
+ }}
+ />
+ ),
+ description: 'The audio player to use for playback (desktop only)',
+ isHidden: !isElectron(),
+ note:
+ status === PlayerStatus.PLAYING ? 'Player must be paused' : undefined,
+ title: 'Audio player',
+ },
+ {
+ control: (
+