Add remote control (#164)

* draft add remotes

* add favorite, rating

* add basic auth
This commit is contained in:
Kendall Garner 2023-07-23 12:23:18 +00:00 committed by GitHub
parent 0a13d047bb
commit c9dbf9b5be
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
66 changed files with 2585 additions and 298 deletions

View file

@ -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>
</>
);
};

View file

@ -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) {

View file

@ -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(() => {

View file

@ -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) {

View file

@ -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,

View file

@ -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);
}
};

View file

@ -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') {

View file

@ -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 (

View file

@ -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"

View file

@ -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,

View file

@ -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;

View file

@ -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(() => {

View file

@ -16,7 +16,7 @@ const localSettings = isElectron() ? window.electron.localSettings : null;
interface EditServerFormProps {
isUpdate?: boolean;
onCancel: () => void;
password?: string;
password: string | null;
server: ServerListItem;
}

View file

@ -74,7 +74,7 @@ export const ApplicationSettings = () => {
zoomFactor: newVal,
},
});
localSettings.setZoomFactor(newVal);
localSettings!.setZoomFactor(newVal);
}}
/>
),

View file

@ -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>
);
};

View file

@ -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>
</>
);
};

View file

@ -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();
}
}}
/>

View file

@ -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);
}
}}
/>

View file

@ -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);
};

View file

@ -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',
});

View file

@ -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);
}

View file

@ -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);
}

View file

@ -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) => {

View file

@ -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) {