mirror of
https://github.com/jeffvli/feishin.git
synced 2026-07-15 07:00:03 +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':
|
||||
state = 'starting';
|
||||
break;
|
||||
case 'timeupdate':
|
||||
case 'unpause':
|
||||
state = 'playing';
|
||||
break;
|
||||
|
||||
@@ -4,50 +4,38 @@ import { generatePath, useNavigate } from 'react-router';
|
||||
|
||||
import { AppRoute } from '/@/renderer/router/routes';
|
||||
import { ContextMenu } from '/@/shared/components/context-menu/context-menu';
|
||||
import {
|
||||
Album,
|
||||
AlbumArtist,
|
||||
Artist,
|
||||
LibraryItem,
|
||||
QueueSong,
|
||||
Song,
|
||||
} from '/@/shared/types/domain-types';
|
||||
import { Album, LibraryItem, QueueSong, Song } from '/@/shared/types/domain-types';
|
||||
|
||||
interface GoToActionProps {
|
||||
items: Album[] | AlbumArtist[] | Artist[] | QueueSong[] | Song[];
|
||||
items: Album[] | QueueSong[] | Song[];
|
||||
}
|
||||
|
||||
export const GoToAction = ({ items }: GoToActionProps) => {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const { albumArtists, albumId } = useMemo(() => {
|
||||
const { albumId, artists } = useMemo(() => {
|
||||
const firstItem = items[0];
|
||||
|
||||
if (firstItem._itemType === LibraryItem.ALBUM) {
|
||||
return {
|
||||
albumArtists: firstItem.albumArtists || [],
|
||||
albumId: firstItem.id,
|
||||
};
|
||||
} else if (firstItem._itemType === LibraryItem.SONG) {
|
||||
return {
|
||||
albumArtists: firstItem.albumArtists || [],
|
||||
albumId: firstItem.albumId,
|
||||
};
|
||||
} else if (
|
||||
firstItem._itemType === LibraryItem.ARTIST ||
|
||||
firstItem._itemType === LibraryItem.ALBUM_ARTIST
|
||||
) {
|
||||
return {
|
||||
albumArtists: [{ id: firstItem.id, name: firstItem.name }],
|
||||
albumId: null,
|
||||
};
|
||||
switch (firstItem._itemType) {
|
||||
case LibraryItem.ALBUM:
|
||||
return {
|
||||
albumId: firstItem.id,
|
||||
artists: firstItem.albumArtists || [],
|
||||
};
|
||||
case LibraryItem.SONG:
|
||||
return {
|
||||
albumId: firstItem.albumId,
|
||||
artists:
|
||||
(firstItem.artists?.length ? firstItem.artists : firstItem.albumArtists) ||
|
||||
[],
|
||||
};
|
||||
default:
|
||||
return {
|
||||
albumId: null,
|
||||
artists: [],
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
albumArtists: [],
|
||||
albumId: null,
|
||||
};
|
||||
}, [items]);
|
||||
|
||||
const handleGoToAlbum = useCallback(() => {
|
||||
@@ -55,7 +43,7 @@ export const GoToAction = ({ items }: GoToActionProps) => {
|
||||
navigate(generatePath(AppRoute.LIBRARY_ALBUMS_DETAIL, { albumId }));
|
||||
}, [albumId, navigate]);
|
||||
|
||||
const handleGoToAlbumArtist = useCallback(
|
||||
const handleGoToArtist = useCallback(
|
||||
(albumArtistId: string) => {
|
||||
navigate(generatePath(AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL, { albumArtistId }));
|
||||
},
|
||||
@@ -81,13 +69,13 @@ export const GoToAction = ({ items }: GoToActionProps) => {
|
||||
{t('page.contextMenu.goToAlbum')}
|
||||
</ContextMenu.Item>
|
||||
)}
|
||||
{albumArtists.map((albumArtist) => (
|
||||
{artists.map((artist) => (
|
||||
<ContextMenu.Item
|
||||
key={albumArtist.id}
|
||||
key={artist.id}
|
||||
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.SubmenuContent>
|
||||
|
||||
@@ -3,7 +3,6 @@ import { useMemo } from 'react';
|
||||
import { AddToPlaylistAction } from '/@/renderer/features/context-menu/actions/add-to-playlist-action';
|
||||
import { DownloadAction } from '/@/renderer/features/context-menu/actions/download-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 { PlayArtistRadioAction } from '/@/renderer/features/context-menu/actions/play-artist-radio-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} />
|
||||
<ShareAction ids={ids} itemType={LibraryItem.ALBUM_ARTIST} />
|
||||
<ContextMenu.Divider />
|
||||
<GoToAction items={items} />
|
||||
<ContextMenu.Divider />
|
||||
<GetInfoAction disabled={items.length === 0} items={items} />
|
||||
</ContextMenu.Content>
|
||||
);
|
||||
|
||||
@@ -3,7 +3,6 @@ import { useMemo } from 'react';
|
||||
import { AddToPlaylistAction } from '/@/renderer/features/context-menu/actions/add-to-playlist-action';
|
||||
import { DownloadAction } from '/@/renderer/features/context-menu/actions/download-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 { PlayArtistRadioAction } from '/@/renderer/features/context-menu/actions/play-artist-radio-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} />
|
||||
<ShareAction ids={ids} itemType={LibraryItem.ARTIST} />
|
||||
<ContextMenu.Divider />
|
||||
<GoToAction items={items} />
|
||||
<ContextMenu.Divider />
|
||||
<GetInfoAction disabled={items.length === 0} items={items} />
|
||||
</ContextMenu.Content>
|
||||
);
|
||||
|
||||
@@ -19,6 +19,8 @@ import {
|
||||
import { toast } from '/@/shared/components/toast/toast';
|
||||
import { PlayerStatus } from '/@/shared/types/types';
|
||||
|
||||
let startupRestoreSessionHandled = false;
|
||||
|
||||
export const useQueueRestoreTimestamp = () => {
|
||||
const { mediaSeekToTimestamp } = usePlayerActions();
|
||||
|
||||
@@ -51,28 +53,65 @@ export const useInitialTimestampRestore = () => {
|
||||
|
||||
const startupRestoreInitializedRef = useRef(false);
|
||||
const startupSeekArmedRef = useRef<null | number>(null);
|
||||
const startupSeekTargetUniqueIdRef = useRef<null | string>(null);
|
||||
const startupSeekAppliedRef = useRef(false);
|
||||
|
||||
const applyStartupSeek = useCallback(() => {
|
||||
const cancelStartupSeek = useCallback(() => {
|
||||
if (startupSeekAppliedRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const seekTimestamp = startupSeekArmedRef.current;
|
||||
if (!seekTimestamp || seekTimestamp <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
startupSeekAppliedRef.current = true;
|
||||
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(() => {
|
||||
mediaSeekToTimestamp(seekTimestamp);
|
||||
}, 100);
|
||||
}, [mediaSeekToTimestamp]);
|
||||
}, [cancelStartupSeek, mediaSeekToTimestamp]);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -81,9 +120,11 @@ export const useInitialTimestampRestore = () => {
|
||||
}
|
||||
|
||||
startupRestoreInitializedRef.current = true;
|
||||
startupRestoreSessionHandled = true;
|
||||
|
||||
if (timestamp > 0) {
|
||||
startupSeekArmedRef.current = timestamp;
|
||||
startupSeekTargetUniqueIdRef.current = currentSong._uniqueId;
|
||||
}
|
||||
|
||||
if (playerStatus === PlayerStatus.PLAYING) {
|
||||
|
||||
@@ -45,7 +45,7 @@ const getPositionValue = (seconds: number, useTicks: boolean) => {
|
||||
return Math.round(seconds * 1e7);
|
||||
}
|
||||
|
||||
return seconds;
|
||||
return seconds * 1000;
|
||||
};
|
||||
|
||||
/*
|
||||
@@ -459,12 +459,14 @@ export const useScrobble = () => {
|
||||
lastProgressEventRef.current = properties.timestamp;
|
||||
lastSeekEventRef.current = now;
|
||||
|
||||
const currentStatus = usePlayerStore.getState().player.status;
|
||||
|
||||
sendScrobble.mutate(
|
||||
{
|
||||
apiClientProps: { serverId: currentSong._serverId || '' },
|
||||
query: {
|
||||
albumId: currentSong.albumId,
|
||||
event: 'timeupdate',
|
||||
event: currentStatus === PlayerStatus.PLAYING ? 'unpause' : 'pause',
|
||||
id: currentSong.id,
|
||||
mediaType: mediaType,
|
||||
playbackRate: playbackRate,
|
||||
|
||||
@@ -1363,7 +1363,7 @@ export type ScrobbleArgs = BaseEndpointArgs & {
|
||||
|
||||
export type ScrobbleQuery = {
|
||||
albumId?: string;
|
||||
event?: 'pause' | 'start' | 'timeupdate' | 'unpause';
|
||||
event?: 'pause' | 'start' | 'unpause';
|
||||
id: string;
|
||||
mediaType: 'podcast' | 'song';
|
||||
playbackRate: number;
|
||||
|
||||
@@ -2,6 +2,20 @@ import type { HotkeyItem } from '@mantine/hooks';
|
||||
|
||||
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.
|
||||
* Mantine matches KeyboardEvent.code via normalizeKey, which turns Digit1 into
|
||||
@@ -25,6 +39,11 @@ export const toPhysicalHotkey = (hotkey: string): string =>
|
||||
return `Digit${part}`;
|
||||
}
|
||||
|
||||
const punctuationPhysical = PUNCTUATION_KEY_TO_PHYSICAL[part];
|
||||
if (punctuationPhysical) {
|
||||
return punctuationPhysical;
|
||||
}
|
||||
|
||||
return part;
|
||||
})
|
||||
.join('+');
|
||||
|
||||
@@ -3,7 +3,12 @@ const CODE_TO_HOTKEY_KEY: Record<string, string> = {
|
||||
ArrowLeft: 'arrowleft',
|
||||
ArrowRight: 'arrowright',
|
||||
ArrowUp: 'arrowup',
|
||||
Backquote: '`',
|
||||
Backslash: '\\',
|
||||
Backspace: 'backspace',
|
||||
BracketLeft: '[',
|
||||
BracketRight: ']',
|
||||
Comma: ',',
|
||||
Delete: 'delete',
|
||||
End: 'end',
|
||||
Enter: 'enter',
|
||||
@@ -14,6 +19,10 @@ const CODE_TO_HOTKEY_KEY: Record<string, string> = {
|
||||
Minus: 'minus',
|
||||
PageDown: 'pagedown',
|
||||
PageUp: 'pageup',
|
||||
Period: '.',
|
||||
Quote: "'",
|
||||
Semicolon: ';',
|
||||
Slash: '/',
|
||||
Space: 'space',
|
||||
Tab: 'tab',
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user