various performance refactors

This commit is contained in:
jeffvli
2026-04-01 21:27:28 -07:00
parent c60610cb42
commit 51425b5e86
14 changed files with 313 additions and 148 deletions
+113 -55
View File
@@ -7,7 +7,7 @@ import '@mantine/core/styles.css';
import '@mantine/dates/styles.css';
import '@mantine/notifications/styles.css';
import isElectron from 'is-electron';
import { lazy, Suspense, useEffect, useMemo, useRef, useState } from 'react';
import { lazy, memo, Suspense, useEffect, useMemo, useRef, useState } from 'react';
import i18n from '/@/i18n/i18n';
import { openSettingsModal } from '/@/renderer/features/settings/utils/open-settings-modal';
@@ -38,67 +38,26 @@ const UpdateAvailableDialog = lazy(() =>
const ipc = isElectron() ? window.api.ipc : null;
export const App = () => {
return <ThemedApp />;
};
const ThemedApp = () => {
const { mode, theme } = useAppTheme();
const language = useLanguage();
const { content, enabled } = useCssSettings();
const { bindings } = useHotkeySettings();
const cssRef = useRef<HTMLStyleElement | null>(null);
useSyncSettingsToMain();
useCheckForUpdates();
return (
<MantineProvider forceColorScheme={mode} theme={theme}>
<AppShell />
</MantineProvider>
);
};
const AppShell = memo(function AppShell() {
const [webAudio, setWebAudio] = useState<WebAudio>();
useEffect(() => {
if (enabled && content) {
// Yes, CSS is sanitized here as well. Prevent a suer from changing the
// localStorage to bypass sanitizing.
const sanitized = sanitizeCss(content);
if (!cssRef.current) {
cssRef.current = document.createElement('style');
document.body.appendChild(cssRef.current);
}
cssRef.current.textContent = sanitized;
return () => {
cssRef.current!.textContent = '';
};
}
return () => {};
}, [content, enabled]);
const webAudioProvider = useMemo(() => {
return { setWebAudio, webAudio };
}, [webAudio]);
useEffect(() => {
if (isElectron()) {
ipc?.send('set-global-shortcuts', bindings);
}
}, [bindings]);
useEffect(() => {
if (language) {
i18n.changeLanguage(language);
}
}, [language]);
useEffect(() => {
if (isElectron()) {
window.api.utils.rendererOpenSettings(() => {
openSettingsModal();
});
return () => {
ipc?.removeAllListeners('renderer-open-settings');
};
}
return undefined;
}, []);
const notificationStyles = useMemo(
() => ({
root: {
@@ -109,7 +68,8 @@ export const App = () => {
);
return (
<MantineProvider forceColorScheme={mode} theme={theme}>
<>
<AppEffects />
<Notifications
containerWidth="300px"
position="bottom-center"
@@ -126,6 +86,104 @@ export const App = () => {
<ReleaseNotesModal />
<UpdateAvailableDialog />
</Suspense>
</MantineProvider>
</>
);
});
const AppEffects = () => (
<>
<SyncSettingsEffect />
<UpdateCheckEffect />
<CssSettingsEffect />
<GlobalShortcutsEffect />
<LanguageEffect />
<OpenSettingsEffect />
</>
);
const SyncSettingsEffect = () => {
useSyncSettingsToMain();
return null;
};
const UpdateCheckEffect = () => {
useCheckForUpdates();
return null;
};
const CssSettingsEffect = () => {
const { content, enabled } = useCssSettings();
const cssRef = useRef<HTMLStyleElement | null>(null);
useEffect(() => {
if (!enabled || !content) {
if (cssRef.current) {
cssRef.current.textContent = '';
}
return;
}
// Yes, CSS is sanitized here as well. Prevent a user from changing the
// localStorage to bypass sanitizing.
const sanitized = sanitizeCss(content);
if (!cssRef.current) {
cssRef.current = document.createElement('style');
document.body.appendChild(cssRef.current);
}
cssRef.current.textContent = sanitized;
return () => {
if (cssRef.current) {
cssRef.current.textContent = '';
}
};
}, [content, enabled]);
return null;
};
const GlobalShortcutsEffect = () => {
const { bindings } = useHotkeySettings();
useEffect(() => {
if (isElectron()) {
ipc?.send('set-global-shortcuts', bindings);
}
}, [bindings]);
return null;
};
const LanguageEffect = () => {
const language = useLanguage();
useEffect(() => {
if (language) {
i18n.changeLanguage(language);
}
}, [language]);
return null;
};
const OpenSettingsEffect = () => {
useEffect(() => {
if (isElectron()) {
window.api.utils.rendererOpenSettings(() => {
openSettingsModal();
});
return () => {
ipc?.removeAllListeners('renderer-open-settings');
};
}
return undefined;
}, []);
return null;
};
+12 -3
View File
@@ -7,15 +7,23 @@ const GARBAGE_COLLECTION_INTERVAL = 1000 * 60 * 5;
export const useGarbageCollection = () => {
const intervalIdRef = useRef<NodeJS.Timeout | null>(null);
const startInterval = () => {
if (intervalIdRef.current) {
clearInterval(intervalIdRef.current);
}
intervalIdRef.current = setInterval(() => {
window.api?.utils?.forceGarbageCollection?.();
}, GARBAGE_COLLECTION_INTERVAL);
};
// Clear the cache on an interval
useEffect(() => {
if (!isElectron()) {
return;
}
intervalIdRef.current = setInterval(() => {
window.api?.utils?.forceGarbageCollection?.();
}, GARBAGE_COLLECTION_INTERVAL);
startInterval();
return () => {
if (intervalIdRef.current) {
@@ -38,5 +46,6 @@ export const useGarbageCollection = () => {
}
window.api?.utils?.forceGarbageCollection?.();
startInterval();
}, [location]);
};
+25 -16
View File
@@ -2,13 +2,13 @@ import { isAxiosError } from 'axios';
import isElectron from 'is-electron';
import debounce from 'lodash/debounce';
import isEqual from 'lodash/isEqual';
import { useCallback, useEffect, useRef, useState } from 'react';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useNavigate } from 'react-router';
import { api } from '/@/renderer/api';
import { controller } from '/@/renderer/api/controller';
import { AppRoute } from '/@/renderer/router/routes';
import { getServerById, useAuthStoreActions, useCurrentServer } from '/@/renderer/store';
import { getServerById, useAuthStoreActions, useCurrentServerId } from '/@/renderer/store';
import { LogCategory, logFn } from '/@/renderer/utils/logger';
import { logMsg } from '/@/renderer/utils/logger-message';
import { toast } from '/@/shared/components/toast/toast';
@@ -40,13 +40,18 @@ const isNetworkError = (error: any): boolean => {
export const useServerAuthenticated = () => {
const priorServerId = useRef<string | undefined>(undefined);
const server = useCurrentServer();
const serverId = useCurrentServerId();
const [ready, setReady] = useState(AuthState.LOADING);
const navigate = useNavigate();
const navigateRef = useRef(navigate);
const retryCountRef = useRef<number>(0);
const { setCurrentServer, updateServer } = useAuthStoreActions();
useEffect(() => {
navigateRef.current = navigate;
}, [navigate]);
const authenticateServer = useCallback(
async (serverWithAuth: NonNullable<ReturnType<typeof getServerById>>, retryAttempt = 0) => {
const authStartTime = Date.now();
@@ -312,7 +317,7 @@ export const useServerAuthenticated = () => {
// Don't clear credentials on network failure - preserve them for when network returns
setReady(AuthState.INVALID);
navigate(AppRoute.NO_NETWORK, { replace: true });
navigateRef.current(AppRoute.NO_NETWORK, { replace: true });
return;
}
@@ -341,18 +346,19 @@ export const useServerAuthenticated = () => {
setReady(AuthState.INVALID);
}
},
[updateServer, setCurrentServer, navigate],
[updateServer, setCurrentServer],
);
const debouncedAuth = debounce(
(serverWithAuth: NonNullable<ReturnType<typeof getServerById>>) => {
authenticateServer(serverWithAuth).catch(console.error);
},
300,
const debouncedAuth = useMemo(
() =>
debounce((serverWithAuth: NonNullable<ReturnType<typeof getServerById>>) => {
authenticateServer(serverWithAuth).catch(console.error);
}, 300),
[authenticateServer],
);
useEffect(() => {
if (!server) {
if (!serverId) {
logFn.debug(logMsg[LogCategory.SYSTEM].serverAuthenticationInvalid, {
category: LogCategory.SYSTEM,
meta: {
@@ -363,9 +369,9 @@ export const useServerAuthenticated = () => {
return;
}
if (priorServerId.current !== server.id) {
const serverWithAuth = getServerById(server.id);
priorServerId.current = server.id;
if (priorServerId.current !== serverId) {
const serverWithAuth = getServerById(serverId);
priorServerId.current = serverId;
retryCountRef.current = 0; // Reset retry count when server changes
if (!serverWithAuth) {
@@ -373,7 +379,7 @@ export const useServerAuthenticated = () => {
category: LogCategory.SYSTEM,
meta: {
reason: 'Server not found in store',
serverId: server.id,
serverId,
},
});
setReady(AuthState.INVALID);
@@ -383,7 +389,10 @@ export const useServerAuthenticated = () => {
setReady(AuthState.LOADING);
debouncedAuth(serverWithAuth);
}
}, [debouncedAuth, server]);
return () => {
debouncedAuth.cancel();
};
}, [debouncedAuth, serverId]);
return ready;
};
+2 -2
View File
@@ -7,7 +7,7 @@ import styles from './default-layout.module.css';
import { ContextMenuController } from '/@/renderer/features/context-menu/context-menu-controller';
import { MainContent } from '/@/renderer/layouts/default-layout/main-content';
import { PlayerBar } from '/@/renderer/layouts/default-layout/player-bar';
import { useSettingsStore, useWindowSettings } from '/@/renderer/store/settings.store';
import { useSettingsStore, useWindowBarStyle } from '/@/renderer/store/settings.store';
import { Platform, PlayerType } from '/@/shared/types/types';
if (!isElectron()) {
@@ -29,7 +29,7 @@ interface DefaultLayoutProps {
}
export const DefaultLayout = ({ shell }: DefaultLayoutProps) => {
const { windowBarStyle } = useWindowSettings();
const windowBarStyle = useWindowBarStyle();
return (
<>
@@ -175,13 +175,18 @@ export const MainContent = ({ shell }: { shell?: boolean }) => {
);
useEffect(() => {
if (!isResizing && !isResizingRight) {
return;
}
window.addEventListener('mousemove', resize);
window.addEventListener('mouseup', stopResizing);
return () => {
window.removeEventListener('mousemove', resize);
window.removeEventListener('mouseup', stopResizing);
};
}, [resize, stopResizing]);
}, [isResizing, isResizingRight, resize, stopResizing]);
return (
<motion.div
@@ -10,8 +10,7 @@ import { FullScreenVisualizer } from '/@/renderer/features/player/components/ful
import { MobileFullscreenPlayer } from '/@/renderer/features/player/components/mobile-fullscreen-player';
import { MobileSidebar } from '/@/renderer/features/sidebar/components/mobile-sidebar';
import { PlayerBar } from '/@/renderer/layouts/default-layout/player-bar';
import { useFullScreenPlayerStore } from '/@/renderer/store';
import { useWindowSettings } from '/@/renderer/store';
import { useFullScreenPlayerOverlayState, useWindowBarStyle } from '/@/renderer/store';
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
import { Drawer } from '/@/shared/components/drawer/drawer';
import { useDisclosure } from '/@/shared/hooks/use-disclosure';
@@ -32,8 +31,8 @@ export const MobileLayout = ({ shell }: MobileLayoutProps) => {
const {
expanded: isFullScreenPlayerExpanded,
visualizerExpanded: isFullScreenVisualizerExpanded,
} = useFullScreenPlayerStore();
const { windowBarStyle } = useWindowSettings();
} = useFullScreenPlayerOverlayState();
const windowBarStyle = useWindowBarStyle();
return (
<>
+61 -28
View File
@@ -1,4 +1,5 @@
import isElectron from 'is-electron';
import { useCallback, useEffect, useMemo } from 'react';
import { useNavigate } from 'react-router';
import { useAppTracker } from '/@/renderer/features/analytics/hooks/use-app-tracker';
@@ -9,8 +10,8 @@ import { DefaultLayout } from '/@/renderer/layouts/default-layout';
import { MobileLayout } from '/@/renderer/layouts/mobile-layout/mobile-layout';
import { AppRoute } from '/@/renderer/router/routes';
import {
useCommandPalette,
useHotkeySettings,
useCommandPaletteState,
useLayoutHotkeyBindings,
useSettingsStoreActions,
useZoomFactor,
} from '/@/renderer/store';
@@ -42,40 +43,72 @@ export const ResponsiveLayout = ({ shell }: ResponsiveLayoutProps) => {
);
};
const localSettings = isElectron() ? window.api.localSettings : null;
const LayoutHotkeys = () => {
const navigate = useNavigate();
const localSettings = isElectron() ? window.api.localSettings : null;
const zoomFactor = useZoomFactor();
const { setSettings } = useSettingsStoreActions();
const { bindings } = useHotkeySettings();
const { opened, ...handlers } = useCommandPalette();
const bindings = useLayoutHotkeyBindings();
const { close, open, opened, toggle } = useCommandPaletteState();
const updateZoom = (increase: number) => {
const newVal = zoomFactor + increase;
if (newVal > 300 || newVal < 50 || !isElectron()) return;
setSettings({
general: {
zoomFactor: newVal,
},
});
localSettings?.setZoomFactor(zoomFactor);
};
localSettings?.setZoomFactor(zoomFactor);
const handlers = useMemo(
() => ({
close,
open,
toggle,
}),
[close, open, toggle],
);
const zoomHotkeys: HotkeyItem[] = [
[bindings.zoomIn.hotkey, () => updateZoom(5)],
[bindings.zoomOut.hotkey, () => updateZoom(-5)],
];
const updateZoom = useCallback(
(increase: number) => {
const newVal = zoomFactor + increase;
if (newVal > 300 || newVal < 50 || !localSettings) return;
useHotkeys([
[bindings.globalSearch.hotkey, () => handlers.open()],
[bindings.browserBack.hotkey, () => navigate(-1)],
[bindings.browserForward.hotkey, () => navigate(1)],
[bindings.navigateHome.hotkey, () => navigate(AppRoute.HOME)],
...(isElectron() ? zoomHotkeys : []),
]);
setSettings({
general: {
zoomFactor: newVal,
},
});
localSettings?.setZoomFactor(newVal);
},
[setSettings, zoomFactor],
);
return <CommandPalette modalProps={{ handlers, opened }} />;
useEffect(() => {
if (localSettings) {
localSettings?.setZoomFactor(zoomFactor);
}
}, [zoomFactor]);
const hotkeys = useMemo<HotkeyItem[]>(
() => [
[bindings.globalSearch.hotkey, open],
[bindings.browserBack.hotkey, () => navigate(-1)],
[bindings.browserForward.hotkey, () => navigate(1)],
[bindings.navigateHome.hotkey, () => navigate(AppRoute.HOME)],
...(localSettings
? ([
[bindings.zoomIn.hotkey, () => updateZoom(5)],
[bindings.zoomOut.hotkey, () => updateZoom(-5)],
] as HotkeyItem[])
: []),
],
[bindings, navigate, open, updateZoom],
);
const modalProps = useMemo(
() => ({
handlers,
opened,
}),
[handlers, opened],
);
useHotkeys(hotkeys);
return <CommandPalette modalProps={modalProps} />;
};
const GarbageCollection = () => {
+28 -20
View File
@@ -1,37 +1,45 @@
import { useMemo } from 'react';
import { useEffect, useMemo } from 'react';
import { Navigate, Outlet } from 'react-router';
import { shallow } from 'zustand/shallow';
import { isServerLock } from '/@/renderer/features/action-required/utils/window-properties';
import { AppRoute } from '/@/renderer/router/routes';
import { useAuthStoreActions, useCurrentServer } from '/@/renderer/store';
import { useAuthStore, useAuthStoreActions } from '/@/renderer/store';
const normalizeUrl = (url: string) => url.replace(/\/$/, '');
export const AppOutlet = () => {
const currentServer = useCurrentServer();
const currentServer = useAuthStore(
(state) =>
state.currentServer
? {
id: state.currentServer.id,
url: state.currentServer.url,
}
: null,
shallow,
);
const { deleteServer, setCurrentServer } = useAuthStoreActions();
const isActionsRequired = useMemo(() => {
// When SERVER_LOCK is enabled and the configured URL has changed,
// clear the stale session so the user re-authenticates against the new server.
if (isServerLock() && currentServer && window.SERVER_URL) {
const configuredUrl = normalizeUrl(window.SERVER_URL);
const persistedUrl = normalizeUrl(currentServer.url);
if (configuredUrl !== persistedUrl) {
deleteServer(currentServer.id);
setCurrentServer(null);
return true;
}
const hasServerLockMismatch = useMemo(() => {
if (!isServerLock() || !currentServer || !window.SERVER_URL) {
return false;
}
const isServerRequired = !currentServer;
const configuredUrl = normalizeUrl(window.SERVER_URL);
const persistedUrl = normalizeUrl(currentServer.url);
const actions = [isServerRequired];
const isActionRequired = actions.some((c) => c);
return configuredUrl !== persistedUrl;
}, [currentServer]);
return isActionRequired;
}, [currentServer, deleteServer, setCurrentServer]);
useEffect(() => {
if (hasServerLockMismatch && currentServer) {
deleteServer(currentServer.id);
setCurrentServer(null);
}
}, [currentServer, deleteServer, hasServerLockMismatch, setCurrentServer]);
const isActionsRequired = !currentServer || hasServerLockMismatch;
if (isActionsRequired) {
return <Navigate replace to={AppRoute.ACTION_REQUIRED} />;
+13 -13
View File
@@ -186,22 +186,22 @@ const VisualizerSettingsContextModal = (props: any) => (
</Suspense>
);
const appRouterModals = {
addToPlaylist: AddToPlaylistContextModal,
base: BaseContextModal,
lyricsSettings: LyricsSettingsContextModal,
saveAndReplace: SaveAndReplaceContextModal,
settings: SettingsContextModal,
shareItem: ShareItemContextModal,
shuffleAll: ShuffleAllContextModal,
updatePlaylist: UpdatePlaylistContextModal,
visualizerSettings: VisualizerSettingsContextModal,
};
export const AppRouter = () => {
const router = (
<HashRouter>
<ModalsProvider
modals={{
addToPlaylist: AddToPlaylistContextModal,
base: BaseContextModal,
lyricsSettings: LyricsSettingsContextModal,
saveAndReplace: SaveAndReplaceContextModal,
settings: SettingsContextModal,
shareItem: ShareItemContextModal,
shuffleAll: ShuffleAllContextModal,
updatePlaylist: UpdatePlaylistContextModal,
visualizerSettings: VisualizerSettingsContextModal,
}}
>
<ModalsProvider modals={appRouterModals}>
<RouterErrorBoundary>
<Routes>
<Route element={<AuthenticationOutlet />}>
+2 -2
View File
@@ -3,11 +3,11 @@ import { Outlet } from 'react-router';
import styles from './titlebar-outlet.module.css';
import { Titlebar } from '/@/renderer/features/titlebar/components/titlebar';
import { useWindowSettings } from '/@/renderer/store/settings.store';
import { useWindowBarStyle } from '/@/renderer/store/settings.store';
import { Platform } from '/@/shared/types/types';
export const TitlebarOutlet = () => {
const { windowBarStyle } = useWindowSettings();
const windowBarStyle = useWindowBarStyle();
return (
<>
+12
View File
@@ -4,6 +4,7 @@ import type { LibraryItem } from '/@/shared/types/domain-types';
import merge from 'lodash/merge';
import { devtools, persist } from 'zustand/middleware';
import { immer } from 'zustand/middleware/immer';
import { shallow } from 'zustand/shallow';
import { createWithEqualityFn } from 'zustand/traditional';
import { AlbumListSort, SongListSort, SortOrder } from '/@/shared/types/domain-types';
@@ -280,6 +281,17 @@ export const useTitlebarStore = () => useAppStore((state) => state.titlebar);
export const useCommandPalette = () => useAppStore((state) => state.commandPalette);
export const useCommandPaletteState = () =>
useAppStore(
(state) => ({
close: state.commandPalette.close,
open: state.commandPalette.open,
opened: state.commandPalette.opened,
toggle: state.commandPalette.toggle,
}),
shallow,
);
export const usePageSidebar = (key: string): [boolean, (value: boolean) => void] => {
const isOpen = useAppStore((state) => state.pageSidebar[key] ?? false);
const setPageSidebar = useAppStore((state) => state.actions.setPageSidebar);
@@ -1,6 +1,7 @@
import merge from 'lodash/merge';
import { devtools, persist } from 'zustand/middleware';
import { immer } from 'zustand/middleware/immer';
import { shallow } from 'zustand/shallow';
import { createWithEqualityFn } from 'zustand/traditional';
export interface FullScreenPlayerSlice extends FullScreenPlayerState {
@@ -62,3 +63,12 @@ export const useFullScreenPlayerStoreActions = () =>
export const useSetFullScreenPlayerStore = () =>
useFullScreenPlayerStore((state) => state.actions.setStore);
export const useFullScreenPlayerOverlayState = () =>
useFullScreenPlayerStore(
(state) => ({
expanded: state.expanded,
visualizerExpanded: state.visualizerExpanded,
}),
shallow,
);
+8 -4
View File
@@ -1,5 +1,6 @@
import merge from 'lodash/merge';
import { nanoid } from 'nanoid';
import { useMemo } from 'react';
import { persist, subscribeWithSelector } from 'zustand/middleware';
import { immer } from 'zustand/middleware/immer';
import { useShallow } from 'zustand/react/shallow';
@@ -1636,10 +1637,13 @@ export const usePlayerActions = () => {
})),
);
return {
...actions,
setTimestamp: setTimestampStore,
};
return useMemo(
() => ({
...actions,
setTimestamp: setTimestampStore,
}),
[actions],
);
};
export type AddToQueueByPlayType = Play;
+18
View File
@@ -2421,8 +2421,26 @@ export const usePlayButtonBehavior = () =>
export const useWindowSettings = () => useSettingsStore((state) => state.window, shallow);
export const useWindowBarStyle = () =>
useSettingsStore((state) => state.window.windowBarStyle, shallow);
export const useHotkeySettings = () => useSettingsStore((state) => state.hotkeys, shallow);
export const useHotkeyBindings = () => useSettingsStore((state) => state.hotkeys.bindings, shallow);
export const useLayoutHotkeyBindings = () =>
useSettingsStore(
(state) => ({
browserBack: state.hotkeys.bindings.browserBack,
browserForward: state.hotkeys.bindings.browserForward,
globalSearch: state.hotkeys.bindings.globalSearch,
navigateHome: state.hotkeys.bindings.navigateHome,
zoomIn: state.hotkeys.bindings.zoomIn,
zoomOut: state.hotkeys.bindings.zoomOut,
}),
shallow,
);
export const useMpvSettings = () =>
useSettingsStore((state) => state.playback.mpvProperties, shallow);