mirror of
https://github.com/jeffvli/feishin.git
synced 2026-07-22 10:26:33 +02:00
Compare commits
5 Commits
4008c8dfdf
...
8f40894926
| Author | SHA1 | Date | |
|---|---|---|---|
| 8f40894926 | |||
| 2befcb4e74 | |||
| 2d78c32a68 | |||
| 3d0500980a | |||
| 3551ee5077 |
@@ -4,3 +4,4 @@ import './player';
|
|||||||
import './remote';
|
import './remote';
|
||||||
import './settings';
|
import './settings';
|
||||||
import './discord-rpc';
|
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 { disableMediaKeys, enableMediaKeys } from './features/core/player/media-keys';
|
||||||
import { shutdownServer } from './features/core/remote';
|
import { shutdownServer } from './features/core/remote';
|
||||||
import { store } from './features/core/settings';
|
import { store } from './features/core/settings';
|
||||||
|
import { canHandleVisualizerDisplayMedia } from './features/core/visualizer';
|
||||||
import MenuBuilder, { MenuPlaybackState } from './menu';
|
import MenuBuilder, { MenuPlaybackState } from './menu';
|
||||||
|
import './features';
|
||||||
import {
|
import {
|
||||||
autoUpdaterLogInterface,
|
autoUpdaterLogInterface,
|
||||||
createLog,
|
createLog,
|
||||||
@@ -40,7 +42,6 @@ import {
|
|||||||
isMacOS,
|
isMacOS,
|
||||||
isWindows,
|
isWindows,
|
||||||
} from './utils';
|
} from './utils';
|
||||||
import './features';
|
|
||||||
|
|
||||||
import { PlayerRepeat, PlayerStatus, PlayerType, TitleTheme } from '/@/shared/types/types';
|
import { PlayerRepeat, PlayerStatus, PlayerType, TitleTheme } from '/@/shared/types/types';
|
||||||
|
|
||||||
@@ -515,7 +516,7 @@ async function createWindow(first = true): Promise<void> {
|
|||||||
backgroundThrottling: false,
|
backgroundThrottling: false,
|
||||||
contextIsolation: true,
|
contextIsolation: true,
|
||||||
devTools: true,
|
devTools: true,
|
||||||
nodeIntegration: true,
|
nodeIntegration: false,
|
||||||
preload: join(__dirname, '../preload/index.js'),
|
preload: join(__dirname, '../preload/index.js'),
|
||||||
sandbox: false,
|
sandbox: false,
|
||||||
webSecurity: !store.get('ignore_cors'),
|
webSecurity: !store.get('ignore_cors'),
|
||||||
@@ -734,6 +735,11 @@ async function createWindow(first = true): Promise<void> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
mainWindow.webContents.session.setDisplayMediaRequestHandler((_request, callback) => {
|
mainWindow.webContents.session.setDisplayMediaRequestHandler((_request, callback) => {
|
||||||
|
if (!canHandleVisualizerDisplayMedia()) {
|
||||||
|
callback({});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!isMacOS()) {
|
if (!isMacOS()) {
|
||||||
callback({ audio: 'loopback' });
|
callback({ audio: 'loopback' });
|
||||||
return;
|
return;
|
||||||
|
|||||||
Vendored
-3
@@ -1,11 +1,8 @@
|
|||||||
import { ElectronAPI } from '@electron-toolkit/preload';
|
|
||||||
|
|
||||||
import { PreloadApi } from './index';
|
import { PreloadApi } from './index';
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
api: PreloadApi;
|
api: PreloadApi;
|
||||||
electron: ElectronAPI;
|
|
||||||
LEGACY_AUTHENTICATION?: boolean;
|
LEGACY_AUTHENTICATION?: boolean;
|
||||||
queryLocalFonts?: () => Promise<Font[]>;
|
queryLocalFonts?: () => Promise<Font[]>;
|
||||||
REMOTE_URL?: string;
|
REMOTE_URL?: string;
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { electronAPI } from '@electron-toolkit/preload';
|
import { contextBridge, webUtils } from 'electron';
|
||||||
import { contextBridge } from 'electron';
|
|
||||||
|
|
||||||
import { autodiscover } from './autodiscover';
|
import { autodiscover } from './autodiscover';
|
||||||
import { browser } from './browser';
|
import { browser } from './browser';
|
||||||
@@ -11,12 +10,14 @@ import { mpris } from './mpris';
|
|||||||
import { mpvPlayer, mpvPlayerListener } from './mpv-player';
|
import { mpvPlayer, mpvPlayerListener } from './mpv-player';
|
||||||
import { remote } from './remote';
|
import { remote } from './remote';
|
||||||
import { utils } from './utils';
|
import { utils } from './utils';
|
||||||
|
import { visualizer } from './visualizer';
|
||||||
|
|
||||||
// Custom APIs for renderer
|
// Custom APIs for renderer
|
||||||
const api = {
|
const api = {
|
||||||
autodiscover,
|
autodiscover,
|
||||||
browser,
|
browser,
|
||||||
discordRpc,
|
discordRpc,
|
||||||
|
getPathForFile: webUtils.getPathForFile,
|
||||||
ipc,
|
ipc,
|
||||||
localSettings,
|
localSettings,
|
||||||
lyrics,
|
lyrics,
|
||||||
@@ -25,6 +26,7 @@ const api = {
|
|||||||
mpvPlayerListener,
|
mpvPlayerListener,
|
||||||
remote,
|
remote,
|
||||||
utils,
|
utils,
|
||||||
|
visualizer,
|
||||||
};
|
};
|
||||||
|
|
||||||
export type PreloadApi = typeof api;
|
export type PreloadApi = typeof api;
|
||||||
@@ -34,14 +36,11 @@ export type PreloadApi = typeof api;
|
|||||||
// just add to the DOM global.
|
// just add to the DOM global.
|
||||||
if (process.contextIsolated) {
|
if (process.contextIsolated) {
|
||||||
try {
|
try {
|
||||||
contextBridge.exposeInMainWorld('electron', electronAPI);
|
|
||||||
contextBridge.exposeInMainWorld('api', api);
|
contextBridge.exposeInMainWorld('api', api);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// @ts-ignore (define in dts)
|
|
||||||
window.electron = electronAPI;
|
|
||||||
// @ts-ignore (define in dts)
|
// @ts-ignore (define in dts)
|
||||||
window.api = api;
|
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 { RatingColumn } from './rating-column';
|
||||||
import { ReleaseDateColumn } from './release-date-column';
|
import { ReleaseDateColumn } from './release-date-column';
|
||||||
import { RowIndexColumn } from './row-index-column';
|
import { RowIndexColumn } from './row-index-column';
|
||||||
|
import { ItemDetailRowPlayControlCell } from './row-play-control-cell';
|
||||||
import { SampleRateColumn } from './sample-rate-column';
|
import { SampleRateColumn } from './sample-rate-column';
|
||||||
import { SizeColumn } from './size-column';
|
import { SizeColumn } from './size-column';
|
||||||
import { TitleArtistColumn } from './title-artist-column';
|
import { TitleArtistColumn } from './title-artist-column';
|
||||||
@@ -111,6 +112,7 @@ export {
|
|||||||
GenreBadgeColumn,
|
GenreBadgeColumn,
|
||||||
GenreColumn,
|
GenreColumn,
|
||||||
ImageColumn,
|
ImageColumn,
|
||||||
|
ItemDetailRowPlayControlCell,
|
||||||
LastPlayedColumn,
|
LastPlayedColumn,
|
||||||
PathColumn,
|
PathColumn,
|
||||||
PlayCountColumn,
|
PlayCountColumn,
|
||||||
|
|||||||
@@ -1,23 +1,40 @@
|
|||||||
import styles from './row-index-column.module.css';
|
import styles from './row-index-column.module.css';
|
||||||
|
import { ItemDetailRowPlayControlCell } from './row-play-control-cell';
|
||||||
import { ItemDetailListCellProps } from './types';
|
import { ItemDetailListCellProps } from './types';
|
||||||
|
import { useDetailRowPlayControl } from './use-detail-row-play-control';
|
||||||
|
|
||||||
import { useIsCurrentSong } from '/@/renderer/features/player/hooks/use-is-current-song';
|
import { isRowPlayControlColumn } from '/@/renderer/components/item-list/helpers/get-row-play-control-column';
|
||||||
import { usePlayerStatus } from '/@/renderer/store';
|
|
||||||
import { Icon } from '/@/shared/components/icon/icon';
|
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;
|
|
||||||
|
|
||||||
if (isActive) {
|
|
||||||
return (
|
|
||||||
<div className={styles.iconWrapper}>
|
|
||||||
<Icon fill="primary" icon={isPlaying ? 'mediaPlay' : 'mediaPause'} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
const DefaultRowIndexColumn = ({ rowIndex }: ItemDetailListCellProps) => {
|
||||||
return <>{String((rowIndex ?? 0) + 1)}</>;
|
return <>{String((rowIndex ?? 0) + 1)}</>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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 <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 { 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 disc = song.discNumber ?? 1;
|
||||||
const track = song.trackNumber.toString().padStart(2, '0');
|
const track = song.trackNumber.toString().padStart(2, '0');
|
||||||
return `${disc}-${track}`;
|
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 { 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';
|
import { Song } from '/@/shared/types/domain-types';
|
||||||
|
|
||||||
export interface ItemDetailListCellProps {
|
export interface ItemDetailListCellProps {
|
||||||
|
columns?: ItemTableListColumnConfig[];
|
||||||
controls?: ItemControls;
|
controls?: ItemControls;
|
||||||
internalState?: ItemListStateActions;
|
internalState?: ItemListStateActions;
|
||||||
isMutatingFavorite?: boolean;
|
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;
|
padding-left: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.row .track-cell-play-control {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
.track-row-dragging {
|
.track-row-dragging {
|
||||||
opacity: 0.5;
|
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 { ItemCardControls } from '/@/renderer/components/item-card/item-card-controls';
|
||||||
import { ItemImage } from '/@/renderer/components/item-image/item-image';
|
import { ItemImage } from '/@/renderer/components/item-image/item-image';
|
||||||
import { getDraggedItems } from '/@/renderer/components/item-list/helpers/get-dragged-items';
|
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 { useDefaultItemListControls } from '/@/renderer/components/item-list/helpers/item-list-controls';
|
||||||
import {
|
import {
|
||||||
ItemListStateActions,
|
ItemListStateActions,
|
||||||
@@ -365,6 +366,7 @@ const TrackRow = memo(
|
|||||||
const isTitleColumn = col.id === TableColumn.TITLE;
|
const isTitleColumn = col.id === TableColumn.TITLE;
|
||||||
const isImageColumn = col.id === TableColumn.IMAGE;
|
const isImageColumn = col.id === TableColumn.IMAGE;
|
||||||
const isIconActionColumn = isNoHorizontalPaddingColumn(col.id);
|
const isIconActionColumn = isNoHorizontalPaddingColumn(col.id);
|
||||||
|
const isPlayControlColumn = isRowPlayControlColumn(col.id, columns);
|
||||||
const showHoverContent = shouldShowHoverOnlyColumnContent(
|
const showHoverContent = shouldShowHoverOnlyColumnContent(
|
||||||
col.id,
|
col.id,
|
||||||
isRowHovered,
|
isRowHovered,
|
||||||
@@ -374,6 +376,7 @@ const TrackRow = memo(
|
|||||||
const content = isSongsLoading ? null : showHoverContent ? (
|
const content = isSongsLoading ? null : showHoverContent ? (
|
||||||
<CellComponent
|
<CellComponent
|
||||||
columnId={col.id}
|
columnId={col.id}
|
||||||
|
columns={columns}
|
||||||
controls={controls}
|
controls={controls}
|
||||||
internalState={internalState}
|
internalState={internalState}
|
||||||
isMutatingFavorite={isMutatingFavorite}
|
isMutatingFavorite={isMutatingFavorite}
|
||||||
@@ -393,6 +396,7 @@ const TrackRow = memo(
|
|||||||
[styles.trackCellImage]: isImageColumn,
|
[styles.trackCellImage]: isImageColumn,
|
||||||
[styles.trackCellMuted]: !isTitleColumn,
|
[styles.trackCellMuted]: !isTitleColumn,
|
||||||
[styles.trackCellNoHPadding]: isIconActionColumn,
|
[styles.trackCellNoHPadding]: isIconActionColumn,
|
||||||
|
[styles.trackCellPlayControl]: isPlayControlColumn,
|
||||||
[styles.trackCellVerticalBorderVisible]:
|
[styles.trackCellVerticalBorderVisible]:
|
||||||
enableVerticalBorders && !isLastColumn,
|
enableVerticalBorders && !isLastColumn,
|
||||||
[styles.trackCellWithVerticalBorder]: !isLastColumn,
|
[styles.trackCellWithVerticalBorder]: !isLastColumn,
|
||||||
|
|||||||
@@ -12,10 +12,15 @@ import { Play } from '/@/shared/types/types';
|
|||||||
|
|
||||||
interface PlayTrackRadioActionProps {
|
interface PlayTrackRadioActionProps {
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
|
skipFirstSong?: boolean;
|
||||||
song: Song;
|
song: Song;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PlayTrackRadioAction = ({ disabled, song }: PlayTrackRadioActionProps) => {
|
export const PlayTrackRadioAction = ({
|
||||||
|
disabled,
|
||||||
|
skipFirstSong,
|
||||||
|
song,
|
||||||
|
}: PlayTrackRadioActionProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const player = usePlayer();
|
const player = usePlayer();
|
||||||
const serverId = useCurrentServerId();
|
const serverId = useCurrentServerId();
|
||||||
@@ -38,13 +43,17 @@ export const PlayTrackRadioAction = ({ disabled, song }: PlayTrackRadioActionPro
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (similarSongs && similarSongs.length > 0) {
|
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) {
|
} catch (error) {
|
||||||
console.error('Failed to load track radio:', error);
|
console.error('Failed to load track radio:', error);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[player, queryClient, serverId, song],
|
[player, queryClient, serverId, skipFirstSong, song],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handlePlayTrackRadioNow = useCallback(() => {
|
const handlePlayTrackRadioNow = useCallback(() => {
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ export const QueueContextMenu = ({ items }: QueueContextMenuProps) => {
|
|||||||
<MoveQueueItemsAction items={items} />
|
<MoveQueueItemsAction items={items} />
|
||||||
<ShuffleItemsAction items={items} />
|
<ShuffleItemsAction items={items} />
|
||||||
<ContextMenu.Divider />
|
<ContextMenu.Divider />
|
||||||
<PlayTrackRadioAction disabled={items.length > 1} song={items[0]} />
|
<PlayTrackRadioAction disabled={items.length > 1} skipFirstSong song={items[0]} />
|
||||||
<ContextMenu.Divider />
|
<ContextMenu.Divider />
|
||||||
<AddToPlaylistAction items={ids} itemType={LibraryItem.SONG} />
|
<AddToPlaylistAction items={ids} itemType={LibraryItem.SONG} />
|
||||||
<ContextMenu.Divider />
|
<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 { MPRISHook } from '/@/renderer/features/player/hooks/use-mpris';
|
||||||
import { PlaybackHotkeysHook } from '/@/renderer/features/player/hooks/use-playback-hotkeys';
|
import { PlaybackHotkeysHook } from '/@/renderer/features/player/hooks/use-playback-hotkeys';
|
||||||
import { PowerSaveBlockerHook } from '/@/renderer/features/player/hooks/use-power-save-blocker';
|
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 { ScrobbleHook } from '/@/renderer/features/player/hooks/use-scrobble';
|
||||||
import { UpdateCurrentSongHook } from '/@/renderer/features/player/hooks/use-update-current-song';
|
import { UpdateCurrentSongHook } from '/@/renderer/features/player/hooks/use-update-current-song';
|
||||||
import { useWebAudio } from '/@/renderer/features/player/hooks/use-webaudio';
|
import { useWebAudio } from '/@/renderer/features/player/hooks/use-webaudio';
|
||||||
@@ -134,6 +137,7 @@ export const AudioPlayers = () => {
|
|||||||
<RemoteHook />
|
<RemoteHook />
|
||||||
<AutoDJHook />
|
<AutoDJHook />
|
||||||
<QueueRestoreTimestampHook />
|
<QueueRestoreTimestampHook />
|
||||||
|
<InitialTimestampRestoreHook />
|
||||||
<UpdateCurrentSongHook />
|
<UpdateCurrentSongHook />
|
||||||
<RadioAudioInstanceHook />
|
<RadioAudioInstanceHook />
|
||||||
<RadioMetadataHook />
|
<RadioMetadataHook />
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import { t } from 'i18next';
|
import { t } from 'i18next';
|
||||||
import { useCallback } from 'react';
|
import { useCallback, useEffect, useRef } from 'react';
|
||||||
|
|
||||||
import { api } from '/@/renderer/api';
|
import { api } from '/@/renderer/api';
|
||||||
import { usePlayerEvents } from '/@/renderer/features/player/audio-player/hooks/use-player-events';
|
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 {
|
import {
|
||||||
setTimestamp,
|
setTimestamp,
|
||||||
useCurrentServerId,
|
useCurrentServerId,
|
||||||
|
usePlayerActions,
|
||||||
|
usePlayerHydrated,
|
||||||
|
usePlayerSong,
|
||||||
|
usePlayerStatus,
|
||||||
usePlayerStore,
|
usePlayerStore,
|
||||||
useTimestampStoreBase,
|
useTimestampStoreBase,
|
||||||
} from '/@/renderer/store';
|
} from '/@/renderer/store';
|
||||||
import { toast } from '/@/shared/components/toast/toast';
|
import { toast } from '/@/shared/components/toast/toast';
|
||||||
|
import { PlayerStatus } from '/@/shared/types/types';
|
||||||
|
|
||||||
export const useQueueRestoreTimestamp = () => {
|
export const useQueueRestoreTimestamp = () => {
|
||||||
const player = usePlayerStore();
|
const { mediaSeekToTimestamp } = usePlayerActions();
|
||||||
|
|
||||||
usePlayerEvents(
|
usePlayerEvents(
|
||||||
{
|
{
|
||||||
@@ -24,7 +29,7 @@ export const useQueueRestoreTimestamp = () => {
|
|||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setTimestamp(position);
|
setTimestamp(position);
|
||||||
player.mediaSeekToTimestamp(position);
|
mediaSeekToTimestamp(position);
|
||||||
}, 100);
|
}, 100);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -37,6 +42,72 @@ export const QueueRestoreTimestampHook = () => {
|
|||||||
return null;
|
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 = () => {
|
export const useSaveQueue = () => {
|
||||||
const serverId = useCurrentServerId();
|
const serverId = useCurrentServerId();
|
||||||
|
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ import { FontType } from '/@/shared/types/types';
|
|||||||
const localSettings = isElectron() ? window.api.localSettings : null;
|
const localSettings = isElectron() ? window.api.localSettings : null;
|
||||||
const ipc = isElectron() ? window.api.ipc : null;
|
const ipc = isElectron() ? window.api.ipc : null;
|
||||||
// Electron 32+ removed file.path, use this which is exposed in preload to get real path
|
// 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 = [
|
const HOME_FEATURE_STYLE_OPTIONS = [
|
||||||
{
|
{
|
||||||
@@ -295,7 +295,7 @@ export const ApplicationSettings = memo(() => {
|
|||||||
setSettings({
|
setSettings({
|
||||||
font: {
|
font: {
|
||||||
...fontSettings,
|
...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() {
|
export function VisualizerSystemAudioBridgeHook() {
|
||||||
const playbackType = usePlaybackType();
|
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) {
|
if (!isElectron() || playbackType !== PlayerType.LOCAL) {
|
||||||
return null;
|
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';
|
import { createWithEqualityFn } from 'zustand/traditional';
|
||||||
|
|
||||||
interface TimestampState {
|
interface TimestampState {
|
||||||
@@ -6,13 +7,36 @@ interface TimestampState {
|
|||||||
timestamp: number;
|
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>()(
|
export const useTimestampStoreBase = createWithEqualityFn<TimestampState>()(
|
||||||
subscribeWithSelector((set) => ({
|
persist(
|
||||||
setTimestamp: (timestamp: number) => {
|
subscribeWithSelector((set) => ({
|
||||||
set({ timestamp });
|
setTimestamp: (timestamp: number) => {
|
||||||
|
set({ timestamp });
|
||||||
|
},
|
||||||
|
timestamp: 0,
|
||||||
|
})),
|
||||||
|
{
|
||||||
|
name: 'player-timestamp',
|
||||||
|
storage: timestampStorage,
|
||||||
|
version: 1,
|
||||||
},
|
},
|
||||||
timestamp: 0,
|
),
|
||||||
})),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
export const subscribePlayerProgress = (
|
export const subscribePlayerProgress = (
|
||||||
|
|||||||
Reference in New Issue
Block a user