mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-09 20:29:36 +02:00
add more dynamic imports to optimize bundle
This commit is contained in:
+10
-3
@@ -7,12 +7,11 @@ import '@mantine/core/styles.css';
|
|||||||
import '@mantine/dates/styles.css';
|
import '@mantine/dates/styles.css';
|
||||||
import '@mantine/notifications/styles.css';
|
import '@mantine/notifications/styles.css';
|
||||||
import isElectron from 'is-electron';
|
import isElectron from 'is-electron';
|
||||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
import { lazy, Suspense, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
|
|
||||||
import i18n from '/@/i18n/i18n';
|
import i18n from '/@/i18n/i18n';
|
||||||
import { WebAudioContext } from '/@/renderer/features/player/context/webaudio-context';
|
import { WebAudioContext } from '/@/renderer/features/player/context/webaudio-context';
|
||||||
import { useSyncSettingsToMain } from '/@/renderer/hooks/use-sync-settings-to-main';
|
import { useSyncSettingsToMain } from '/@/renderer/hooks/use-sync-settings-to-main';
|
||||||
import { ReleaseNotesModal } from './release-notes-modal';
|
|
||||||
import { AppRouter } from '/@/renderer/router/app-router';
|
import { AppRouter } from '/@/renderer/router/app-router';
|
||||||
import { useCssSettings, useHotkeySettings, useLanguage } from '/@/renderer/store';
|
import { useCssSettings, useHotkeySettings, useLanguage } from '/@/renderer/store';
|
||||||
import { useAppTheme } from '/@/renderer/themes/use-app-theme';
|
import { useAppTheme } from '/@/renderer/themes/use-app-theme';
|
||||||
@@ -22,6 +21,12 @@ import '/@/shared/styles/global.css';
|
|||||||
import { PlayerProvider } from '/@/renderer/features/player/context/player-context';
|
import { PlayerProvider } from '/@/renderer/features/player/context/player-context';
|
||||||
import { AudioPlayers } from '/@/renderer/features/player/components/audio-players';
|
import { AudioPlayers } from '/@/renderer/features/player/components/audio-players';
|
||||||
|
|
||||||
|
const ReleaseNotesModal = lazy(() =>
|
||||||
|
import('./release-notes-modal').then((module) => ({
|
||||||
|
default: module.ReleaseNotesModal,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
|
||||||
const ipc = isElectron() ? window.api.ipc : null;
|
const ipc = isElectron() ? window.api.ipc : null;
|
||||||
|
|
||||||
export const App = () => {
|
export const App = () => {
|
||||||
@@ -95,7 +100,9 @@ export const App = () => {
|
|||||||
<AppRouter />
|
<AppRouter />
|
||||||
</PlayerProvider>
|
</PlayerProvider>
|
||||||
</WebAudioContext.Provider>
|
</WebAudioContext.Provider>
|
||||||
<ReleaseNotesModal />
|
<Suspense fallback={null}>
|
||||||
|
<ReleaseNotesModal />
|
||||||
|
</Suspense>
|
||||||
</MantineProvider>
|
</MantineProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { openUpdatePlaylistModal } from '/@/renderer/features/playlists/components/update-playlist-form';
|
import { openUpdatePlaylistModal } from '/@/renderer/features/playlists/components/update-playlist-modal';
|
||||||
import { ContextMenu } from '/@/shared/components/context-menu/context-menu';
|
import { ContextMenu } from '/@/shared/components/context-menu/context-menu';
|
||||||
import { Playlist } from '/@/shared/types/domain-types';
|
import { Playlist } from '/@/shared/types/domain-types';
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +1,63 @@
|
|||||||
import { useQueryClient } from '@tanstack/react-query';
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
import { useEffect, useRef } from 'react';
|
import { lazy, Suspense, useEffect, useRef } from 'react';
|
||||||
import { createCallable } from 'react-call';
|
import { createCallable } from 'react-call';
|
||||||
import { useParams } from 'react-router';
|
import { useParams } from 'react-router';
|
||||||
|
|
||||||
import { AlbumArtistContextMenu } from '/@/renderer/features/context-menu/menus/album-artist-context-menu';
|
|
||||||
import { AlbumContextMenu } from '/@/renderer/features/context-menu/menus/album-context-menu';
|
|
||||||
import { ArtistContextMenu } from '/@/renderer/features/context-menu/menus/artist-context-menu';
|
|
||||||
import { FolderContextMenu } from '/@/renderer/features/context-menu/menus/folder-context-menu';
|
|
||||||
import { GenreContextMenu } from '/@/renderer/features/context-menu/menus/genre-context-menu';
|
|
||||||
import { PlaylistContextMenu } from '/@/renderer/features/context-menu/menus/playlist-context-menu';
|
|
||||||
import { PlaylistSongContextMenu } from '/@/renderer/features/context-menu/menus/playlist-song-context-menu';
|
|
||||||
import { QueueContextMenu } from '/@/renderer/features/context-menu/menus/queue-context-menu';
|
|
||||||
import { SongContextMenu } from '/@/renderer/features/context-menu/menus/song-context-menu';
|
|
||||||
import { ContextMenu } from '/@/shared/components/context-menu/context-menu';
|
import { ContextMenu } from '/@/shared/components/context-menu/context-menu';
|
||||||
|
|
||||||
|
const AlbumArtistContextMenu = lazy(() =>
|
||||||
|
import('/@/renderer/features/context-menu/menus/album-artist-context-menu').then((module) => ({
|
||||||
|
default: module.AlbumArtistContextMenu,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
|
||||||
|
const AlbumContextMenu = lazy(() =>
|
||||||
|
import('/@/renderer/features/context-menu/menus/album-context-menu').then((module) => ({
|
||||||
|
default: module.AlbumContextMenu,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
|
||||||
|
const ArtistContextMenu = lazy(() =>
|
||||||
|
import('/@/renderer/features/context-menu/menus/artist-context-menu').then((module) => ({
|
||||||
|
default: module.ArtistContextMenu,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
|
||||||
|
const FolderContextMenu = lazy(() =>
|
||||||
|
import('/@/renderer/features/context-menu/menus/folder-context-menu').then((module) => ({
|
||||||
|
default: module.FolderContextMenu,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
|
||||||
|
const GenreContextMenu = lazy(() =>
|
||||||
|
import('/@/renderer/features/context-menu/menus/genre-context-menu').then((module) => ({
|
||||||
|
default: module.GenreContextMenu,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
|
||||||
|
const PlaylistContextMenu = lazy(() =>
|
||||||
|
import('/@/renderer/features/context-menu/menus/playlist-context-menu').then((module) => ({
|
||||||
|
default: module.PlaylistContextMenu,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
|
||||||
|
const PlaylistSongContextMenu = lazy(() =>
|
||||||
|
import('/@/renderer/features/context-menu/menus/playlist-song-context-menu').then((module) => ({
|
||||||
|
default: module.PlaylistSongContextMenu,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
|
||||||
|
const QueueContextMenu = lazy(() =>
|
||||||
|
import('/@/renderer/features/context-menu/menus/queue-context-menu').then((module) => ({
|
||||||
|
default: module.QueueContextMenu,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
|
||||||
|
const SongContextMenu = lazy(() =>
|
||||||
|
import('/@/renderer/features/context-menu/menus/song-context-menu').then((module) => ({
|
||||||
|
default: module.SongContextMenu,
|
||||||
|
})),
|
||||||
|
);
|
||||||
import {
|
import {
|
||||||
Album,
|
Album,
|
||||||
AlbumArtist,
|
AlbumArtist,
|
||||||
@@ -80,15 +125,17 @@ export const ContextMenuController = createCallable<ContextMenuControllerProps,
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</ContextMenu.Target>
|
</ContextMenu.Target>
|
||||||
{cmd.type === LibraryItem.QUEUE_SONG && <QueueContextMenu {...cmd} />}
|
<Suspense fallback={null}>
|
||||||
{cmd.type === LibraryItem.ALBUM && <AlbumContextMenu {...cmd} />}
|
{cmd.type === LibraryItem.QUEUE_SONG && <QueueContextMenu {...cmd} />}
|
||||||
{cmd.type === LibraryItem.ALBUM_ARTIST && <AlbumArtistContextMenu {...cmd} />}
|
{cmd.type === LibraryItem.ALBUM && <AlbumContextMenu {...cmd} />}
|
||||||
{cmd.type === LibraryItem.ARTIST && <ArtistContextMenu {...cmd} />}
|
{cmd.type === LibraryItem.ALBUM_ARTIST && <AlbumArtistContextMenu {...cmd} />}
|
||||||
{cmd.type === LibraryItem.FOLDER && <FolderContextMenu {...cmd} />}
|
{cmd.type === LibraryItem.ARTIST && <ArtistContextMenu {...cmd} />}
|
||||||
{cmd.type === LibraryItem.GENRE && <GenreContextMenu {...cmd} />}
|
{cmd.type === LibraryItem.FOLDER && <FolderContextMenu {...cmd} />}
|
||||||
{cmd.type === LibraryItem.PLAYLIST && <PlaylistContextMenu {...cmd} />}
|
{cmd.type === LibraryItem.GENRE && <GenreContextMenu {...cmd} />}
|
||||||
{cmd.type === LibraryItem.PLAYLIST_SONG && <PlaylistSongContextMenu {...cmd} />}
|
{cmd.type === LibraryItem.PLAYLIST && <PlaylistContextMenu {...cmd} />}
|
||||||
{cmd.type === LibraryItem.SONG && <SongContextMenu {...cmd} />}
|
{cmd.type === LibraryItem.PLAYLIST_SONG && <PlaylistSongContextMenu {...cmd} />}
|
||||||
|
{cmd.type === LibraryItem.SONG && <SongContextMenu {...cmd} />}
|
||||||
|
</Suspense>
|
||||||
</ContextMenu>
|
</ContextMenu>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { usePlayerEvents } from '/@/renderer/features/player/audio-player/hooks/
|
|||||||
import { getSongUrl } from '/@/renderer/features/player/audio-player/hooks/use-stream-url';
|
import { getSongUrl } from '/@/renderer/features/player/audio-player/hooks/use-stream-url';
|
||||||
import { AudioPlayer, PlayerOnProgressProps } from '/@/renderer/features/player/audio-player/types';
|
import { AudioPlayer, PlayerOnProgressProps } from '/@/renderer/features/player/audio-player/types';
|
||||||
import { useRadioStore } from '/@/renderer/features/radio/hooks/use-radio-player';
|
import { useRadioStore } from '/@/renderer/features/radio/hooks/use-radio-player';
|
||||||
import { getMpvProperties } from '/@/renderer/features/settings/components/playback/mpv-settings';
|
import { getMpvProperties } from '/@/renderer/features/settings/components/playback/mpv-properties';
|
||||||
import {
|
import {
|
||||||
usePlaybackSettings,
|
usePlaybackSettings,
|
||||||
usePlayerActions,
|
usePlayerActions,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { RefObject } from 'react';
|
import type { RefObject } from 'react';
|
||||||
|
import type ReactPlayer from 'react-player';
|
||||||
|
|
||||||
import { useCallback, useEffect, useImperativeHandle, useRef, useState } from 'react';
|
import { useCallback, useEffect, useImperativeHandle, useRef, useState } from 'react';
|
||||||
import ReactPlayer from 'react-player';
|
|
||||||
|
|
||||||
import { AudioPlayer, PlayerOnProgressProps } from '/@/renderer/features/player/audio-player/types';
|
import { AudioPlayer, PlayerOnProgressProps } from '/@/renderer/features/player/audio-player/types';
|
||||||
import { convertToLogVolume } from '/@/renderer/features/player/audio-player/utils/player-utils';
|
import { convertToLogVolume } from '/@/renderer/features/player/audio-player/utils/player-utils';
|
||||||
@@ -69,6 +69,31 @@ export const WebPlayerEngine = (props: WebPlayerEngineProps) => {
|
|||||||
|
|
||||||
const player1Ref = useRef<null | ReactPlayer>(null);
|
const player1Ref = useRef<null | ReactPlayer>(null);
|
||||||
const player2Ref = useRef<null | ReactPlayer>(null);
|
const player2Ref = useRef<null | ReactPlayer>(null);
|
||||||
|
const [ReactPlayerComponent, setReactPlayerComponent] = useState<any>(null);
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let isMounted = true;
|
||||||
|
|
||||||
|
const loadReactPlayer = async () => {
|
||||||
|
try {
|
||||||
|
const module = await import('react-player');
|
||||||
|
if (isMounted) {
|
||||||
|
setReactPlayerComponent(() => module.default);
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load react-player:', error);
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
loadReactPlayer();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
isMounted = false;
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
const [internalVolume1, setInternalVolume1] = useState(volume / 100 || 0);
|
const [internalVolume1, setInternalVolume1] = useState(volume / 100 || 0);
|
||||||
const [internalVolume2, setInternalVolume2] = useState(volume / 100 || 0);
|
const [internalVolume2, setInternalVolume2] = useState(volume / 100 || 0);
|
||||||
@@ -184,9 +209,13 @@ export const WebPlayerEngine = (props: WebPlayerEngineProps) => {
|
|||||||
[onStartedPlayer2, preservesPitch],
|
[onStartedPlayer2, preservesPitch],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (isLoading || !ReactPlayerComponent) {
|
||||||
|
return <div id="web-player-engine" style={{ display: 'none' }} />;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div id="web-player-engine" style={{ display: 'none' }}>
|
<div id="web-player-engine" style={{ display: 'none' }}>
|
||||||
<ReactPlayer
|
<ReactPlayerComponent
|
||||||
config={{
|
config={{
|
||||||
file: { attributes: { crossOrigin: 'anonymous' }, forceAudio: true },
|
file: { attributes: { crossOrigin: 'anonymous' }, forceAudio: true },
|
||||||
}}
|
}}
|
||||||
@@ -206,7 +235,7 @@ export const WebPlayerEngine = (props: WebPlayerEngineProps) => {
|
|||||||
volume={volume1}
|
volume={volume1}
|
||||||
width={0}
|
width={0}
|
||||||
/>
|
/>
|
||||||
<ReactPlayer
|
<ReactPlayerComponent
|
||||||
config={{
|
config={{
|
||||||
file: { attributes: { crossOrigin: 'anonymous' }, forceAudio: true },
|
file: { attributes: { crossOrigin: 'anonymous' }, forceAudio: true },
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -1,16 +1,22 @@
|
|||||||
import formatDuration from 'format-duration';
|
import formatDuration from 'format-duration';
|
||||||
import { memo } from 'react';
|
import { lazy, memo, Suspense } from 'react';
|
||||||
|
|
||||||
import styles from './mobile-fullscreen-player.module.css';
|
import styles from './mobile-fullscreen-player.module.css';
|
||||||
|
|
||||||
import { PlayerbarSeekSlider } from '/@/renderer/features/player/components/playerbar-seek-slider';
|
import { PlayerbarSeekSlider } from '/@/renderer/features/player/components/playerbar-seek-slider';
|
||||||
import { PlayerbarWaveform } from '/@/renderer/features/player/components/playerbar-waveform';
|
|
||||||
import { usePlayerTimestamp } from '/@/renderer/store';
|
import { usePlayerTimestamp } from '/@/renderer/store';
|
||||||
import { PlayerbarSliderType, usePlayerbarSlider } from '/@/renderer/store/settings.store';
|
import { PlayerbarSliderType, usePlayerbarSlider } from '/@/renderer/store/settings.store';
|
||||||
|
import { Spinner } from '/@/shared/components/spinner/spinner';
|
||||||
import { Text } from '/@/shared/components/text/text';
|
import { Text } from '/@/shared/components/text/text';
|
||||||
import { PlaybackSelectors } from '/@/shared/constants/playback-selectors';
|
import { PlaybackSelectors } from '/@/shared/constants/playback-selectors';
|
||||||
import { QueueSong } from '/@/shared/types/domain-types';
|
import { QueueSong } from '/@/shared/types/domain-types';
|
||||||
|
|
||||||
|
const PlayerbarWaveform = lazy(() =>
|
||||||
|
import('/@/renderer/features/player/components/playerbar-waveform').then((module) => ({
|
||||||
|
default: module.PlayerbarWaveform,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
|
||||||
interface MobileFullscreenPlayerProgressProps {
|
interface MobileFullscreenPlayerProgressProps {
|
||||||
currentSong?: QueueSong;
|
currentSong?: QueueSong;
|
||||||
}
|
}
|
||||||
@@ -38,7 +44,9 @@ export const MobileFullscreenPlayerProgress = memo(
|
|||||||
</div>
|
</div>
|
||||||
<div className={styles.sliderWrapper}>
|
<div className={styles.sliderWrapper}>
|
||||||
{isWaveform ? (
|
{isWaveform ? (
|
||||||
<PlayerbarWaveform />
|
<Suspense fallback={<Spinner />}>
|
||||||
|
<PlayerbarWaveform />
|
||||||
|
</Suspense>
|
||||||
) : (
|
) : (
|
||||||
<PlayerbarSeekSlider max={songDuration} min={0} />
|
<PlayerbarSeekSlider max={songDuration} min={0} />
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import formatDuration from 'format-duration';
|
import formatDuration from 'format-duration';
|
||||||
|
import { lazy, Suspense } from 'react';
|
||||||
|
|
||||||
import { PlayerbarSeekSlider } from './playerbar-seek-slider';
|
import { PlayerbarSeekSlider } from './playerbar-seek-slider';
|
||||||
import styles from './playerbar-slider.module.css';
|
import styles from './playerbar-slider.module.css';
|
||||||
import { PlayerbarWaveform } from './playerbar-waveform';
|
|
||||||
|
|
||||||
import { useRemote } from '/@/renderer/features/remote/hooks/use-remote';
|
import { useRemote } from '/@/renderer/features/remote/hooks/use-remote';
|
||||||
import {
|
import {
|
||||||
@@ -13,9 +13,16 @@ import {
|
|||||||
} from '/@/renderer/store';
|
} from '/@/renderer/store';
|
||||||
import { PlayerbarSliderType, usePlayerbarSlider } from '/@/renderer/store/settings.store';
|
import { PlayerbarSliderType, usePlayerbarSlider } from '/@/renderer/store/settings.store';
|
||||||
import { Slider, SliderProps } from '/@/shared/components/slider/slider';
|
import { Slider, SliderProps } from '/@/shared/components/slider/slider';
|
||||||
|
import { Spinner } from '/@/shared/components/spinner/spinner';
|
||||||
import { Text } from '/@/shared/components/text/text';
|
import { Text } from '/@/shared/components/text/text';
|
||||||
import { PlaybackSelectors } from '/@/shared/constants/playback-selectors';
|
import { PlaybackSelectors } from '/@/shared/constants/playback-selectors';
|
||||||
|
|
||||||
|
const PlayerbarWaveform = lazy(() =>
|
||||||
|
import('./playerbar-waveform').then((module) => ({
|
||||||
|
default: module.PlayerbarWaveform,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
|
||||||
export const PlayerbarSlider = () => {
|
export const PlayerbarSlider = () => {
|
||||||
const currentSong = usePlayerSong();
|
const currentSong = usePlayerSong();
|
||||||
const playerbarSlider = usePlayerbarSlider();
|
const playerbarSlider = usePlayerbarSlider();
|
||||||
@@ -51,7 +58,9 @@ export const PlayerbarSlider = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div className={styles.sliderWrapper}>
|
<div className={styles.sliderWrapper}>
|
||||||
{isWaveform ? (
|
{isWaveform ? (
|
||||||
<PlayerbarWaveform />
|
<Suspense fallback={<Spinner />}>
|
||||||
|
<PlayerbarWaveform />
|
||||||
|
</Suspense>
|
||||||
) : (
|
) : (
|
||||||
<PlayerbarSeekSlider max={songDuration} min={0} />
|
<PlayerbarSeekSlider max={songDuration} min={0} />
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,13 +1,19 @@
|
|||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import { MouseEvent } from 'react';
|
import { lazy, MouseEvent, Suspense } from 'react';
|
||||||
|
|
||||||
import styles from './playerbar.module.css';
|
import styles from './playerbar.module.css';
|
||||||
|
|
||||||
import { CenterControls } from '/@/renderer/features/player/components/center-controls';
|
import { CenterControls } from '/@/renderer/features/player/components/center-controls';
|
||||||
import { LeftControls } from '/@/renderer/features/player/components/left-controls';
|
import { LeftControls } from '/@/renderer/features/player/components/left-controls';
|
||||||
import { MobilePlayerbar } from '/@/renderer/features/player/components/mobile-playerbar';
|
|
||||||
import { RightControls } from '/@/renderer/features/player/components/right-controls';
|
import { RightControls } from '/@/renderer/features/player/components/right-controls';
|
||||||
import { useIsMobile } from '/@/renderer/hooks/use-is-mobile';
|
import { useIsMobile } from '/@/renderer/hooks/use-is-mobile';
|
||||||
|
import { Spinner } from '/@/shared/components/spinner/spinner';
|
||||||
|
|
||||||
|
const MobilePlayerbar = lazy(() =>
|
||||||
|
import('./mobile-playerbar').then((module) => ({
|
||||||
|
default: module.MobilePlayerbar,
|
||||||
|
})),
|
||||||
|
);
|
||||||
import { useFullScreenPlayerStore, useSetFullScreenPlayerStore } from '/@/renderer/store';
|
import { useFullScreenPlayerStore, useSetFullScreenPlayerStore } from '/@/renderer/store';
|
||||||
import { usePlayerbarOpenDrawer } from '/@/renderer/store';
|
import { usePlayerbarOpenDrawer } from '/@/renderer/store';
|
||||||
import { PlaybackSelectors } from '/@/shared/constants/playback-selectors';
|
import { PlaybackSelectors } from '/@/shared/constants/playback-selectors';
|
||||||
@@ -24,7 +30,11 @@ export const Playerbar = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (isMobile) {
|
if (isMobile) {
|
||||||
return <MobilePlayerbar />;
|
return (
|
||||||
|
<Suspense fallback={<Spinner />}>
|
||||||
|
<MobilePlayerbar />
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -2,8 +2,6 @@ import { useQueryClient, useSuspenseQuery } from '@tanstack/react-query';
|
|||||||
import { lazy, Suspense, useEffect, useMemo, useRef, useState } from 'react';
|
import { lazy, Suspense, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { useParams } from 'react-router';
|
import { useParams } from 'react-router';
|
||||||
|
|
||||||
import { PlaylistDetailSongListEditTable } from './playlist-detail-song-list-table';
|
|
||||||
|
|
||||||
import { ItemListHandle } from '/@/renderer/components/item-list/types';
|
import { ItemListHandle } from '/@/renderer/components/item-list/types';
|
||||||
import { useListContext } from '/@/renderer/context/list-context';
|
import { useListContext } from '/@/renderer/context/list-context';
|
||||||
import { eventEmitter } from '/@/renderer/events/event-emitter';
|
import { eventEmitter } from '/@/renderer/events/event-emitter';
|
||||||
@@ -21,6 +19,14 @@ const PlaylistDetailSongListTable = lazy(() =>
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const PlaylistDetailSongListEditTable = lazy(() =>
|
||||||
|
import('/@/renderer/features/playlists/components/playlist-detail-song-list-table').then(
|
||||||
|
(module) => ({
|
||||||
|
default: module.PlaylistDetailSongListEditTable,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
const PlaylistDetailSongListGrid = lazy(() =>
|
const PlaylistDetailSongListGrid = lazy(() =>
|
||||||
import('/@/renderer/features/playlists/components/playlist-detail-song-list-grid').then(
|
import('/@/renderer/features/playlists/components/playlist-detail-song-list-grid').then(
|
||||||
(module) => ({
|
(module) => ({
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import { closeModal, ContextModalProps, openContextModal } from '@mantine/modals';
|
import { closeModal, ContextModalProps } from '@mantine/modals';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { t } from 'i18next';
|
import { t } from 'i18next';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import i18n from '/@/i18n/i18n';
|
|
||||||
import { useUpdatePlaylist } from '/@/renderer/features/playlists/mutations/update-playlist-mutation';
|
import { useUpdatePlaylist } from '/@/renderer/features/playlists/mutations/update-playlist-mutation';
|
||||||
import { sharedQueries } from '/@/renderer/features/shared/api/shared-api';
|
import { sharedQueries } from '/@/renderer/features/shared/api/shared-api';
|
||||||
import { useCurrentServer, useCurrentServerId, usePermissions } from '/@/renderer/store';
|
import { useCurrentServer, useCurrentServerId, usePermissions } from '/@/renderer/store';
|
||||||
@@ -17,7 +16,6 @@ import { TextInput } from '/@/shared/components/text-input/text-input';
|
|||||||
import { toast } from '/@/shared/components/toast/toast';
|
import { toast } from '/@/shared/components/toast/toast';
|
||||||
import { useForm } from '/@/shared/hooks/use-form';
|
import { useForm } from '/@/shared/hooks/use-form';
|
||||||
import {
|
import {
|
||||||
Playlist,
|
|
||||||
ServerType,
|
ServerType,
|
||||||
SortOrder,
|
SortOrder,
|
||||||
UpdatePlaylistBody,
|
UpdatePlaylistBody,
|
||||||
@@ -167,24 +165,3 @@ const OwnerSelect = ({ form }: { form: ReturnType<typeof useForm<UpdatePlaylistB
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const openUpdatePlaylistModal = async (args: { playlist: Playlist }) => {
|
|
||||||
const { playlist } = args;
|
|
||||||
|
|
||||||
openContextModal({
|
|
||||||
innerProps: {
|
|
||||||
body: {
|
|
||||||
comment: playlist?.description || undefined,
|
|
||||||
genres: playlist?.genres,
|
|
||||||
name: playlist?.name,
|
|
||||||
ownerId: playlist?.ownerId || undefined,
|
|
||||||
public: playlist?.public || false,
|
|
||||||
queryBuilderRules: playlist?.rules || undefined,
|
|
||||||
sync: playlist?.sync || undefined,
|
|
||||||
},
|
|
||||||
query: { id: playlist?.id },
|
|
||||||
},
|
|
||||||
modalKey: 'updatePlaylist',
|
|
||||||
title: i18n.t('form.editPlaylist.title', { postProcess: 'titleCase' }) as string,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { openContextModal } from '@mantine/modals';
|
||||||
|
|
||||||
|
import i18n from '/@/i18n/i18n';
|
||||||
|
import { Playlist } from '/@/shared/types/domain-types';
|
||||||
|
|
||||||
|
export const openUpdatePlaylistModal = async (args: { playlist: Playlist }) => {
|
||||||
|
const { playlist } = args;
|
||||||
|
|
||||||
|
openContextModal({
|
||||||
|
innerProps: {
|
||||||
|
body: {
|
||||||
|
comment: playlist?.description || undefined,
|
||||||
|
genres: playlist?.genres,
|
||||||
|
name: playlist?.name,
|
||||||
|
ownerId: playlist?.ownerId || undefined,
|
||||||
|
public: playlist?.public || false,
|
||||||
|
queryBuilderRules: playlist?.rules || undefined,
|
||||||
|
sync: playlist?.sync || undefined,
|
||||||
|
},
|
||||||
|
query: { id: playlist?.id },
|
||||||
|
},
|
||||||
|
modalKey: 'updatePlaylist',
|
||||||
|
title: i18n.t('form.editPlaylist.title', { postProcess: 'titleCase' }) as string,
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import type { SettingsState } from '/@/renderer/store/settings.store';
|
||||||
|
|
||||||
|
export const getMpvSetting = (
|
||||||
|
key: keyof SettingsState['playback']['mpvProperties'],
|
||||||
|
value: any,
|
||||||
|
) => {
|
||||||
|
switch (key) {
|
||||||
|
case 'audioExclusiveMode':
|
||||||
|
return { 'audio-exclusive': value || 'no' };
|
||||||
|
case 'audioSampleRateHz':
|
||||||
|
return { 'audio-samplerate': value };
|
||||||
|
case 'gaplessAudio':
|
||||||
|
return { 'gapless-audio': value || 'weak' };
|
||||||
|
case 'replayGainClip':
|
||||||
|
return { 'replaygain-clip': value || 'no' };
|
||||||
|
case 'replayGainFallbackDB':
|
||||||
|
return { 'replaygain-fallback': value };
|
||||||
|
case 'replayGainMode':
|
||||||
|
return { replaygain: value || 'no' };
|
||||||
|
case 'replayGainPreampDB':
|
||||||
|
return { 'replaygain-preamp': value || 0 };
|
||||||
|
default:
|
||||||
|
return { 'audio-format': value };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getMpvProperties = (settings: SettingsState['playback']['mpvProperties']) => {
|
||||||
|
const properties: Record<string, any> = {
|
||||||
|
'audio-exclusive': settings.audioExclusiveMode || 'no',
|
||||||
|
'audio-samplerate':
|
||||||
|
settings.audioSampleRateHz === 0 ? undefined : settings.audioSampleRateHz,
|
||||||
|
'gapless-audio': settings.gaplessAudio || 'weak',
|
||||||
|
replaygain: settings.replayGainMode || 'no',
|
||||||
|
'replaygain-clip': settings.replayGainClip || 'no',
|
||||||
|
'replaygain-fallback': settings.replayGainFallbackDB,
|
||||||
|
'replaygain-preamp': settings.replayGainPreampDB || 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
Object.keys(properties).forEach((key) =>
|
||||||
|
properties[key] === undefined ? delete properties[key] : {},
|
||||||
|
);
|
||||||
|
|
||||||
|
return properties;
|
||||||
|
};
|
||||||
@@ -2,6 +2,8 @@ import isElectron from 'is-electron';
|
|||||||
import { memo, useEffect, useState } from 'react';
|
import { memo, useEffect, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import { getMpvSetting } from './mpv-properties';
|
||||||
|
|
||||||
import { eventEmitter } from '/@/renderer/events/event-emitter';
|
import { eventEmitter } from '/@/renderer/events/event-emitter';
|
||||||
import { usePlayer } from '/@/renderer/features/player/context/player-context';
|
import { usePlayer } from '/@/renderer/features/player/context/player-context';
|
||||||
import {
|
import {
|
||||||
@@ -27,49 +29,6 @@ import { PlayerType } from '/@/shared/types/types';
|
|||||||
const localSettings = isElectron() ? window.api.localSettings : null;
|
const localSettings = isElectron() ? window.api.localSettings : null;
|
||||||
const mpvPlayer = isElectron() ? window.api.mpvPlayer : null;
|
const mpvPlayer = isElectron() ? window.api.mpvPlayer : null;
|
||||||
|
|
||||||
export const getMpvSetting = (
|
|
||||||
key: keyof SettingsState['playback']['mpvProperties'],
|
|
||||||
value: any,
|
|
||||||
) => {
|
|
||||||
switch (key) {
|
|
||||||
case 'audioExclusiveMode':
|
|
||||||
return { 'audio-exclusive': value || 'no' };
|
|
||||||
case 'audioSampleRateHz':
|
|
||||||
return { 'audio-samplerate': value };
|
|
||||||
case 'gaplessAudio':
|
|
||||||
return { 'gapless-audio': value || 'weak' };
|
|
||||||
case 'replayGainClip':
|
|
||||||
return { 'replaygain-clip': value || 'no' };
|
|
||||||
case 'replayGainFallbackDB':
|
|
||||||
return { 'replaygain-fallback': value };
|
|
||||||
case 'replayGainMode':
|
|
||||||
return { replaygain: value || 'no' };
|
|
||||||
case 'replayGainPreampDB':
|
|
||||||
return { 'replaygain-preamp': value || 0 };
|
|
||||||
default:
|
|
||||||
return { 'audio-format': value };
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getMpvProperties = (settings: SettingsState['playback']['mpvProperties']) => {
|
|
||||||
const properties: Record<string, any> = {
|
|
||||||
'audio-exclusive': settings.audioExclusiveMode || 'no',
|
|
||||||
'audio-samplerate':
|
|
||||||
settings.audioSampleRateHz === 0 ? undefined : settings.audioSampleRateHz,
|
|
||||||
'gapless-audio': settings.gaplessAudio || 'weak',
|
|
||||||
replaygain: settings.replayGainMode || 'no',
|
|
||||||
'replaygain-clip': settings.replayGainClip || 'no',
|
|
||||||
'replaygain-fallback': settings.replayGainFallbackDB,
|
|
||||||
'replaygain-preamp': settings.replayGainPreampDB || 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
Object.keys(properties).forEach((key) =>
|
|
||||||
properties[key] === undefined ? delete properties[key] : {},
|
|
||||||
);
|
|
||||||
|
|
||||||
return properties;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const MpvSettings = memo(() => {
|
export const MpvSettings = memo(() => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const settings = usePlaybackSettings();
|
const settings = usePlaybackSettings();
|
||||||
|
|||||||
@@ -1,15 +1,41 @@
|
|||||||
import isElectron from 'is-electron';
|
import isElectron from 'is-electron';
|
||||||
|
import { lazy, Suspense } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { AdvancedTab } from '/@/renderer/features/settings/components/advanced/advanced-tab';
|
|
||||||
import { GeneralTab } from '/@/renderer/features/settings/components/general/general-tab';
|
|
||||||
import { HotkeysTab } from '/@/renderer/features/settings/components/hotkeys/hotkeys-tab';
|
|
||||||
import { PlaybackTab } from '/@/renderer/features/settings/components/playback/playback-tab';
|
|
||||||
import { WindowTab } from '/@/renderer/features/settings/components/window/window-tab';
|
|
||||||
import { LibraryContainer } from '/@/renderer/features/shared/components/library-container';
|
import { LibraryContainer } from '/@/renderer/features/shared/components/library-container';
|
||||||
import { useSettingsStore, useSettingsStoreActions } from '/@/renderer/store/settings.store';
|
import { useSettingsStore, useSettingsStoreActions } from '/@/renderer/store/settings.store';
|
||||||
import { Tabs } from '/@/shared/components/tabs/tabs';
|
import { Tabs } from '/@/shared/components/tabs/tabs';
|
||||||
|
|
||||||
|
const GeneralTab = lazy(() =>
|
||||||
|
import('/@/renderer/features/settings/components/general/general-tab').then((module) => ({
|
||||||
|
default: module.GeneralTab,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
|
||||||
|
const PlaybackTab = lazy(() =>
|
||||||
|
import('/@/renderer/features/settings/components/playback/playback-tab').then((module) => ({
|
||||||
|
default: module.PlaybackTab,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
|
||||||
|
const HotkeysTab = lazy(() =>
|
||||||
|
import('/@/renderer/features/settings/components/hotkeys/hotkeys-tab').then((module) => ({
|
||||||
|
default: module.HotkeysTab,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
|
||||||
|
const WindowTab = lazy(() =>
|
||||||
|
import('/@/renderer/features/settings/components/window/window-tab').then((module) => ({
|
||||||
|
default: module.WindowTab,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
|
||||||
|
const AdvancedTab = lazy(() =>
|
||||||
|
import('/@/renderer/features/settings/components/advanced/advanced-tab').then((module) => ({
|
||||||
|
default: module.AdvancedTab,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
|
||||||
export const SettingsContent = () => {
|
export const SettingsContent = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const currentTab = useSettingsStore((state) => state.tab);
|
const currentTab = useSettingsStore((state) => state.tab);
|
||||||
@@ -45,21 +71,31 @@ export const SettingsContent = () => {
|
|||||||
</Tabs.Tab>
|
</Tabs.Tab>
|
||||||
</Tabs.List>
|
</Tabs.List>
|
||||||
<Tabs.Panel value="general">
|
<Tabs.Panel value="general">
|
||||||
<GeneralTab />
|
<Suspense fallback={null}>
|
||||||
|
<GeneralTab />
|
||||||
|
</Suspense>
|
||||||
</Tabs.Panel>
|
</Tabs.Panel>
|
||||||
<Tabs.Panel value="playback">
|
<Tabs.Panel value="playback">
|
||||||
<PlaybackTab />
|
<Suspense fallback={null}>
|
||||||
|
<PlaybackTab />
|
||||||
|
</Suspense>
|
||||||
</Tabs.Panel>
|
</Tabs.Panel>
|
||||||
<Tabs.Panel value="hotkeys">
|
<Tabs.Panel value="hotkeys">
|
||||||
<HotkeysTab />
|
<Suspense fallback={null}>
|
||||||
|
<HotkeysTab />
|
||||||
|
</Suspense>
|
||||||
</Tabs.Panel>
|
</Tabs.Panel>
|
||||||
{isElectron() && (
|
{isElectron() && (
|
||||||
<Tabs.Panel value="window">
|
<Tabs.Panel value="window">
|
||||||
<WindowTab />
|
<Suspense fallback={null}>
|
||||||
|
<WindowTab />
|
||||||
|
</Suspense>
|
||||||
</Tabs.Panel>
|
</Tabs.Panel>
|
||||||
)}
|
)}
|
||||||
<Tabs.Panel value="advanced">
|
<Tabs.Panel value="advanced">
|
||||||
<AdvancedTab />
|
<Suspense fallback={null}>
|
||||||
|
<AdvancedTab />
|
||||||
|
</Suspense>
|
||||||
</Tabs.Panel>
|
</Tabs.Panel>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,12 +1,17 @@
|
|||||||
import { useState } from 'react';
|
import { lazy, Suspense, useState } from 'react';
|
||||||
|
|
||||||
import { SettingsContent } from '/@/renderer/features/settings/components/settings-content';
|
import { SettingsContent } from '/@/renderer/features/settings/components/settings-content';
|
||||||
import { SettingsHeader } from '/@/renderer/features/settings/components/settings-header';
|
|
||||||
import { SettingSearchContext } from '/@/renderer/features/settings/context/search-context';
|
import { SettingSearchContext } from '/@/renderer/features/settings/context/search-context';
|
||||||
import { AnimatedPage } from '/@/renderer/features/shared/components/animated-page';
|
import { AnimatedPage } from '/@/renderer/features/shared/components/animated-page';
|
||||||
import { LibraryContainer } from '/@/renderer/features/shared/components/library-container';
|
import { LibraryContainer } from '/@/renderer/features/shared/components/library-container';
|
||||||
import { Flex } from '/@/shared/components/flex/flex';
|
import { Flex } from '/@/shared/components/flex/flex';
|
||||||
|
|
||||||
|
const SettingsHeader = lazy(() =>
|
||||||
|
import('/@/renderer/features/settings/components/settings-header').then((module) => ({
|
||||||
|
default: module.SettingsHeader,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
|
||||||
const SettingsRoute = () => {
|
const SettingsRoute = () => {
|
||||||
const [search, setSearch] = useState('');
|
const [search, setSearch] = useState('');
|
||||||
|
|
||||||
@@ -15,7 +20,9 @@ const SettingsRoute = () => {
|
|||||||
<SettingSearchContext.Provider value={search}>
|
<SettingSearchContext.Provider value={search}>
|
||||||
<LibraryContainer>
|
<LibraryContainer>
|
||||||
<Flex direction="column" h="100%" w="100%">
|
<Flex direction="column" h="100%" w="100%">
|
||||||
<SettingsHeader setSearch={setSearch} />
|
<Suspense fallback={<></>}>
|
||||||
|
<SettingsHeader setSearch={setSearch} />
|
||||||
|
</Suspense>
|
||||||
<SettingsContent />
|
<SettingsContent />
|
||||||
</Flex>
|
</Flex>
|
||||||
</LibraryContainer>
|
</LibraryContainer>
|
||||||
|
|||||||
+34
-15
@@ -1,4 +1,3 @@
|
|||||||
import butterchurnPresets from 'butterchurn-presets';
|
|
||||||
import { nanoid } from 'nanoid';
|
import { nanoid } from 'nanoid';
|
||||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
@@ -25,6 +24,38 @@ import { Text } from '/@/shared/components/text/text';
|
|||||||
import { Textarea } from '/@/shared/components/textarea/textarea';
|
import { Textarea } from '/@/shared/components/textarea/textarea';
|
||||||
import { toast } from '/@/shared/components/toast/toast';
|
import { toast } from '/@/shared/components/toast/toast';
|
||||||
|
|
||||||
|
type ButterchurnPresetOption = { label: string; value: string };
|
||||||
|
|
||||||
|
let butterchurnPresetOptionsCache: ButterchurnPresetOption[] | null = null;
|
||||||
|
|
||||||
|
const loadButterchurnPresetOptions = async (): Promise<ButterchurnPresetOption[]> => {
|
||||||
|
if (butterchurnPresetOptionsCache) return butterchurnPresetOptionsCache;
|
||||||
|
|
||||||
|
const mod = await import('butterchurn-presets');
|
||||||
|
const presets = (mod as any).default ?? mod;
|
||||||
|
const presetNames = Object.keys(presets);
|
||||||
|
|
||||||
|
butterchurnPresetOptionsCache = presetNames.map((presetName) => ({
|
||||||
|
label: presetName,
|
||||||
|
value: presetName,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return butterchurnPresetOptionsCache;
|
||||||
|
};
|
||||||
|
|
||||||
|
const useButterchurnPresetOptions = () => {
|
||||||
|
const [options, setOptions] = useState<ButterchurnPresetOption[]>(
|
||||||
|
butterchurnPresetOptionsCache ?? [],
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (butterchurnPresetOptionsCache) return;
|
||||||
|
void loadButterchurnPresetOptions().then(setOptions);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return options;
|
||||||
|
};
|
||||||
|
|
||||||
const modeOptions: { label: string; value: string }[] = [
|
const modeOptions: { label: string; value: string }[] = [
|
||||||
{ label: i18n.t('visualizer.options.mode.0') as string, value: '0' },
|
{ label: i18n.t('visualizer.options.mode.0') as string, value: '0' },
|
||||||
{ label: i18n.t('visualizer.options.mode.1') as string, value: '1' },
|
{ label: i18n.t('visualizer.options.mode.1') as string, value: '1' },
|
||||||
@@ -2068,13 +2099,7 @@ const ButterchurnGeneralSettings = () => {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { updateProperty, visualizer } = useUpdateButterchurn();
|
const { updateProperty, visualizer } = useUpdateButterchurn();
|
||||||
|
|
||||||
const presetOptions = useMemo(() => {
|
const presetOptions = useButterchurnPresetOptions();
|
||||||
const presets = butterchurnPresets;
|
|
||||||
return Object.keys(presets).map((presetName) => ({
|
|
||||||
label: presetName,
|
|
||||||
value: presetName,
|
|
||||||
}));
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fieldset legend={t('visualizer.general')}>
|
<Fieldset legend={t('visualizer.general')}>
|
||||||
@@ -2124,13 +2149,7 @@ const ButterChurnCycleSettings = () => {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { updateProperty, visualizer } = useUpdateButterchurn();
|
const { updateProperty, visualizer } = useUpdateButterchurn();
|
||||||
|
|
||||||
const presetOptions = useMemo(() => {
|
const presetOptions = useButterchurnPresetOptions();
|
||||||
const presets = butterchurnPresets;
|
|
||||||
return Object.keys(presets).map((presetName) => ({
|
|
||||||
label: presetName,
|
|
||||||
value: presetName,
|
|
||||||
}));
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fieldset legend={t('visualizer.cyclePresets')}>
|
<Fieldset legend={t('visualizer.cyclePresets')}>
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import AudioMotionAnalyzer from 'audiomotion-analyzer';
|
import { createRef, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { createRef, useCallback, useEffect, useMemo, useState } from 'react';
|
|
||||||
|
|
||||||
import styles from './visualizer.module.css';
|
import styles from './visualizer.module.css';
|
||||||
|
|
||||||
@@ -15,7 +14,31 @@ const VisualizerInner = () => {
|
|||||||
const accent = useAccent();
|
const accent = useAccent();
|
||||||
const visualizer = useSettingsStore((store) => store.visualizer);
|
const visualizer = useSettingsStore((store) => store.visualizer);
|
||||||
const opacity = useSettingsStore((store) => store.visualizer.audiomotionanalyzer.opacity);
|
const opacity = useSettingsStore((store) => store.visualizer.audiomotionanalyzer.opacity);
|
||||||
const [motion, setMotion] = useState<AudioMotionAnalyzer>();
|
const [motion, setMotion] = useState<any>();
|
||||||
|
const [libraryLoaded, setLibraryLoaded] = useState(false);
|
||||||
|
const AudioMotionAnalyzerRef = useRef<any>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let isMounted = true;
|
||||||
|
|
||||||
|
const loadLibrary = async () => {
|
||||||
|
try {
|
||||||
|
const module = await import('audiomotion-analyzer');
|
||||||
|
if (isMounted) {
|
||||||
|
AudioMotionAnalyzerRef.current = module.default;
|
||||||
|
setLibraryLoaded(true);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load AudioMotionAnalyzer library:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
loadLibrary();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
isMounted = false;
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
// Check if a gradient name is a custom gradient
|
// Check if a gradient name is a custom gradient
|
||||||
const isCustomGradient = useCallback(
|
const isCustomGradient = useCallback(
|
||||||
@@ -162,7 +185,7 @@ const VisualizerInner = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const registerCustomGradients = useCallback(
|
const registerCustomGradients = useCallback(
|
||||||
(audioMotionInstance: AudioMotionAnalyzer) => {
|
(audioMotionInstance: any) => {
|
||||||
if (visualizer.type !== 'audiomotionanalyzer') {
|
if (visualizer.type !== 'audiomotionanalyzer') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -187,8 +210,11 @@ const VisualizerInner = () => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const { context, gains } = webAudio || {};
|
const { context, gains } = webAudio || {};
|
||||||
let audioMotion: AudioMotionAnalyzer | undefined;
|
let audioMotion: any | undefined;
|
||||||
if (gains && context && canvasRef.current && !motion) {
|
if (gains && context && canvasRef.current && !motion && libraryLoaded) {
|
||||||
|
const AudioMotionAnalyzer = AudioMotionAnalyzerRef.current;
|
||||||
|
if (!AudioMotionAnalyzer) return;
|
||||||
|
|
||||||
// Reset gradients registered flag on new instance
|
// Reset gradients registered flag on new instance
|
||||||
setGradientsRegistered(false);
|
setGradientsRegistered(false);
|
||||||
|
|
||||||
@@ -236,6 +262,7 @@ const VisualizerInner = () => {
|
|||||||
options,
|
options,
|
||||||
isCustomGradient,
|
isCustomGradient,
|
||||||
motion,
|
motion,
|
||||||
|
libraryLoaded,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Re-register custom gradients when they change
|
// Re-register custom gradients when they change
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import butterchurn from 'butterchurn';
|
|
||||||
import butterchurnPresets from 'butterchurn-presets';
|
|
||||||
import { createRef, useEffect, useRef, useState } from 'react';
|
import { createRef, useEffect, useRef, useState } from 'react';
|
||||||
|
|
||||||
import styles from './visualizer.module.css';
|
import styles from './visualizer.module.css';
|
||||||
@@ -27,6 +25,9 @@ const VisualizerInner = () => {
|
|||||||
const visualizerRef = useRef<ButterchurnVisualizer | undefined>(undefined);
|
const visualizerRef = useRef<ButterchurnVisualizer | undefined>(undefined);
|
||||||
const isInitializedRef = useRef(false);
|
const isInitializedRef = useRef(false);
|
||||||
const [isVisualizerReady, setIsVisualizerReady] = useState(false);
|
const [isVisualizerReady, setIsVisualizerReady] = useState(false);
|
||||||
|
const [librariesLoaded, setLibrariesLoaded] = useState(false);
|
||||||
|
const butterchurnRef = useRef<any>(null);
|
||||||
|
const butterchurnPresetsRef = useRef<any>(null);
|
||||||
const animationFrameRef = useRef<number | undefined>(undefined);
|
const animationFrameRef = useRef<number | undefined>(undefined);
|
||||||
const resizeObserverRef = useRef<ResizeObserver | undefined>(undefined);
|
const resizeObserverRef = useRef<ResizeObserver | undefined>(undefined);
|
||||||
const cycleTimerRef = useRef<NodeJS.Timeout | undefined>(undefined);
|
const cycleTimerRef = useRef<NodeJS.Timeout | undefined>(undefined);
|
||||||
@@ -39,6 +40,33 @@ const VisualizerInner = () => {
|
|||||||
const playerStatus = usePlayerStatus();
|
const playerStatus = usePlayerStatus();
|
||||||
const isPlaying = playerStatus === PlayerStatus.PLAYING;
|
const isPlaying = playerStatus === PlayerStatus.PLAYING;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let isMounted = true;
|
||||||
|
|
||||||
|
const loadLibraries = async () => {
|
||||||
|
try {
|
||||||
|
const [butterchurnModule, presetsModule] = await Promise.all([
|
||||||
|
import('butterchurn'),
|
||||||
|
import('butterchurn-presets'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (isMounted) {
|
||||||
|
butterchurnRef.current = butterchurnModule.default;
|
||||||
|
butterchurnPresetsRef.current = presetsModule.default;
|
||||||
|
setLibrariesLoaded(true);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load butterchurn libraries:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
loadLibraries();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
isMounted = false;
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
const cleanupVisualizer = () => {
|
const cleanupVisualizer = () => {
|
||||||
if (animationFrameRef.current) {
|
if (animationFrameRef.current) {
|
||||||
cancelAnimationFrame(animationFrameRef.current);
|
cancelAnimationFrame(animationFrameRef.current);
|
||||||
@@ -79,6 +107,7 @@ const VisualizerInner = () => {
|
|||||||
canvas &&
|
canvas &&
|
||||||
container &&
|
container &&
|
||||||
isPlaying &&
|
isPlaying &&
|
||||||
|
librariesLoaded &&
|
||||||
(!isInitializedRef.current || !visualizerRef.current);
|
(!isInitializedRef.current || !visualizerRef.current);
|
||||||
|
|
||||||
if (!needsInitialization) {
|
if (!needsInitialization) {
|
||||||
@@ -107,13 +136,16 @@ const VisualizerInner = () => {
|
|||||||
initializeVisualizer(dimensions.width, dimensions.height);
|
initializeVisualizer(dimensions.width, dimensions.height);
|
||||||
}
|
}
|
||||||
|
|
||||||
function initializeVisualizer(width: number, height: number) {
|
async function initializeVisualizer(width: number, height: number) {
|
||||||
if (!gains || gains.length === 0 || !canvas || !context) return;
|
if (!gains || gains.length === 0 || !canvas || !context || !librariesLoaded) return;
|
||||||
|
|
||||||
canvas.width = width;
|
canvas.width = width;
|
||||||
canvas.height = height;
|
canvas.height = height;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const butterchurn = butterchurnRef.current;
|
||||||
|
if (!butterchurn) return;
|
||||||
|
|
||||||
const butterchurnInstance = butterchurn.createVisualizer(context, canvas, {
|
const butterchurnInstance = butterchurn.createVisualizer(context, canvas, {
|
||||||
height,
|
height,
|
||||||
width,
|
width,
|
||||||
@@ -138,7 +170,7 @@ const VisualizerInner = () => {
|
|||||||
cleanupVisualizer();
|
cleanupVisualizer();
|
||||||
};
|
};
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [webAudio, isPlaying]);
|
}, [webAudio, isPlaying, librariesLoaded]);
|
||||||
|
|
||||||
// Kill visualizer after 5 seconds of pause
|
// Kill visualizer after 5 seconds of pause
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -204,9 +236,11 @@ const VisualizerInner = () => {
|
|||||||
// Load initial preset when visualizer is ready
|
// Load initial preset when visualizer is ready
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const visualizer = visualizerRef.current;
|
const visualizer = visualizerRef.current;
|
||||||
if (!visualizer || !isVisualizerReady || initialPresetLoadedRef.current) return;
|
if (!visualizer || !isVisualizerReady || initialPresetLoadedRef.current || !librariesLoaded)
|
||||||
|
return;
|
||||||
|
|
||||||
const presets = butterchurnPresets;
|
const presets = butterchurnPresetsRef.current;
|
||||||
|
if (!presets) return;
|
||||||
const presetNames = Object.keys(presets);
|
const presetNames = Object.keys(presets);
|
||||||
|
|
||||||
if (presetNames.length > 0) {
|
if (presetNames.length > 0) {
|
||||||
@@ -222,14 +256,24 @@ const VisualizerInner = () => {
|
|||||||
initialPresetLoadedRef.current = true;
|
initialPresetLoadedRef.current = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [isVisualizerReady, butterchurnSettings.currentPreset, butterchurnSettings.blendTime]);
|
}, [
|
||||||
|
isVisualizerReady,
|
||||||
|
butterchurnSettings.currentPreset,
|
||||||
|
butterchurnSettings.blendTime,
|
||||||
|
librariesLoaded,
|
||||||
|
]);
|
||||||
|
|
||||||
// Update preset when currentPreset or blendTime changes (but not when cycling)
|
// Update preset when currentPreset or blendTime changes (but not when cycling)
|
||||||
const isCyclingRef = useRef(false);
|
const isCyclingRef = useRef(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const visualizer = visualizerRef.current;
|
const visualizer = visualizerRef.current;
|
||||||
if (!visualizer || !butterchurnSettings.currentPreset || !initialPresetLoadedRef.current)
|
if (
|
||||||
|
!visualizer ||
|
||||||
|
!butterchurnSettings.currentPreset ||
|
||||||
|
!initialPresetLoadedRef.current ||
|
||||||
|
!librariesLoaded
|
||||||
|
)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
// Skip if we're currently cycling (to avoid reloading preset)
|
// Skip if we're currently cycling (to avoid reloading preset)
|
||||||
@@ -238,7 +282,8 @@ const VisualizerInner = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const presets = butterchurnPresets;
|
const presets = butterchurnPresetsRef.current;
|
||||||
|
if (!presets) return;
|
||||||
const preset = presets[butterchurnSettings.currentPreset];
|
const preset = presets[butterchurnSettings.currentPreset];
|
||||||
|
|
||||||
if (preset) {
|
if (preset) {
|
||||||
@@ -246,12 +291,17 @@ const VisualizerInner = () => {
|
|||||||
// Reset cycle timer when preset changes manually
|
// Reset cycle timer when preset changes manually
|
||||||
cycleStartTimeRef.current = Date.now();
|
cycleStartTimeRef.current = Date.now();
|
||||||
}
|
}
|
||||||
}, [butterchurnSettings.currentPreset, butterchurnSettings.blendTime]);
|
}, [butterchurnSettings.currentPreset, butterchurnSettings.blendTime, librariesLoaded]);
|
||||||
|
|
||||||
// Handle preset cycling
|
// Handle preset cycling
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const visualizer = visualizerRef.current;
|
const visualizer = visualizerRef.current;
|
||||||
if (!visualizer || !butterchurnSettings.cyclePresets || !initialPresetLoadedRef.current) {
|
if (
|
||||||
|
!visualizer ||
|
||||||
|
!butterchurnSettings.cyclePresets ||
|
||||||
|
!initialPresetLoadedRef.current ||
|
||||||
|
!librariesLoaded
|
||||||
|
) {
|
||||||
// Clear cycle timer if cycling is disabled or visualizer not ready
|
// Clear cycle timer if cycling is disabled or visualizer not ready
|
||||||
if (cycleTimerRef.current) {
|
if (cycleTimerRef.current) {
|
||||||
clearInterval(cycleTimerRef.current);
|
clearInterval(cycleTimerRef.current);
|
||||||
@@ -260,7 +310,8 @@ const VisualizerInner = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const presets = butterchurnPresets;
|
const presets = butterchurnPresetsRef.current;
|
||||||
|
if (!presets) return;
|
||||||
const allPresetNames = Object.keys(presets);
|
const allPresetNames = Object.keys(presets);
|
||||||
|
|
||||||
// Get the list of presets to cycle through
|
// Get the list of presets to cycle through
|
||||||
@@ -359,6 +410,7 @@ const VisualizerInner = () => {
|
|||||||
butterchurnSettings.randomizeNextPreset,
|
butterchurnSettings.randomizeNextPreset,
|
||||||
butterchurnSettings.currentPreset,
|
butterchurnSettings.currentPreset,
|
||||||
setSettings,
|
setSettings,
|
||||||
|
librariesLoaded,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -1,12 +1,22 @@
|
|||||||
import { useRef } from 'react';
|
import { lazy, Suspense, useRef } from 'react';
|
||||||
|
|
||||||
import styles from './left-sidebar.module.css';
|
import styles from './left-sidebar.module.css';
|
||||||
|
|
||||||
import { ResizeHandle } from '/@/renderer/features/shared/components/resize-handle';
|
import { ResizeHandle } from '/@/renderer/features/shared/components/resize-handle';
|
||||||
import { CollapsedSidebar } from '/@/renderer/features/sidebar/components/collapsed-sidebar';
|
|
||||||
import { Sidebar } from '/@/renderer/features/sidebar/components/sidebar';
|
|
||||||
import { useAppStore } from '/@/renderer/store';
|
import { useAppStore } from '/@/renderer/store';
|
||||||
|
|
||||||
|
const CollapsedSidebar = lazy(() =>
|
||||||
|
import('/@/renderer/features/sidebar/components/collapsed-sidebar').then((module) => ({
|
||||||
|
default: module.CollapsedSidebar,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
|
||||||
|
const Sidebar = lazy(() =>
|
||||||
|
import('/@/renderer/features/sidebar/components/sidebar').then((module) => ({
|
||||||
|
default: module.Sidebar,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
|
||||||
interface LeftSidebarProps {
|
interface LeftSidebarProps {
|
||||||
isResizing: boolean;
|
isResizing: boolean;
|
||||||
startResizing: (direction: 'left' | 'right', mouseEvent?: MouseEvent) => void;
|
startResizing: (direction: 'left' | 'right', mouseEvent?: MouseEvent) => void;
|
||||||
@@ -27,7 +37,7 @@ export const LeftSidebar = ({ isResizing, startResizing }: LeftSidebarProps) =>
|
|||||||
placement="right"
|
placement="right"
|
||||||
ref={sidebarRef}
|
ref={sidebarRef}
|
||||||
/>
|
/>
|
||||||
{collapsed ? <CollapsedSidebar /> : <Sidebar />}
|
<Suspense fallback={<></>}>{collapsed ? <CollapsedSidebar /> : <Sidebar />}</Suspense>
|
||||||
</aside>
|
</aside>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,15 +1,7 @@
|
|||||||
import { lazy, Suspense } from 'react';
|
import { lazy, Suspense } from 'react';
|
||||||
import { HashRouter, Route, Routes } from 'react-router';
|
import { HashRouter, Route, Routes } from 'react-router';
|
||||||
|
|
||||||
import { LyricsSettingsContextModal } from '/@/renderer/features/lyrics/components/lyrics-settings-modal';
|
|
||||||
import { ShuffleAllContextModal } from '/@/renderer/features/player/components/shuffle-all-modal';
|
|
||||||
import { AddToPlaylistContextModal } from '/@/renderer/features/playlists/components/add-to-playlist-context-modal';
|
|
||||||
import { SaveAndReplaceContextModal } from '/@/renderer/features/playlists/components/save-and-replace-context-modal';
|
|
||||||
import { UpdatePlaylistContextModal } from '/@/renderer/features/playlists/components/update-playlist-form';
|
|
||||||
import { SettingsContextModal } from '/@/renderer/features/settings/components/settings-modal';
|
|
||||||
import { RouterErrorBoundary } from '/@/renderer/features/shared/components/router-error-boundary';
|
import { RouterErrorBoundary } from '/@/renderer/features/shared/components/router-error-boundary';
|
||||||
import { ShareItemContextModal } from '/@/renderer/features/sharing/components/share-item-context-modal';
|
|
||||||
import { VisualizerSettingsContextModal } from '/@/renderer/features/visualizer/components/audiomotionanalyzer/visualizer-settings-modal';
|
|
||||||
import { AuthenticationOutlet } from '/@/renderer/layouts/authentication-outlet';
|
import { AuthenticationOutlet } from '/@/renderer/layouts/authentication-outlet';
|
||||||
import { ResponsiveLayout } from '/@/renderer/layouts/responsive-layout';
|
import { ResponsiveLayout } from '/@/renderer/layouts/responsive-layout';
|
||||||
import { AppOutlet } from '/@/renderer/router/app-outlet';
|
import { AppOutlet } from '/@/renderer/router/app-outlet';
|
||||||
@@ -87,6 +79,108 @@ const FavoritesRoute = lazy(() => import('/@/renderer/features/favorites/routes/
|
|||||||
|
|
||||||
const SettingsRoute = lazy(() => import('/@/renderer/features/settings/routes/settings-route'));
|
const SettingsRoute = lazy(() => import('/@/renderer/features/settings/routes/settings-route'));
|
||||||
|
|
||||||
|
const LazyLyricsSettingsContextModal = lazy(() =>
|
||||||
|
import('/@/renderer/features/lyrics/components/lyrics-settings-modal').then((module) => ({
|
||||||
|
default: module.LyricsSettingsContextModal,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
|
||||||
|
const LyricsSettingsContextModal = (props: any) => (
|
||||||
|
<Suspense fallback={<></>}>
|
||||||
|
<LazyLyricsSettingsContextModal {...props} />
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
|
||||||
|
const LazyShuffleAllContextModal = lazy(() =>
|
||||||
|
import('/@/renderer/features/player/components/shuffle-all-modal').then((module) => ({
|
||||||
|
default: module.ShuffleAllContextModal,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
|
||||||
|
const ShuffleAllContextModal = (props: any) => (
|
||||||
|
<Suspense fallback={<></>}>
|
||||||
|
<LazyShuffleAllContextModal {...props} />
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
|
||||||
|
const LazyAddToPlaylistContextModal = lazy(() =>
|
||||||
|
import('/@/renderer/features/playlists/components/add-to-playlist-context-modal').then(
|
||||||
|
(module) => ({
|
||||||
|
default: module.AddToPlaylistContextModal,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const AddToPlaylistContextModal = (props: any) => (
|
||||||
|
<Suspense fallback={<></>}>
|
||||||
|
<LazyAddToPlaylistContextModal {...props} />
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
|
||||||
|
const LazySaveAndReplaceContextModal = lazy(() =>
|
||||||
|
import('/@/renderer/features/playlists/components/save-and-replace-context-modal').then(
|
||||||
|
(module) => ({
|
||||||
|
default: module.SaveAndReplaceContextModal,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const SaveAndReplaceContextModal = (props: any) => (
|
||||||
|
<Suspense fallback={<></>}>
|
||||||
|
<LazySaveAndReplaceContextModal {...props} />
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
|
||||||
|
const LazyUpdatePlaylistContextModal = lazy(() =>
|
||||||
|
import('/@/renderer/features/playlists/components/update-playlist-form').then((module) => ({
|
||||||
|
default: module.UpdatePlaylistContextModal,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
|
||||||
|
const UpdatePlaylistContextModal = (props: any) => (
|
||||||
|
<Suspense fallback={<></>}>
|
||||||
|
<LazyUpdatePlaylistContextModal {...props} />
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
|
||||||
|
const LazySettingsContextModal = lazy(() =>
|
||||||
|
import('/@/renderer/features/settings/components/settings-modal').then((module) => ({
|
||||||
|
default: module.SettingsContextModal,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
|
||||||
|
const SettingsContextModal = (props: any) => (
|
||||||
|
<Suspense fallback={<></>}>
|
||||||
|
<LazySettingsContextModal {...props} />
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
|
||||||
|
const LazyShareItemContextModal = lazy(() =>
|
||||||
|
import('/@/renderer/features/sharing/components/share-item-context-modal').then((module) => ({
|
||||||
|
default: module.ShareItemContextModal,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
|
||||||
|
const ShareItemContextModal = (props: any) => (
|
||||||
|
<Suspense fallback={<></>}>
|
||||||
|
<LazyShareItemContextModal {...props} />
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
|
||||||
|
const LazyVisualizerSettingsContextModal = lazy(() =>
|
||||||
|
import(
|
||||||
|
'/@/renderer/features/visualizer/components/audiomotionanalyzer/visualizer-settings-modal'
|
||||||
|
).then((module) => ({
|
||||||
|
default: module.VisualizerSettingsContextModal,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
|
||||||
|
const VisualizerSettingsContextModal = (props: any) => (
|
||||||
|
<Suspense fallback={<></>}>
|
||||||
|
<LazyVisualizerSettingsContextModal {...props} />
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
|
||||||
export const AppRouter = () => {
|
export const AppRouter = () => {
|
||||||
const router = (
|
const router = (
|
||||||
<HashRouter>
|
<HashRouter>
|
||||||
|
|||||||
Reference in New Issue
Block a user