From ef5daad1dd241783f837b727d23d5dbac68edcec Mon Sep 17 00:00:00 2001 From: jeffvli Date: Sat, 17 Jan 2026 07:32:16 -0800 Subject: [PATCH] add more dynamic imports to optimize bundle --- src/renderer/app.tsx | 13 ++- .../actions/edit-playlist-action.tsx | 2 +- .../context-menu/context-menu-controller.tsx | 85 +++++++++++--- .../audio-player/engine/mpv-player-engine.tsx | 2 +- .../audio-player/engine/web-player-engine.tsx | 35 +++++- .../mobile-fullscreen-player-progress.tsx | 14 ++- .../player/components/playerbar-slider.tsx | 13 ++- .../features/player/components/playerbar.tsx | 16 ++- .../playlist-detail-song-list-content.tsx | 10 +- .../components/update-playlist-form.tsx | 25 +--- .../components/update-playlist-modal.ts | 25 ++++ .../components/playback/mpv-properties.ts | 44 +++++++ .../components/playback/mpv-settings.tsx | 45 +------ .../settings/components/settings-content.tsx | 56 +++++++-- .../settings/routes/settings-route.tsx | 13 ++- .../visualizer-settings-form.tsx | 49 +++++--- .../audiomotionanalyzer/visualizer.tsx | 39 ++++++- .../components/butternchurn/visualizer.tsx | 78 ++++++++++--- .../layouts/default-layout/left-sidebar.tsx | 18 ++- src/renderer/router/app-router.tsx | 110 ++++++++++++++++-- 20 files changed, 529 insertions(+), 163 deletions(-) create mode 100644 src/renderer/features/playlists/components/update-playlist-modal.ts create mode 100644 src/renderer/features/settings/components/playback/mpv-properties.ts diff --git a/src/renderer/app.tsx b/src/renderer/app.tsx index 3ee95a8f1..18adc6dfd 100644 --- a/src/renderer/app.tsx +++ b/src/renderer/app.tsx @@ -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 = () => { - + + + ); }; diff --git a/src/renderer/features/context-menu/actions/edit-playlist-action.tsx b/src/renderer/features/context-menu/actions/edit-playlist-action.tsx index a4b653191..d24f14d9a 100644 --- a/src/renderer/features/context-menu/actions/edit-playlist-action.tsx +++ b/src/renderer/features/context-menu/actions/edit-playlist-action.tsx @@ -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'; diff --git a/src/renderer/features/context-menu/context-menu-controller.tsx b/src/renderer/features/context-menu/context-menu-controller.tsx index 31ec9bbd7..e34c6b506 100644 --- a/src/renderer/features/context-menu/context-menu-controller.tsx +++ b/src/renderer/features/context-menu/context-menu-controller.tsx @@ -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 - {cmd.type === LibraryItem.QUEUE_SONG && } - {cmd.type === LibraryItem.ALBUM && } - {cmd.type === LibraryItem.ALBUM_ARTIST && } - {cmd.type === LibraryItem.ARTIST && } - {cmd.type === LibraryItem.FOLDER && } - {cmd.type === LibraryItem.GENRE && } - {cmd.type === LibraryItem.PLAYLIST && } - {cmd.type === LibraryItem.PLAYLIST_SONG && } - {cmd.type === LibraryItem.SONG && } + + {cmd.type === LibraryItem.QUEUE_SONG && } + {cmd.type === LibraryItem.ALBUM && } + {cmd.type === LibraryItem.ALBUM_ARTIST && } + {cmd.type === LibraryItem.ARTIST && } + {cmd.type === LibraryItem.FOLDER && } + {cmd.type === LibraryItem.GENRE && } + {cmd.type === LibraryItem.PLAYLIST && } + {cmd.type === LibraryItem.PLAYLIST_SONG && } + {cmd.type === LibraryItem.SONG && } + ); }, diff --git a/src/renderer/features/player/audio-player/engine/mpv-player-engine.tsx b/src/renderer/features/player/audio-player/engine/mpv-player-engine.tsx index c231d2f07..7726772b2 100644 --- a/src/renderer/features/player/audio-player/engine/mpv-player-engine.tsx +++ b/src/renderer/features/player/audio-player/engine/mpv-player-engine.tsx @@ -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, diff --git a/src/renderer/features/player/audio-player/engine/web-player-engine.tsx b/src/renderer/features/player/audio-player/engine/web-player-engine.tsx index f2824d7f0..dec9ff203 100644 --- a/src/renderer/features/player/audio-player/engine/web-player-engine.tsx +++ b/src/renderer/features/player/audio-player/engine/web-player-engine.tsx @@ -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); const player2Ref = useRef(null); + const [ReactPlayerComponent, setReactPlayerComponent] = useState(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
; + } + return (
- { volume={volume1} width={0} /> - + import('/@/renderer/features/player/components/playerbar-waveform').then((module) => ({ + default: module.PlayerbarWaveform, + })), +); + interface MobileFullscreenPlayerProgressProps { currentSong?: QueueSong; } @@ -38,7 +44,9 @@ export const MobileFullscreenPlayerProgress = memo(
{isWaveform ? ( - + }> + + ) : ( )} diff --git a/src/renderer/features/player/components/playerbar-slider.tsx b/src/renderer/features/player/components/playerbar-slider.tsx index 8a36b84bc..0d58f8933 100644 --- a/src/renderer/features/player/components/playerbar-slider.tsx +++ b/src/renderer/features/player/components/playerbar-slider.tsx @@ -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 = () => {
{isWaveform ? ( - + }> + + ) : ( )} diff --git a/src/renderer/features/player/components/playerbar.tsx b/src/renderer/features/player/components/playerbar.tsx index 89adccbd0..cc56ffad7 100644 --- a/src/renderer/features/player/components/playerbar.tsx +++ b/src/renderer/features/player/components/playerbar.tsx @@ -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 ; + return ( + }> + + + ); } return ( diff --git a/src/renderer/features/playlists/components/playlist-detail-song-list-content.tsx b/src/renderer/features/playlists/components/playlist-detail-song-list-content.tsx index a8c81da23..8243be7f6 100644 --- a/src/renderer/features/playlists/components/playlist-detail-song-list-content.tsx +++ b/src/renderer/features/playlists/components/playlist-detail-song-list-content.tsx @@ -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) => ({ diff --git a/src/renderer/features/playlists/components/update-playlist-form.tsx b/src/renderer/features/playlists/components/update-playlist-form.tsx index 0640f0c0f..dd5730505 100644 --- a/src/renderer/features/playlists/components/update-playlist-form.tsx +++ b/src/renderer/features/playlists/components/update-playlist-form.tsx @@ -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 ); }; - -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, - }); -}; diff --git a/src/renderer/features/playlists/components/update-playlist-modal.ts b/src/renderer/features/playlists/components/update-playlist-modal.ts new file mode 100644 index 000000000..fff8c95bf --- /dev/null +++ b/src/renderer/features/playlists/components/update-playlist-modal.ts @@ -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, + }); +}; diff --git a/src/renderer/features/settings/components/playback/mpv-properties.ts b/src/renderer/features/settings/components/playback/mpv-properties.ts new file mode 100644 index 000000000..af3252b46 --- /dev/null +++ b/src/renderer/features/settings/components/playback/mpv-properties.ts @@ -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 = { + '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; +}; diff --git a/src/renderer/features/settings/components/playback/mpv-settings.tsx b/src/renderer/features/settings/components/playback/mpv-settings.tsx index 6f1589255..c54e17bee 100644 --- a/src/renderer/features/settings/components/playback/mpv-settings.tsx +++ b/src/renderer/features/settings/components/playback/mpv-settings.tsx @@ -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 = { - '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(); diff --git a/src/renderer/features/settings/components/settings-content.tsx b/src/renderer/features/settings/components/settings-content.tsx index 74fd36079..1b39660aa 100644 --- a/src/renderer/features/settings/components/settings-content.tsx +++ b/src/renderer/features/settings/components/settings-content.tsx @@ -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 = () => { - + + + - + + + - + + + {isElectron() && ( - + + + )} - + + +
diff --git a/src/renderer/features/settings/routes/settings-route.tsx b/src/renderer/features/settings/routes/settings-route.tsx index b3a24b708..d24743d63 100644 --- a/src/renderer/features/settings/routes/settings-route.tsx +++ b/src/renderer/features/settings/routes/settings-route.tsx @@ -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 = () => { - + }> + + diff --git a/src/renderer/features/visualizer/components/audiomotionanalyzer/visualizer-settings-form.tsx b/src/renderer/features/visualizer/components/audiomotionanalyzer/visualizer-settings-form.tsx index 825de019d..2dbaefc51 100644 --- a/src/renderer/features/visualizer/components/audiomotionanalyzer/visualizer-settings-form.tsx +++ b/src/renderer/features/visualizer/components/audiomotionanalyzer/visualizer-settings-form.tsx @@ -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 => { + 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( + 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 (
@@ -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 (
diff --git a/src/renderer/features/visualizer/components/audiomotionanalyzer/visualizer.tsx b/src/renderer/features/visualizer/components/audiomotionanalyzer/visualizer.tsx index 4b671800e..f012080b8 100644 --- a/src/renderer/features/visualizer/components/audiomotionanalyzer/visualizer.tsx +++ b/src/renderer/features/visualizer/components/audiomotionanalyzer/visualizer.tsx @@ -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(); + const [motion, setMotion] = useState(); + const [libraryLoaded, setLibraryLoaded] = useState(false); + const AudioMotionAnalyzerRef = useRef(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 diff --git a/src/renderer/features/visualizer/components/butternchurn/visualizer.tsx b/src/renderer/features/visualizer/components/butternchurn/visualizer.tsx index 8208e9be4..d336a76f1 100644 --- a/src/renderer/features/visualizer/components/butternchurn/visualizer.tsx +++ b/src/renderer/features/visualizer/components/butternchurn/visualizer.tsx @@ -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(undefined); const isInitializedRef = useRef(false); const [isVisualizerReady, setIsVisualizerReady] = useState(false); + const [librariesLoaded, setLibrariesLoaded] = useState(false); + const butterchurnRef = useRef(null); + const butterchurnPresetsRef = useRef(null); const animationFrameRef = useRef(undefined); const resizeObserverRef = useRef(undefined); const cycleTimerRef = useRef(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(() => { diff --git a/src/renderer/layouts/default-layout/left-sidebar.tsx b/src/renderer/layouts/default-layout/left-sidebar.tsx index 405905329..ea0e651bc 100644 --- a/src/renderer/layouts/default-layout/left-sidebar.tsx +++ b/src/renderer/layouts/default-layout/left-sidebar.tsx @@ -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 ? : } + }>{collapsed ? : } ); }; diff --git a/src/renderer/router/app-router.tsx b/src/renderer/router/app-router.tsx index d085f52a6..1a92cf576 100644 --- a/src/renderer/router/app-router.tsx +++ b/src/renderer/router/app-router.tsx @@ -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) => ( + }> + + +); + +const LazyShuffleAllContextModal = lazy(() => + import('/@/renderer/features/player/components/shuffle-all-modal').then((module) => ({ + default: module.ShuffleAllContextModal, + })), +); + +const ShuffleAllContextModal = (props: any) => ( + }> + + +); + +const LazyAddToPlaylistContextModal = lazy(() => + import('/@/renderer/features/playlists/components/add-to-playlist-context-modal').then( + (module) => ({ + default: module.AddToPlaylistContextModal, + }), + ), +); + +const AddToPlaylistContextModal = (props: any) => ( + }> + + +); + +const LazySaveAndReplaceContextModal = lazy(() => + import('/@/renderer/features/playlists/components/save-and-replace-context-modal').then( + (module) => ({ + default: module.SaveAndReplaceContextModal, + }), + ), +); + +const SaveAndReplaceContextModal = (props: any) => ( + }> + + +); + +const LazyUpdatePlaylistContextModal = lazy(() => + import('/@/renderer/features/playlists/components/update-playlist-form').then((module) => ({ + default: module.UpdatePlaylistContextModal, + })), +); + +const UpdatePlaylistContextModal = (props: any) => ( + }> + + +); + +const LazySettingsContextModal = lazy(() => + import('/@/renderer/features/settings/components/settings-modal').then((module) => ({ + default: module.SettingsContextModal, + })), +); + +const SettingsContextModal = (props: any) => ( + }> + + +); + +const LazyShareItemContextModal = lazy(() => + import('/@/renderer/features/sharing/components/share-item-context-modal').then((module) => ({ + default: module.ShareItemContextModal, + })), +); + +const ShareItemContextModal = (props: any) => ( + }> + + +); + +const LazyVisualizerSettingsContextModal = lazy(() => + import( + '/@/renderer/features/visualizer/components/audiomotionanalyzer/visualizer-settings-modal' + ).then((module) => ({ + default: module.VisualizerSettingsContextModal, + })), +); + +const VisualizerSettingsContextModal = (props: any) => ( + }> + + +); + export const AppRouter = () => { const router = (