mirror of
https://github.com/jeffvli/feishin.git
synced 2026-07-22 18:36:34 +02:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8f40894926 | |||
| 2befcb4e74 | |||
| 2d78c32a68 | |||
| 3d0500980a | |||
| 3551ee5077 |
@@ -4,3 +4,4 @@ import './player';
|
||||
import './remote';
|
||||
import './settings';
|
||||
import './discord-rpc';
|
||||
import './visualizer';
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import { ipcMain } from 'electron';
|
||||
|
||||
import { getMpvInstance } from '../player';
|
||||
import { store } from '../settings';
|
||||
|
||||
import { PlayerType } from '/@/shared/types/types';
|
||||
|
||||
let isLocalVisualizerSurfaceVisible = false;
|
||||
|
||||
export const setLocalVisualizerSurfaceVisible = (visible: boolean) => {
|
||||
isLocalVisualizerSurfaceVisible = visible;
|
||||
};
|
||||
|
||||
export const canHandleVisualizerDisplayMedia = (): boolean => {
|
||||
const playbackType = store.get('playbackType', PlayerType.WEB) as PlayerType;
|
||||
|
||||
if (playbackType !== PlayerType.LOCAL) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isLocalVisualizerSurfaceVisible) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return Boolean(getMpvInstance()?.isRunning());
|
||||
};
|
||||
|
||||
ipcMain.on('visualizer-set-local-surface-visible', (_event, visible: boolean) => {
|
||||
setLocalVisualizerSurfaceVisible(Boolean(visible));
|
||||
});
|
||||
+8
-2
@@ -30,7 +30,9 @@ import packageJson from '../../package.json';
|
||||
import { disableMediaKeys, enableMediaKeys } from './features/core/player/media-keys';
|
||||
import { shutdownServer } from './features/core/remote';
|
||||
import { store } from './features/core/settings';
|
||||
import { canHandleVisualizerDisplayMedia } from './features/core/visualizer';
|
||||
import MenuBuilder, { MenuPlaybackState } from './menu';
|
||||
import './features';
|
||||
import {
|
||||
autoUpdaterLogInterface,
|
||||
createLog,
|
||||
@@ -40,7 +42,6 @@ import {
|
||||
isMacOS,
|
||||
isWindows,
|
||||
} from './utils';
|
||||
import './features';
|
||||
|
||||
import { PlayerRepeat, PlayerStatus, PlayerType, TitleTheme } from '/@/shared/types/types';
|
||||
|
||||
@@ -515,7 +516,7 @@ async function createWindow(first = true): Promise<void> {
|
||||
backgroundThrottling: false,
|
||||
contextIsolation: true,
|
||||
devTools: true,
|
||||
nodeIntegration: true,
|
||||
nodeIntegration: false,
|
||||
preload: join(__dirname, '../preload/index.js'),
|
||||
sandbox: false,
|
||||
webSecurity: !store.get('ignore_cors'),
|
||||
@@ -734,6 +735,11 @@ async function createWindow(first = true): Promise<void> {
|
||||
});
|
||||
|
||||
mainWindow.webContents.session.setDisplayMediaRequestHandler((_request, callback) => {
|
||||
if (!canHandleVisualizerDisplayMedia()) {
|
||||
callback({});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isMacOS()) {
|
||||
callback({ audio: 'loopback' });
|
||||
return;
|
||||
|
||||
Vendored
-3
@@ -1,11 +1,8 @@
|
||||
import { ElectronAPI } from '@electron-toolkit/preload';
|
||||
|
||||
import { PreloadApi } from './index';
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
api: PreloadApi;
|
||||
electron: ElectronAPI;
|
||||
LEGACY_AUTHENTICATION?: boolean;
|
||||
queryLocalFonts?: () => Promise<Font[]>;
|
||||
REMOTE_URL?: string;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { electronAPI } from '@electron-toolkit/preload';
|
||||
import { contextBridge } from 'electron';
|
||||
import { contextBridge, webUtils } from 'electron';
|
||||
|
||||
import { autodiscover } from './autodiscover';
|
||||
import { browser } from './browser';
|
||||
@@ -11,12 +10,14 @@ import { mpris } from './mpris';
|
||||
import { mpvPlayer, mpvPlayerListener } from './mpv-player';
|
||||
import { remote } from './remote';
|
||||
import { utils } from './utils';
|
||||
import { visualizer } from './visualizer';
|
||||
|
||||
// Custom APIs for renderer
|
||||
const api = {
|
||||
autodiscover,
|
||||
browser,
|
||||
discordRpc,
|
||||
getPathForFile: webUtils.getPathForFile,
|
||||
ipc,
|
||||
localSettings,
|
||||
lyrics,
|
||||
@@ -25,6 +26,7 @@ const api = {
|
||||
mpvPlayerListener,
|
||||
remote,
|
||||
utils,
|
||||
visualizer,
|
||||
};
|
||||
|
||||
export type PreloadApi = typeof api;
|
||||
@@ -34,14 +36,11 @@ export type PreloadApi = typeof api;
|
||||
// just add to the DOM global.
|
||||
if (process.contextIsolated) {
|
||||
try {
|
||||
contextBridge.exposeInMainWorld('electron', electronAPI);
|
||||
contextBridge.exposeInMainWorld('api', api);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
} else {
|
||||
// @ts-ignore (define in dts)
|
||||
window.electron = electronAPI;
|
||||
// @ts-ignore (define in dts)
|
||||
window.api = api;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import { ipcRenderer } from 'electron';
|
||||
|
||||
const setLocalSurfaceVisible = (visible: boolean) => {
|
||||
ipcRenderer.send('visualizer-set-local-surface-visible', visible);
|
||||
};
|
||||
|
||||
export const visualizer = {
|
||||
setLocalSurfaceVisible,
|
||||
};
|
||||
|
||||
export type VisualizerApi = typeof visualizer;
|
||||
@@ -27,6 +27,7 @@ import { PlayCountColumn } from './play-count-column';
|
||||
import { RatingColumn } from './rating-column';
|
||||
import { ReleaseDateColumn } from './release-date-column';
|
||||
import { RowIndexColumn } from './row-index-column';
|
||||
import { ItemDetailRowPlayControlCell } from './row-play-control-cell';
|
||||
import { SampleRateColumn } from './sample-rate-column';
|
||||
import { SizeColumn } from './size-column';
|
||||
import { TitleArtistColumn } from './title-artist-column';
|
||||
@@ -111,6 +112,7 @@ export {
|
||||
GenreBadgeColumn,
|
||||
GenreColumn,
|
||||
ImageColumn,
|
||||
ItemDetailRowPlayControlCell,
|
||||
LastPlayedColumn,
|
||||
PathColumn,
|
||||
PlayCountColumn,
|
||||
|
||||
@@ -1,23 +1,40 @@
|
||||
import styles from './row-index-column.module.css';
|
||||
import { ItemDetailRowPlayControlCell } from './row-play-control-cell';
|
||||
import { ItemDetailListCellProps } from './types';
|
||||
import { useDetailRowPlayControl } from './use-detail-row-play-control';
|
||||
|
||||
import { useIsCurrentSong } from '/@/renderer/features/player/hooks/use-is-current-song';
|
||||
import { usePlayerStatus } from '/@/renderer/store';
|
||||
import { isRowPlayControlColumn } from '/@/renderer/components/item-list/helpers/get-row-play-control-column';
|
||||
import { Icon } from '/@/shared/components/icon/icon';
|
||||
import { PlayerStatus } from '/@/shared/types/types';
|
||||
import { TableColumn } from '/@/shared/types/types';
|
||||
|
||||
export const RowIndexColumn = ({ rowIndex, song }: ItemDetailListCellProps) => {
|
||||
const status = usePlayerStatus();
|
||||
const { isActive } = useIsCurrentSong(song);
|
||||
const isPlaying = isActive && status === PlayerStatus.PLAYING;
|
||||
const DefaultRowIndexColumn = ({ rowIndex }: ItemDetailListCellProps) => {
|
||||
return <>{String((rowIndex ?? 0) + 1)}</>;
|
||||
};
|
||||
|
||||
if (isActive) {
|
||||
return (
|
||||
const PlayableRowIndexColumn = (props: ItemDetailListCellProps) => {
|
||||
const { handlePlay, isActive, isPlaying, showPlayControls } = useDetailRowPlayControl(props);
|
||||
|
||||
const indexContent = isActive ? (
|
||||
<div className={styles.iconWrapper}>
|
||||
<Icon fill="primary" icon={isPlaying ? 'mediaPlay' : 'mediaPause'} />
|
||||
</div>
|
||||
) : (
|
||||
String((props.rowIndex ?? 0) + 1)
|
||||
);
|
||||
|
||||
return (
|
||||
<ItemDetailRowPlayControlCell
|
||||
indexContent={indexContent}
|
||||
onPlay={handlePlay}
|
||||
showPlayControls={showPlayControls}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const RowIndexColumn = (props: ItemDetailListCellProps) => {
|
||||
if (!props.columns || !isRowPlayControlColumn(TableColumn.ROW_INDEX, props.columns)) {
|
||||
return <DefaultRowIndexColumn {...props} />;
|
||||
}
|
||||
|
||||
return <>{String((rowIndex ?? 0) + 1)}</>;
|
||||
return <PlayableRowIndexColumn {...props} />;
|
||||
};
|
||||
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
.cell-wrapper {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
.play-target {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.index-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.icon-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
import styles from './row-play-control-cell.module.css';
|
||||
|
||||
import { ItemRowPlayControls } from '/@/renderer/features/shared/components/item-row-play-controls';
|
||||
import { HoverCard } from '/@/shared/components/hover-card/hover-card';
|
||||
import { Play } from '/@/shared/types/types';
|
||||
|
||||
export const ItemDetailRowPlayControlCell = ({
|
||||
indexContent,
|
||||
onPlay,
|
||||
showPlayControls,
|
||||
}: {
|
||||
indexContent: ReactNode;
|
||||
onPlay: (playType: Play) => void;
|
||||
showPlayControls: boolean;
|
||||
}) => {
|
||||
if (!showPlayControls) {
|
||||
return <>{indexContent}</>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.cellWrapper}>
|
||||
<HoverCard openDelay={300} position="top" withArrow>
|
||||
<HoverCard.Target>
|
||||
<div className={styles.playTarget}>{indexContent}</div>
|
||||
</HoverCard.Target>
|
||||
<HoverCard.Dropdown onClick={(e) => e.stopPropagation()}>
|
||||
<ItemRowPlayControls onPlay={onPlay} />
|
||||
</HoverCard.Dropdown>
|
||||
</HoverCard>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,7 +1,46 @@
|
||||
import { ItemDetailRowPlayControlCell } from './row-play-control-cell';
|
||||
import styles from './row-play-control-cell.module.css';
|
||||
import { ItemDetailListCellProps } from './types';
|
||||
import { useDetailRowPlayControl } from './use-detail-row-play-control';
|
||||
|
||||
export const TrackNumberColumn = ({ song }: ItemDetailListCellProps) => {
|
||||
import { isRowPlayControlColumn } from '/@/renderer/components/item-list/helpers/get-row-play-control-column';
|
||||
import { Icon } from '/@/shared/components/icon/icon';
|
||||
import { TableColumn } from '/@/shared/types/types';
|
||||
|
||||
const formatTrackNumber = (song: ItemDetailListCellProps['song']) => {
|
||||
const disc = song.discNumber ?? 1;
|
||||
const track = song.trackNumber.toString().padStart(2, '0');
|
||||
return `${disc}-${track}`;
|
||||
};
|
||||
|
||||
const DefaultTrackNumberColumn = ({ song }: ItemDetailListCellProps) => {
|
||||
return <>{formatTrackNumber(song)}</>;
|
||||
};
|
||||
|
||||
const PlayableTrackNumberColumn = (props: ItemDetailListCellProps) => {
|
||||
const { handlePlay, isActive, isPlaying, showPlayControls } = useDetailRowPlayControl(props);
|
||||
|
||||
const indexContent = isActive ? (
|
||||
<div className={styles.iconWrapper}>
|
||||
<Icon fill="primary" icon={isPlaying ? 'mediaPlay' : 'mediaPause'} />
|
||||
</div>
|
||||
) : (
|
||||
formatTrackNumber(props.song)
|
||||
);
|
||||
|
||||
return (
|
||||
<ItemDetailRowPlayControlCell
|
||||
indexContent={indexContent}
|
||||
onPlay={handlePlay}
|
||||
showPlayControls={showPlayControls}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const TrackNumberColumn = (props: ItemDetailListCellProps) => {
|
||||
if (!props.columns || !isRowPlayControlColumn(TableColumn.TRACK_NUMBER, props.columns)) {
|
||||
return <DefaultTrackNumberColumn {...props} />;
|
||||
}
|
||||
|
||||
return <PlayableTrackNumberColumn {...props} />;
|
||||
};
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { ItemListStateActions } from '/@/renderer/components/item-list/helpers/item-list-state';
|
||||
import { ItemControls } from '/@/renderer/components/item-list/types';
|
||||
import { ItemControls, ItemTableListColumnConfig } from '/@/renderer/components/item-list/types';
|
||||
import { Song } from '/@/shared/types/domain-types';
|
||||
|
||||
export interface ItemDetailListCellProps {
|
||||
columns?: ItemTableListColumnConfig[];
|
||||
controls?: ItemControls;
|
||||
internalState?: ItemListStateActions;
|
||||
isMutatingFavorite?: boolean;
|
||||
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { ItemDetailListCellProps } from './types';
|
||||
|
||||
import { playSongFromItemListControl } from '/@/renderer/components/item-list/helpers/play-row-from-list';
|
||||
import { usePlayer } from '/@/renderer/features/player/context/player-context';
|
||||
import { useIsCurrentSong } from '/@/renderer/features/player/hooks/use-is-current-song';
|
||||
import { usePlayerStatus } from '/@/renderer/store';
|
||||
import { Song } from '/@/shared/types/domain-types';
|
||||
import { Play, PlayerStatus } from '/@/shared/types/types';
|
||||
|
||||
export const useDetailRowPlayControl = ({
|
||||
internalState,
|
||||
rowIndex = 0,
|
||||
song,
|
||||
}: Pick<ItemDetailListCellProps, 'internalState' | 'rowIndex' | 'song'>) => {
|
||||
const status = usePlayerStatus();
|
||||
const player = usePlayer();
|
||||
const { isActive } = useIsCurrentSong(song);
|
||||
const isPlaying = isActive && status === PlayerStatus.PLAYING;
|
||||
|
||||
const showPlayControls = !!song?.id;
|
||||
|
||||
const handlePlay = useCallback(
|
||||
(playType: Play) => {
|
||||
if (!song) {
|
||||
return;
|
||||
}
|
||||
|
||||
playSongFromItemListControl({
|
||||
index: rowIndex,
|
||||
internalState,
|
||||
item: song as Song,
|
||||
meta: { playType, singleSongOnly: true },
|
||||
player,
|
||||
});
|
||||
},
|
||||
[internalState, player, rowIndex, song],
|
||||
);
|
||||
|
||||
return {
|
||||
handlePlay,
|
||||
isActive,
|
||||
isPlaying,
|
||||
showPlayControls,
|
||||
};
|
||||
};
|
||||
@@ -447,6 +447,14 @@
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.row .track-cell-play-control {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.track-row-dragging {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ import styles from './item-detail-list.module.css';
|
||||
import { ItemCardControls } from '/@/renderer/components/item-card/item-card-controls';
|
||||
import { ItemImage } from '/@/renderer/components/item-image/item-image';
|
||||
import { getDraggedItems } from '/@/renderer/components/item-list/helpers/get-dragged-items';
|
||||
import { isRowPlayControlColumn } from '/@/renderer/components/item-list/helpers/get-row-play-control-column';
|
||||
import { useDefaultItemListControls } from '/@/renderer/components/item-list/helpers/item-list-controls';
|
||||
import {
|
||||
ItemListStateActions,
|
||||
@@ -365,6 +366,7 @@ const TrackRow = memo(
|
||||
const isTitleColumn = col.id === TableColumn.TITLE;
|
||||
const isImageColumn = col.id === TableColumn.IMAGE;
|
||||
const isIconActionColumn = isNoHorizontalPaddingColumn(col.id);
|
||||
const isPlayControlColumn = isRowPlayControlColumn(col.id, columns);
|
||||
const showHoverContent = shouldShowHoverOnlyColumnContent(
|
||||
col.id,
|
||||
isRowHovered,
|
||||
@@ -374,6 +376,7 @@ const TrackRow = memo(
|
||||
const content = isSongsLoading ? null : showHoverContent ? (
|
||||
<CellComponent
|
||||
columnId={col.id}
|
||||
columns={columns}
|
||||
controls={controls}
|
||||
internalState={internalState}
|
||||
isMutatingFavorite={isMutatingFavorite}
|
||||
@@ -393,6 +396,7 @@ const TrackRow = memo(
|
||||
[styles.trackCellImage]: isImageColumn,
|
||||
[styles.trackCellMuted]: !isTitleColumn,
|
||||
[styles.trackCellNoHPadding]: isIconActionColumn,
|
||||
[styles.trackCellPlayControl]: isPlayControlColumn,
|
||||
[styles.trackCellVerticalBorderVisible]:
|
||||
enableVerticalBorders && !isLastColumn,
|
||||
[styles.trackCellWithVerticalBorder]: !isLastColumn,
|
||||
|
||||
@@ -12,10 +12,15 @@ import { Play } from '/@/shared/types/types';
|
||||
|
||||
interface PlayTrackRadioActionProps {
|
||||
disabled?: boolean;
|
||||
skipFirstSong?: boolean;
|
||||
song: Song;
|
||||
}
|
||||
|
||||
export const PlayTrackRadioAction = ({ disabled, song }: PlayTrackRadioActionProps) => {
|
||||
export const PlayTrackRadioAction = ({
|
||||
disabled,
|
||||
skipFirstSong,
|
||||
song,
|
||||
}: PlayTrackRadioActionProps) => {
|
||||
const { t } = useTranslation();
|
||||
const player = usePlayer();
|
||||
const serverId = useCurrentServerId();
|
||||
@@ -38,13 +43,17 @@ export const PlayTrackRadioAction = ({ disabled, song }: PlayTrackRadioActionPro
|
||||
});
|
||||
|
||||
if (similarSongs && similarSongs.length > 0) {
|
||||
player.addToQueueByData([song, ...similarSongs], playType);
|
||||
// We need to skip the first song when adding to the queue as NEXT or LAST, otherwise you will have a duplicate song
|
||||
const shouldSkipFirstSong =
|
||||
skipFirstSong && (playType === Play.NEXT || playType === Play.LAST);
|
||||
const queueSongs = shouldSkipFirstSong ? similarSongs : [song, ...similarSongs];
|
||||
player.addToQueueByData(queueSongs, playType);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load track radio:', error);
|
||||
}
|
||||
},
|
||||
[player, queryClient, serverId, song],
|
||||
[player, queryClient, serverId, skipFirstSong, song],
|
||||
);
|
||||
|
||||
const handlePlayTrackRadioNow = useCallback(() => {
|
||||
|
||||
@@ -35,7 +35,7 @@ export const QueueContextMenu = ({ items }: QueueContextMenuProps) => {
|
||||
<MoveQueueItemsAction items={items} />
|
||||
<ShuffleItemsAction items={items} />
|
||||
<ContextMenu.Divider />
|
||||
<PlayTrackRadioAction disabled={items.length > 1} song={items[0]} />
|
||||
<PlayTrackRadioAction disabled={items.length > 1} skipFirstSong song={items[0]} />
|
||||
<ContextMenu.Divider />
|
||||
<AddToPlaylistAction items={ids} itemType={LibraryItem.SONG} />
|
||||
<ContextMenu.Divider />
|
||||
|
||||
@@ -14,7 +14,10 @@ import { MediaSessionHook } from '/@/renderer/features/player/hooks/use-media-se
|
||||
import { MPRISHook } from '/@/renderer/features/player/hooks/use-mpris';
|
||||
import { PlaybackHotkeysHook } from '/@/renderer/features/player/hooks/use-playback-hotkeys';
|
||||
import { PowerSaveBlockerHook } from '/@/renderer/features/player/hooks/use-power-save-blocker';
|
||||
import { QueueRestoreTimestampHook } from '/@/renderer/features/player/hooks/use-queue-restore';
|
||||
import {
|
||||
InitialTimestampRestoreHook,
|
||||
QueueRestoreTimestampHook,
|
||||
} from '/@/renderer/features/player/hooks/use-queue-restore';
|
||||
import { ScrobbleHook } from '/@/renderer/features/player/hooks/use-scrobble';
|
||||
import { UpdateCurrentSongHook } from '/@/renderer/features/player/hooks/use-update-current-song';
|
||||
import { useWebAudio } from '/@/renderer/features/player/hooks/use-webaudio';
|
||||
@@ -134,6 +137,7 @@ export const AudioPlayers = () => {
|
||||
<RemoteHook />
|
||||
<AutoDJHook />
|
||||
<QueueRestoreTimestampHook />
|
||||
<InitialTimestampRestoreHook />
|
||||
<UpdateCurrentSongHook />
|
||||
<RadioAudioInstanceHook />
|
||||
<RadioMetadataHook />
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { t } from 'i18next';
|
||||
import { useCallback } from 'react';
|
||||
import { useCallback, useEffect, useRef } from 'react';
|
||||
|
||||
import { api } from '/@/renderer/api';
|
||||
import { usePlayerEvents } from '/@/renderer/features/player/audio-player/hooks/use-player-events';
|
||||
@@ -9,13 +9,18 @@ import { songsQueries } from '/@/renderer/features/songs/api/songs-api';
|
||||
import {
|
||||
setTimestamp,
|
||||
useCurrentServerId,
|
||||
usePlayerActions,
|
||||
usePlayerHydrated,
|
||||
usePlayerSong,
|
||||
usePlayerStatus,
|
||||
usePlayerStore,
|
||||
useTimestampStoreBase,
|
||||
} from '/@/renderer/store';
|
||||
import { toast } from '/@/shared/components/toast/toast';
|
||||
import { PlayerStatus } from '/@/shared/types/types';
|
||||
|
||||
export const useQueueRestoreTimestamp = () => {
|
||||
const player = usePlayerStore();
|
||||
const { mediaSeekToTimestamp } = usePlayerActions();
|
||||
|
||||
usePlayerEvents(
|
||||
{
|
||||
@@ -24,7 +29,7 @@ export const useQueueRestoreTimestamp = () => {
|
||||
|
||||
setTimeout(() => {
|
||||
setTimestamp(position);
|
||||
player.mediaSeekToTimestamp(position);
|
||||
mediaSeekToTimestamp(position);
|
||||
}, 100);
|
||||
},
|
||||
},
|
||||
@@ -37,6 +42,72 @@ export const QueueRestoreTimestampHook = () => {
|
||||
return null;
|
||||
};
|
||||
|
||||
export const useInitialTimestampRestore = () => {
|
||||
const { mediaSeekToTimestamp } = usePlayerActions();
|
||||
const playerHydrated = usePlayerHydrated();
|
||||
const currentSong = usePlayerSong();
|
||||
const playerStatus = usePlayerStatus();
|
||||
const timestamp = useTimestampStoreBase((state) => state.timestamp);
|
||||
|
||||
const startupRestoreInitializedRef = useRef(false);
|
||||
const startupSeekArmedRef = useRef<null | number>(null);
|
||||
const startupSeekAppliedRef = useRef(false);
|
||||
|
||||
const applyStartupSeek = useCallback(() => {
|
||||
if (startupSeekAppliedRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const seekTimestamp = startupSeekArmedRef.current;
|
||||
if (!seekTimestamp || seekTimestamp <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
startupSeekAppliedRef.current = true;
|
||||
startupSeekArmedRef.current = null;
|
||||
|
||||
setTimeout(() => {
|
||||
mediaSeekToTimestamp(seekTimestamp);
|
||||
}, 100);
|
||||
}, [mediaSeekToTimestamp]);
|
||||
|
||||
useEffect(() => {
|
||||
if (startupRestoreInitializedRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!playerHydrated || !currentSong) {
|
||||
return;
|
||||
}
|
||||
|
||||
startupRestoreInitializedRef.current = true;
|
||||
|
||||
if (timestamp > 0) {
|
||||
startupSeekArmedRef.current = timestamp;
|
||||
}
|
||||
|
||||
if (playerStatus === PlayerStatus.PLAYING) {
|
||||
applyStartupSeek();
|
||||
}
|
||||
}, [applyStartupSeek, currentSong, playerHydrated, playerStatus, timestamp]);
|
||||
|
||||
usePlayerEvents(
|
||||
{
|
||||
onPlayerStatus: (properties) => {
|
||||
if (properties.status === PlayerStatus.PLAYING) {
|
||||
applyStartupSeek();
|
||||
}
|
||||
},
|
||||
},
|
||||
[applyStartupSeek],
|
||||
);
|
||||
};
|
||||
|
||||
export const InitialTimestampRestoreHook = () => {
|
||||
useInitialTimestampRestore();
|
||||
return null;
|
||||
};
|
||||
|
||||
export const useSaveQueue = () => {
|
||||
const serverId = useCurrentServerId();
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ import { FontType } from '/@/shared/types/types';
|
||||
const localSettings = isElectron() ? window.api.localSettings : null;
|
||||
const ipc = isElectron() ? window.api.ipc : null;
|
||||
// Electron 32+ removed file.path, use this which is exposed in preload to get real path
|
||||
const webUtils = isElectron() ? window.electron.webUtils : null;
|
||||
const getPathForFile = isElectron() ? window.api.getPathForFile : null;
|
||||
|
||||
const HOME_FEATURE_STYLE_OPTIONS = [
|
||||
{
|
||||
@@ -295,7 +295,7 @@ export const ApplicationSettings = memo(() => {
|
||||
setSettings({
|
||||
font: {
|
||||
...fontSettings,
|
||||
custom: e ? webUtils?.getPathForFile(e) || null : null,
|
||||
custom: e ? getPathForFile?.(e) || null : null,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -21,6 +21,21 @@ type PromptState = 'loading' | { consent: boolean };
|
||||
|
||||
export function VisualizerSystemAudioBridgeHook() {
|
||||
const playbackType = usePlaybackType();
|
||||
const isVisualizerSurfaceVisible = useIsLocalVisualizerSurfaceVisible();
|
||||
|
||||
useEffect(() => {
|
||||
if (!isElectron()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const shouldReportVisible = playbackType === PlayerType.LOCAL && isVisualizerSurfaceVisible;
|
||||
|
||||
window.api.visualizer.setLocalSurfaceVisible(shouldReportVisible);
|
||||
|
||||
return () => {
|
||||
window.api.visualizer.setLocalSurfaceVisible(false);
|
||||
};
|
||||
}, [playbackType, isVisualizerSurfaceVisible]);
|
||||
|
||||
if (!isElectron() || playbackType !== PlayerType.LOCAL) {
|
||||
return null;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { subscribeWithSelector } from 'zustand/middleware';
|
||||
import { del, get, set } from 'idb-keyval';
|
||||
import { persist, subscribeWithSelector } from 'zustand/middleware';
|
||||
import { createWithEqualityFn } from 'zustand/traditional';
|
||||
|
||||
interface TimestampState {
|
||||
@@ -6,13 +7,36 @@ interface TimestampState {
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
const timestampStorage = {
|
||||
getItem: async (name: string) => {
|
||||
const value = await get(name);
|
||||
if (value === undefined) {
|
||||
return null;
|
||||
}
|
||||
return { state: { timestamp: value }, version: 1 } as const;
|
||||
},
|
||||
removeItem: async (name: string) => {
|
||||
await del(name);
|
||||
},
|
||||
setItem: async (name: string, value: { state: { timestamp: number }; version?: number }) => {
|
||||
await set(name, value.state.timestamp);
|
||||
},
|
||||
};
|
||||
|
||||
export const useTimestampStoreBase = createWithEqualityFn<TimestampState>()(
|
||||
persist(
|
||||
subscribeWithSelector((set) => ({
|
||||
setTimestamp: (timestamp: number) => {
|
||||
set({ timestamp });
|
||||
},
|
||||
timestamp: 0,
|
||||
})),
|
||||
{
|
||||
name: 'player-timestamp',
|
||||
storage: timestampStorage,
|
||||
version: 1,
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
export const subscribePlayerProgress = (
|
||||
|
||||
Reference in New Issue
Block a user