Compare commits

...

4 Commits

9 changed files with 108 additions and 56 deletions
@@ -2333,7 +2333,6 @@ export const SubsonicController: InternalControllerEndpoint = {
case 'start': case 'start':
state = 'starting'; state = 'starting';
break; break;
case 'timeupdate':
case 'unpause': case 'unpause':
state = 'playing'; state = 'playing';
break; break;
@@ -4,50 +4,38 @@ import { generatePath, useNavigate } from 'react-router';
import { AppRoute } from '/@/renderer/router/routes'; import { AppRoute } from '/@/renderer/router/routes';
import { ContextMenu } from '/@/shared/components/context-menu/context-menu'; import { ContextMenu } from '/@/shared/components/context-menu/context-menu';
import { import { Album, LibraryItem, QueueSong, Song } from '/@/shared/types/domain-types';
Album,
AlbumArtist,
Artist,
LibraryItem,
QueueSong,
Song,
} from '/@/shared/types/domain-types';
interface GoToActionProps { interface GoToActionProps {
items: Album[] | AlbumArtist[] | Artist[] | QueueSong[] | Song[]; items: Album[] | QueueSong[] | Song[];
} }
export const GoToAction = ({ items }: GoToActionProps) => { export const GoToAction = ({ items }: GoToActionProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const navigate = useNavigate(); const navigate = useNavigate();
const { albumArtists, albumId } = useMemo(() => { const { albumId, artists } = useMemo(() => {
const firstItem = items[0]; const firstItem = items[0];
if (firstItem._itemType === LibraryItem.ALBUM) { switch (firstItem._itemType) {
return { case LibraryItem.ALBUM:
albumArtists: firstItem.albumArtists || [], return {
albumId: firstItem.id, albumId: firstItem.id,
}; artists: firstItem.albumArtists || [],
} else if (firstItem._itemType === LibraryItem.SONG) { };
return { case LibraryItem.SONG:
albumArtists: firstItem.albumArtists || [], return {
albumId: firstItem.albumId, albumId: firstItem.albumId,
}; artists:
} else if ( (firstItem.artists?.length ? firstItem.artists : firstItem.albumArtists) ||
firstItem._itemType === LibraryItem.ARTIST || [],
firstItem._itemType === LibraryItem.ALBUM_ARTIST };
) { default:
return { return {
albumArtists: [{ id: firstItem.id, name: firstItem.name }], albumId: null,
albumId: null, artists: [],
}; };
} }
return {
albumArtists: [],
albumId: null,
};
}, [items]); }, [items]);
const handleGoToAlbum = useCallback(() => { const handleGoToAlbum = useCallback(() => {
@@ -55,7 +43,7 @@ export const GoToAction = ({ items }: GoToActionProps) => {
navigate(generatePath(AppRoute.LIBRARY_ALBUMS_DETAIL, { albumId })); navigate(generatePath(AppRoute.LIBRARY_ALBUMS_DETAIL, { albumId }));
}, [albumId, navigate]); }, [albumId, navigate]);
const handleGoToAlbumArtist = useCallback( const handleGoToArtist = useCallback(
(albumArtistId: string) => { (albumArtistId: string) => {
navigate(generatePath(AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL, { albumArtistId })); navigate(generatePath(AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL, { albumArtistId }));
}, },
@@ -81,13 +69,13 @@ export const GoToAction = ({ items }: GoToActionProps) => {
{t('page.contextMenu.goToAlbum')} {t('page.contextMenu.goToAlbum')}
</ContextMenu.Item> </ContextMenu.Item>
)} )}
{albumArtists.map((albumArtist) => ( {artists.map((artist) => (
<ContextMenu.Item <ContextMenu.Item
key={albumArtist.id} key={artist.id}
leftIcon="artist" leftIcon="artist"
onSelect={() => handleGoToAlbumArtist(albumArtist.id)} onSelect={() => handleGoToArtist(artist.id)}
> >
{`${t('page.contextMenu.goTo')} ${albumArtist.name}`} {`${t('page.contextMenu.goTo')} ${artist.name}`}
</ContextMenu.Item> </ContextMenu.Item>
))} ))}
</ContextMenu.SubmenuContent> </ContextMenu.SubmenuContent>
@@ -3,7 +3,6 @@ import { useMemo } from 'react';
import { AddToPlaylistAction } from '/@/renderer/features/context-menu/actions/add-to-playlist-action'; import { AddToPlaylistAction } from '/@/renderer/features/context-menu/actions/add-to-playlist-action';
import { DownloadAction } from '/@/renderer/features/context-menu/actions/download-action'; import { DownloadAction } from '/@/renderer/features/context-menu/actions/download-action';
import { GetInfoAction } from '/@/renderer/features/context-menu/actions/get-info-action'; import { GetInfoAction } from '/@/renderer/features/context-menu/actions/get-info-action';
import { GoToAction } from '/@/renderer/features/context-menu/actions/go-to-action';
import { PlayAction } from '/@/renderer/features/context-menu/actions/play-action'; import { PlayAction } from '/@/renderer/features/context-menu/actions/play-action';
import { PlayArtistRadioAction } from '/@/renderer/features/context-menu/actions/play-artist-radio-action'; import { PlayArtistRadioAction } from '/@/renderer/features/context-menu/actions/play-artist-radio-action';
import { SetFavoriteAction } from '/@/renderer/features/context-menu/actions/set-favorite-action'; import { SetFavoriteAction } from '/@/renderer/features/context-menu/actions/set-favorite-action';
@@ -39,8 +38,6 @@ export const AlbumArtistContextMenu = ({ items, type }: AlbumArtistContextMenuPr
<DownloadAction ids={ids} /> <DownloadAction ids={ids} />
<ShareAction ids={ids} itemType={LibraryItem.ALBUM_ARTIST} /> <ShareAction ids={ids} itemType={LibraryItem.ALBUM_ARTIST} />
<ContextMenu.Divider /> <ContextMenu.Divider />
<GoToAction items={items} />
<ContextMenu.Divider />
<GetInfoAction disabled={items.length === 0} items={items} /> <GetInfoAction disabled={items.length === 0} items={items} />
</ContextMenu.Content> </ContextMenu.Content>
); );
@@ -3,7 +3,6 @@ import { useMemo } from 'react';
import { AddToPlaylistAction } from '/@/renderer/features/context-menu/actions/add-to-playlist-action'; import { AddToPlaylistAction } from '/@/renderer/features/context-menu/actions/add-to-playlist-action';
import { DownloadAction } from '/@/renderer/features/context-menu/actions/download-action'; import { DownloadAction } from '/@/renderer/features/context-menu/actions/download-action';
import { GetInfoAction } from '/@/renderer/features/context-menu/actions/get-info-action'; import { GetInfoAction } from '/@/renderer/features/context-menu/actions/get-info-action';
import { GoToAction } from '/@/renderer/features/context-menu/actions/go-to-action';
import { PlayAction } from '/@/renderer/features/context-menu/actions/play-action'; import { PlayAction } from '/@/renderer/features/context-menu/actions/play-action';
import { PlayArtistRadioAction } from '/@/renderer/features/context-menu/actions/play-artist-radio-action'; import { PlayArtistRadioAction } from '/@/renderer/features/context-menu/actions/play-artist-radio-action';
import { SetFavoriteAction } from '/@/renderer/features/context-menu/actions/set-favorite-action'; import { SetFavoriteAction } from '/@/renderer/features/context-menu/actions/set-favorite-action';
@@ -39,8 +38,6 @@ export const ArtistContextMenu = ({ items, type }: ArtistContextMenuProps) => {
<DownloadAction ids={ids} /> <DownloadAction ids={ids} />
<ShareAction ids={ids} itemType={LibraryItem.ARTIST} /> <ShareAction ids={ids} itemType={LibraryItem.ARTIST} />
<ContextMenu.Divider /> <ContextMenu.Divider />
<GoToAction items={items} />
<ContextMenu.Divider />
<GetInfoAction disabled={items.length === 0} items={items} /> <GetInfoAction disabled={items.length === 0} items={items} />
</ContextMenu.Content> </ContextMenu.Content>
); );
@@ -19,6 +19,8 @@ import {
import { toast } from '/@/shared/components/toast/toast'; import { toast } from '/@/shared/components/toast/toast';
import { PlayerStatus } from '/@/shared/types/types'; import { PlayerStatus } from '/@/shared/types/types';
let startupRestoreSessionHandled = false;
export const useQueueRestoreTimestamp = () => { export const useQueueRestoreTimestamp = () => {
const { mediaSeekToTimestamp } = usePlayerActions(); const { mediaSeekToTimestamp } = usePlayerActions();
@@ -51,28 +53,65 @@ export const useInitialTimestampRestore = () => {
const startupRestoreInitializedRef = useRef(false); const startupRestoreInitializedRef = useRef(false);
const startupSeekArmedRef = useRef<null | number>(null); const startupSeekArmedRef = useRef<null | number>(null);
const startupSeekTargetUniqueIdRef = useRef<null | string>(null);
const startupSeekAppliedRef = useRef(false); const startupSeekAppliedRef = useRef(false);
const applyStartupSeek = useCallback(() => { const cancelStartupSeek = useCallback(() => {
if (startupSeekAppliedRef.current) { if (startupSeekAppliedRef.current) {
return; return;
} }
const seekTimestamp = startupSeekArmedRef.current;
if (!seekTimestamp || seekTimestamp <= 0) {
return;
}
startupSeekAppliedRef.current = true; startupSeekAppliedRef.current = true;
startupSeekArmedRef.current = null; startupSeekArmedRef.current = null;
startupSeekTargetUniqueIdRef.current = null;
}, []);
const applyStartupSeek = useCallback(() => {
const seekTimestamp = startupSeekArmedRef.current;
if (startupSeekAppliedRef.current) {
return;
}
if (!seekTimestamp || seekTimestamp <= 0) {
return;
}
const targetUniqueId = startupSeekTargetUniqueIdRef.current;
const currentUniqueId = usePlayerStore.getState().getQueue().items[
usePlayerStore.getState().player.index
]?._uniqueId;
if (targetUniqueId && currentUniqueId !== targetUniqueId) {
cancelStartupSeek();
return;
}
startupSeekAppliedRef.current = true;
startupSeekArmedRef.current = null;
startupSeekTargetUniqueIdRef.current = null;
setTimeout(() => { setTimeout(() => {
mediaSeekToTimestamp(seekTimestamp); mediaSeekToTimestamp(seekTimestamp);
}, 100); }, 100);
}, [mediaSeekToTimestamp]); }, [cancelStartupSeek, mediaSeekToTimestamp]);
useEffect(() => { useEffect(() => {
if (startupRestoreInitializedRef.current) { const targetUniqueId = startupSeekTargetUniqueIdRef.current;
if (
!targetUniqueId ||
startupSeekAppliedRef.current ||
!currentSong ||
currentSong._uniqueId === targetUniqueId
) {
return;
}
cancelStartupSeek();
}, [cancelStartupSeek, currentSong]);
useEffect(() => {
if (startupRestoreInitializedRef.current || startupRestoreSessionHandled) {
return; return;
} }
@@ -81,9 +120,11 @@ export const useInitialTimestampRestore = () => {
} }
startupRestoreInitializedRef.current = true; startupRestoreInitializedRef.current = true;
startupRestoreSessionHandled = true;
if (timestamp > 0) { if (timestamp > 0) {
startupSeekArmedRef.current = timestamp; startupSeekArmedRef.current = timestamp;
startupSeekTargetUniqueIdRef.current = currentSong._uniqueId;
} }
if (playerStatus === PlayerStatus.PLAYING) { if (playerStatus === PlayerStatus.PLAYING) {
@@ -45,7 +45,7 @@ const getPositionValue = (seconds: number, useTicks: boolean) => {
return Math.round(seconds * 1e7); return Math.round(seconds * 1e7);
} }
return seconds; return seconds * 1000;
}; };
/* /*
@@ -459,12 +459,14 @@ export const useScrobble = () => {
lastProgressEventRef.current = properties.timestamp; lastProgressEventRef.current = properties.timestamp;
lastSeekEventRef.current = now; lastSeekEventRef.current = now;
const currentStatus = usePlayerStore.getState().player.status;
sendScrobble.mutate( sendScrobble.mutate(
{ {
apiClientProps: { serverId: currentSong._serverId || '' }, apiClientProps: { serverId: currentSong._serverId || '' },
query: { query: {
albumId: currentSong.albumId, albumId: currentSong.albumId,
event: 'timeupdate', event: currentStatus === PlayerStatus.PLAYING ? 'unpause' : 'pause',
id: currentSong.id, id: currentSong.id,
mediaType: mediaType, mediaType: mediaType,
playbackRate: playbackRate, playbackRate: playbackRate,
+1 -1
View File
@@ -1363,7 +1363,7 @@ export type ScrobbleArgs = BaseEndpointArgs & {
export type ScrobbleQuery = { export type ScrobbleQuery = {
albumId?: string; albumId?: string;
event?: 'pause' | 'start' | 'timeupdate' | 'unpause'; event?: 'pause' | 'start' | 'unpause';
id: string; id: string;
mediaType: 'podcast' | 'song'; mediaType: 'podcast' | 'song';
playbackRate: number; playbackRate: number;
+19
View File
@@ -2,6 +2,20 @@ import type { HotkeyItem } from '@mantine/hooks';
const RESERVED_KEYS = new Set(['alt', 'ctrl', 'meta', 'mod', 'shift']); const RESERVED_KEYS = new Set(['alt', 'ctrl', 'meta', 'mod', 'shift']);
const PUNCTUATION_KEY_TO_PHYSICAL: Record<string, string> = {
"'": 'Quote',
',': 'Comma',
'-': 'Minus',
'.': 'Period',
'/': 'Slash',
';': 'Semicolon',
'=': 'Equal',
'[': 'BracketLeft',
'\\': 'Backslash',
']': 'BracketRight',
'`': 'Backquote',
};
/** /**
* Converts stored hotkey strings to Mantine's physical-key format. * Converts stored hotkey strings to Mantine's physical-key format.
* Mantine matches KeyboardEvent.code via normalizeKey, which turns Digit1 into * Mantine matches KeyboardEvent.code via normalizeKey, which turns Digit1 into
@@ -25,6 +39,11 @@ export const toPhysicalHotkey = (hotkey: string): string =>
return `Digit${part}`; return `Digit${part}`;
} }
const punctuationPhysical = PUNCTUATION_KEY_TO_PHYSICAL[part];
if (punctuationPhysical) {
return punctuationPhysical;
}
return part; return part;
}) })
.join('+'); .join('+');
@@ -3,7 +3,12 @@ const CODE_TO_HOTKEY_KEY: Record<string, string> = {
ArrowLeft: 'arrowleft', ArrowLeft: 'arrowleft',
ArrowRight: 'arrowright', ArrowRight: 'arrowright',
ArrowUp: 'arrowup', ArrowUp: 'arrowup',
Backquote: '`',
Backslash: '\\',
Backspace: 'backspace', Backspace: 'backspace',
BracketLeft: '[',
BracketRight: ']',
Comma: ',',
Delete: 'delete', Delete: 'delete',
End: 'end', End: 'end',
Enter: 'enter', Enter: 'enter',
@@ -14,6 +19,10 @@ const CODE_TO_HOTKEY_KEY: Record<string, string> = {
Minus: 'minus', Minus: 'minus',
PageDown: 'pagedown', PageDown: 'pagedown',
PageUp: 'pageup', PageUp: 'pageup',
Period: '.',
Quote: "'",
Semicolon: ';',
Slash: '/',
Space: 'space', Space: 'space',
Tab: 'tab', Tab: 'tab',
}; };