add more dynamic imports to optimize bundle

This commit is contained in:
jeffvli
2026-01-17 07:32:16 -08:00
parent 6cb5c95c1f
commit ef5daad1dd
20 changed files with 529 additions and 163 deletions
+10 -3
View File
@@ -7,12 +7,11 @@ import '@mantine/core/styles.css';
import '@mantine/dates/styles.css';
import '@mantine/notifications/styles.css';
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 { WebAudioContext } from '/@/renderer/features/player/context/webaudio-context';
import { useSyncSettingsToMain } from '/@/renderer/hooks/use-sync-settings-to-main';
import { ReleaseNotesModal } from './release-notes-modal';
import { AppRouter } from '/@/renderer/router/app-router';
import { useCssSettings, useHotkeySettings, useLanguage } from '/@/renderer/store';
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 { 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;
export const App = () => {
@@ -95,7 +100,9 @@ export const App = () => {
<AppRouter />
</PlayerProvider>
</WebAudioContext.Provider>
<ReleaseNotesModal />
<Suspense fallback={null}>
<ReleaseNotesModal />
</Suspense>
</MantineProvider>
);
};
@@ -1,7 +1,7 @@
import { useCallback } from 'react';
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 { Playlist } from '/@/shared/types/domain-types';
@@ -1,18 +1,63 @@
import { useQueryClient } from '@tanstack/react-query';
import { useEffect, useRef } from 'react';
import { lazy, Suspense, useEffect, useRef } from 'react';
import { createCallable } from 'react-call';
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';
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 {
Album,
AlbumArtist,
@@ -80,15 +125,17 @@ export const ContextMenuController = createCallable<ContextMenuControllerProps,
}}
/>
</ContextMenu.Target>
{cmd.type === LibraryItem.QUEUE_SONG && <QueueContextMenu {...cmd} />}
{cmd.type === LibraryItem.ALBUM && <AlbumContextMenu {...cmd} />}
{cmd.type === LibraryItem.ALBUM_ARTIST && <AlbumArtistContextMenu {...cmd} />}
{cmd.type === LibraryItem.ARTIST && <ArtistContextMenu {...cmd} />}
{cmd.type === LibraryItem.FOLDER && <FolderContextMenu {...cmd} />}
{cmd.type === LibraryItem.GENRE && <GenreContextMenu {...cmd} />}
{cmd.type === LibraryItem.PLAYLIST && <PlaylistContextMenu {...cmd} />}
{cmd.type === LibraryItem.PLAYLIST_SONG && <PlaylistSongContextMenu {...cmd} />}
{cmd.type === LibraryItem.SONG && <SongContextMenu {...cmd} />}
<Suspense fallback={null}>
{cmd.type === LibraryItem.QUEUE_SONG && <QueueContextMenu {...cmd} />}
{cmd.type === LibraryItem.ALBUM && <AlbumContextMenu {...cmd} />}
{cmd.type === LibraryItem.ALBUM_ARTIST && <AlbumArtistContextMenu {...cmd} />}
{cmd.type === LibraryItem.ARTIST && <ArtistContextMenu {...cmd} />}
{cmd.type === LibraryItem.FOLDER && <FolderContextMenu {...cmd} />}
{cmd.type === LibraryItem.GENRE && <GenreContextMenu {...cmd} />}
{cmd.type === LibraryItem.PLAYLIST && <PlaylistContextMenu {...cmd} />}
{cmd.type === LibraryItem.PLAYLIST_SONG && <PlaylistSongContextMenu {...cmd} />}
{cmd.type === LibraryItem.SONG && <SongContextMenu {...cmd} />}
</Suspense>
</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 { AudioPlayer, PlayerOnProgressProps } from '/@/renderer/features/player/audio-player/types';
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 {
usePlaybackSettings,
usePlayerActions,
@@ -1,7 +1,7 @@
import type { RefObject } from 'react';
import type ReactPlayer from 'react-player';
import { useCallback, useEffect, useImperativeHandle, useRef, useState } from 'react';
import ReactPlayer from 'react-player';
import { AudioPlayer, PlayerOnProgressProps } from '/@/renderer/features/player/audio-player/types';
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 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 [internalVolume2, setInternalVolume2] = useState(volume / 100 || 0);
@@ -184,9 +209,13 @@ export const WebPlayerEngine = (props: WebPlayerEngineProps) => {
[onStartedPlayer2, preservesPitch],
);
if (isLoading || !ReactPlayerComponent) {
return <div id="web-player-engine" style={{ display: 'none' }} />;
}
return (
<div id="web-player-engine" style={{ display: 'none' }}>
<ReactPlayer
<ReactPlayerComponent
config={{
file: { attributes: { crossOrigin: 'anonymous' }, forceAudio: true },
}}
@@ -206,7 +235,7 @@ export const WebPlayerEngine = (props: WebPlayerEngineProps) => {
volume={volume1}
width={0}
/>
<ReactPlayer
<ReactPlayerComponent
config={{
file: { attributes: { crossOrigin: 'anonymous' }, forceAudio: true },
}}
@@ -1,16 +1,22 @@
import formatDuration from 'format-duration';
import { memo } from 'react';
import { lazy, memo, Suspense } from 'react';
import styles from './mobile-fullscreen-player.module.css';
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 { PlayerbarSliderType, usePlayerbarSlider } from '/@/renderer/store/settings.store';
import { Spinner } from '/@/shared/components/spinner/spinner';
import { Text } from '/@/shared/components/text/text';
import { PlaybackSelectors } from '/@/shared/constants/playback-selectors';
import { QueueSong } from '/@/shared/types/domain-types';
const PlayerbarWaveform = lazy(() =>
import('/@/renderer/features/player/components/playerbar-waveform').then((module) => ({
default: module.PlayerbarWaveform,
})),
);
interface MobileFullscreenPlayerProgressProps {
currentSong?: QueueSong;
}
@@ -38,7 +44,9 @@ export const MobileFullscreenPlayerProgress = memo(
</div>
<div className={styles.sliderWrapper}>
{isWaveform ? (
<PlayerbarWaveform />
<Suspense fallback={<Spinner />}>
<PlayerbarWaveform />
</Suspense>
) : (
<PlayerbarSeekSlider max={songDuration} min={0} />
)}
@@ -1,8 +1,8 @@
import formatDuration from 'format-duration';
import { lazy, Suspense } from 'react';
import { PlayerbarSeekSlider } from './playerbar-seek-slider';
import styles from './playerbar-slider.module.css';
import { PlayerbarWaveform } from './playerbar-waveform';
import { useRemote } from '/@/renderer/features/remote/hooks/use-remote';
import {
@@ -13,9 +13,16 @@ import {
} from '/@/renderer/store';
import { PlayerbarSliderType, usePlayerbarSlider } from '/@/renderer/store/settings.store';
import { Slider, SliderProps } from '/@/shared/components/slider/slider';
import { Spinner } from '/@/shared/components/spinner/spinner';
import { Text } from '/@/shared/components/text/text';
import { PlaybackSelectors } from '/@/shared/constants/playback-selectors';
const PlayerbarWaveform = lazy(() =>
import('./playerbar-waveform').then((module) => ({
default: module.PlayerbarWaveform,
})),
);
export const PlayerbarSlider = () => {
const currentSong = usePlayerSong();
const playerbarSlider = usePlayerbarSlider();
@@ -51,7 +58,9 @@ export const PlayerbarSlider = () => {
</div>
<div className={styles.sliderWrapper}>
{isWaveform ? (
<PlayerbarWaveform />
<Suspense fallback={<Spinner />}>
<PlayerbarWaveform />
</Suspense>
) : (
<PlayerbarSeekSlider max={songDuration} min={0} />
)}
@@ -1,13 +1,19 @@
import clsx from 'clsx';
import { MouseEvent } from 'react';
import { lazy, MouseEvent, Suspense } from 'react';
import styles from './playerbar.module.css';
import { CenterControls } from '/@/renderer/features/player/components/center-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 { 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 { usePlayerbarOpenDrawer } from '/@/renderer/store';
import { PlaybackSelectors } from '/@/shared/constants/playback-selectors';
@@ -24,7 +30,11 @@ export const Playerbar = () => {
};
if (isMobile) {
return <MobilePlayerbar />;
return (
<Suspense fallback={<Spinner />}>
<MobilePlayerbar />
</Suspense>
);
}
return (
@@ -2,8 +2,6 @@ import { useQueryClient, useSuspenseQuery } from '@tanstack/react-query';
import { lazy, Suspense, useEffect, useMemo, useRef, useState } from 'react';
import { useParams } from 'react-router';
import { PlaylistDetailSongListEditTable } from './playlist-detail-song-list-table';
import { ItemListHandle } from '/@/renderer/components/item-list/types';
import { useListContext } from '/@/renderer/context/list-context';
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(() =>
import('/@/renderer/features/playlists/components/playlist-detail-song-list-grid').then(
(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 { t } from 'i18next';
import { useTranslation } from 'react-i18next';
import i18n from '/@/i18n/i18n';
import { useUpdatePlaylist } from '/@/renderer/features/playlists/mutations/update-playlist-mutation';
import { sharedQueries } from '/@/renderer/features/shared/api/shared-api';
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 { useForm } from '/@/shared/hooks/use-form';
import {
Playlist,
ServerType,
SortOrder,
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 { useTranslation } from 'react-i18next';
import { getMpvSetting } from './mpv-properties';
import { eventEmitter } from '/@/renderer/events/event-emitter';
import { usePlayer } from '/@/renderer/features/player/context/player-context';
import {
@@ -27,49 +29,6 @@ import { PlayerType } from '/@/shared/types/types';
const localSettings = isElectron() ? window.api.localSettings : 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(() => {
const { t } = useTranslation();
const settings = usePlaybackSettings();
@@ -1,15 +1,41 @@
import isElectron from 'is-electron';
import { lazy, Suspense } from 'react';
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 { useSettingsStore, useSettingsStoreActions } from '/@/renderer/store/settings.store';
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 = () => {
const { t } = useTranslation();
const currentTab = useSettingsStore((state) => state.tab);
@@ -45,21 +71,31 @@ export const SettingsContent = () => {
</Tabs.Tab>
</Tabs.List>
<Tabs.Panel value="general">
<GeneralTab />
<Suspense fallback={null}>
<GeneralTab />
</Suspense>
</Tabs.Panel>
<Tabs.Panel value="playback">
<PlaybackTab />
<Suspense fallback={null}>
<PlaybackTab />
</Suspense>
</Tabs.Panel>
<Tabs.Panel value="hotkeys">
<HotkeysTab />
<Suspense fallback={null}>
<HotkeysTab />
</Suspense>
</Tabs.Panel>
{isElectron() && (
<Tabs.Panel value="window">
<WindowTab />
<Suspense fallback={null}>
<WindowTab />
</Suspense>
</Tabs.Panel>
)}
<Tabs.Panel value="advanced">
<AdvancedTab />
<Suspense fallback={null}>
<AdvancedTab />
</Suspense>
</Tabs.Panel>
</Tabs>
</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 { SettingsHeader } from '/@/renderer/features/settings/components/settings-header';
import { SettingSearchContext } from '/@/renderer/features/settings/context/search-context';
import { AnimatedPage } from '/@/renderer/features/shared/components/animated-page';
import { LibraryContainer } from '/@/renderer/features/shared/components/library-container';
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 [search, setSearch] = useState('');
@@ -15,7 +20,9 @@ const SettingsRoute = () => {
<SettingSearchContext.Provider value={search}>
<LibraryContainer>
<Flex direction="column" h="100%" w="100%">
<SettingsHeader setSearch={setSearch} />
<Suspense fallback={<></>}>
<SettingsHeader setSearch={setSearch} />
</Suspense>
<SettingsContent />
</Flex>
</LibraryContainer>
@@ -1,4 +1,3 @@
import butterchurnPresets from 'butterchurn-presets';
import { nanoid } from 'nanoid';
import { useEffect, useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
@@ -25,6 +24,38 @@ import { Text } from '/@/shared/components/text/text';
import { Textarea } from '/@/shared/components/textarea/textarea';
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 }[] = [
{ label: i18n.t('visualizer.options.mode.0') as string, value: '0' },
{ label: i18n.t('visualizer.options.mode.1') as string, value: '1' },
@@ -2068,13 +2099,7 @@ const ButterchurnGeneralSettings = () => {
const { t } = useTranslation();
const { updateProperty, visualizer } = useUpdateButterchurn();
const presetOptions = useMemo(() => {
const presets = butterchurnPresets;
return Object.keys(presets).map((presetName) => ({
label: presetName,
value: presetName,
}));
}, []);
const presetOptions = useButterchurnPresetOptions();
return (
<Fieldset legend={t('visualizer.general')}>
@@ -2124,13 +2149,7 @@ const ButterChurnCycleSettings = () => {
const { t } = useTranslation();
const { updateProperty, visualizer } = useUpdateButterchurn();
const presetOptions = useMemo(() => {
const presets = butterchurnPresets;
return Object.keys(presets).map((presetName) => ({
label: presetName,
value: presetName,
}));
}, []);
const presetOptions = useButterchurnPresetOptions();
return (
<Fieldset legend={t('visualizer.cyclePresets')}>
@@ -1,5 +1,4 @@
import AudioMotionAnalyzer from 'audiomotion-analyzer';
import { createRef, useCallback, useEffect, useMemo, useState } from 'react';
import { createRef, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import styles from './visualizer.module.css';
@@ -15,7 +14,31 @@ const VisualizerInner = () => {
const accent = useAccent();
const visualizer = useSettingsStore((store) => store.visualizer);
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
const isCustomGradient = useCallback(
@@ -162,7 +185,7 @@ const VisualizerInner = () => {
);
const registerCustomGradients = useCallback(
(audioMotionInstance: AudioMotionAnalyzer) => {
(audioMotionInstance: any) => {
if (visualizer.type !== 'audiomotionanalyzer') {
return;
}
@@ -187,8 +210,11 @@ const VisualizerInner = () => {
useEffect(() => {
const { context, gains } = webAudio || {};
let audioMotion: AudioMotionAnalyzer | undefined;
if (gains && context && canvasRef.current && !motion) {
let audioMotion: any | undefined;
if (gains && context && canvasRef.current && !motion && libraryLoaded) {
const AudioMotionAnalyzer = AudioMotionAnalyzerRef.current;
if (!AudioMotionAnalyzer) return;
// Reset gradients registered flag on new instance
setGradientsRegistered(false);
@@ -236,6 +262,7 @@ const VisualizerInner = () => {
options,
isCustomGradient,
motion,
libraryLoaded,
]);
// 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 styles from './visualizer.module.css';
@@ -27,6 +25,9 @@ const VisualizerInner = () => {
const visualizerRef = useRef<ButterchurnVisualizer | undefined>(undefined);
const isInitializedRef = useRef(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 resizeObserverRef = useRef<ResizeObserver | undefined>(undefined);
const cycleTimerRef = useRef<NodeJS.Timeout | undefined>(undefined);
@@ -39,6 +40,33 @@ const VisualizerInner = () => {
const playerStatus = usePlayerStatus();
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 = () => {
if (animationFrameRef.current) {
cancelAnimationFrame(animationFrameRef.current);
@@ -79,6 +107,7 @@ const VisualizerInner = () => {
canvas &&
container &&
isPlaying &&
librariesLoaded &&
(!isInitializedRef.current || !visualizerRef.current);
if (!needsInitialization) {
@@ -107,13 +136,16 @@ const VisualizerInner = () => {
initializeVisualizer(dimensions.width, dimensions.height);
}
function initializeVisualizer(width: number, height: number) {
if (!gains || gains.length === 0 || !canvas || !context) return;
async function initializeVisualizer(width: number, height: number) {
if (!gains || gains.length === 0 || !canvas || !context || !librariesLoaded) return;
canvas.width = width;
canvas.height = height;
try {
const butterchurn = butterchurnRef.current;
if (!butterchurn) return;
const butterchurnInstance = butterchurn.createVisualizer(context, canvas, {
height,
width,
@@ -138,7 +170,7 @@ const VisualizerInner = () => {
cleanupVisualizer();
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [webAudio, isPlaying]);
}, [webAudio, isPlaying, librariesLoaded]);
// Kill visualizer after 5 seconds of pause
useEffect(() => {
@@ -204,9 +236,11 @@ const VisualizerInner = () => {
// Load initial preset when visualizer is ready
useEffect(() => {
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);
if (presetNames.length > 0) {
@@ -222,14 +256,24 @@ const VisualizerInner = () => {
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)
const isCyclingRef = useRef(false);
useEffect(() => {
const visualizer = visualizerRef.current;
if (!visualizer || !butterchurnSettings.currentPreset || !initialPresetLoadedRef.current)
if (
!visualizer ||
!butterchurnSettings.currentPreset ||
!initialPresetLoadedRef.current ||
!librariesLoaded
)
return;
// Skip if we're currently cycling (to avoid reloading preset)
@@ -238,7 +282,8 @@ const VisualizerInner = () => {
return;
}
const presets = butterchurnPresets;
const presets = butterchurnPresetsRef.current;
if (!presets) return;
const preset = presets[butterchurnSettings.currentPreset];
if (preset) {
@@ -246,12 +291,17 @@ const VisualizerInner = () => {
// Reset cycle timer when preset changes manually
cycleStartTimeRef.current = Date.now();
}
}, [butterchurnSettings.currentPreset, butterchurnSettings.blendTime]);
}, [butterchurnSettings.currentPreset, butterchurnSettings.blendTime, librariesLoaded]);
// Handle preset cycling
useEffect(() => {
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
if (cycleTimerRef.current) {
clearInterval(cycleTimerRef.current);
@@ -260,7 +310,8 @@ const VisualizerInner = () => {
return;
}
const presets = butterchurnPresets;
const presets = butterchurnPresetsRef.current;
if (!presets) return;
const allPresetNames = Object.keys(presets);
// Get the list of presets to cycle through
@@ -359,6 +410,7 @@ const VisualizerInner = () => {
butterchurnSettings.randomizeNextPreset,
butterchurnSettings.currentPreset,
setSettings,
librariesLoaded,
]);
useEffect(() => {
@@ -1,12 +1,22 @@
import { useRef } from 'react';
import { lazy, Suspense, useRef } from 'react';
import styles from './left-sidebar.module.css';
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';
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 {
isResizing: boolean;
startResizing: (direction: 'left' | 'right', mouseEvent?: MouseEvent) => void;
@@ -27,7 +37,7 @@ export const LeftSidebar = ({ isResizing, startResizing }: LeftSidebarProps) =>
placement="right"
ref={sidebarRef}
/>
{collapsed ? <CollapsedSidebar /> : <Sidebar />}
<Suspense fallback={<></>}>{collapsed ? <CollapsedSidebar /> : <Sidebar />}</Suspense>
</aside>
);
};
+102 -8
View File
@@ -1,15 +1,7 @@
import { lazy, Suspense } from 'react';
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 { 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 { ResponsiveLayout } from '/@/renderer/layouts/responsive-layout';
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 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 = () => {
const router = (
<HashRouter>