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