mirror of
https://github.com/jeffvli/feishin.git
synced 2026-07-17 16:06:27 +02:00
Compare commits
4 Commits
08b4c620f2
...
7243ed7f15
| Author | SHA1 | Date | |
|---|---|---|---|
| 7243ed7f15 | |||
| 7e9a78898f | |||
| 6aab8d4121 | |||
| 70594a696b |
@@ -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) {
|
||||||
|
case LibraryItem.ALBUM:
|
||||||
return {
|
return {
|
||||||
albumArtists: firstItem.albumArtists || [],
|
|
||||||
albumId: firstItem.id,
|
albumId: firstItem.id,
|
||||||
|
artists: firstItem.albumArtists || [],
|
||||||
};
|
};
|
||||||
} else if (firstItem._itemType === LibraryItem.SONG) {
|
case LibraryItem.SONG:
|
||||||
return {
|
return {
|
||||||
albumArtists: firstItem.albumArtists || [],
|
|
||||||
albumId: firstItem.albumId,
|
albumId: firstItem.albumId,
|
||||||
|
artists:
|
||||||
|
(firstItem.artists?.length ? firstItem.artists : firstItem.albumArtists) ||
|
||||||
|
[],
|
||||||
};
|
};
|
||||||
} else if (
|
default:
|
||||||
firstItem._itemType === LibraryItem.ARTIST ||
|
|
||||||
firstItem._itemType === LibraryItem.ALBUM_ARTIST
|
|
||||||
) {
|
|
||||||
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,
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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',
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user