Refactor settings store and components
This commit is contained in:
parent
373441e4c6
commit
eecbcddea3
30 changed files with 894 additions and 832 deletions
|
@ -52,7 +52,7 @@ export const AudioPlayer = forwardRef(
|
||||||
const player1Ref = useRef<any>(null);
|
const player1Ref = useRef<any>(null);
|
||||||
const player2Ref = useRef<any>(null);
|
const player2Ref = useRef<any>(null);
|
||||||
const [isTransitioning, setIsTransitioning] = useState(false);
|
const [isTransitioning, setIsTransitioning] = useState(false);
|
||||||
const audioDeviceId = useSettingsStore((state) => state.player.audioDeviceId);
|
const audioDeviceId = useSettingsStore((state) => state.playback.audioDeviceId);
|
||||||
|
|
||||||
useImperativeHandle(ref, () => ({
|
useImperativeHandle(ref, () => ({
|
||||||
get player1() {
|
get player1() {
|
||||||
|
|
|
@ -7,7 +7,7 @@ import styled from 'styled-components';
|
||||||
import { _Button } from '/@/renderer/components/button';
|
import { _Button } from '/@/renderer/components/button';
|
||||||
import type { PlayQueueAddOptions } from '/@/renderer/types';
|
import type { PlayQueueAddOptions } from '/@/renderer/types';
|
||||||
import { Play } from '/@/renderer/types';
|
import { Play } from '/@/renderer/types';
|
||||||
import { useSettingsStore } from '/@/renderer/store/settings.store';
|
import { usePlayButtonBehavior } from '/@/renderer/store/settings.store';
|
||||||
import { LibraryItem } from '/@/renderer/api/types';
|
import { LibraryItem } from '/@/renderer/api/types';
|
||||||
import { useHandleGeneralContextMenu } from '/@/renderer/features/context-menu/hooks/use-handle-context-menu';
|
import { useHandleGeneralContextMenu } from '/@/renderer/features/context-menu/hooks/use-handle-context-menu';
|
||||||
import {
|
import {
|
||||||
|
@ -112,7 +112,7 @@ export const CardControls = ({
|
||||||
itemData: any;
|
itemData: any;
|
||||||
itemType: LibraryItem;
|
itemType: LibraryItem;
|
||||||
}) => {
|
}) => {
|
||||||
const playButtonBehavior = useSettingsStore((state) => state.player.playButtonBehavior);
|
const playButtonBehavior = usePlayButtonBehavior();
|
||||||
|
|
||||||
const handlePlay = (e: MouseEvent<HTMLButtonElement>, playType?: Play) => {
|
const handlePlay = (e: MouseEvent<HTMLButtonElement>, playType?: Play) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
|
import { useRef } from 'react';
|
||||||
import { Flex, FlexProps } from '@mantine/core';
|
import { Flex, FlexProps } from '@mantine/core';
|
||||||
import { AnimatePresence, motion, Variants } from 'framer-motion';
|
import { AnimatePresence, motion, Variants } from 'framer-motion';
|
||||||
import { useRef } from 'react';
|
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import { useShouldPadTitlebar, useTheme } from '/@/renderer/hooks';
|
import { useShouldPadTitlebar, useTheme } from '/@/renderer/hooks';
|
||||||
import { useGeneralSettings } from '/@/renderer/store/settings.store';
|
import { useWindowSettings } from '/@/renderer/store/settings.store';
|
||||||
import { Platform } from '/@/renderer/types';
|
import { Platform } from '/@/renderer/types';
|
||||||
|
|
||||||
const Container = styled(motion(Flex))<{
|
const Container = styled(motion(Flex))<{
|
||||||
|
@ -94,7 +94,7 @@ export const PageHeader = ({
|
||||||
}: PageHeaderProps) => {
|
}: PageHeaderProps) => {
|
||||||
const ref = useRef(null);
|
const ref = useRef(null);
|
||||||
const padRight = useShouldPadTitlebar();
|
const padRight = useShouldPadTitlebar();
|
||||||
const { windowBarStyle } = useGeneralSettings();
|
const { windowBarStyle } = useWindowSettings();
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -5,7 +5,7 @@ import { useMergedRef, useTimeout } from '@mantine/hooks';
|
||||||
import { motion, useScroll } from 'framer-motion';
|
import { motion, useScroll } from 'framer-motion';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import { PageHeader, PageHeaderProps } from '/@/renderer/components/page-header';
|
import { PageHeader, PageHeaderProps } from '/@/renderer/components/page-header';
|
||||||
import { useGeneralSettings } from '/@/renderer/store/settings.store';
|
import { useWindowSettings } from '/@/renderer/store/settings.store';
|
||||||
import { Platform } from '/@/renderer/types';
|
import { Platform } from '/@/renderer/types';
|
||||||
|
|
||||||
interface ScrollAreaProps extends MantineScrollAreaProps {
|
interface ScrollAreaProps extends MantineScrollAreaProps {
|
||||||
|
@ -78,7 +78,7 @@ export const NativeScrollArea = forwardRef(
|
||||||
}: NativeScrollAreaProps,
|
}: NativeScrollAreaProps,
|
||||||
ref: Ref<HTMLDivElement>,
|
ref: Ref<HTMLDivElement>,
|
||||||
) => {
|
) => {
|
||||||
const { windowBarStyle } = useGeneralSettings();
|
const { windowBarStyle } = useWindowSettings();
|
||||||
const [hideScrollbar, setHideScrollbar] = useState(false);
|
const [hideScrollbar, setHideScrollbar] = useState(false);
|
||||||
const [hideHeader, setHideHeader] = useState(true);
|
const [hideHeader, setHideHeader] = useState(true);
|
||||||
const { start, clear } = useTimeout(
|
const { start, clear } = useTimeout(
|
||||||
|
|
|
@ -6,7 +6,7 @@ import styled from 'styled-components';
|
||||||
import { _Button } from '/@/renderer/components/button';
|
import { _Button } from '/@/renderer/components/button';
|
||||||
import type { PlayQueueAddOptions } from '/@/renderer/types';
|
import type { PlayQueueAddOptions } from '/@/renderer/types';
|
||||||
import { Play } from '/@/renderer/types';
|
import { Play } from '/@/renderer/types';
|
||||||
import { useSettingsStore } from '/@/renderer/store/settings.store';
|
import { usePlayButtonBehavior } from '/@/renderer/store/settings.store';
|
||||||
import { LibraryItem } from '/@/renderer/api/types';
|
import { LibraryItem } from '/@/renderer/api/types';
|
||||||
import { useHandleGeneralContextMenu } from '/@/renderer/features/context-menu/hooks/use-handle-context-menu';
|
import { useHandleGeneralContextMenu } from '/@/renderer/features/context-menu/hooks/use-handle-context-menu';
|
||||||
import {
|
import {
|
||||||
|
@ -105,7 +105,7 @@ export const GridCardControls = ({
|
||||||
itemData: any;
|
itemData: any;
|
||||||
itemType: LibraryItem;
|
itemType: LibraryItem;
|
||||||
}) => {
|
}) => {
|
||||||
const playButtonBehavior = useSettingsStore((state) => state.player.playButtonBehavior);
|
const playButtonBehavior = usePlayButtonBehavior();
|
||||||
|
|
||||||
const handlePlay = async (e: MouseEvent<HTMLButtonElement>, playType?: Play) => {
|
const handlePlay = async (e: MouseEvent<HTMLButtonElement>, playType?: Play) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
@ -144,7 +144,6 @@ export const GridCardControls = ({
|
||||||
<BottomControls>
|
<BottomControls>
|
||||||
<SecondaryButton
|
<SecondaryButton
|
||||||
p={5}
|
p={5}
|
||||||
sx={{ svg: { fill: 'white !important' } }}
|
|
||||||
variant="subtle"
|
variant="subtle"
|
||||||
onClick={handleFavorites}
|
onClick={handleFavorites}
|
||||||
>
|
>
|
||||||
|
@ -161,7 +160,6 @@ export const GridCardControls = ({
|
||||||
</SecondaryButton>
|
</SecondaryButton>
|
||||||
<SecondaryButton
|
<SecondaryButton
|
||||||
p={5}
|
p={5}
|
||||||
sx={{ svg: { fill: 'white !important' } }}
|
|
||||||
variant="subtle"
|
variant="subtle"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
import { useEffect, useRef } from 'react';
|
import { useEffect, useRef } from 'react';
|
||||||
import { useInView } from 'framer-motion';
|
import { useInView } from 'framer-motion';
|
||||||
import { useGeneralSettings } from '/@/renderer/store/settings.store';
|
import { useWindowSettings } from '/@/renderer/store/settings.store';
|
||||||
import { Platform } from '/@/renderer/types';
|
import { Platform } from '/@/renderer/types';
|
||||||
|
|
||||||
export const useFixedTableHeader = () => {
|
export const useFixedTableHeader = () => {
|
||||||
const intersectRef = useRef<HTMLDivElement | null>(null);
|
const intersectRef = useRef<HTMLDivElement | null>(null);
|
||||||
const tableContainerRef = useRef<HTMLDivElement | null>(null);
|
const tableContainerRef = useRef<HTMLDivElement | null>(null);
|
||||||
const { windowBarStyle } = useGeneralSettings();
|
const { windowBarStyle } = useWindowSettings();
|
||||||
|
|
||||||
const isNotPastTableIntersection = useInView(intersectRef, {
|
const isNotPastTableIntersection = useInView(intersectRef, {
|
||||||
margin: windowBarStyle === Platform.WEB ? '-68px 0px 0px 0px' : '-98px 0px 0px 0px',
|
margin: windowBarStyle === Platform.WEB ? '-68px 0px 0px 0px' : '-98px 0px 0px 0px',
|
||||||
|
|
|
@ -5,12 +5,12 @@ import { PlayQueue } from '/@/renderer/features/now-playing/components/play-queu
|
||||||
import { PlayQueueListControls } from './play-queue-list-controls';
|
import { PlayQueueListControls } from './play-queue-list-controls';
|
||||||
import { Song } from '/@/renderer/api/types';
|
import { Song } from '/@/renderer/api/types';
|
||||||
import { PageHeader, Paper, VirtualGridContainer } from '/@/renderer/components';
|
import { PageHeader, Paper, VirtualGridContainer } from '/@/renderer/components';
|
||||||
import { useGeneralSettings } from '/@/renderer/store/settings.store';
|
import { useWindowSettings } from '/@/renderer/store/settings.store';
|
||||||
import { Platform } from '/@/renderer/types';
|
import { Platform } from '/@/renderer/types';
|
||||||
|
|
||||||
export const SidebarPlayQueue = () => {
|
export const SidebarPlayQueue = () => {
|
||||||
const queueRef = useRef<{ grid: AgGridReactType<Song> } | null>(null);
|
const queueRef = useRef<{ grid: AgGridReactType<Song> } | null>(null);
|
||||||
const { windowBarStyle } = useGeneralSettings();
|
const { windowBarStyle } = useWindowSettings();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<VirtualGridContainer>
|
<VirtualGridContainer>
|
||||||
|
|
|
@ -69,7 +69,7 @@ export const CenterControls = ({ playersRef }: CenterControlsProps) => {
|
||||||
const [isSeeking, setIsSeeking] = useState(false);
|
const [isSeeking, setIsSeeking] = useState(false);
|
||||||
const currentSong = useCurrentSong();
|
const currentSong = useCurrentSong();
|
||||||
const songDuration = currentSong?.duration;
|
const songDuration = currentSong?.duration;
|
||||||
const skip = useSettingsStore((state) => state.player.skipButtons);
|
const skip = useSettingsStore((state) => state.general.skipButtons);
|
||||||
const playerType = usePlayerType();
|
const playerType = usePlayerType();
|
||||||
const player1 = playersRef?.current?.player1;
|
const player1 = playersRef?.current?.player1;
|
||||||
const player2 = playersRef?.current?.player2;
|
const player2 = playersRef?.current?.player2;
|
||||||
|
|
|
@ -56,7 +56,7 @@ const mpris = isElectron() && utils?.isLinux() ? window.electron.mpris : null;
|
||||||
|
|
||||||
export const Playerbar = () => {
|
export const Playerbar = () => {
|
||||||
const playersRef = useRef<any>();
|
const playersRef = useRef<any>();
|
||||||
const settings = useSettingsStore((state) => state.player);
|
const settings = useSettingsStore((state) => state.playback);
|
||||||
const volume = useVolume();
|
const volume = useVolume();
|
||||||
const player1 = usePlayer1Data();
|
const player1 = usePlayer1Data();
|
||||||
const player2 = usePlayer2Data();
|
const player2 = usePlayer2Data();
|
||||||
|
|
|
@ -25,7 +25,7 @@ const mpris = isElectron() && utils?.isLinux() ? window.electron.mpris : null;
|
||||||
export const useCenterControls = (args: { playersRef: any }) => {
|
export const useCenterControls = (args: { playersRef: any }) => {
|
||||||
const { playersRef } = args;
|
const { playersRef } = args;
|
||||||
|
|
||||||
const settings = useSettingsStore((state) => state.player);
|
const settings = useSettingsStore((state) => state.playback);
|
||||||
const currentPlayer = useCurrentPlayer();
|
const currentPlayer = useCurrentPlayer();
|
||||||
const { setShuffle, setRepeat, play, pause, previous, next, setCurrentIndex, autoNext } =
|
const { setShuffle, setRepeat, play, pause, previous, next, setCurrentIndex, autoNext } =
|
||||||
usePlayerControls();
|
usePlayerControls();
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { useEffect, useCallback, useState, useRef } from 'react';
|
||||||
import { QueueSong, ServerType } from '/@/renderer/api/types';
|
import { QueueSong, ServerType } from '/@/renderer/api/types';
|
||||||
import { useSendScrobble } from '/@/renderer/features/player/mutations/scrobble-mutation';
|
import { useSendScrobble } from '/@/renderer/features/player/mutations/scrobble-mutation';
|
||||||
import { useCurrentStatus, usePlayerStore } from '/@/renderer/store';
|
import { useCurrentStatus, usePlayerStore } from '/@/renderer/store';
|
||||||
import { usePlayerSettings } from '/@/renderer/store/settings.store';
|
import { usePlaybackSettings } from '/@/renderer/store/settings.store';
|
||||||
import { PlayerStatus } from '/@/renderer/types';
|
import { PlayerStatus } from '/@/renderer/types';
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -49,7 +49,7 @@ const checkScrobbleConditions = (args: {
|
||||||
|
|
||||||
export const useScrobble = () => {
|
export const useScrobble = () => {
|
||||||
const status = useCurrentStatus();
|
const status = useCurrentStatus();
|
||||||
const scrobbleSettings = usePlayerSettings().scrobble;
|
const scrobbleSettings = usePlaybackSettings().scrobble;
|
||||||
const isScrobbleEnabled = scrobbleSettings?.enabled;
|
const isScrobbleEnabled = scrobbleSettings?.enabled;
|
||||||
const sendScrobble = useSendScrobble();
|
const sendScrobble = useSendScrobble();
|
||||||
|
|
||||||
|
|
|
@ -1,278 +0,0 @@
|
||||||
import { Divider, Stack } from '@mantine/core';
|
|
||||||
import { Select, Slider, Switch } from '/@/renderer/components';
|
|
||||||
import isElectron from 'is-electron';
|
|
||||||
import { SettingsOptions } from '/@/renderer/features/settings/components/settings-option';
|
|
||||||
import { THEME_DATA } from '/@/renderer/hooks';
|
|
||||||
import {
|
|
||||||
useGeneralSettings,
|
|
||||||
useSettingsStoreActions,
|
|
||||||
SideQueueType,
|
|
||||||
} from '/@/renderer/store/settings.store';
|
|
||||||
import { AppTheme } from '/@/renderer/themes/types';
|
|
||||||
import { Platform } from '/@/renderer/types';
|
|
||||||
|
|
||||||
const FONT_OPTIONS = [
|
|
||||||
{ label: 'Archivo', value: 'Archivo' },
|
|
||||||
{ label: 'Fredoka', value: 'Fredoka' },
|
|
||||||
{ label: 'League Spartan', value: 'League Spartan' },
|
|
||||||
{ label: 'Lexend', value: 'Lexend' },
|
|
||||||
{ label: 'Poppins', value: 'Poppins' },
|
|
||||||
{ label: 'Raleway', value: 'Raleway' },
|
|
||||||
{ label: 'Sora', value: 'Sora' },
|
|
||||||
{ label: 'Work Sans', value: 'Work Sans' },
|
|
||||||
];
|
|
||||||
|
|
||||||
const SIDE_QUEUE_OPTIONS = [
|
|
||||||
{ label: 'Fixed', value: 'sideQueue' },
|
|
||||||
{ label: 'Floating', value: 'sideDrawerQueue' },
|
|
||||||
];
|
|
||||||
|
|
||||||
const WINDOW_BAR_OPTIONS = [
|
|
||||||
{ label: 'Web (hidden)', value: Platform.WEB },
|
|
||||||
{ label: 'Windows', value: Platform.WINDOWS },
|
|
||||||
{ label: 'macOS', value: Platform.MACOS },
|
|
||||||
];
|
|
||||||
|
|
||||||
export const GeneralTab = () => {
|
|
||||||
const settings = useGeneralSettings();
|
|
||||||
const { setSettings } = useSettingsStoreActions();
|
|
||||||
|
|
||||||
const options = [
|
|
||||||
{
|
|
||||||
control: (
|
|
||||||
<Select
|
|
||||||
data={WINDOW_BAR_OPTIONS}
|
|
||||||
disabled={!isElectron()}
|
|
||||||
value={settings.windowBarStyle}
|
|
||||||
onChange={(e) => {
|
|
||||||
if (!e) return;
|
|
||||||
setSettings({
|
|
||||||
general: {
|
|
||||||
...settings,
|
|
||||||
windowBarStyle: e as Platform,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
description: 'Adjust the style of the application window bar',
|
|
||||||
isHidden: !isElectron(),
|
|
||||||
title: 'Window bar style',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
control: (
|
|
||||||
<Select
|
|
||||||
disabled
|
|
||||||
data={[]}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
description: 'Sets the application language',
|
|
||||||
isHidden: false,
|
|
||||||
title: 'Language',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
control: (
|
|
||||||
<Select
|
|
||||||
searchable
|
|
||||||
data={FONT_OPTIONS}
|
|
||||||
defaultValue={settings.fontContent}
|
|
||||||
onChange={(e) => {
|
|
||||||
if (!e) return;
|
|
||||||
setSettings({
|
|
||||||
general: {
|
|
||||||
...settings,
|
|
||||||
fontContent: e,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
description: 'Sets the application content font',
|
|
||||||
isHidden: false,
|
|
||||||
title: 'Font (Content)',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const themeOptions = [
|
|
||||||
{
|
|
||||||
control: (
|
|
||||||
<Switch
|
|
||||||
defaultChecked={settings.followSystemTheme}
|
|
||||||
onChange={(e) => {
|
|
||||||
setSettings({
|
|
||||||
general: {
|
|
||||||
...settings,
|
|
||||||
followSystemTheme: e.currentTarget.checked,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
description: 'Follows the system-defined light or dark preference',
|
|
||||||
isHidden: false,
|
|
||||||
title: 'Use system theme',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
control: (
|
|
||||||
<Select
|
|
||||||
data={THEME_DATA}
|
|
||||||
defaultValue={settings.theme}
|
|
||||||
onChange={(e) => {
|
|
||||||
setSettings({
|
|
||||||
general: {
|
|
||||||
...settings,
|
|
||||||
theme: e as AppTheme,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
description: 'Sets the default theme',
|
|
||||||
isHidden: settings.followSystemTheme,
|
|
||||||
title: 'Theme',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
control: (
|
|
||||||
<Select
|
|
||||||
data={THEME_DATA}
|
|
||||||
defaultValue={settings.themeDark}
|
|
||||||
onChange={(e) => {
|
|
||||||
setSettings({
|
|
||||||
general: {
|
|
||||||
...settings,
|
|
||||||
themeDark: e as AppTheme,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
description: 'Sets the dark theme',
|
|
||||||
isHidden: !settings.followSystemTheme,
|
|
||||||
title: 'Theme (dark)',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
control: (
|
|
||||||
<Select
|
|
||||||
data={THEME_DATA}
|
|
||||||
defaultValue={settings.themeLight}
|
|
||||||
onChange={(e) => {
|
|
||||||
setSettings({
|
|
||||||
general: {
|
|
||||||
...settings,
|
|
||||||
themeLight: e as AppTheme,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
description: 'Sets the light theme',
|
|
||||||
isHidden: !settings.followSystemTheme,
|
|
||||||
title: 'Theme (light)',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const layoutOptions = [
|
|
||||||
{
|
|
||||||
control: (
|
|
||||||
<Select
|
|
||||||
data={SIDE_QUEUE_OPTIONS}
|
|
||||||
defaultValue={settings.sideQueueType}
|
|
||||||
onChange={(e) => {
|
|
||||||
setSettings({
|
|
||||||
general: {
|
|
||||||
...settings,
|
|
||||||
sideQueueType: e as SideQueueType,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
description: 'The style of the sidebar play queue',
|
|
||||||
isHidden: false,
|
|
||||||
title: 'Side play queue style',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
control: (
|
|
||||||
<Switch
|
|
||||||
defaultChecked={settings.showQueueDrawerButton}
|
|
||||||
onChange={(e) => {
|
|
||||||
setSettings({
|
|
||||||
general: {
|
|
||||||
...settings,
|
|
||||||
showQueueDrawerButton: e.currentTarget.checked,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
description: 'Display a hover icon on the right side of the application view the play queue',
|
|
||||||
isHidden: false,
|
|
||||||
title: 'Show floating queue hover area',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const miscOptions = [
|
|
||||||
{
|
|
||||||
control: (
|
|
||||||
<Slider
|
|
||||||
defaultValue={settings.volumeWheelStep}
|
|
||||||
max={20}
|
|
||||||
min={1}
|
|
||||||
w={100}
|
|
||||||
onChangeEnd={(e) => {
|
|
||||||
setSettings({
|
|
||||||
general: {
|
|
||||||
...settings,
|
|
||||||
volumeWheelStep: e,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
description:
|
|
||||||
'The amount of volume to change when scrolling the mouse wheel on the volume slider',
|
|
||||||
isHidden: false,
|
|
||||||
title: 'Volume wheel step',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Stack spacing="md">
|
|
||||||
{options
|
|
||||||
.filter((o) => !o.isHidden)
|
|
||||||
.map((option) => (
|
|
||||||
<SettingsOptions
|
|
||||||
key={`general-${option.title}`}
|
|
||||||
{...option}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
<Divider />
|
|
||||||
{themeOptions
|
|
||||||
.filter((o) => !o.isHidden)
|
|
||||||
.map((option) => (
|
|
||||||
<SettingsOptions
|
|
||||||
key={`general-${option.title}`}
|
|
||||||
{...option}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
<Divider />
|
|
||||||
{layoutOptions
|
|
||||||
.filter((o) => !o.isHidden)
|
|
||||||
.map((option) => (
|
|
||||||
<SettingsOptions
|
|
||||||
key={`general-${option.title}`}
|
|
||||||
{...option}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
<Divider />
|
|
||||||
{miscOptions
|
|
||||||
.filter((o) => !o.isHidden)
|
|
||||||
.map((option) => (
|
|
||||||
<SettingsOptions
|
|
||||||
key={`general-${option.title}`}
|
|
||||||
{...option}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</Stack>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -0,0 +1,59 @@
|
||||||
|
import { Select } from '/@/renderer/components';
|
||||||
|
import {
|
||||||
|
SettingsSection,
|
||||||
|
SettingOption,
|
||||||
|
} from '/@/renderer/features/settings/components/settings-section';
|
||||||
|
import { useGeneralSettings, useSettingsStoreActions } from '/@/renderer/store/settings.store';
|
||||||
|
|
||||||
|
const FONT_OPTIONS = [
|
||||||
|
{ label: 'Archivo', value: 'Archivo' },
|
||||||
|
{ label: 'Fredoka', value: 'Fredoka' },
|
||||||
|
{ label: 'League Spartan', value: 'League Spartan' },
|
||||||
|
{ label: 'Lexend', value: 'Lexend' },
|
||||||
|
{ label: 'Poppins', value: 'Poppins' },
|
||||||
|
{ label: 'Raleway', value: 'Raleway' },
|
||||||
|
{ label: 'Sora', value: 'Sora' },
|
||||||
|
{ label: 'Work Sans', value: 'Work Sans' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const ApplicationSettings = () => {
|
||||||
|
const settings = useGeneralSettings();
|
||||||
|
const { setSettings } = useSettingsStoreActions();
|
||||||
|
|
||||||
|
const options: SettingOption[] = [
|
||||||
|
{
|
||||||
|
control: (
|
||||||
|
<Select
|
||||||
|
disabled
|
||||||
|
data={[]}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
description: 'Sets the application language',
|
||||||
|
isHidden: false,
|
||||||
|
title: 'Language',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
control: (
|
||||||
|
<Select
|
||||||
|
searchable
|
||||||
|
data={FONT_OPTIONS}
|
||||||
|
defaultValue={settings.fontContent}
|
||||||
|
onChange={(e) => {
|
||||||
|
if (!e) return;
|
||||||
|
setSettings({
|
||||||
|
general: {
|
||||||
|
...settings,
|
||||||
|
fontContent: e,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
description: 'Sets the application content font',
|
||||||
|
isHidden: false,
|
||||||
|
title: 'Font (Content)',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return <SettingsSection options={options} />;
|
||||||
|
};
|
|
@ -0,0 +1,176 @@
|
||||||
|
import { Group } from '@mantine/core';
|
||||||
|
import { Select, Tooltip, NumberInput, Switch, Slider } from '/@/renderer/components';
|
||||||
|
import { SettingsSection } from '/@/renderer/features/settings/components/settings-section';
|
||||||
|
import {
|
||||||
|
SideQueueType,
|
||||||
|
useGeneralSettings,
|
||||||
|
useSettingsStoreActions,
|
||||||
|
} from '/@/renderer/store/settings.store';
|
||||||
|
import { Play } from '/@/renderer/types';
|
||||||
|
|
||||||
|
const SIDE_QUEUE_OPTIONS = [
|
||||||
|
{ label: 'Fixed', value: 'sideQueue' },
|
||||||
|
{ label: 'Floating', value: 'sideDrawerQueue' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const ControlSettings = () => {
|
||||||
|
const settings = useGeneralSettings();
|
||||||
|
const { setSettings } = useSettingsStoreActions();
|
||||||
|
|
||||||
|
const controlOptions = [
|
||||||
|
{
|
||||||
|
control: (
|
||||||
|
<Switch
|
||||||
|
aria-label="Toggle skip buttons"
|
||||||
|
defaultChecked={settings.skipButtons?.enabled}
|
||||||
|
onChange={(e) =>
|
||||||
|
setSettings({
|
||||||
|
general: {
|
||||||
|
...settings,
|
||||||
|
skipButtons: {
|
||||||
|
...settings.skipButtons,
|
||||||
|
enabled: e.currentTarget.checked,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
description: 'Show or hide the skip buttons on the playerbar',
|
||||||
|
isHidden: false,
|
||||||
|
title: 'Show skip buttons',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
control: (
|
||||||
|
<Group>
|
||||||
|
<Tooltip label="Backward">
|
||||||
|
<NumberInput
|
||||||
|
defaultValue={settings.skipButtons.skipBackwardSeconds}
|
||||||
|
min={0}
|
||||||
|
width={75}
|
||||||
|
onBlur={(e) =>
|
||||||
|
setSettings({
|
||||||
|
general: {
|
||||||
|
...settings,
|
||||||
|
skipButtons: {
|
||||||
|
...settings.skipButtons,
|
||||||
|
skipBackwardSeconds: e.currentTarget.value
|
||||||
|
? Number(e.currentTarget.value)
|
||||||
|
: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip label="Forward">
|
||||||
|
<NumberInput
|
||||||
|
defaultValue={settings.skipButtons.skipForwardSeconds}
|
||||||
|
min={0}
|
||||||
|
width={75}
|
||||||
|
onBlur={(e) =>
|
||||||
|
setSettings({
|
||||||
|
general: {
|
||||||
|
...settings,
|
||||||
|
skipButtons: {
|
||||||
|
...settings.skipButtons,
|
||||||
|
skipForwardSeconds: e.currentTarget.value ? Number(e.currentTarget.value) : 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</Group>
|
||||||
|
),
|
||||||
|
description:
|
||||||
|
'The number (in seconds) to skip forward or backward when using the skip buttons',
|
||||||
|
isHidden: false,
|
||||||
|
title: 'Skip duration',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
control: (
|
||||||
|
<Select
|
||||||
|
data={[
|
||||||
|
{ label: 'Now', value: Play.NOW },
|
||||||
|
{ label: 'Next', value: Play.NEXT },
|
||||||
|
{ label: 'Last', value: Play.LAST },
|
||||||
|
]}
|
||||||
|
defaultValue={settings.playButtonBehavior}
|
||||||
|
onChange={(e) =>
|
||||||
|
setSettings({
|
||||||
|
general: {
|
||||||
|
...settings,
|
||||||
|
playButtonBehavior: e as Play,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
description: 'The default behavior of the play button when adding songs to the queue',
|
||||||
|
isHidden: false,
|
||||||
|
title: 'Play button behavior',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
control: (
|
||||||
|
<Select
|
||||||
|
data={SIDE_QUEUE_OPTIONS}
|
||||||
|
defaultValue={settings.sideQueueType}
|
||||||
|
onChange={(e) => {
|
||||||
|
setSettings({
|
||||||
|
general: {
|
||||||
|
...settings,
|
||||||
|
sideQueueType: e as SideQueueType,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
description: 'The style of the sidebar play queue',
|
||||||
|
isHidden: false,
|
||||||
|
title: 'Side play queue style',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
control: (
|
||||||
|
<Switch
|
||||||
|
defaultChecked={settings.showQueueDrawerButton}
|
||||||
|
onChange={(e) => {
|
||||||
|
setSettings({
|
||||||
|
general: {
|
||||||
|
...settings,
|
||||||
|
showQueueDrawerButton: e.currentTarget.checked,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
description: 'Display a hover icon on the right side of the application view the play queue',
|
||||||
|
isHidden: false,
|
||||||
|
title: 'Show floating queue hover area',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
control: (
|
||||||
|
<Slider
|
||||||
|
defaultValue={settings.volumeWheelStep}
|
||||||
|
max={20}
|
||||||
|
min={1}
|
||||||
|
w={100}
|
||||||
|
onChangeEnd={(e) => {
|
||||||
|
setSettings({
|
||||||
|
general: {
|
||||||
|
...settings,
|
||||||
|
volumeWheelStep: e,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
description:
|
||||||
|
'The amount of volume to change when scrolling the mouse wheel on the volume slider',
|
||||||
|
isHidden: false,
|
||||||
|
title: 'Volume wheel step',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return <SettingsSection options={controlOptions} />;
|
||||||
|
};
|
|
@ -0,0 +1,16 @@
|
||||||
|
import { Divider, Stack } from '@mantine/core';
|
||||||
|
import { ApplicationSettings } from '/@/renderer/features/settings/components/general/application-settings';
|
||||||
|
import { ControlSettings } from '/@/renderer/features/settings/components/general/control-settings';
|
||||||
|
import { ThemeSettings } from '/@/renderer/features/settings/components/general/theme-settings';
|
||||||
|
|
||||||
|
export const GeneralTab = () => {
|
||||||
|
return (
|
||||||
|
<Stack spacing="md">
|
||||||
|
<ApplicationSettings />
|
||||||
|
<Divider />
|
||||||
|
<ThemeSettings />
|
||||||
|
<Divider />
|
||||||
|
<ControlSettings />
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,93 @@
|
||||||
|
import { Switch, Select } from '/@/renderer/components';
|
||||||
|
import {
|
||||||
|
SettingsSection,
|
||||||
|
SettingOption,
|
||||||
|
} from '/@/renderer/features/settings/components/settings-section';
|
||||||
|
import { THEME_DATA } from '/@/renderer/hooks';
|
||||||
|
import { useGeneralSettings, useSettingsStoreActions } from '/@/renderer/store/settings.store';
|
||||||
|
import { AppTheme } from '/@/renderer/themes/types';
|
||||||
|
|
||||||
|
export const ThemeSettings = () => {
|
||||||
|
const settings = useGeneralSettings();
|
||||||
|
const { setSettings } = useSettingsStoreActions();
|
||||||
|
|
||||||
|
const themeOptions: SettingOption[] = [
|
||||||
|
{
|
||||||
|
control: (
|
||||||
|
<Switch
|
||||||
|
defaultChecked={settings.followSystemTheme}
|
||||||
|
onChange={(e) => {
|
||||||
|
setSettings({
|
||||||
|
general: {
|
||||||
|
...settings,
|
||||||
|
followSystemTheme: e.currentTarget.checked,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
description: 'Follows the system-defined light or dark preference',
|
||||||
|
isHidden: false,
|
||||||
|
title: 'Use system theme',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
control: (
|
||||||
|
<Select
|
||||||
|
data={THEME_DATA}
|
||||||
|
defaultValue={settings.theme}
|
||||||
|
onChange={(e) => {
|
||||||
|
setSettings({
|
||||||
|
general: {
|
||||||
|
...settings,
|
||||||
|
theme: e as AppTheme,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
description: 'Sets the default theme',
|
||||||
|
isHidden: settings.followSystemTheme,
|
||||||
|
title: 'Theme',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
control: (
|
||||||
|
<Select
|
||||||
|
data={THEME_DATA}
|
||||||
|
defaultValue={settings.themeDark}
|
||||||
|
onChange={(e) => {
|
||||||
|
setSettings({
|
||||||
|
general: {
|
||||||
|
...settings,
|
||||||
|
themeDark: e as AppTheme,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
description: 'Sets the dark theme',
|
||||||
|
isHidden: !settings.followSystemTheme,
|
||||||
|
title: 'Theme (dark)',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
control: (
|
||||||
|
<Select
|
||||||
|
data={THEME_DATA}
|
||||||
|
defaultValue={settings.themeLight}
|
||||||
|
onChange={(e) => {
|
||||||
|
setSettings({
|
||||||
|
general: {
|
||||||
|
...settings,
|
||||||
|
themeLight: e as AppTheme,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
description: 'Sets the light theme',
|
||||||
|
isHidden: !settings.followSystemTheme,
|
||||||
|
title: 'Theme (light)',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return <SettingsSection options={themeOptions} />;
|
||||||
|
};
|
|
@ -1,478 +1,13 @@
|
||||||
import { useEffect, useState } from 'react';
|
import { Divider, Stack } from '@mantine/core';
|
||||||
import { Divider, Group, SelectItem, Stack } from '@mantine/core';
|
import { AudioSettings } from '/@/renderer/features/settings/components/playback/audio-settings';
|
||||||
import {
|
import { ScrobbleSettings } from '/@/renderer/features/settings/components/playback/scrobble-settings';
|
||||||
FileInput,
|
|
||||||
NumberInput,
|
|
||||||
Select,
|
|
||||||
Slider,
|
|
||||||
Switch,
|
|
||||||
Textarea,
|
|
||||||
Tooltip,
|
|
||||||
toast,
|
|
||||||
Text,
|
|
||||||
} from '/@/renderer/components';
|
|
||||||
import isElectron from 'is-electron';
|
|
||||||
import { SettingsOptions } from '/@/renderer/features/settings/components/settings-option';
|
|
||||||
import { useCurrentStatus, usePlayerStore } from '/@/renderer/store';
|
|
||||||
import { useSettingsStore, useSettingsStoreActions } from '/@/renderer/store/settings.store';
|
|
||||||
import { PlaybackType, PlayerStatus, PlaybackStyle, CrossfadeStyle, Play } from '/@/renderer/types';
|
|
||||||
|
|
||||||
const localSettings = isElectron() ? window.electron.localSettings : null;
|
|
||||||
const mpvPlayer = isElectron() ? window.electron.mpvPlayer : null;
|
|
||||||
|
|
||||||
const getAudioDevice = async () => {
|
|
||||||
const devices = await navigator.mediaDevices.enumerateDevices();
|
|
||||||
return (devices || []).filter((dev: MediaDeviceInfo) => dev.kind === 'audiooutput');
|
|
||||||
};
|
|
||||||
|
|
||||||
export const PlaybackTab = () => {
|
export const PlaybackTab = () => {
|
||||||
const settings = useSettingsStore((state) => state.player);
|
|
||||||
const { setSettings } = useSettingsStoreActions();
|
|
||||||
const status = useCurrentStatus();
|
|
||||||
const [audioDevices, setAudioDevices] = useState<SelectItem[]>([]);
|
|
||||||
const [mpvPath, setMpvPath] = useState('');
|
|
||||||
const [mpvParameters, setMpvParameters] = useState('');
|
|
||||||
|
|
||||||
const handleSetMpvPath = (e: File) => {
|
|
||||||
localSettings.set('mpv_path', e.path);
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const getMpvPath = async () => {
|
|
||||||
if (!isElectron()) return setMpvPath('');
|
|
||||||
const mpvPath = (await localSettings.get('mpv_path')) as string;
|
|
||||||
return setMpvPath(mpvPath);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getMpvParameters = async () => {
|
|
||||||
if (!isElectron()) return setMpvPath('');
|
|
||||||
const mpvParametersFromSettings = (await localSettings.get('mpv_parameters')) as string[];
|
|
||||||
const mpvParameters = mpvParametersFromSettings?.join('\n');
|
|
||||||
return setMpvParameters(mpvParameters);
|
|
||||||
};
|
|
||||||
|
|
||||||
getMpvPath();
|
|
||||||
getMpvParameters();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
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: (
|
|
||||||
<Select
|
|
||||||
data={[
|
|
||||||
{
|
|
||||||
disabled: !isElectron(),
|
|
||||||
label: 'MPV',
|
|
||||||
value: PlaybackType.LOCAL,
|
|
||||||
},
|
|
||||||
{ label: 'Web', value: PlaybackType.WEB },
|
|
||||||
]}
|
|
||||||
defaultValue={settings.type}
|
|
||||||
disabled={status === PlayerStatus.PLAYING}
|
|
||||||
onChange={(e) => {
|
|
||||||
setSettings({ player: { ...settings, type: e as PlaybackType } });
|
|
||||||
if (isElectron() && e === PlaybackType.LOCAL) {
|
|
||||||
const queueData = usePlayerStore.getState().actions.getPlayerData();
|
|
||||||
mpvPlayer.setQueue(queueData);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
description: 'The audio player to use for playback',
|
|
||||||
isHidden: !isElectron(),
|
|
||||||
note: status === PlayerStatus.PLAYING ? 'Player must be paused' : undefined,
|
|
||||||
title: 'Audio player',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
control: (
|
|
||||||
<FileInput
|
|
||||||
placeholder={mpvPath}
|
|
||||||
width={225}
|
|
||||||
onChange={handleSetMpvPath}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
description: 'The location of your mpv executable',
|
|
||||||
isHidden: settings.type !== PlaybackType.LOCAL,
|
|
||||||
note: 'Restart required',
|
|
||||||
title: 'MPV executable path',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
control: (
|
|
||||||
<Stack spacing="xs">
|
|
||||||
<Textarea
|
|
||||||
autosize
|
|
||||||
defaultValue={mpvParameters}
|
|
||||||
minRows={4}
|
|
||||||
placeholder={'(Add one per line):\n--gapless-audio=weak\n--prefetch-playlist=yes'}
|
|
||||||
width={225}
|
|
||||||
onBlur={(e) => {
|
|
||||||
if (isElectron()) {
|
|
||||||
localSettings.set('mpv_parameters', e.currentTarget.value.split('\n'));
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Stack>
|
|
||||||
),
|
|
||||||
description: (
|
|
||||||
<Stack spacing={0}>
|
|
||||||
<Text
|
|
||||||
$noSelect
|
|
||||||
$secondary
|
|
||||||
>
|
|
||||||
Options to pass to the player
|
|
||||||
</Text>
|
|
||||||
<Text>
|
|
||||||
<a
|
|
||||||
href="https://mpv.io/manual/stable/#audio"
|
|
||||||
rel="noreferrer"
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
https://mpv.io/manual/stable/#audio
|
|
||||||
</a>
|
|
||||||
</Text>
|
|
||||||
</Stack>
|
|
||||||
),
|
|
||||||
isHidden: settings.type !== PlaybackType.LOCAL,
|
|
||||||
note: 'Restart required.',
|
|
||||||
title: 'MPV parameters',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
control: (
|
|
||||||
<Select
|
|
||||||
clearable
|
|
||||||
data={audioDevices}
|
|
||||||
defaultValue={settings.audioDeviceId}
|
|
||||||
disabled={settings.type !== PlaybackType.WEB}
|
|
||||||
onChange={(e) => setSettings({ player: { ...settings, audioDeviceId: e } })}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
description: 'The audio device to use for playback (web player only)',
|
|
||||||
isHidden: !isElectron() || settings.type !== PlaybackType.WEB,
|
|
||||||
title: 'Audio device',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
control: (
|
|
||||||
<Select
|
|
||||||
data={[
|
|
||||||
{ label: 'Normal', value: PlaybackStyle.GAPLESS },
|
|
||||||
{ label: 'Crossfade', value: PlaybackStyle.CROSSFADE },
|
|
||||||
]}
|
|
||||||
defaultValue={settings.style}
|
|
||||||
disabled={settings.type !== PlaybackType.WEB || status === PlayerStatus.PLAYING}
|
|
||||||
onChange={(e) => setSettings({ player: { ...settings, style: e as PlaybackStyle } })}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
description: 'Adjust the playback style (web player only)',
|
|
||||||
isHidden: settings.type !== PlaybackType.WEB,
|
|
||||||
note: status === PlayerStatus.PLAYING ? 'Player must be paused' : undefined,
|
|
||||||
title: 'Playback style',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
control: (
|
|
||||||
<Slider
|
|
||||||
defaultValue={settings.crossfadeDuration}
|
|
||||||
disabled={
|
|
||||||
settings.type !== PlaybackType.WEB ||
|
|
||||||
settings.style !== PlaybackStyle.CROSSFADE ||
|
|
||||||
status === PlayerStatus.PLAYING
|
|
||||||
}
|
|
||||||
max={15}
|
|
||||||
min={0}
|
|
||||||
w={100}
|
|
||||||
onChangeEnd={(e) => setSettings({ player: { ...settings, crossfadeDuration: e } })}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
description: 'Adjust the crossfade duration (web player only)',
|
|
||||||
isHidden: settings.type !== PlaybackType.WEB,
|
|
||||||
note: status === PlayerStatus.PLAYING ? 'Player must be paused' : undefined,
|
|
||||||
title: 'Crossfade Duration',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
control: (
|
|
||||||
<Select
|
|
||||||
data={[
|
|
||||||
{ label: 'Linear', value: CrossfadeStyle.LINEAR },
|
|
||||||
{ label: 'Constant Power', value: CrossfadeStyle.CONSTANT_POWER },
|
|
||||||
{
|
|
||||||
label: 'Constant Power (Slow cut)',
|
|
||||||
value: CrossfadeStyle.CONSTANT_POWER_SLOW_CUT,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Constant Power (Slow fade)',
|
|
||||||
value: CrossfadeStyle.CONSTANT_POWER_SLOW_FADE,
|
|
||||||
},
|
|
||||||
{ label: 'Dipped', value: CrossfadeStyle.DIPPED },
|
|
||||||
{ label: 'Equal Power', value: CrossfadeStyle.EQUALPOWER },
|
|
||||||
]}
|
|
||||||
defaultValue={settings.crossfadeStyle}
|
|
||||||
disabled={
|
|
||||||
settings.type !== PlaybackType.WEB ||
|
|
||||||
settings.style !== PlaybackStyle.CROSSFADE ||
|
|
||||||
status === PlayerStatus.PLAYING
|
|
||||||
}
|
|
||||||
width={200}
|
|
||||||
onChange={(e) => {
|
|
||||||
if (!e) return;
|
|
||||||
setSettings({
|
|
||||||
player: { ...settings, crossfadeStyle: e as CrossfadeStyle },
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
description: 'Change the crossfade algorithm (web player only)',
|
|
||||||
isHidden: settings.type !== PlaybackType.WEB,
|
|
||||||
note: status === PlayerStatus.PLAYING ? 'Player must be paused' : undefined,
|
|
||||||
title: 'Crossfade Style',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
control: (
|
|
||||||
<Switch
|
|
||||||
aria-label="Toggle global media hotkeys"
|
|
||||||
defaultChecked={settings.globalMediaHotkeys}
|
|
||||||
disabled={!isElectron()}
|
|
||||||
onChange={(e) => {
|
|
||||||
setSettings({
|
|
||||||
player: {
|
|
||||||
...settings,
|
|
||||||
globalMediaHotkeys: e.currentTarget.checked,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
localSettings.set('global_media_hotkeys', e.currentTarget.checked);
|
|
||||||
|
|
||||||
if (e.currentTarget.checked) {
|
|
||||||
localSettings.enableMediaKeys();
|
|
||||||
} else {
|
|
||||||
localSettings.disableMediaKeys();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
description:
|
|
||||||
'Enable or disable the usage of your system media hotkeys to control the audio player (desktop only)',
|
|
||||||
isHidden: !isElectron(),
|
|
||||||
title: 'Global media hotkeys',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const scrobbleOptions = [
|
|
||||||
{
|
|
||||||
control: (
|
|
||||||
<Switch
|
|
||||||
aria-label="Toggle scrobble"
|
|
||||||
defaultChecked={settings.scrobble.enabled}
|
|
||||||
onChange={(e) => {
|
|
||||||
setSettings({
|
|
||||||
player: {
|
|
||||||
...settings,
|
|
||||||
scrobble: {
|
|
||||||
...settings.scrobble,
|
|
||||||
enabled: e.currentTarget.checked,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
description: 'Enable or disable scrobbling to your media server',
|
|
||||||
isHidden: !isElectron(),
|
|
||||||
title: 'Scrobble',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
control: (
|
|
||||||
<Slider
|
|
||||||
aria-label="Scrobble percentage"
|
|
||||||
defaultValue={settings.scrobble.scrobbleAtPercentage}
|
|
||||||
label={`${settings.scrobble.scrobbleAtPercentage}%`}
|
|
||||||
max={90}
|
|
||||||
min={25}
|
|
||||||
w={100}
|
|
||||||
onChange={(e) => {
|
|
||||||
setSettings({
|
|
||||||
player: {
|
|
||||||
...settings,
|
|
||||||
scrobble: {
|
|
||||||
...settings.scrobble,
|
|
||||||
scrobbleAtPercentage: e,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
description: 'The percentage of the song that must be played before submitting a scrobble',
|
|
||||||
isHidden: !isElectron(),
|
|
||||||
title: 'Minimum scrobble percentage*',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
control: (
|
|
||||||
<NumberInput
|
|
||||||
aria-label="Scrobble duration in seconds"
|
|
||||||
defaultValue={settings.scrobble.scrobbleAtDuration}
|
|
||||||
max={1200}
|
|
||||||
min={0}
|
|
||||||
width={75}
|
|
||||||
onChange={(e) => {
|
|
||||||
if (e === '') return;
|
|
||||||
setSettings({
|
|
||||||
player: {
|
|
||||||
...settings,
|
|
||||||
scrobble: {
|
|
||||||
...settings.scrobble,
|
|
||||||
scrobbleAtDuration: e,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
description:
|
|
||||||
'The duration in seconds of a song that must be played before submitting a scrobble',
|
|
||||||
isHidden: !isElectron(),
|
|
||||||
title: 'Minimum scrobble duration (seconds)*',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const otherOptions = [
|
|
||||||
{
|
|
||||||
control: (
|
|
||||||
<Select
|
|
||||||
data={[
|
|
||||||
{ label: 'Now', value: Play.NOW },
|
|
||||||
{ label: 'Next', value: Play.NEXT },
|
|
||||||
{ label: 'Last', value: Play.LAST },
|
|
||||||
]}
|
|
||||||
defaultValue={settings.playButtonBehavior}
|
|
||||||
onChange={(e) =>
|
|
||||||
setSettings({
|
|
||||||
player: {
|
|
||||||
...settings,
|
|
||||||
playButtonBehavior: e as Play,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
description: 'The default behavior of the play button when adding songs to the queue',
|
|
||||||
isHidden: false,
|
|
||||||
title: 'Play button behavior',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
control: (
|
|
||||||
<Group>
|
|
||||||
<Tooltip label="Backward">
|
|
||||||
<NumberInput
|
|
||||||
defaultValue={settings.skipButtons.skipBackwardSeconds}
|
|
||||||
min={0}
|
|
||||||
width={75}
|
|
||||||
onBlur={(e) =>
|
|
||||||
setSettings({
|
|
||||||
player: {
|
|
||||||
...settings,
|
|
||||||
skipButtons: {
|
|
||||||
...settings.skipButtons,
|
|
||||||
skipBackwardSeconds: e.currentTarget.value
|
|
||||||
? Number(e.currentTarget.value)
|
|
||||||
: 0,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
<Tooltip label="Forward">
|
|
||||||
<NumberInput
|
|
||||||
defaultValue={settings.skipButtons.skipForwardSeconds}
|
|
||||||
min={0}
|
|
||||||
width={75}
|
|
||||||
onBlur={(e) =>
|
|
||||||
setSettings({
|
|
||||||
player: {
|
|
||||||
...settings,
|
|
||||||
skipButtons: {
|
|
||||||
...settings.skipButtons,
|
|
||||||
skipForwardSeconds: e.currentTarget.value ? Number(e.currentTarget.value) : 0,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
</Group>
|
|
||||||
),
|
|
||||||
description:
|
|
||||||
'The number (in seconds) to skip forward or backward when using the skip buttons',
|
|
||||||
isHidden: false,
|
|
||||||
title: 'Skip duration',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
control: (
|
|
||||||
<Switch
|
|
||||||
aria-label="Toggle skip buttons"
|
|
||||||
defaultChecked={settings.skipButtons?.enabled}
|
|
||||||
onChange={(e) =>
|
|
||||||
setSettings({
|
|
||||||
player: {
|
|
||||||
...settings,
|
|
||||||
skipButtons: {
|
|
||||||
...settings.skipButtons,
|
|
||||||
enabled: e.currentTarget.checked,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
description: 'Show or hide the skip buttons on the playerbar',
|
|
||||||
isHidden: false,
|
|
||||||
title: 'Show skip buttons',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack spacing="md">
|
<Stack spacing="md">
|
||||||
{playerOptions
|
<AudioSettings />
|
||||||
.filter((o) => !o.isHidden)
|
|
||||||
.map((option) => (
|
|
||||||
<SettingsOptions
|
|
||||||
key={`playback-${option.title}`}
|
|
||||||
{...option}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
<Divider />
|
<Divider />
|
||||||
|
<ScrobbleSettings />
|
||||||
{scrobbleOptions
|
|
||||||
.filter((o) => !o.isHidden)
|
|
||||||
.map((option) => (
|
|
||||||
<SettingsOptions
|
|
||||||
key={`'scrobble-${option.title}`}
|
|
||||||
{...option}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
<Text
|
|
||||||
$secondary
|
|
||||||
size="sm"
|
|
||||||
>
|
|
||||||
*The scrobble will be submitted if one or more of the above conditions is met
|
|
||||||
</Text>
|
|
||||||
<Divider />
|
|
||||||
{otherOptions
|
|
||||||
.filter((o) => !o.isHidden)
|
|
||||||
.map((option) => (
|
|
||||||
<SettingsOptions
|
|
||||||
key={`playerbar-${option.title}`}
|
|
||||||
{...option}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -0,0 +1,262 @@
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { SelectItem, Stack } from '@mantine/core';
|
||||||
|
import isElectron from 'is-electron';
|
||||||
|
import { Select, FileInput, Slider, Switch, Textarea, Text, toast } from '/@/renderer/components';
|
||||||
|
import {
|
||||||
|
SettingsSection,
|
||||||
|
SettingOption,
|
||||||
|
} from '/@/renderer/features/settings/components/settings-section';
|
||||||
|
import { useCurrentStatus, usePlayerStore } from '/@/renderer/store';
|
||||||
|
import { usePlaybackSettings, useSettingsStoreActions } from '/@/renderer/store/settings.store';
|
||||||
|
import { PlaybackType, PlayerStatus, PlaybackStyle, CrossfadeStyle } from '/@/renderer/types';
|
||||||
|
|
||||||
|
const localSettings = isElectron() ? window.electron.localSettings : null;
|
||||||
|
const mpvPlayer = isElectron() ? window.electron.mpvPlayer : null;
|
||||||
|
|
||||||
|
const getAudioDevice = async () => {
|
||||||
|
const devices = await navigator.mediaDevices.enumerateDevices();
|
||||||
|
return (devices || []).filter((dev: MediaDeviceInfo) => dev.kind === 'audiooutput');
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AudioSettings = () => {
|
||||||
|
const settings = usePlaybackSettings();
|
||||||
|
const { setSettings } = useSettingsStoreActions();
|
||||||
|
const status = useCurrentStatus();
|
||||||
|
|
||||||
|
const [audioDevices, setAudioDevices] = useState<SelectItem[]>([]);
|
||||||
|
const [mpvPath, setMpvPath] = useState('');
|
||||||
|
const [mpvParameters, setMpvParameters] = useState('');
|
||||||
|
|
||||||
|
const handleSetMpvPath = (e: File) => {
|
||||||
|
localSettings.set('mpv_path', e.path);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const getMpvPath = async () => {
|
||||||
|
if (!isElectron()) return setMpvPath('');
|
||||||
|
const mpvPath = (await localSettings.get('mpv_path')) as string;
|
||||||
|
return setMpvPath(mpvPath);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getMpvParameters = async () => {
|
||||||
|
if (!isElectron()) return setMpvPath('');
|
||||||
|
const mpvParametersFromSettings = (await localSettings.get('mpv_parameters')) as string[];
|
||||||
|
const mpvParameters = mpvParametersFromSettings?.join('\n');
|
||||||
|
return setMpvParameters(mpvParameters);
|
||||||
|
};
|
||||||
|
|
||||||
|
getMpvPath();
|
||||||
|
getMpvParameters();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
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 audioOptions: SettingOption[] = [
|
||||||
|
{
|
||||||
|
control: (
|
||||||
|
<Select
|
||||||
|
data={[
|
||||||
|
{
|
||||||
|
disabled: !isElectron(),
|
||||||
|
label: 'MPV',
|
||||||
|
value: PlaybackType.LOCAL,
|
||||||
|
},
|
||||||
|
{ label: 'Web', value: PlaybackType.WEB },
|
||||||
|
]}
|
||||||
|
defaultValue={settings.type}
|
||||||
|
disabled={status === PlayerStatus.PLAYING}
|
||||||
|
onChange={(e) => {
|
||||||
|
setSettings({ playback: { ...settings, type: e as PlaybackType } });
|
||||||
|
if (isElectron() && e === PlaybackType.LOCAL) {
|
||||||
|
const queueData = usePlayerStore.getState().actions.getPlayerData();
|
||||||
|
mpvPlayer.setQueue(queueData);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
description: 'The audio player to use for playback',
|
||||||
|
isHidden: !isElectron(),
|
||||||
|
note: status === PlayerStatus.PLAYING ? 'Player must be paused' : undefined,
|
||||||
|
title: 'Audio player',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
control: (
|
||||||
|
<FileInput
|
||||||
|
placeholder={mpvPath}
|
||||||
|
width={225}
|
||||||
|
onChange={handleSetMpvPath}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
description: 'The location of your mpv executable',
|
||||||
|
isHidden: settings.type !== PlaybackType.LOCAL,
|
||||||
|
note: 'Restart required',
|
||||||
|
title: 'MPV executable path',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
control: (
|
||||||
|
<Stack spacing="xs">
|
||||||
|
<Textarea
|
||||||
|
autosize
|
||||||
|
defaultValue={mpvParameters}
|
||||||
|
minRows={4}
|
||||||
|
placeholder={'(Add one per line):\n--gapless-audio=weak\n--prefetch-playlist=yes'}
|
||||||
|
width={225}
|
||||||
|
onBlur={(e) => {
|
||||||
|
if (isElectron()) {
|
||||||
|
localSettings.set('mpv_parameters', e.currentTarget.value.split('\n'));
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
),
|
||||||
|
description: (
|
||||||
|
<Stack spacing={0}>
|
||||||
|
<Text
|
||||||
|
$noSelect
|
||||||
|
$secondary
|
||||||
|
>
|
||||||
|
Options to pass to the player
|
||||||
|
</Text>
|
||||||
|
<Text>
|
||||||
|
<a
|
||||||
|
href="https://mpv.io/manual/stable/#audio"
|
||||||
|
rel="noreferrer"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
https://mpv.io/manual/stable/#audio
|
||||||
|
</a>
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
),
|
||||||
|
isHidden: settings.type !== PlaybackType.LOCAL,
|
||||||
|
note: 'Restart required.',
|
||||||
|
title: 'MPV parameters',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
control: (
|
||||||
|
<Select
|
||||||
|
clearable
|
||||||
|
data={audioDevices}
|
||||||
|
defaultValue={settings.audioDeviceId}
|
||||||
|
disabled={settings.type !== PlaybackType.WEB}
|
||||||
|
onChange={(e) => setSettings({ playback: { ...settings, audioDeviceId: e } })}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
description: 'The audio device to use for playback (web player only)',
|
||||||
|
isHidden: !isElectron() || settings.type !== PlaybackType.WEB,
|
||||||
|
title: 'Audio device',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
control: (
|
||||||
|
<Select
|
||||||
|
data={[
|
||||||
|
{ label: 'Normal', value: PlaybackStyle.GAPLESS },
|
||||||
|
{ label: 'Crossfade', value: PlaybackStyle.CROSSFADE },
|
||||||
|
]}
|
||||||
|
defaultValue={settings.style}
|
||||||
|
disabled={settings.type !== PlaybackType.WEB || status === PlayerStatus.PLAYING}
|
||||||
|
onChange={(e) => setSettings({ playback: { ...settings, style: e as PlaybackStyle } })}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
description: 'Adjust the playback style (web player only)',
|
||||||
|
isHidden: settings.type !== PlaybackType.WEB,
|
||||||
|
note: status === PlayerStatus.PLAYING ? 'Player must be paused' : undefined,
|
||||||
|
title: 'Playback style',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
control: (
|
||||||
|
<Slider
|
||||||
|
defaultValue={settings.crossfadeDuration}
|
||||||
|
disabled={
|
||||||
|
settings.type !== PlaybackType.WEB ||
|
||||||
|
settings.style !== PlaybackStyle.CROSSFADE ||
|
||||||
|
status === PlayerStatus.PLAYING
|
||||||
|
}
|
||||||
|
max={15}
|
||||||
|
min={0}
|
||||||
|
w={100}
|
||||||
|
onChangeEnd={(e) => setSettings({ playback: { ...settings, crossfadeDuration: e } })}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
description: 'Adjust the crossfade duration (web player only)',
|
||||||
|
isHidden: settings.type !== PlaybackType.WEB,
|
||||||
|
note: status === PlayerStatus.PLAYING ? 'Player must be paused' : undefined,
|
||||||
|
title: 'Crossfade Duration',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
control: (
|
||||||
|
<Select
|
||||||
|
data={[
|
||||||
|
{ label: 'Linear', value: CrossfadeStyle.LINEAR },
|
||||||
|
{ label: 'Constant Power', value: CrossfadeStyle.CONSTANT_POWER },
|
||||||
|
{
|
||||||
|
label: 'Constant Power (Slow cut)',
|
||||||
|
value: CrossfadeStyle.CONSTANT_POWER_SLOW_CUT,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Constant Power (Slow fade)',
|
||||||
|
value: CrossfadeStyle.CONSTANT_POWER_SLOW_FADE,
|
||||||
|
},
|
||||||
|
{ label: 'Dipped', value: CrossfadeStyle.DIPPED },
|
||||||
|
{ label: 'Equal Power', value: CrossfadeStyle.EQUALPOWER },
|
||||||
|
]}
|
||||||
|
defaultValue={settings.crossfadeStyle}
|
||||||
|
disabled={
|
||||||
|
settings.type !== PlaybackType.WEB ||
|
||||||
|
settings.style !== PlaybackStyle.CROSSFADE ||
|
||||||
|
status === PlayerStatus.PLAYING
|
||||||
|
}
|
||||||
|
width={200}
|
||||||
|
onChange={(e) => {
|
||||||
|
if (!e) return;
|
||||||
|
setSettings({
|
||||||
|
playback: { ...settings, crossfadeStyle: e as CrossfadeStyle },
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
description: 'Change the crossfade algorithm (web player only)',
|
||||||
|
isHidden: settings.type !== PlaybackType.WEB,
|
||||||
|
note: status === PlayerStatus.PLAYING ? 'Player must be paused' : undefined,
|
||||||
|
title: 'Crossfade Style',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
control: (
|
||||||
|
<Switch
|
||||||
|
aria-label="Toggle global media hotkeys"
|
||||||
|
defaultChecked={settings.globalMediaHotkeys}
|
||||||
|
disabled={!isElectron()}
|
||||||
|
onChange={(e) => {
|
||||||
|
setSettings({
|
||||||
|
playback: {
|
||||||
|
...settings,
|
||||||
|
globalMediaHotkeys: e.currentTarget.checked,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
localSettings.set('global_media_hotkeys', e.currentTarget.checked);
|
||||||
|
|
||||||
|
if (e.currentTarget.checked) {
|
||||||
|
localSettings.enableMediaKeys();
|
||||||
|
} else {
|
||||||
|
localSettings.disableMediaKeys();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
description:
|
||||||
|
'Enable or disable the usage of your system media hotkeys to control the audio player (desktop only)',
|
||||||
|
isHidden: !isElectron(),
|
||||||
|
title: 'Global media hotkeys',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return <SettingsSection options={audioOptions} />;
|
||||||
|
};
|
|
@ -0,0 +1,99 @@
|
||||||
|
import isElectron from 'is-electron';
|
||||||
|
import { NumberInput, Slider, Switch, Text } from '/@/renderer/components';
|
||||||
|
import { usePlaybackSettings, useSettingsStoreActions } from '/@/renderer/store/settings.store';
|
||||||
|
import { SettingOption, SettingsSection } from '../settings-section';
|
||||||
|
|
||||||
|
export const ScrobbleSettings = () => {
|
||||||
|
const settings = usePlaybackSettings();
|
||||||
|
const { setSettings } = useSettingsStoreActions();
|
||||||
|
|
||||||
|
const scrobbleOptions: SettingOption[] = [
|
||||||
|
{
|
||||||
|
control: (
|
||||||
|
<Switch
|
||||||
|
aria-label="Toggle scrobble"
|
||||||
|
defaultChecked={settings.scrobble.enabled}
|
||||||
|
onChange={(e) => {
|
||||||
|
setSettings({
|
||||||
|
playback: {
|
||||||
|
...settings,
|
||||||
|
scrobble: {
|
||||||
|
...settings.scrobble,
|
||||||
|
enabled: e.currentTarget.checked,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
description: 'Enable or disable scrobbling to your media server',
|
||||||
|
isHidden: !isElectron(),
|
||||||
|
title: 'Scrobble',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
control: (
|
||||||
|
<Slider
|
||||||
|
aria-label="Scrobble percentage"
|
||||||
|
defaultValue={settings.scrobble.scrobbleAtPercentage}
|
||||||
|
label={`${settings.scrobble.scrobbleAtPercentage}%`}
|
||||||
|
max={90}
|
||||||
|
min={25}
|
||||||
|
w={100}
|
||||||
|
onChange={(e) => {
|
||||||
|
setSettings({
|
||||||
|
playback: {
|
||||||
|
...settings,
|
||||||
|
scrobble: {
|
||||||
|
...settings.scrobble,
|
||||||
|
scrobbleAtPercentage: e,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
description: 'The percentage of the song that must be played before submitting a scrobble',
|
||||||
|
isHidden: !isElectron(),
|
||||||
|
title: 'Minimum scrobble percentage*',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
control: (
|
||||||
|
<NumberInput
|
||||||
|
aria-label="Scrobble duration in seconds"
|
||||||
|
defaultValue={settings.scrobble.scrobbleAtDuration}
|
||||||
|
max={1200}
|
||||||
|
min={0}
|
||||||
|
width={75}
|
||||||
|
onChange={(e) => {
|
||||||
|
if (e === '') return;
|
||||||
|
setSettings({
|
||||||
|
playback: {
|
||||||
|
...settings,
|
||||||
|
scrobble: {
|
||||||
|
...settings.scrobble,
|
||||||
|
scrobbleAtDuration: e,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
description:
|
||||||
|
'The duration in seconds of a song that must be played before submitting a scrobble',
|
||||||
|
isHidden: !isElectron(),
|
||||||
|
title: 'Minimum scrobble duration (seconds)*',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<SettingsSection options={scrobbleOptions} />
|
||||||
|
<Text
|
||||||
|
$secondary
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
*The scrobble will be submitted if one or more of the above conditions is met
|
||||||
|
</Text>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
|
@ -1,10 +1,10 @@
|
||||||
import { lazy } from 'react';
|
import { lazy } from 'react';
|
||||||
import { Box } from '@mantine/core';
|
|
||||||
import { Tabs } from '/@/renderer/components';
|
import { Tabs } from '/@/renderer/components';
|
||||||
import { useSettingsStore, useSettingsStoreActions } from '/@/renderer/store/settings.store';
|
import { useSettingsStore, useSettingsStoreActions } from '/@/renderer/store/settings.store';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
|
||||||
const GeneralTab = lazy(() =>
|
const GeneralTab = lazy(() =>
|
||||||
import('/@/renderer/features/settings/components/general-tab').then((module) => ({
|
import('/@/renderer/features/settings/components/general/general-tab').then((module) => ({
|
||||||
default: module.GeneralTab,
|
default: module.GeneralTab,
|
||||||
})),
|
})),
|
||||||
);
|
);
|
||||||
|
@ -15,16 +15,25 @@ const PlaybackTab = lazy(() =>
|
||||||
})),
|
})),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const ApplicationTab = lazy(() =>
|
||||||
|
import('/@/renderer/features/settings/components/window/window-tab').then((module) => ({
|
||||||
|
default: module.WindowTab,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
|
||||||
|
const TabContainer = styled.div`
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
padding: 1rem;
|
||||||
|
overflow: scroll;
|
||||||
|
`;
|
||||||
|
|
||||||
export const SettingsContent = () => {
|
export const SettingsContent = () => {
|
||||||
const currentTab = useSettingsStore((state) => state.tab);
|
const currentTab = useSettingsStore((state) => state.tab);
|
||||||
const { setSettings } = useSettingsStoreActions();
|
const { setSettings } = useSettingsStoreActions();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<TabContainer>
|
||||||
h="100%"
|
|
||||||
p="1rem"
|
|
||||||
sx={{ overflow: 'scroll' }}
|
|
||||||
>
|
|
||||||
<Tabs
|
<Tabs
|
||||||
keepMounted={false}
|
keepMounted={false}
|
||||||
orientation="horizontal"
|
orientation="horizontal"
|
||||||
|
@ -35,6 +44,7 @@ export const SettingsContent = () => {
|
||||||
<Tabs.List>
|
<Tabs.List>
|
||||||
<Tabs.Tab value="general">General</Tabs.Tab>
|
<Tabs.Tab value="general">General</Tabs.Tab>
|
||||||
<Tabs.Tab value="playback">Playback</Tabs.Tab>
|
<Tabs.Tab value="playback">Playback</Tabs.Tab>
|
||||||
|
<Tabs.Tab value="window">Window</Tabs.Tab>
|
||||||
</Tabs.List>
|
</Tabs.List>
|
||||||
<Tabs.Panel value="general">
|
<Tabs.Panel value="general">
|
||||||
<GeneralTab />
|
<GeneralTab />
|
||||||
|
@ -42,7 +52,10 @@ export const SettingsContent = () => {
|
||||||
<Tabs.Panel value="playback">
|
<Tabs.Panel value="playback">
|
||||||
<PlaybackTab />
|
<PlaybackTab />
|
||||||
</Tabs.Panel>
|
</Tabs.Panel>
|
||||||
|
<Tabs.Panel value="window">
|
||||||
|
<ApplicationTab />
|
||||||
|
</Tabs.Panel>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</Box>
|
</TabContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -0,0 +1,28 @@
|
||||||
|
import { SettingsOptions } from '/@/renderer/features/settings/components/settings-option';
|
||||||
|
|
||||||
|
export type SettingOption = {
|
||||||
|
control: JSX.Element;
|
||||||
|
description: string | JSX.Element;
|
||||||
|
isHidden?: boolean;
|
||||||
|
note?: string;
|
||||||
|
title: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface SettingsSectionProps {
|
||||||
|
options: SettingOption[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SettingsSection = ({ options }: SettingsSectionProps) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{options
|
||||||
|
.filter((o) => !o.isHidden)
|
||||||
|
.map((option) => (
|
||||||
|
<SettingsOptions
|
||||||
|
key={`general-${option.title}`}
|
||||||
|
{...option}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,45 @@
|
||||||
|
import isElectron from 'is-electron';
|
||||||
|
import { Platform } from '/@/renderer/types';
|
||||||
|
import { useWindowSettings, useSettingsStoreActions } from '../../../../store/settings.store';
|
||||||
|
import {
|
||||||
|
SettingsSection,
|
||||||
|
SettingOption,
|
||||||
|
} from '/@/renderer/features/settings/components/settings-section';
|
||||||
|
import { Select } from '/@/renderer/components';
|
||||||
|
|
||||||
|
const WINDOW_BAR_OPTIONS = [
|
||||||
|
{ label: 'Web (hidden)', value: Platform.WEB },
|
||||||
|
{ label: 'Windows', value: Platform.WINDOWS },
|
||||||
|
{ label: 'macOS', value: Platform.MACOS },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const WindowSettings = () => {
|
||||||
|
const settings = useWindowSettings();
|
||||||
|
const { setSettings } = useSettingsStoreActions();
|
||||||
|
|
||||||
|
const windowOptions: SettingOption[] = [
|
||||||
|
{
|
||||||
|
control: (
|
||||||
|
<Select
|
||||||
|
data={WINDOW_BAR_OPTIONS}
|
||||||
|
disabled={!isElectron()}
|
||||||
|
value={settings.windowBarStyle}
|
||||||
|
onChange={(e) => {
|
||||||
|
if (!e) return;
|
||||||
|
setSettings({
|
||||||
|
window: {
|
||||||
|
...settings,
|
||||||
|
windowBarStyle: e as Platform,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
description: 'Adjust the style of the application window bar',
|
||||||
|
isHidden: !isElectron(),
|
||||||
|
title: 'Window bar style',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return <SettingsSection options={windowOptions} />;
|
||||||
|
};
|
|
@ -0,0 +1,10 @@
|
||||||
|
import { Stack } from '@mantine/core';
|
||||||
|
import { WindowSettings } from './window-settings';
|
||||||
|
|
||||||
|
export const WindowTab = () => {
|
||||||
|
return (
|
||||||
|
<Stack spacing="md">
|
||||||
|
<WindowSettings />
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
|
@ -1 +0,0 @@
|
||||||
export * from './components/settings';
|
|
|
@ -40,7 +40,7 @@ import { SidebarPlaylistList } from '/@/renderer/features/sidebar/components/sid
|
||||||
import { useContainerQuery } from '/@/renderer/hooks';
|
import { useContainerQuery } from '/@/renderer/hooks';
|
||||||
import { ActionBar } from '/@/renderer/features/sidebar/components/action-bar';
|
import { ActionBar } from '/@/renderer/features/sidebar/components/action-bar';
|
||||||
import { Platform } from '/@/renderer/types';
|
import { Platform } from '/@/renderer/types';
|
||||||
import { useGeneralSettings } from '../../../store/settings.store';
|
import { useWindowSettings } from '../../../store/settings.store';
|
||||||
|
|
||||||
const SidebarContainer = styled.div<{ windowBarStyle: Platform }>`
|
const SidebarContainer = styled.div<{ windowBarStyle: Platform }>`
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
@ -78,7 +78,7 @@ export const Sidebar = () => {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const sidebar = useSidebarStore();
|
const sidebar = useSidebarStore();
|
||||||
const { setSideBar } = useAppStoreActions();
|
const { setSideBar } = useAppStoreActions();
|
||||||
const { windowBarStyle } = useGeneralSettings();
|
const { windowBarStyle } = useWindowSettings();
|
||||||
const imageUrl = useCurrentSong()?.imageUrl;
|
const imageUrl = useCurrentSong()?.imageUrl;
|
||||||
const server = useCurrentServer();
|
const server = useCurrentServer();
|
||||||
|
|
||||||
|
|
|
@ -1,15 +1,15 @@
|
||||||
import { lazy } from 'react';
|
import { lazy } from 'react';
|
||||||
import isElectron from 'is-electron';
|
import isElectron from 'is-electron';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import { useGeneralSettings, useSettingsStore } from '/@/renderer/store/settings.store';
|
import { useWindowSettings, useSettingsStore } from '/@/renderer/store/settings.store';
|
||||||
import { Platform, PlaybackType } from '/@/renderer/types';
|
import { Platform, PlaybackType } from '/@/renderer/types';
|
||||||
import { MainContent } from '/@/renderer/layouts/default-layout/main-content';
|
import { MainContent } from '/@/renderer/layouts/default-layout/main-content';
|
||||||
import { PlayerBar } from '/@/renderer/layouts/default-layout/player-bar';
|
import { PlayerBar } from '/@/renderer/layouts/default-layout/player-bar';
|
||||||
|
|
||||||
if (!isElectron()) {
|
if (!isElectron()) {
|
||||||
useSettingsStore.getState().actions.setSettings({
|
useSettingsStore.getState().actions.setSettings({
|
||||||
player: {
|
playback: {
|
||||||
...useSettingsStore.getState().player,
|
...useSettingsStore.getState().playback,
|
||||||
type: PlaybackType.WEB,
|
type: PlaybackType.WEB,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -41,7 +41,7 @@ interface DefaultLayoutProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DefaultLayout = ({ shell }: DefaultLayoutProps) => {
|
export const DefaultLayout = ({ shell }: DefaultLayoutProps) => {
|
||||||
const { windowBarStyle } = useGeneralSettings();
|
const { windowBarStyle } = useWindowSettings();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
|
@ -10,7 +10,7 @@ import { FullScreenPlayer } from '/@/renderer/features/player/components/full-sc
|
||||||
import { Sidebar } from '/@/renderer/features/sidebar/components/sidebar';
|
import { Sidebar } from '/@/renderer/features/sidebar/components/sidebar';
|
||||||
import { AppRoute } from '/@/renderer/router/routes';
|
import { AppRoute } from '/@/renderer/router/routes';
|
||||||
import { useAppStore, useAppStoreActions, useFullScreenPlayerStore } from '/@/renderer/store';
|
import { useAppStore, useAppStoreActions, useFullScreenPlayerStore } from '/@/renderer/store';
|
||||||
import { useGeneralSettings } from '/@/renderer/store/settings.store';
|
import { useWindowSettings, useGeneralSettings } from '/@/renderer/store/settings.store';
|
||||||
import { Platform } from '/@/renderer/types';
|
import { Platform } from '/@/renderer/types';
|
||||||
import { constrainSidebarWidth, constrainRightSidebarWidth } from '/@/renderer/utils';
|
import { constrainSidebarWidth, constrainRightSidebarWidth } from '/@/renderer/utils';
|
||||||
|
|
||||||
|
@ -154,7 +154,7 @@ export const MainContent = ({ shell }: { shell?: boolean }) => {
|
||||||
const [drawer, drawerHandler] = useDisclosure(false);
|
const [drawer, drawerHandler] = useDisclosure(false);
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const { sideQueueType, showQueueDrawerButton } = useGeneralSettings();
|
const { sideQueueType, showQueueDrawerButton } = useGeneralSettings();
|
||||||
const { windowBarStyle } = useGeneralSettings();
|
const { windowBarStyle } = useWindowSettings();
|
||||||
|
|
||||||
const sidebarRef = useRef<HTMLDivElement | null>(null);
|
const sidebarRef = useRef<HTMLDivElement | null>(null);
|
||||||
const rightSidebarRef = useRef<HTMLDivElement | null>(null);
|
const rightSidebarRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
|
|
@ -3,7 +3,7 @@ import isElectron from 'is-electron';
|
||||||
import { RiCheckboxBlankLine, RiCloseLine, RiSubtractLine } from 'react-icons/ri';
|
import { RiCheckboxBlankLine, RiCloseLine, RiSubtractLine } from 'react-icons/ri';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import { useCurrentStatus, useQueueStatus } from '/@/renderer/store';
|
import { useCurrentStatus, useQueueStatus } from '/@/renderer/store';
|
||||||
import { useGeneralSettings } from '/@/renderer/store/settings.store';
|
import { useWindowSettings } from '/@/renderer/store/settings.store';
|
||||||
import { Platform, PlayerStatus } from '/@/renderer/types';
|
import { Platform, PlayerStatus } from '/@/renderer/types';
|
||||||
import appIcon from '../../../assets/icon.svg';
|
import appIcon from '../../../assets/icon.svg';
|
||||||
import macCloseHover from './assets/close-mac-hover.png';
|
import macCloseHover from './assets/close-mac-hover.png';
|
||||||
|
@ -212,7 +212,7 @@ const MacOsControls = ({ controls, title }: WindowBarControlsProps) => {
|
||||||
export const WindowBar = () => {
|
export const WindowBar = () => {
|
||||||
const playerStatus = useCurrentStatus();
|
const playerStatus = useCurrentStatus();
|
||||||
const { currentSong, index, length } = useQueueStatus();
|
const { currentSong, index, length } = useQueueStatus();
|
||||||
const { windowBarStyle } = useGeneralSettings();
|
const { windowBarStyle } = useWindowSettings();
|
||||||
|
|
||||||
const statusString = playerStatus === PlayerStatus.PAUSED ? '(Paused) ' : '';
|
const statusString = playerStatus === PlayerStatus.PAUSED ? '(Paused) ' : '';
|
||||||
const queueString = length ? `(${index + 1} / ${length}) ` : '';
|
const queueString = length ? `(${index + 1} / ${length}) ` : '';
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { Outlet } from 'react-router';
|
import { Outlet } from 'react-router';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import { Titlebar } from '/@/renderer/features/titlebar/components/titlebar';
|
import { Titlebar } from '/@/renderer/features/titlebar/components/titlebar';
|
||||||
import { useGeneralSettings } from '/@/renderer/store/settings.store';
|
import { useWindowSettings } from '/@/renderer/store/settings.store';
|
||||||
import { Platform } from '/@/renderer/types';
|
import { Platform } from '/@/renderer/types';
|
||||||
|
|
||||||
const TitlebarContainer = styled.header`
|
const TitlebarContainer = styled.header`
|
||||||
|
@ -15,7 +15,7 @@ const TitlebarContainer = styled.header`
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const TitlebarOutlet = () => {
|
export const TitlebarOutlet = () => {
|
||||||
const { windowBarStyle } = useGeneralSettings();
|
const { windowBarStyle } = useWindowSettings();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
|
@ -34,35 +34,35 @@ export interface SettingsState {
|
||||||
general: {
|
general: {
|
||||||
followSystemTheme: boolean;
|
followSystemTheme: boolean;
|
||||||
fontContent: string;
|
fontContent: string;
|
||||||
|
playButtonBehavior: Play;
|
||||||
showQueueDrawerButton: boolean;
|
showQueueDrawerButton: boolean;
|
||||||
sideQueueType: SideQueueType;
|
sideQueueType: SideQueueType;
|
||||||
theme: AppTheme;
|
|
||||||
themeDark: AppTheme;
|
|
||||||
themeLight: AppTheme;
|
|
||||||
volumeWheelStep: number;
|
|
||||||
windowBarStyle: Platform;
|
|
||||||
};
|
|
||||||
player: {
|
|
||||||
audioDeviceId?: string | null;
|
|
||||||
crossfadeDuration: number;
|
|
||||||
crossfadeStyle: CrossfadeStyle;
|
|
||||||
globalMediaHotkeys: boolean;
|
|
||||||
muted: boolean;
|
|
||||||
playButtonBehavior: Play;
|
|
||||||
scrobble: {
|
|
||||||
enabled: boolean;
|
|
||||||
scrobbleAtDuration: number;
|
|
||||||
scrobbleAtPercentage: number;
|
|
||||||
};
|
|
||||||
skipButtons: {
|
skipButtons: {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
skipBackwardSeconds: number;
|
skipBackwardSeconds: number;
|
||||||
skipForwardSeconds: number;
|
skipForwardSeconds: number;
|
||||||
};
|
};
|
||||||
|
theme: AppTheme;
|
||||||
|
themeDark: AppTheme;
|
||||||
|
themeLight: AppTheme;
|
||||||
|
volumeWheelStep: number;
|
||||||
|
};
|
||||||
|
playback: {
|
||||||
|
audioDeviceId?: string | null;
|
||||||
|
crossfadeDuration: number;
|
||||||
|
crossfadeStyle: CrossfadeStyle;
|
||||||
|
globalMediaHotkeys: boolean;
|
||||||
|
muted: boolean;
|
||||||
|
scrobble: {
|
||||||
|
enabled: boolean;
|
||||||
|
scrobbleAtDuration: number;
|
||||||
|
scrobbleAtPercentage: number;
|
||||||
|
};
|
||||||
|
|
||||||
style: PlaybackStyle;
|
style: PlaybackStyle;
|
||||||
type: PlaybackType;
|
type: PlaybackType;
|
||||||
};
|
};
|
||||||
tab: 'general' | 'playback' | 'view' | string;
|
tab: 'general' | 'playback' | 'window' | string;
|
||||||
tables: {
|
tables: {
|
||||||
fullScreen: DataTableProps;
|
fullScreen: DataTableProps;
|
||||||
nowPlaying: DataTableProps;
|
nowPlaying: DataTableProps;
|
||||||
|
@ -70,6 +70,9 @@ export interface SettingsState {
|
||||||
sideQueue: DataTableProps;
|
sideQueue: DataTableProps;
|
||||||
songs: DataTableProps;
|
songs: DataTableProps;
|
||||||
};
|
};
|
||||||
|
window: {
|
||||||
|
windowBarStyle: Platform;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SettingsSlice extends SettingsState {
|
export interface SettingsSlice extends SettingsState {
|
||||||
|
@ -90,35 +93,34 @@ export const useSettingsStore = create<SettingsSlice>()(
|
||||||
general: {
|
general: {
|
||||||
followSystemTheme: false,
|
followSystemTheme: false,
|
||||||
fontContent: 'Poppins',
|
fontContent: 'Poppins',
|
||||||
|
playButtonBehavior: Play.NOW,
|
||||||
showQueueDrawerButton: false,
|
showQueueDrawerButton: false,
|
||||||
sideQueueType: 'sideDrawerQueue',
|
sideQueueType: 'sideDrawerQueue',
|
||||||
theme: AppTheme.DEFAULT_DARK,
|
|
||||||
themeDark: AppTheme.DEFAULT_DARK,
|
|
||||||
themeLight: AppTheme.DEFAULT_LIGHT,
|
|
||||||
volumeWheelStep: 5,
|
|
||||||
windowBarStyle: Platform.WEB,
|
|
||||||
},
|
|
||||||
player: {
|
|
||||||
audioDeviceId: undefined,
|
|
||||||
crossfadeDuration: 5,
|
|
||||||
crossfadeStyle: CrossfadeStyle.EQUALPOWER,
|
|
||||||
globalMediaHotkeys: false,
|
|
||||||
muted: false,
|
|
||||||
playButtonBehavior: Play.NOW,
|
|
||||||
scrobble: {
|
|
||||||
enabled: true,
|
|
||||||
scrobbleAtDuration: 240,
|
|
||||||
scrobbleAtPercentage: 75,
|
|
||||||
},
|
|
||||||
skipButtons: {
|
skipButtons: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
skipBackwardSeconds: 5,
|
skipBackwardSeconds: 5,
|
||||||
skipForwardSeconds: 10,
|
skipForwardSeconds: 10,
|
||||||
},
|
},
|
||||||
|
theme: AppTheme.DEFAULT_DARK,
|
||||||
|
themeDark: AppTheme.DEFAULT_DARK,
|
||||||
|
themeLight: AppTheme.DEFAULT_LIGHT,
|
||||||
|
volumeWheelStep: 5,
|
||||||
|
},
|
||||||
|
playback: {
|
||||||
|
audioDeviceId: undefined,
|
||||||
|
crossfadeDuration: 5,
|
||||||
|
crossfadeStyle: CrossfadeStyle.EQUALPOWER,
|
||||||
|
globalMediaHotkeys: false,
|
||||||
|
muted: false,
|
||||||
|
scrobble: {
|
||||||
|
enabled: true,
|
||||||
|
scrobbleAtDuration: 240,
|
||||||
|
scrobbleAtPercentage: 75,
|
||||||
|
},
|
||||||
|
|
||||||
style: PlaybackStyle.GAPLESS,
|
style: PlaybackStyle.GAPLESS,
|
||||||
type: PlaybackType.LOCAL,
|
type: PlaybackType.LOCAL,
|
||||||
},
|
},
|
||||||
|
|
||||||
tab: 'general',
|
tab: 'general',
|
||||||
tables: {
|
tables: {
|
||||||
fullScreen: {
|
fullScreen: {
|
||||||
|
@ -240,6 +242,9 @@ export const useSettingsStore = create<SettingsSlice>()(
|
||||||
rowHeight: 60,
|
rowHeight: 60,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
window: {
|
||||||
|
windowBarStyle: Platform.WEB,
|
||||||
|
},
|
||||||
})),
|
})),
|
||||||
{ name: 'store_settings' },
|
{ name: 'store_settings' },
|
||||||
),
|
),
|
||||||
|
@ -248,21 +253,23 @@ export const useSettingsStore = create<SettingsSlice>()(
|
||||||
return merge(currentState, persistedState);
|
return merge(currentState, persistedState);
|
||||||
},
|
},
|
||||||
name: 'store_settings',
|
name: 'store_settings',
|
||||||
version: 4,
|
version: 5,
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
export const useSettingsStoreActions = () => useSettingsStore((state) => state.actions);
|
export const useSettingsStoreActions = () => useSettingsStore((state) => state.actions);
|
||||||
|
|
||||||
export const usePlayerSettings = () => useSettingsStore((state) => state.player, shallow);
|
export const usePlaybackSettings = () => useSettingsStore((state) => state.playback, shallow);
|
||||||
|
|
||||||
export const useTableSettings = (type: TableType) =>
|
export const useTableSettings = (type: TableType) =>
|
||||||
useSettingsStore((state) => state.tables[type]);
|
useSettingsStore((state) => state.tables[type]);
|
||||||
|
|
||||||
export const useGeneralSettings = () => useSettingsStore((state) => state.general, shallow);
|
export const useGeneralSettings = () => useSettingsStore((state) => state.general, shallow);
|
||||||
|
|
||||||
export const usePlayerType = () => useSettingsStore((state) => state.player.type, shallow);
|
export const usePlayerType = () => useSettingsStore((state) => state.playback.type, shallow);
|
||||||
|
|
||||||
export const usePlayButtonBehavior = () =>
|
export const usePlayButtonBehavior = () =>
|
||||||
useSettingsStore((state) => state.player.playButtonBehavior, shallow);
|
useSettingsStore((state) => state.general.playButtonBehavior, shallow);
|
||||||
|
|
||||||
|
export const useWindowSettings = () => useSettingsStore((state) => state.window, shallow);
|
||||||
|
|
Reference in a new issue