mirror of
https://github.com/antebudimir/feishin.git
synced 2026-01-01 02:13:33 +00:00
Add remote control (#164)
* draft add remotes * add favorite, rating * add basic auth
This commit is contained in:
parent
0a13d047bb
commit
c9dbf9b5be
66 changed files with 2585 additions and 298 deletions
|
|
@ -1,7 +1,7 @@
|
|||
import { initClient, initContract } from '@ts-rest/core';
|
||||
import axios, { Method, AxiosError, AxiosResponse, isAxiosError } from 'axios';
|
||||
import isElectron from 'is-electron';
|
||||
import { debounce } from 'lodash';
|
||||
import debounce from 'lodash/debounce';
|
||||
import omitBy from 'lodash/omitBy';
|
||||
import qs from 'qs';
|
||||
import { ndType } from './navidrome-types';
|
||||
|
|
|
|||
|
|
@ -5,12 +5,16 @@ import { InfiniteRowModelModule } from '@ag-grid-community/infinite-row-model';
|
|||
import { MantineProvider } from '@mantine/core';
|
||||
import { ModalsProvider } from '@mantine/modals';
|
||||
import { initSimpleImg } from 'react-simple-img';
|
||||
import { BaseContextModal } from './components';
|
||||
import { BaseContextModal, toast } from './components';
|
||||
import { useTheme } from './hooks';
|
||||
import { AppRouter } from './router/app-router';
|
||||
import { useHotkeySettings, usePlaybackSettings, useSettingsStore } from './store/settings.store';
|
||||
import {
|
||||
useHotkeySettings,
|
||||
usePlaybackSettings,
|
||||
useRemoteSettings,
|
||||
useSettingsStore,
|
||||
} from './store/settings.store';
|
||||
import './styles/global.scss';
|
||||
import '@ag-grid-community/styles/ag-grid.css';
|
||||
import { ContextMenuProvider } from '/@/renderer/features/context-menu';
|
||||
import { useHandlePlayQueueAdd } from '/@/renderer/features/player/hooks/use-handle-playqueue-add';
|
||||
import { PlayQueueHandlerContext } from '/@/renderer/features/player';
|
||||
|
|
@ -27,6 +31,7 @@ initSimpleImg({ threshold: 0.05 }, true);
|
|||
const mpvPlayer = isElectron() ? window.electron.mpvPlayer : null;
|
||||
const mpvPlayerListener = isElectron() ? window.electron.mpvPlayerListener : null;
|
||||
const ipc = isElectron() ? window.electron.ipc : null;
|
||||
const remote = isElectron() ? window.electron.remote : null;
|
||||
|
||||
export const App = () => {
|
||||
const theme = useTheme();
|
||||
|
|
@ -35,6 +40,7 @@ export const App = () => {
|
|||
const { bindings } = useHotkeySettings();
|
||||
const handlePlayQueueAdd = useHandlePlayQueueAdd();
|
||||
const { clearQueue, restoreQueue } = useQueueControls();
|
||||
const remoteSettings = useRemoteSettings();
|
||||
|
||||
useEffect(() => {
|
||||
const root = document.documentElement;
|
||||
|
|
@ -80,9 +86,9 @@ export const App = () => {
|
|||
|
||||
useEffect(() => {
|
||||
if (isElectron()) {
|
||||
mpvPlayer.restoreQueue();
|
||||
mpvPlayer!.restoreQueue();
|
||||
|
||||
mpvPlayerListener.rendererSaveQueue(() => {
|
||||
mpvPlayerListener!.rendererSaveQueue(() => {
|
||||
const { current, queue } = usePlayerStore.getState();
|
||||
const stateToSave: Partial<Pick<PlayerState, 'current' | 'queue'>> = {
|
||||
current: {
|
||||
|
|
@ -91,13 +97,13 @@ export const App = () => {
|
|||
},
|
||||
queue,
|
||||
};
|
||||
mpvPlayer.saveQueue(stateToSave);
|
||||
mpvPlayer!.saveQueue(stateToSave);
|
||||
});
|
||||
|
||||
mpvPlayerListener.rendererRestoreQueue((_event: any, data: Partial<PlayerState>) => {
|
||||
mpvPlayerListener!.rendererRestoreQueue((_event: any, data) => {
|
||||
const playerData = restoreQueue(data);
|
||||
if (playbackType === PlaybackType.LOCAL) {
|
||||
mpvPlayer.setQueue(playerData, true);
|
||||
mpvPlayer!.setQueue(playerData, true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -108,6 +114,23 @@ export const App = () => {
|
|||
};
|
||||
}, [playbackType, restoreQueue]);
|
||||
|
||||
useEffect(() => {
|
||||
if (remote) {
|
||||
remote
|
||||
?.updateSetting(
|
||||
remoteSettings.enabled,
|
||||
remoteSettings.port,
|
||||
remoteSettings.username,
|
||||
remoteSettings.password,
|
||||
)
|
||||
.catch((error) => {
|
||||
toast.warn({ message: error, title: 'Failed to enable remote' });
|
||||
});
|
||||
}
|
||||
// We only want to fire this once
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<MantineProvider
|
||||
withGlobalStyles
|
||||
|
|
|
|||
|
|
@ -7,12 +7,12 @@ const localSettings = isElectron() ? window.electron.localSettings : null;
|
|||
export const MpvRequired = () => {
|
||||
const [mpvPath, setMpvPath] = useState('');
|
||||
const handleSetMpvPath = (e: File) => {
|
||||
localSettings.set('mpv_path', e.path);
|
||||
localSettings?.set('mpv_path', e.path);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const getMpvPath = async () => {
|
||||
if (!isElectron()) return setMpvPath('');
|
||||
if (!localSettings) return setMpvPath('');
|
||||
const mpvPath = localSettings.get('mpv_path') as string;
|
||||
return setMpvPath(mpvPath);
|
||||
};
|
||||
|
|
@ -37,7 +37,7 @@ export const MpvRequired = () => {
|
|||
placeholder={mpvPath}
|
||||
onChange={handleSetMpvPath}
|
||||
/>
|
||||
<Button onClick={() => localSettings.restart()}>Restart</Button>
|
||||
<Button onClick={() => localSettings?.restart()}>Restart</Button>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ const ActionRequiredRoute = () => {
|
|||
|
||||
useEffect(() => {
|
||||
const getMpvPath = async () => {
|
||||
if (!isElectron()) return setIsMpvRequired(false);
|
||||
if (!localSettings) return setIsMpvRequired(false);
|
||||
const mpvPath = await localSettings.get('mpv_path');
|
||||
|
||||
if (mpvPath) {
|
||||
|
|
|
|||
|
|
@ -80,6 +80,7 @@ const JELLYFIN_IGNORED_MENU_ITEMS: ContextMenuItemType[] = ['setRating'];
|
|||
// const SUBSONIC_IGNORED_MENU_ITEMS: ContextMenuItemType[] = [];
|
||||
|
||||
const mpvPlayer = isElectron() ? window.electron.mpvPlayer : null;
|
||||
const remote = isElectron() ? window.electron.remote : null;
|
||||
|
||||
export interface ContextMenuProviderProps {
|
||||
children: React.ReactNode;
|
||||
|
|
@ -555,7 +556,7 @@ export const ContextMenuProvider = ({ children }: ContextMenuProviderProps) => {
|
|||
const playerData = moveToBottomOfQueue(uniqueIds);
|
||||
|
||||
if (playerType === PlaybackType.LOCAL) {
|
||||
mpvPlayer.setQueueNext(playerData);
|
||||
mpvPlayer!.setQueueNext(playerData);
|
||||
}
|
||||
}, [ctx.dataNodes, moveToBottomOfQueue, playerType]);
|
||||
|
||||
|
|
@ -566,7 +567,7 @@ export const ContextMenuProvider = ({ children }: ContextMenuProviderProps) => {
|
|||
const playerData = moveToTopOfQueue(uniqueIds);
|
||||
|
||||
if (playerType === PlaybackType.LOCAL) {
|
||||
mpvPlayer.setQueueNext(playerData);
|
||||
mpvPlayer!.setQueueNext(playerData);
|
||||
}
|
||||
}, [ctx.dataNodes, moveToTopOfQueue, playerType]);
|
||||
|
||||
|
|
@ -580,11 +581,15 @@ export const ContextMenuProvider = ({ children }: ContextMenuProviderProps) => {
|
|||
|
||||
if (playerType === PlaybackType.LOCAL) {
|
||||
if (isCurrentSongRemoved) {
|
||||
mpvPlayer.setQueue(playerData);
|
||||
mpvPlayer!.setQueue(playerData);
|
||||
} else {
|
||||
mpvPlayer.setQueueNext(playerData);
|
||||
mpvPlayer!.setQueueNext(playerData);
|
||||
}
|
||||
}
|
||||
|
||||
if (isCurrentSongRemoved) {
|
||||
remote?.updateSong({ song: playerData.current.song });
|
||||
}
|
||||
}, [ctx.dataNodes, playerType, removeFromQueue]);
|
||||
|
||||
const handleDeselectAll = useCallback(() => {
|
||||
|
|
|
|||
|
|
@ -152,8 +152,8 @@ export const useSongLyricsByRemoteId = (
|
|||
enabled: !!query.remoteSongId && !!query.remoteSource,
|
||||
onError: () => {},
|
||||
queryFn: async () => {
|
||||
const remoteLyricsResult: string | null = await lyricsIpc?.getRemoteLyricsByRemoteId(
|
||||
query,
|
||||
const remoteLyricsResult = await lyricsIpc?.getRemoteLyricsByRemoteId(
|
||||
query as LyricGetQuery,
|
||||
);
|
||||
|
||||
if (remoteLyricsResult) {
|
||||
|
|
|
|||
|
|
@ -16,7 +16,12 @@ export const useLyricSearch = (args: Omit<QueryHookArgs<LyricSearchQuery>, 'serv
|
|||
return useQuery<Record<LyricSource, InternetProviderLyricSearchResponse[]>>({
|
||||
cacheTime: 1000 * 60 * 1,
|
||||
enabled: !!query.artist || !!query.name,
|
||||
queryFn: () => lyricsIpc?.searchRemoteLyrics(query),
|
||||
queryFn: () => {
|
||||
if (lyricsIpc) {
|
||||
return lyricsIpc.searchRemoteLyrics(query);
|
||||
}
|
||||
return {} as Record<LyricSource, InternetProviderLyricSearchResponse[]>;
|
||||
},
|
||||
queryKey: queryKeys.songs.lyricsSearch(query),
|
||||
staleTime: 1000 * 60 * 1,
|
||||
...options,
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import { usePlayerStore, useSetCurrentTime } from '../../../store/player.store';
|
|||
import { TableConfigDropdown } from '/@/renderer/components/virtual-table';
|
||||
|
||||
const mpvPlayer = isElectron() ? window.electron.mpvPlayer : null;
|
||||
const remote = isElectron() ? window.electron.remote : null;
|
||||
|
||||
interface PlayQueueListOptionsProps {
|
||||
tableRef: MutableRefObject<{ grid: AgGridReactType<Song> } | null>;
|
||||
|
|
@ -42,7 +43,7 @@ export const PlayQueueListControls = ({ type, tableRef }: PlayQueueListOptionsPr
|
|||
const playerData = moveToBottomOfQueue(uniqueIds);
|
||||
|
||||
if (playerType === PlaybackType.LOCAL) {
|
||||
mpvPlayer.setQueueNext(playerData);
|
||||
mpvPlayer!.setQueueNext(playerData);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -54,7 +55,7 @@ export const PlayQueueListControls = ({ type, tableRef }: PlayQueueListOptionsPr
|
|||
const playerData = moveToTopOfQueue(uniqueIds);
|
||||
|
||||
if (playerType === PlaybackType.LOCAL) {
|
||||
mpvPlayer.setQueueNext(playerData);
|
||||
mpvPlayer!.setQueueNext(playerData);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -69,21 +70,27 @@ export const PlayQueueListControls = ({ type, tableRef }: PlayQueueListOptionsPr
|
|||
|
||||
if (playerType === PlaybackType.LOCAL) {
|
||||
if (isCurrentSongRemoved) {
|
||||
mpvPlayer.setQueue(playerData);
|
||||
mpvPlayer!.setQueue(playerData);
|
||||
} else {
|
||||
mpvPlayer.setQueueNext(playerData);
|
||||
mpvPlayer!.setQueueNext(playerData);
|
||||
}
|
||||
}
|
||||
|
||||
if (isCurrentSongRemoved) {
|
||||
remote?.updateSong({ song: playerData.current.song });
|
||||
}
|
||||
};
|
||||
|
||||
const handleClearQueue = () => {
|
||||
const playerData = clearQueue();
|
||||
|
||||
if (playerType === PlaybackType.LOCAL) {
|
||||
mpvPlayer.setQueue(playerData);
|
||||
mpvPlayer.pause();
|
||||
mpvPlayer!.setQueue(playerData);
|
||||
mpvPlayer!.pause();
|
||||
}
|
||||
|
||||
remote?.updateSong({ song: undefined });
|
||||
|
||||
setCurrentTime(0);
|
||||
pause();
|
||||
};
|
||||
|
|
@ -92,7 +99,7 @@ export const PlayQueueListControls = ({ type, tableRef }: PlayQueueListOptionsPr
|
|||
const playerData = shuffleQueue();
|
||||
|
||||
if (playerType === PlaybackType.LOCAL) {
|
||||
mpvPlayer.setQueueNext(playerData);
|
||||
mpvPlayer!.setQueueNext(playerData);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -28,15 +28,14 @@ import debounce from 'lodash/debounce';
|
|||
import { ErrorBoundary } from 'react-error-boundary';
|
||||
import { getColumnDefs, VirtualTable } from '/@/renderer/components/virtual-table';
|
||||
import { ErrorFallback } from '/@/renderer/features/action-required';
|
||||
import { PlaybackType, TableType } from '/@/renderer/types';
|
||||
import { PlaybackType, PlayerStatus, TableType } from '/@/renderer/types';
|
||||
import { LibraryItem, QueueSong } from '/@/renderer/api/types';
|
||||
import { useHandleTableContextMenu } from '/@/renderer/features/context-menu';
|
||||
import { QUEUE_CONTEXT_MENU_ITEMS } from '/@/renderer/features/context-menu/context-menu-items';
|
||||
import { VirtualGridAutoSizerContainer } from '/@/renderer/components/virtual-grid';
|
||||
|
||||
const mpvPlayer = isElectron() ? window.electron.mpvPlayer : null;
|
||||
const utils = isElectron() ? window.electron.utils : null;
|
||||
const mpris = isElectron() && utils?.isLinux() ? window.electron.mpris : null;
|
||||
const remote = isElectron() ? window.electron.remote : null;
|
||||
|
||||
type QueueProps = {
|
||||
type: TableType;
|
||||
|
|
@ -72,15 +71,15 @@ export const PlayQueue = forwardRef(({ type }: QueueProps, ref: Ref<any>) => {
|
|||
|
||||
const handleDoubleClick = (e: CellDoubleClickedEvent) => {
|
||||
const playerData = setCurrentTrack(e.data.uniqueId);
|
||||
mpris?.updateSong({
|
||||
remote?.updateSong({
|
||||
currentTime: 0,
|
||||
song: playerData.current.song,
|
||||
status: 'Playing',
|
||||
status: PlayerStatus.PLAYING,
|
||||
});
|
||||
|
||||
if (playerType === PlaybackType.LOCAL) {
|
||||
mpvPlayer.setQueue(playerData);
|
||||
mpvPlayer.play();
|
||||
mpvPlayer!.setQueue(playerData);
|
||||
mpvPlayer!.play();
|
||||
}
|
||||
|
||||
play();
|
||||
|
|
@ -102,7 +101,7 @@ export const PlayQueue = forwardRef(({ type }: QueueProps, ref: Ref<any>) => {
|
|||
const playerData = reorderQueue(selectedUniqueIds as string[], e.overNode?.data?.uniqueId);
|
||||
|
||||
if (playerType === PlaybackType.LOCAL) {
|
||||
mpvPlayer.setQueueNext(playerData);
|
||||
mpvPlayer!.setQueueNext(playerData);
|
||||
}
|
||||
|
||||
if (type === 'sideDrawerQueue') {
|
||||
|
|
|
|||
|
|
@ -59,8 +59,7 @@ const CenterGridItem = styled.div`
|
|||
overflow: hidden;
|
||||
`;
|
||||
|
||||
const utils = isElectron() ? window.electron.utils : null;
|
||||
const mpris = isElectron() && utils?.isLinux() ? window.electron.mpris : null;
|
||||
const remote = isElectron() ? window.electron.remote : null;
|
||||
|
||||
export const Playerbar = () => {
|
||||
const playersRef = PlayersRef;
|
||||
|
|
@ -74,11 +73,13 @@ export const Playerbar = () => {
|
|||
const { autoNext } = usePlayerControls();
|
||||
|
||||
const autoNextFn = useCallback(() => {
|
||||
const playerData = autoNext();
|
||||
mpris?.updateSong({
|
||||
currentTime: 0,
|
||||
song: playerData.current.song,
|
||||
});
|
||||
if (remote) {
|
||||
const playerData = autoNext();
|
||||
remote.updateSong({
|
||||
currentTime: 0,
|
||||
song: playerData.current.song,
|
||||
});
|
||||
}
|
||||
}, [autoNext]);
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { MouseEvent } from 'react';
|
||||
import { MouseEvent, useEffect } from 'react';
|
||||
import { Flex, Group } from '@mantine/core';
|
||||
import { useHotkeys, useMediaQuery } from '@mantine/hooks';
|
||||
import isElectron from 'is-electron';
|
||||
import { HiOutlineQueueList } from 'react-icons/hi2';
|
||||
import {
|
||||
RiVolumeUpFill,
|
||||
|
|
@ -20,11 +21,14 @@ import {
|
|||
} from '/@/renderer/store';
|
||||
import { useRightControls } from '../hooks/use-right-controls';
|
||||
import { PlayerButton } from './player-button';
|
||||
import { LibraryItem, ServerType } from '/@/renderer/api/types';
|
||||
import { LibraryItem, ServerType, Song } from '/@/renderer/api/types';
|
||||
import { useCreateFavorite, useDeleteFavorite, useSetRating } from '/@/renderer/features/shared';
|
||||
import { Rating } from '/@/renderer/components';
|
||||
import { PlayerbarSlider } from '/@/renderer/features/player/components/playerbar-slider';
|
||||
|
||||
const ipc = isElectron() ? window.electron.ipc : null;
|
||||
const remote = isElectron() ? window.electron.remote : null;
|
||||
|
||||
export const RightControls = () => {
|
||||
const isMinWidth = useMediaQuery('(max-width: 480px)');
|
||||
const volume = useVolume();
|
||||
|
|
@ -113,6 +117,44 @@ export const RightControls = () => {
|
|||
[bindings.toggleQueue.isGlobal ? '' : bindings.toggleQueue.hotkey, handleToggleQueue],
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (remote) {
|
||||
remote.requestFavorite((_event, { favorite, id, serverId }) => {
|
||||
const mutator = favorite ? addToFavoritesMutation : removeFromFavoritesMutation;
|
||||
mutator.mutate({
|
||||
query: {
|
||||
id: [id],
|
||||
type: LibraryItem.SONG,
|
||||
},
|
||||
serverId,
|
||||
});
|
||||
});
|
||||
|
||||
remote.requestRating((_event, { id, rating, serverId }) => {
|
||||
updateRatingMutation.mutate({
|
||||
query: {
|
||||
item: [
|
||||
{
|
||||
id,
|
||||
itemType: LibraryItem.SONG,
|
||||
serverId,
|
||||
} as Song, // This is not a type-safe cast, but it works because those are all the prop
|
||||
],
|
||||
rating,
|
||||
},
|
||||
serverId,
|
||||
});
|
||||
});
|
||||
|
||||
return () => {
|
||||
ipc?.removeAllListeners('request-favorite');
|
||||
ipc?.removeAllListeners('request-rating');
|
||||
};
|
||||
}
|
||||
|
||||
return () => {};
|
||||
}, [addToFavoritesMutation, removeFromFavoritesMutation, updateRatingMutation]);
|
||||
|
||||
return (
|
||||
<Flex
|
||||
align="flex-end"
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ const mpvPlayerListener = isElectron() ? window.electron.mpvPlayerListener : nul
|
|||
const ipc = isElectron() ? window.electron.ipc : null;
|
||||
const utils = isElectron() ? window.electron.utils : null;
|
||||
const mpris = isElectron() && utils?.isLinux() ? window.electron.mpris : null;
|
||||
const remote = isElectron() ? window.electron.remote : null;
|
||||
const mediaSession = !isElectron() || !utils?.isLinux() ? navigator.mediaSession : null;
|
||||
|
||||
export const useCenterControls = (args: { playersRef: any }) => {
|
||||
|
|
@ -87,12 +88,12 @@ export const useCenterControls = (args: { playersRef: any }) => {
|
|||
const playStatus = status || usePlayerStore.getState().current.status;
|
||||
const track = song || usePlayerStore.getState().current.song;
|
||||
|
||||
mpris?.updateSong({
|
||||
remote?.updateSong({
|
||||
currentTime: time,
|
||||
repeat: usePlayerStore.getState().repeat,
|
||||
shuffle: usePlayerStore.getState().shuffle,
|
||||
shuffle: usePlayerStore.getState().shuffle !== PlayerShuffle.NONE,
|
||||
song: track,
|
||||
status: playStatus === PlayerStatus.PLAYING ? 'Playing' : 'Paused',
|
||||
status: playStatus,
|
||||
});
|
||||
|
||||
if (mediaSession) {
|
||||
|
|
@ -133,7 +134,7 @@ export const useCenterControls = (args: { playersRef: any }) => {
|
|||
|
||||
if (isMpvPlayer) {
|
||||
mpvPlayer?.volume(usePlayerStore.getState().volume);
|
||||
mpvPlayer.play();
|
||||
mpvPlayer!.play();
|
||||
} else {
|
||||
currentPlayerRef.getInternalPlayer().play();
|
||||
}
|
||||
|
|
@ -145,7 +146,7 @@ export const useCenterControls = (args: { playersRef: any }) => {
|
|||
mprisUpdateSong({ status: PlayerStatus.PAUSED });
|
||||
|
||||
if (isMpvPlayer) {
|
||||
mpvPlayer.pause();
|
||||
mpvPlayer!.pause();
|
||||
}
|
||||
|
||||
pause();
|
||||
|
|
@ -155,8 +156,8 @@ export const useCenterControls = (args: { playersRef: any }) => {
|
|||
mprisUpdateSong({ status: PlayerStatus.PAUSED });
|
||||
|
||||
if (isMpvPlayer) {
|
||||
mpvPlayer.pause();
|
||||
mpvPlayer.seekTo(0);
|
||||
mpvPlayer!.pause();
|
||||
mpvPlayer!.seekTo(0);
|
||||
} else {
|
||||
stopPlayback();
|
||||
}
|
||||
|
|
@ -168,29 +169,29 @@ export const useCenterControls = (args: { playersRef: any }) => {
|
|||
const handleToggleShuffle = useCallback(() => {
|
||||
if (shuffleStatus === PlayerShuffle.NONE) {
|
||||
const playerData = setShuffle(PlayerShuffle.TRACK);
|
||||
mpris?.updateShuffle(true);
|
||||
return mpvPlayer.setQueueNext(playerData);
|
||||
remote?.updateShuffle(true);
|
||||
return mpvPlayer?.setQueueNext(playerData);
|
||||
}
|
||||
|
||||
const playerData = setShuffle(PlayerShuffle.NONE);
|
||||
mpris?.updateShuffle(false);
|
||||
return mpvPlayer.setQueueNext(playerData);
|
||||
remote?.updateShuffle(false);
|
||||
return mpvPlayer?.setQueueNext(playerData);
|
||||
}, [setShuffle, shuffleStatus]);
|
||||
|
||||
const handleToggleRepeat = useCallback(() => {
|
||||
if (repeatStatus === PlayerRepeat.NONE) {
|
||||
const playerData = setRepeat(PlayerRepeat.ALL);
|
||||
mpris?.updateRepeat('Playlist');
|
||||
return mpvPlayer.setQueueNext(playerData);
|
||||
remote?.updateRepeat(PlayerRepeat.ALL);
|
||||
return mpvPlayer?.setQueueNext(playerData);
|
||||
}
|
||||
|
||||
if (repeatStatus === PlayerRepeat.ALL) {
|
||||
const playerData = setRepeat(PlayerRepeat.ONE);
|
||||
mpris?.updateRepeat('Track');
|
||||
return mpvPlayer.setQueueNext(playerData);
|
||||
remote?.updateRepeat(PlayerRepeat.ONE);
|
||||
return mpvPlayer?.setQueueNext(playerData);
|
||||
}
|
||||
|
||||
mpris?.updateRepeat('None');
|
||||
remote?.updateRepeat(PlayerRepeat.NONE);
|
||||
return setRepeat(PlayerRepeat.NONE);
|
||||
}, [repeatStatus, setRepeat]);
|
||||
|
||||
|
|
@ -209,7 +210,7 @@ export const useCenterControls = (args: { playersRef: any }) => {
|
|||
local: () => {
|
||||
const playerData = autoNext();
|
||||
mprisUpdateSong({ song: playerData.current.song, status: PlayerStatus.PLAYING });
|
||||
mpvPlayer.autoNext(playerData);
|
||||
mpvPlayer!.autoNext(playerData);
|
||||
play();
|
||||
},
|
||||
web: () => {
|
||||
|
|
@ -223,7 +224,7 @@ export const useCenterControls = (args: { playersRef: any }) => {
|
|||
if (isLastTrack) {
|
||||
const playerData = setCurrentIndex(0);
|
||||
mprisUpdateSong({ song: playerData.current.song, status: PlayerStatus.PAUSED });
|
||||
mpvPlayer.setQueue(playerData, true);
|
||||
mpvPlayer!.setQueue(playerData, true);
|
||||
pause();
|
||||
} else {
|
||||
const playerData = autoNext();
|
||||
|
|
@ -231,7 +232,7 @@ export const useCenterControls = (args: { playersRef: any }) => {
|
|||
song: playerData.current.song,
|
||||
status: PlayerStatus.PLAYING,
|
||||
});
|
||||
mpvPlayer.autoNext(playerData);
|
||||
mpvPlayer!.autoNext(playerData);
|
||||
play();
|
||||
}
|
||||
},
|
||||
|
|
@ -255,7 +256,7 @@ export const useCenterControls = (args: { playersRef: any }) => {
|
|||
local: () => {
|
||||
const playerData = autoNext();
|
||||
mprisUpdateSong({ song: playerData.current.song, status: PlayerStatus.PLAYING });
|
||||
mpvPlayer.autoNext(playerData);
|
||||
mpvPlayer!.autoNext(playerData);
|
||||
play();
|
||||
},
|
||||
web: () => {
|
||||
|
|
@ -306,8 +307,8 @@ export const useCenterControls = (args: { playersRef: any }) => {
|
|||
local: () => {
|
||||
const playerData = next();
|
||||
mprisUpdateSong({ song: playerData.current.song, status: PlayerStatus.PLAYING });
|
||||
mpvPlayer.setQueue(playerData);
|
||||
mpvPlayer.next();
|
||||
mpvPlayer!.setQueue(playerData);
|
||||
mpvPlayer!.next();
|
||||
},
|
||||
web: () => {
|
||||
const playerData = next();
|
||||
|
|
@ -320,8 +321,8 @@ export const useCenterControls = (args: { playersRef: any }) => {
|
|||
if (isLastTrack) {
|
||||
const playerData = setCurrentIndex(0);
|
||||
mprisUpdateSong({ song: playerData.current.song, status: PlayerStatus.PAUSED });
|
||||
mpvPlayer.setQueue(playerData);
|
||||
mpvPlayer.pause();
|
||||
mpvPlayer!.setQueue(playerData);
|
||||
mpvPlayer!.pause();
|
||||
pause();
|
||||
} else {
|
||||
const playerData = next();
|
||||
|
|
@ -329,8 +330,8 @@ export const useCenterControls = (args: { playersRef: any }) => {
|
|||
song: playerData.current.song,
|
||||
status: PlayerStatus.PLAYING,
|
||||
});
|
||||
mpvPlayer.setQueue(playerData);
|
||||
mpvPlayer.next();
|
||||
mpvPlayer!.setQueue(playerData);
|
||||
mpvPlayer!.next();
|
||||
}
|
||||
},
|
||||
web: () => {
|
||||
|
|
@ -357,8 +358,8 @@ export const useCenterControls = (args: { playersRef: any }) => {
|
|||
local: () => {
|
||||
const playerData = next();
|
||||
mprisUpdateSong({ song: playerData.current.song, status: PlayerStatus.PLAYING });
|
||||
mpvPlayer.setQueue(playerData);
|
||||
mpvPlayer.next();
|
||||
mpvPlayer!.setQueue(playerData);
|
||||
mpvPlayer!.next();
|
||||
},
|
||||
web: () => {
|
||||
if (!isLastTrack) {
|
||||
|
|
@ -409,7 +410,7 @@ export const useCenterControls = (args: { playersRef: any }) => {
|
|||
handleScrobbleFromSongRestart(currentTime);
|
||||
mpris?.updateSeek(0);
|
||||
if (isMpvPlayer) {
|
||||
return mpvPlayer.seekTo(0);
|
||||
return mpvPlayer!.seekTo(0);
|
||||
}
|
||||
return currentPlayerRef.seekTo(0);
|
||||
}
|
||||
|
|
@ -424,16 +425,16 @@ export const useCenterControls = (args: { playersRef: any }) => {
|
|||
song: playerData.current.song,
|
||||
status: PlayerStatus.PLAYING,
|
||||
});
|
||||
mpvPlayer.setQueue(playerData);
|
||||
mpvPlayer.previous();
|
||||
mpvPlayer!.setQueue(playerData);
|
||||
mpvPlayer!.previous();
|
||||
} else {
|
||||
const playerData = setCurrentIndex(queue.length - 1);
|
||||
mprisUpdateSong({
|
||||
song: playerData.current.song,
|
||||
status: PlayerStatus.PLAYING,
|
||||
});
|
||||
mpvPlayer.setQueue(playerData);
|
||||
mpvPlayer.previous();
|
||||
mpvPlayer!.setQueue(playerData);
|
||||
mpvPlayer!.previous();
|
||||
}
|
||||
},
|
||||
web: () => {
|
||||
|
|
@ -458,12 +459,12 @@ export const useCenterControls = (args: { playersRef: any }) => {
|
|||
const handleRepeatNone = {
|
||||
local: () => {
|
||||
const playerData = previous();
|
||||
mpris?.updateSong({
|
||||
remote?.updateSong({
|
||||
currentTime: usePlayerStore.getState().current.time,
|
||||
song: playerData.current.song,
|
||||
});
|
||||
mpvPlayer.setQueue(playerData);
|
||||
mpvPlayer.previous();
|
||||
mpvPlayer!.setQueue(playerData);
|
||||
mpvPlayer!.previous();
|
||||
},
|
||||
web: () => {
|
||||
if (isFirstTrack) {
|
||||
|
|
@ -489,10 +490,10 @@ export const useCenterControls = (args: { playersRef: any }) => {
|
|||
song: playerData.current.song,
|
||||
status: PlayerStatus.PLAYING,
|
||||
});
|
||||
mpvPlayer.setQueue(playerData);
|
||||
mpvPlayer.previous();
|
||||
mpvPlayer!.setQueue(playerData);
|
||||
mpvPlayer!.previous();
|
||||
} else {
|
||||
mpvPlayer.stop();
|
||||
mpvPlayer!.stop();
|
||||
}
|
||||
},
|
||||
web: () => {
|
||||
|
|
@ -556,7 +557,7 @@ export const useCenterControls = (args: { playersRef: any }) => {
|
|||
mpris?.updateSeek(newTime);
|
||||
|
||||
if (isMpvPlayer) {
|
||||
mpvPlayer.seek(-seconds);
|
||||
mpvPlayer!.seek(-seconds);
|
||||
} else {
|
||||
resetNextPlayer();
|
||||
currentPlayerRef.seekTo(newTime);
|
||||
|
|
@ -570,7 +571,7 @@ export const useCenterControls = (args: { playersRef: any }) => {
|
|||
|
||||
if (isMpvPlayer) {
|
||||
const newTime = currentTime + seconds;
|
||||
mpvPlayer.seek(seconds);
|
||||
mpvPlayer!.seek(seconds);
|
||||
mpris?.updateSeek(newTime);
|
||||
setCurrentTime(newTime, true);
|
||||
} else {
|
||||
|
|
@ -588,7 +589,7 @@ export const useCenterControls = (args: { playersRef: any }) => {
|
|||
|
||||
const debouncedSeek = debounce((e: number) => {
|
||||
if (isMpvPlayer) {
|
||||
mpvPlayer.seekTo(e);
|
||||
mpvPlayer!.seekTo(e);
|
||||
} else {
|
||||
currentPlayerRef.seekTo(e);
|
||||
}
|
||||
|
|
@ -606,20 +607,20 @@ export const useCenterControls = (args: { playersRef: any }) => {
|
|||
);
|
||||
|
||||
const handleQuit = useCallback(() => {
|
||||
mpvPlayer.quit();
|
||||
mpvPlayer!.quit();
|
||||
}, []);
|
||||
|
||||
const handleError = useCallback(
|
||||
(message: string) => {
|
||||
toast.error({ id: 'mpv-error', message, title: 'An error occurred during playback' });
|
||||
pause();
|
||||
mpvPlayer.pause();
|
||||
mpvPlayer!.pause();
|
||||
},
|
||||
[pause],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (isElectron()) {
|
||||
if (mpvPlayerListener) {
|
||||
mpvPlayerListener.rendererPlayPause(() => {
|
||||
handlePlayPause();
|
||||
});
|
||||
|
|
@ -769,12 +770,39 @@ export const useCenterControls = (args: { playersRef: any }) => {
|
|||
|
||||
useEffect(() => {
|
||||
if (utils?.isLinux()) {
|
||||
mpris.requestPosition((_e: any, data: { position: number }) => {
|
||||
mpris!.requestToggleRepeat((_e: any, data: { repeat: string }) => {
|
||||
if (data.repeat === 'Playlist') {
|
||||
usePlayerStore.getState().actions.setRepeat(PlayerRepeat.ALL);
|
||||
} else if (data.repeat === 'Track') {
|
||||
usePlayerStore.getState().actions.setRepeat(PlayerRepeat.ONE);
|
||||
} else {
|
||||
usePlayerStore.getState().actions.setRepeat(PlayerRepeat.NONE);
|
||||
}
|
||||
});
|
||||
|
||||
mpris!.requestToggleShuffle((_e: any, data: { shuffle: boolean }) => {
|
||||
usePlayerStore
|
||||
.getState()
|
||||
.actions.setShuffle(data.shuffle ? PlayerShuffle.TRACK : PlayerShuffle.NONE);
|
||||
});
|
||||
|
||||
return () => {
|
||||
ipc?.removeAllListeners('mpris-request-toggle-repeat');
|
||||
ipc?.removeAllListeners('mpris-request-toggle-shuffle');
|
||||
};
|
||||
}
|
||||
|
||||
return () => {};
|
||||
}, [handleSeekSlider, isMpvPlayer]);
|
||||
|
||||
useEffect(() => {
|
||||
if (remote) {
|
||||
remote.requestPosition((_e: any, data: { position: number }) => {
|
||||
const newTime = data.position;
|
||||
handleSeekSlider(newTime);
|
||||
});
|
||||
|
||||
mpris.requestSeek((_e: any, data: { offset: number }) => {
|
||||
remote.requestSeek((_e: any, data: { offset: number }) => {
|
||||
const currentTime = usePlayerStore.getState().current.time;
|
||||
const currentSongDuration = usePlayerStore.getState().current.song?.duration || 0;
|
||||
const resultingTime = currentTime + data.offset;
|
||||
|
|
@ -791,50 +819,23 @@ export const useCenterControls = (args: { playersRef: any }) => {
|
|||
handleSeekSlider(newTime);
|
||||
});
|
||||
|
||||
mpris.requestVolume((_e: any, data: { volume: number }) => {
|
||||
let newVolume = Math.round(data.volume * 100);
|
||||
|
||||
if (newVolume > 100) {
|
||||
newVolume = 100;
|
||||
} else if (newVolume < 0) {
|
||||
newVolume = 0;
|
||||
}
|
||||
|
||||
usePlayerStore.getState().actions.setVolume(newVolume);
|
||||
mpris.updateVolume(data.volume);
|
||||
remote.requestVolume((_e: any, data: { volume: number }) => {
|
||||
usePlayerStore.getState().actions.setVolume(data.volume);
|
||||
|
||||
if (isMpvPlayer) {
|
||||
mpvPlayer.volume(newVolume);
|
||||
mpvPlayer!.volume(data.volume);
|
||||
}
|
||||
});
|
||||
|
||||
mpris.requestToggleRepeat((_e: any, data: { repeat: string }) => {
|
||||
if (data.repeat === 'Playlist') {
|
||||
usePlayerStore.getState().actions.setRepeat(PlayerRepeat.ALL);
|
||||
} else if (data.repeat === 'Track') {
|
||||
usePlayerStore.getState().actions.setRepeat(PlayerRepeat.ONE);
|
||||
} else {
|
||||
usePlayerStore.getState().actions.setRepeat(PlayerRepeat.NONE);
|
||||
}
|
||||
});
|
||||
|
||||
mpris.requestToggleShuffle((_e: any, data: { shuffle: boolean }) => {
|
||||
usePlayerStore
|
||||
.getState()
|
||||
.actions.setShuffle(data.shuffle ? PlayerShuffle.TRACK : PlayerShuffle.NONE);
|
||||
});
|
||||
|
||||
return () => {
|
||||
ipc?.removeAllListeners('mpris-request-position');
|
||||
ipc?.removeAllListeners('mpris-request-seek');
|
||||
ipc?.removeAllListeners('mpris-request-volume');
|
||||
ipc?.removeAllListeners('mpris-request-toggle-repeat');
|
||||
ipc?.removeAllListeners('mpris-request-toggle-shuffle');
|
||||
ipc?.removeAllListeners('request-position');
|
||||
ipc?.removeAllListeners('request-seek');
|
||||
ipc?.removeAllListeners('request-volume');
|
||||
};
|
||||
}
|
||||
|
||||
return () => {};
|
||||
}, [handleSeekSlider, isMpvPlayer]);
|
||||
});
|
||||
|
||||
return {
|
||||
handleNextTrack,
|
||||
|
|
|
|||
|
|
@ -2,7 +2,13 @@ import { useCallback, useRef } from 'react';
|
|||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { useCurrentServer, usePlayerControls, usePlayerStore } from '/@/renderer/store';
|
||||
import { usePlayerType } from '/@/renderer/store/settings.store';
|
||||
import { PlayQueueAddOptions, Play, PlaybackType } from '/@/renderer/types';
|
||||
import {
|
||||
PlayQueueAddOptions,
|
||||
Play,
|
||||
PlaybackType,
|
||||
PlayerStatus,
|
||||
PlayerShuffle,
|
||||
} from '/@/renderer/types';
|
||||
import { toast } from '/@/renderer/components/toast/index';
|
||||
import isElectron from 'is-electron';
|
||||
import { nanoid } from 'nanoid/non-secure';
|
||||
|
|
@ -47,8 +53,7 @@ const getRootQueryKey = (itemType: LibraryItem, serverId: string) => {
|
|||
};
|
||||
|
||||
const mpvPlayer = isElectron() ? window.electron.mpvPlayer : null;
|
||||
const utils = isElectron() ? window.electron.utils : null;
|
||||
const mpris = isElectron() && utils?.isLinux() ? window.electron.mpris : null;
|
||||
const remote = isElectron() ? window.electron.remote : null;
|
||||
|
||||
const addToQueue = usePlayerStore.getState().actions.addToQueue;
|
||||
|
||||
|
|
@ -154,26 +159,26 @@ export const useHandlePlayQueueAdd = () => {
|
|||
const playerData = addToQueue({ initialIndex: initialSongIndex, playType, songs });
|
||||
|
||||
if (playerType === PlaybackType.LOCAL) {
|
||||
mpvPlayer?.volume(usePlayerStore.getState().volume);
|
||||
mpvPlayer!.volume(usePlayerStore.getState().volume);
|
||||
|
||||
if (playType === Play.NEXT || playType === Play.LAST) {
|
||||
mpvPlayer?.setQueueNext(playerData);
|
||||
mpvPlayer!.setQueueNext(playerData);
|
||||
}
|
||||
|
||||
if (playType === Play.NOW) {
|
||||
mpvPlayer?.setQueue(playerData);
|
||||
mpvPlayer?.play();
|
||||
mpvPlayer!.setQueue(playerData);
|
||||
mpvPlayer!.play();
|
||||
}
|
||||
}
|
||||
|
||||
play();
|
||||
|
||||
mpris?.updateSong({
|
||||
remote?.updateSong({
|
||||
currentTime: usePlayerStore.getState().current.time,
|
||||
repeat: usePlayerStore.getState().repeat,
|
||||
shuffle: usePlayerStore.getState().shuffle,
|
||||
shuffle: usePlayerStore.getState().shuffle !== PlayerShuffle.NONE,
|
||||
song: playerData.current.song,
|
||||
status: 'Playing',
|
||||
status: PlayerStatus.PLAYING,
|
||||
});
|
||||
|
||||
return null;
|
||||
|
|
|
|||
|
|
@ -6,8 +6,7 @@ import { useGeneralSettings } from '/@/renderer/store/settings.store';
|
|||
const mpvPlayer = isElectron() ? window.electron.mpvPlayer : null;
|
||||
const mpvPlayerListener = isElectron() ? window.electron.mpvPlayerListener : null;
|
||||
const ipc = isElectron() ? window.electron.ipc : null;
|
||||
const utils = isElectron() ? window.electron.utils : null;
|
||||
const mpris = isElectron() && utils?.isLinux() ? window.electron.mpris : null;
|
||||
const remote = isElectron() ? window.electron.remote : null;
|
||||
|
||||
const calculateVolumeUp = (volume: number, volumeWheelStep: number) => {
|
||||
let volumeToSet;
|
||||
|
|
@ -41,12 +40,13 @@ export const useRightControls = () => {
|
|||
|
||||
// Ensure that the mpv player volume is set on startup
|
||||
useEffect(() => {
|
||||
if (isElectron()) {
|
||||
remote?.updateVolume(volume);
|
||||
|
||||
if (mpvPlayer) {
|
||||
mpvPlayer.volume(volume);
|
||||
mpris?.updateVolume(volume / 100);
|
||||
|
||||
if (muted) {
|
||||
mpvPlayer.mute();
|
||||
mpvPlayer.mute(muted);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -55,26 +55,26 @@ export const useRightControls = () => {
|
|||
|
||||
const handleVolumeSlider = (e: number) => {
|
||||
mpvPlayer?.volume(e);
|
||||
mpris?.updateVolume(e / 100);
|
||||
remote?.updateVolume(e);
|
||||
setVolume(e);
|
||||
};
|
||||
|
||||
const handleVolumeSliderState = (e: number) => {
|
||||
mpris?.updateVolume(e / 100);
|
||||
remote?.updateVolume(e);
|
||||
setVolume(e);
|
||||
};
|
||||
|
||||
const handleVolumeDown = useCallback(() => {
|
||||
const volumeToSet = calculateVolumeDown(volume, volumeWheelStep);
|
||||
mpvPlayer?.volume(volumeToSet);
|
||||
mpris?.updateVolume(volumeToSet / 100);
|
||||
remote?.updateVolume(volumeToSet);
|
||||
setVolume(volumeToSet);
|
||||
}, [setVolume, volume, volumeWheelStep]);
|
||||
|
||||
const handleVolumeUp = useCallback(() => {
|
||||
const volumeToSet = calculateVolumeUp(volume, volumeWheelStep);
|
||||
mpvPlayer?.volume(volumeToSet);
|
||||
mpris?.updateVolume(volumeToSet / 100);
|
||||
remote?.updateVolume(volumeToSet);
|
||||
setVolume(volumeToSet);
|
||||
}, [setVolume, volume, volumeWheelStep]);
|
||||
|
||||
|
|
@ -88,7 +88,7 @@ export const useRightControls = () => {
|
|||
}
|
||||
|
||||
mpvPlayer?.volume(volumeToSet);
|
||||
mpris?.updateVolume(volumeToSet / 100);
|
||||
remote?.updateVolume(volumeToSet);
|
||||
setVolume(volumeToSet);
|
||||
},
|
||||
[setVolume, volume, volumeWheelStep],
|
||||
|
|
@ -96,7 +96,7 @@ export const useRightControls = () => {
|
|||
|
||||
const handleMute = useCallback(() => {
|
||||
setMuted(!muted);
|
||||
mpvPlayer?.mute();
|
||||
mpvPlayer?.mute(!muted);
|
||||
}, [muted, setMuted]);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ const localSettings = isElectron() ? window.electron.localSettings : null;
|
|||
interface EditServerFormProps {
|
||||
isUpdate?: boolean;
|
||||
onCancel: () => void;
|
||||
password?: string;
|
||||
password: string | null;
|
||||
server: ServerListItem;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -74,7 +74,7 @@ export const ApplicationSettings = () => {
|
|||
zoomFactor: newVal,
|
||||
},
|
||||
});
|
||||
localSettings.setZoomFactor(newVal);
|
||||
localSettings!.setZoomFactor(newVal);
|
||||
}}
|
||||
/>
|
||||
),
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { ApplicationSettings } from '/@/renderer/features/settings/components/ge
|
|||
import { ControlSettings } from '/@/renderer/features/settings/components/general/control-settings';
|
||||
import { SidebarSettings } from '/@/renderer/features/settings/components/general/sidebar-settings';
|
||||
import { ThemeSettings } from '/@/renderer/features/settings/components/general/theme-settings';
|
||||
import { RemoteSettings } from '/@/renderer/features/settings/components/general/remote-settings';
|
||||
|
||||
export const GeneralTab = () => {
|
||||
return (
|
||||
|
|
@ -14,6 +15,8 @@ export const GeneralTab = () => {
|
|||
<ControlSettings />
|
||||
<Divider />
|
||||
<SidebarSettings />
|
||||
<Divider />
|
||||
<RemoteSettings />
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,156 @@
|
|||
import isElectron from 'is-electron';
|
||||
import { SettingsSection } from '/@/renderer/features/settings/components/settings-section';
|
||||
import { useRemoteSettings, useSettingsStoreActions } from '/@/renderer/store';
|
||||
import { NumberInput, Switch, Text, TextInput, toast } from '/@/renderer/components';
|
||||
import debounce from 'lodash/debounce';
|
||||
|
||||
const remote = isElectron() ? window.electron.remote : null;
|
||||
|
||||
export const RemoteSettings = () => {
|
||||
const settings = useRemoteSettings();
|
||||
const { setSettings } = useSettingsStoreActions();
|
||||
|
||||
const url = `http://localhost:${settings.port}`;
|
||||
|
||||
const debouncedEnableRemote = debounce(async (enabled: boolean) => {
|
||||
const errorMsg = await remote!.setRemoteEnabled(enabled);
|
||||
|
||||
if (errorMsg === null) {
|
||||
setSettings({
|
||||
remote: {
|
||||
...settings,
|
||||
enabled,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
toast.error({
|
||||
message: errorMsg,
|
||||
title: enabled ? 'Error enabling remote' : 'Error disabling remote',
|
||||
});
|
||||
}
|
||||
}, 100);
|
||||
|
||||
const debouncedChangeRemotePort = debounce(async (port: number) => {
|
||||
const errorMsg = await remote!.setRemotePort(port);
|
||||
if (errorMsg === null) {
|
||||
setSettings({
|
||||
remote: {
|
||||
...settings,
|
||||
port,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
toast.error({
|
||||
message: errorMsg,
|
||||
title: 'Error setting port',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const isHidden = !isElectron();
|
||||
|
||||
const controlOptions = [
|
||||
{
|
||||
control: (
|
||||
<Switch
|
||||
aria-label="Enable remote control server"
|
||||
defaultChecked={settings.enabled}
|
||||
onChange={async (e) => {
|
||||
const enabled = e.currentTarget.checked;
|
||||
await debouncedEnableRemote(enabled);
|
||||
}}
|
||||
/>
|
||||
),
|
||||
description: (
|
||||
<div>
|
||||
Start an HTTP server to remotely control Feishin. This will listen on{' '}
|
||||
<a
|
||||
href={url}
|
||||
rel="noreferrer noopener"
|
||||
target="_blank"
|
||||
>
|
||||
{url}
|
||||
</a>
|
||||
</div>
|
||||
),
|
||||
isHidden,
|
||||
title: 'Enable remote control',
|
||||
},
|
||||
{
|
||||
control: (
|
||||
<NumberInput
|
||||
aria-label="Set remote port"
|
||||
max={65535}
|
||||
value={settings.port}
|
||||
onBlur={async (e) => {
|
||||
if (!e) return;
|
||||
const port = Number(e.currentTarget.value);
|
||||
await debouncedChangeRemotePort(port);
|
||||
}}
|
||||
/>
|
||||
),
|
||||
description:
|
||||
'Remote server port. Changes here only take effect when you enable the remote',
|
||||
isHidden,
|
||||
title: 'Remove server port',
|
||||
},
|
||||
{
|
||||
control: (
|
||||
<TextInput
|
||||
aria-label="Set remote username"
|
||||
defaultValue={settings.username}
|
||||
onBlur={(e) => {
|
||||
const username = e.currentTarget.value;
|
||||
if (username === settings.username) return;
|
||||
remote!.updateUsername(username);
|
||||
setSettings({
|
||||
remote: {
|
||||
...settings,
|
||||
username,
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
),
|
||||
description:
|
||||
'Username that must be provided to access remote. If both username and password are empty, disable authentication',
|
||||
isHidden,
|
||||
title: 'Remote username',
|
||||
},
|
||||
{
|
||||
control: (
|
||||
<TextInput
|
||||
aria-label="Set remote password"
|
||||
defaultValue={settings.password}
|
||||
onBlur={(e) => {
|
||||
const password = e.currentTarget.value;
|
||||
if (password === settings.password) return;
|
||||
remote!.updatePassword(password);
|
||||
setSettings({
|
||||
remote: {
|
||||
...settings,
|
||||
password,
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
),
|
||||
description: 'Password to access remote',
|
||||
isHidden,
|
||||
title: 'Remote password',
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<SettingsSection options={controlOptions} />
|
||||
<Text size="lg">
|
||||
<b>
|
||||
NOTE: these credentials are by default transferred insecurely. Do not use a
|
||||
password you care about. Changing username/password will disconnect clients and
|
||||
require them to reauthenticate
|
||||
</b>
|
||||
</Text>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
@ -23,12 +23,12 @@ export const WindowHotkeySettings = () => {
|
|||
globalMediaHotkeys: e.currentTarget.checked,
|
||||
},
|
||||
});
|
||||
localSettings.set('global_media_hotkeys', e.currentTarget.checked);
|
||||
localSettings!.set('global_media_hotkeys', e.currentTarget.checked);
|
||||
|
||||
if (e.currentTarget.checked) {
|
||||
localSettings.enableMediaKeys();
|
||||
localSettings!.enableMediaKeys();
|
||||
} else {
|
||||
localSettings.disableMediaKeys();
|
||||
localSettings!.disableMediaKeys();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ export const AudioSettings = () => {
|
|||
setSettings({ playback: { ...settings, type: e as PlaybackType } });
|
||||
if (isElectron() && e === PlaybackType.LOCAL) {
|
||||
const queueData = usePlayerStore.getState().actions.getPlayerData();
|
||||
mpvPlayer.setQueue(queueData);
|
||||
mpvPlayer!.setQueue(queueData);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ export const getMpvSetting = (
|
|||
case 'replayGainPreampDB':
|
||||
return { 'replaygain-preamp': value || 0 };
|
||||
default:
|
||||
return key;
|
||||
return { 'audio-format': value };
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -66,12 +66,12 @@ export const MpvSettings = () => {
|
|||
const [mpvPath, setMpvPath] = useState('');
|
||||
|
||||
const handleSetMpvPath = (e: File) => {
|
||||
localSettings.set('mpv_path', e.path);
|
||||
localSettings?.set('mpv_path', e.path);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const getMpvPath = async () => {
|
||||
if (!isElectron()) return setMpvPath('');
|
||||
if (!localSettings) return setMpvPath('');
|
||||
const mpvPath = (await localSettings.get('mpv_path')) as string;
|
||||
return setMpvPath(mpvPath);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ export const WindowSettings = () => {
|
|||
message:
|
||||
'Restart to apply changes... close the notification to restart Feishin',
|
||||
onClose: () => {
|
||||
window.electron.ipc.send('app-restart');
|
||||
window.electron.ipc!.send('app-restart');
|
||||
},
|
||||
title: 'Restart required',
|
||||
});
|
||||
|
|
|
|||
|
|
@ -11,6 +11,9 @@ import {
|
|||
} from '/@/renderer/api/types';
|
||||
import { MutationHookArgs } from '/@/renderer/lib/react-query';
|
||||
import { getServerById, useSetAlbumListItemDataById, useSetQueueFavorite } from '/@/renderer/store';
|
||||
import isElectron from 'is-electron';
|
||||
|
||||
const remote = isElectron() ? window.electron.remote : null;
|
||||
|
||||
export const useCreateFavorite = (args: MutationHookArgs) => {
|
||||
const { options } = args || {};
|
||||
|
|
@ -42,6 +45,7 @@ export const useCreateFavorite = (args: MutationHookArgs) => {
|
|||
}
|
||||
|
||||
if (variables.query.type === LibraryItem.SONG) {
|
||||
remote?.updateFavorite(true, serverId, variables.query.id);
|
||||
setQueueFavorite(variables.query.id, true);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,9 @@ import {
|
|||
} from '/@/renderer/api/types';
|
||||
import { MutationHookArgs } from '/@/renderer/lib/react-query';
|
||||
import { getServerById, useSetAlbumListItemDataById, useSetQueueFavorite } from '/@/renderer/store';
|
||||
import isElectron from 'is-electron';
|
||||
|
||||
const remote = isElectron() ? window.electron.remote : null;
|
||||
|
||||
export const useDeleteFavorite = (args: MutationHookArgs) => {
|
||||
const { options } = args || {};
|
||||
|
|
@ -42,6 +45,7 @@ export const useDeleteFavorite = (args: MutationHookArgs) => {
|
|||
}
|
||||
|
||||
if (variables.query.type === LibraryItem.SONG) {
|
||||
remote?.updateFavorite(false, serverId, variables.query.id);
|
||||
setQueueFavorite(variables.query.id, false);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -14,6 +14,9 @@ import {
|
|||
} from '/@/renderer/api/types';
|
||||
import { MutationHookArgs } from '/@/renderer/lib/react-query';
|
||||
import { getServerById, useSetAlbumListItemDataById, useSetQueueRating } from '/@/renderer/store';
|
||||
import isElectron from 'is-electron';
|
||||
|
||||
const remote = isElectron() ? window.electron.remote : null;
|
||||
|
||||
export const useSetRating = (args: MutationHookArgs) => {
|
||||
const { options } = args || {};
|
||||
|
|
@ -56,6 +59,11 @@ export const useSetRating = (args: MutationHookArgs) => {
|
|||
}
|
||||
}
|
||||
|
||||
if (remote) {
|
||||
const ids = variables.query.item.map((item) => item.id);
|
||||
remote.updateRating(variables.query.rating, variables.query.item[0].serverId, ids);
|
||||
}
|
||||
|
||||
return { previous: { items: variables.query.item } };
|
||||
},
|
||||
onSuccess: (_data, variables) => {
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ export const AppMenu = () => {
|
|||
};
|
||||
|
||||
const handleCredentialsModal = async (server: ServerListItem) => {
|
||||
let password: string | undefined;
|
||||
let password: string | null = null;
|
||||
|
||||
try {
|
||||
if (localSettings && server.savePassword) {
|
||||
|
|
|
|||
24
src/renderer/preload.d.ts
vendored
24
src/renderer/preload.d.ts
vendored
|
|
@ -1,12 +1,19 @@
|
|||
import { IpcRendererEvent } from 'electron';
|
||||
import { PlayerData, PlayerState } from './store';
|
||||
import { InternetProviderLyricResponse, QueueSong } from '/@/renderer/api/types';
|
||||
import { Remote } from '/@/main/preload/remote';
|
||||
import { Mpris } from '/@/main/preload/mpris';
|
||||
import { MpvPLayer, MpvPlayerListener } from '/@/main/preload/mpv-player';
|
||||
import { Lyrics } from '/@/main/preload/lyrics';
|
||||
import { Utils } from '/@/main/preload/utils';
|
||||
import { LocalSettings } from '/@/main/preload/local-settings';
|
||||
import { Ipc } from '/@/main/preload/ipc';
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
electron: {
|
||||
browser: any;
|
||||
ipc: any;
|
||||
ipc?: Ipc;
|
||||
ipcRenderer: {
|
||||
APP_RESTART(): void;
|
||||
LYRIC_FETCH(data: QueueSong): void;
|
||||
|
|
@ -37,6 +44,8 @@ declare global {
|
|||
PLAYER_SET_QUEUE_NEXT(data: PlayerData): void;
|
||||
PLAYER_STOP(): void;
|
||||
PLAYER_VOLUME(value: number): void;
|
||||
REMOTE_ENABLE(enabled: boolean): Promise<string | null>;
|
||||
REMOTE_PORT(port: number): Promise<string | null>;
|
||||
RENDERER_PLAYER_AUTO_NEXT(cb: (event: IpcRendererEvent, data: any) => void): void;
|
||||
RENDERER_PLAYER_CURRENT_TIME(
|
||||
cb: (event: IpcRendererEvent, data: any) => void,
|
||||
|
|
@ -59,12 +68,13 @@ declare global {
|
|||
windowMinimize(): void;
|
||||
windowUnmaximize(): void;
|
||||
};
|
||||
localSettings: any;
|
||||
lyrics: any;
|
||||
mpris: any;
|
||||
mpvPlayer: any;
|
||||
mpvPlayerListener: any;
|
||||
utils: any;
|
||||
localSettings: LocalSettings;
|
||||
lyrics?: Lyrics;
|
||||
mpris?: Mpris;
|
||||
mpvPlayer?: MpvPLayer;
|
||||
mpvPlayerListener?: MpvPlayerListener;
|
||||
remote?: Remote;
|
||||
utils?: Utils;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ export const AppOutlet = () => {
|
|||
|
||||
const isActionsRequired = useMemo(() => {
|
||||
const isMpvRequired = () => {
|
||||
if (!isElectron()) return false;
|
||||
if (!localSettings) return false;
|
||||
const mpvPath = localSettings.get('mpv_path');
|
||||
if (mpvPath) return false;
|
||||
return true;
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import {
|
|||
TableType,
|
||||
Platform,
|
||||
} from '/@/renderer/types';
|
||||
import { randomString } from '/@/renderer/utils';
|
||||
|
||||
const utils = isElectron() ? window.electron.utils : null;
|
||||
|
||||
|
|
@ -151,6 +152,12 @@ export interface SettingsState {
|
|||
style: PlaybackStyle;
|
||||
type: PlaybackType;
|
||||
};
|
||||
remote: {
|
||||
enabled: boolean;
|
||||
password: string;
|
||||
port: number;
|
||||
username: string;
|
||||
};
|
||||
tab: 'general' | 'playback' | 'window' | 'hotkeys' | string;
|
||||
tables: {
|
||||
fullScreen: DataTableProps;
|
||||
|
|
@ -177,7 +184,7 @@ export interface SettingsSlice extends SettingsState {
|
|||
|
||||
// Determines the default/initial windowBarStyle value based on the current platform.
|
||||
const getPlatformDefaultWindowBarStyle = (): Platform => {
|
||||
return isElectron() ? (utils.isMacOS() ? Platform.MACOS : Platform.WINDOWS) : Platform.WEB;
|
||||
return utils ? (utils.isMacOS() ? Platform.MACOS : Platform.WINDOWS) : Platform.WEB;
|
||||
};
|
||||
|
||||
const platformDefaultWindowBarStyle: Platform = getPlatformDefaultWindowBarStyle();
|
||||
|
|
@ -258,6 +265,12 @@ const initialState: SettingsState = {
|
|||
style: PlaybackStyle.GAPLESS,
|
||||
type: PlaybackType.LOCAL,
|
||||
},
|
||||
remote: {
|
||||
enabled: false,
|
||||
password: randomString(8),
|
||||
port: 4333,
|
||||
username: 'feishin',
|
||||
},
|
||||
tab: 'general',
|
||||
tables: {
|
||||
fullScreen: {
|
||||
|
|
@ -450,3 +463,5 @@ export const useMpvSettings = () =>
|
|||
useSettingsStore((state) => state.playback.mpvProperties, shallow);
|
||||
|
||||
export const useLyricsSettings = () => useSettingsStore((state) => state.lyrics, shallow);
|
||||
|
||||
export const useRemoteSettings = () => useSettingsStore((state) => state.remote, shallow);
|
||||
|
|
|
|||
|
|
@ -187,3 +187,13 @@ export type GridCardData = {
|
|||
resetInfiniteLoaderCache: () => void;
|
||||
route: CardRoute;
|
||||
};
|
||||
|
||||
export type SongUpdate = {
|
||||
currentTime?: number;
|
||||
repeat?: PlayerRepeat;
|
||||
shuffle?: boolean;
|
||||
song?: QueueSong;
|
||||
status?: PlayerStatus;
|
||||
/** This volume is in range 0-100 */
|
||||
volume?: number;
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue