mirror of
https://github.com/jeffvli/feishin.git
synced 2026-08-07 12:53:14 +02:00
fix: f11 fullscreen was completely broken on linux/windows; feat: butterchurn preset expansion + randomized preset arrows (#2280)
* added a ton of visualizers * added f11 toggle functionality, f11 makes visualizer cover entire screen when pressed while visualizer is maximized Bundling the additional butterchurn preset packs (extra, md1) pushed renderer build memory past the default ~2GB Node heap on the macOS CI runner, causing OOM crashes during the build. --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+1
-1
@@ -22,7 +22,7 @@
|
||||
"main": "./out/main/index.js",
|
||||
"scripts": {
|
||||
"build": "pnpm run build:electron && pnpm run build:remote",
|
||||
"build:electron": "electron-vite build",
|
||||
"build:electron": "cross-env NODE_OPTIONS=--max-old-space-size=4096 electron-vite build",
|
||||
"build:remote": "vite build --config remote.vite.config.ts",
|
||||
"build:web": "vite build --config web.vite.config.ts",
|
||||
"dev": "electron-vite dev",
|
||||
|
||||
@@ -12,6 +12,7 @@ import { lazy, memo, Suspense, useEffect, useMemo, useRef, useState } from 'reac
|
||||
import i18n from '/@/i18n/i18n';
|
||||
import { WebAudioContext } from '/@/renderer/features/player/context/webaudio-context';
|
||||
import { useCheckForUpdates } from '/@/renderer/hooks/use-check-for-updates';
|
||||
import { useFullscreenToggle } from '/@/renderer/hooks/use-fullscreen-toggle';
|
||||
import { useNativeMenuSync } from '/@/renderer/hooks/use-native-menu-sync';
|
||||
import { useSyncSettingsToMain } from '/@/renderer/hooks/use-sync-settings-to-main';
|
||||
import { AppRouter } from '/@/renderer/router/app-router';
|
||||
@@ -118,6 +119,7 @@ const AppEffects = () => (
|
||||
<GlobalShortcutsEffect />
|
||||
<LanguageEffect />
|
||||
<NativeMenuSyncEffect />
|
||||
<FullscreenToggleEffect />
|
||||
<InputFocusEffect />
|
||||
</>
|
||||
);
|
||||
@@ -262,6 +264,12 @@ const NativeMenuSyncEffect = () => {
|
||||
return null;
|
||||
};
|
||||
|
||||
const FullscreenToggleEffect = () => {
|
||||
useFullscreenToggle();
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const InputFocusEffect = () => {
|
||||
useEffect(() => {
|
||||
if (!isElectron()) return;
|
||||
|
||||
@@ -64,3 +64,9 @@
|
||||
line-height: 1.3;
|
||||
color: #ddd;
|
||||
}
|
||||
|
||||
.visualizer-container:fullscreen {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background: var(--theme-colors-background);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { motion, Variants } from 'motion/react';
|
||||
import { lazy, memo, ReactNode, Suspense, useLayoutEffect, useRef } from 'react';
|
||||
import { lazy, memo, ReactNode, Suspense, useEffect, useLayoutEffect, useRef } from 'react';
|
||||
import { useLocation } from 'react-router';
|
||||
|
||||
import styles from './full-screen-visualizer.module.css';
|
||||
|
||||
import { FullScreenVisualizerSongInfo } from '/@/renderer/features/player/components/full-screen-visualizer-song-info';
|
||||
import { VISUALIZER_FULLSCREEN_TARGET_ID } from '/@/renderer/hooks/use-fullscreen-toggle';
|
||||
import { useHotkeys } from '/@/renderer/hooks/use-hotkeys';
|
||||
import { useIsMobile } from '/@/renderer/hooks/use-is-mobile';
|
||||
import { useFullScreenPlayerStoreActions } from '/@/renderer/store/full-screen-player.store';
|
||||
@@ -139,11 +140,25 @@ export const FullScreenVisualizer = () => {
|
||||
const isOpenedRef = useRef<boolean | null>(null);
|
||||
|
||||
const handleCloseVisualizer = () => {
|
||||
// While fullscreen, Escape is the browser's own "leave fullscreen" gesture.
|
||||
// Let it drop back to the expanded-but-windowed visualizer instead of closing.
|
||||
if (document.fullscreenElement) return;
|
||||
|
||||
setStore({ visualizerExpanded: false });
|
||||
};
|
||||
|
||||
useHotkeys([['Escape', handleCloseVisualizer]]);
|
||||
|
||||
// Never leave the window stuck in fullscreen if the visualizer goes away while
|
||||
// fullscreened (route change, close button, etc.).
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (document.fullscreenElement) {
|
||||
document.exitFullscreen().catch(() => {});
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (isOpenedRef.current !== null) {
|
||||
setStore({ visualizerExpanded: false });
|
||||
@@ -154,7 +169,7 @@ export const FullScreenVisualizer = () => {
|
||||
|
||||
return (
|
||||
<VisualizerContainer isMobile={isMobile} windowBarStyle={windowBarStyle}>
|
||||
<div className={styles.visualizerContainer}>
|
||||
<div className={styles.visualizerContainer} id={VISUALIZER_FULLSCREEN_TARGET_ID}>
|
||||
{webAudio ? (
|
||||
<Suspense fallback={<></>}>
|
||||
{visualizerType === 'butterchurn' ? (
|
||||
|
||||
+8
-3
@@ -5,7 +5,10 @@ import { useTranslation } from 'react-i18next';
|
||||
import styles from './visualizer-settings-form.module.css';
|
||||
|
||||
import i18n from '/@/i18n/i18n';
|
||||
import { getButterchurnPresetOptions } from '/@/renderer/features/visualizer/components/butternchurn/visualizer';
|
||||
import {
|
||||
getButterchurnPresetOptions,
|
||||
loadAllButterchurnPresets,
|
||||
} from '/@/renderer/features/visualizer/components/butternchurn/visualizer';
|
||||
import { useSettingsStoreActions, useVisualizerSettings } from '/@/renderer/store/settings.store';
|
||||
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
|
||||
import { Button } from '/@/shared/components/button/button';
|
||||
@@ -32,8 +35,8 @@ let butterchurnPresetOptionsCache: ButterchurnPresetOption[] | null = null;
|
||||
const loadButterchurnPresetOptions = async (): Promise<ButterchurnPresetOption[]> => {
|
||||
if (butterchurnPresetOptionsCache) return butterchurnPresetOptionsCache;
|
||||
|
||||
const mod = await import('butterchurn-presets');
|
||||
const presets = getButterchurnPresetOptions((mod as any).default ?? mod);
|
||||
const mergedPresets = await loadAllButterchurnPresets();
|
||||
const presets = getButterchurnPresetOptions(mergedPresets);
|
||||
const presetNames = Object.keys(presets);
|
||||
|
||||
butterchurnPresetOptionsCache = presetNames.map((presetName) => ({
|
||||
@@ -2186,6 +2189,7 @@ const ButterChurnCycleSettings = () => {
|
||||
}
|
||||
label={t('visualizer.selectedPresets')}
|
||||
onChange={(values) => updateProperty('selectedPresets', values)}
|
||||
searchable
|
||||
value={visualizer.butterchurn.selectedPresets}
|
||||
/>
|
||||
<MultiSelect
|
||||
@@ -2193,6 +2197,7 @@ const ButterChurnCycleSettings = () => {
|
||||
disabled={!visualizer.butterchurn.cyclePresets}
|
||||
label={t('visualizer.ignoredPresets')}
|
||||
onChange={(values) => updateProperty('ignoredPresets', values)}
|
||||
searchable
|
||||
value={visualizer.butterchurn.ignoredPresets}
|
||||
/>
|
||||
|
||||
|
||||
@@ -5,3 +5,15 @@ declare module 'butterchurn' {
|
||||
declare module 'butterchurn-presets' {
|
||||
export default butterchurnPresets;
|
||||
}
|
||||
|
||||
declare module 'butterchurn-presets/dist/base.js' {
|
||||
export default butterchurnPresets;
|
||||
}
|
||||
|
||||
declare module 'butterchurn-presets/dist/extra.js' {
|
||||
export default butterchurnPresets;
|
||||
}
|
||||
|
||||
declare module 'butterchurn-presets/dist/md1.js' {
|
||||
export default butterchurnPresets;
|
||||
}
|
||||
|
||||
@@ -40,6 +40,31 @@ export function getButterchurnPresetOptions(presets: Record<string, string>) {
|
||||
);
|
||||
}
|
||||
|
||||
// Merges every non-image preset bundle shipped in butterchurn-presets (base, extra,
|
||||
// md1). Skips image.js/imageData.js on purpose since those presets
|
||||
// depend on separate texture data and are more likely to break/require extra wiring.
|
||||
let mergedPresetsCache: null | Record<string, any> = null;
|
||||
|
||||
export async function loadAllButterchurnPresets(): Promise<Record<string, any>> {
|
||||
if (mergedPresetsCache) return mergedPresetsCache;
|
||||
|
||||
const [base, extra, md1] = await Promise.all([
|
||||
import('butterchurn-presets/dist/base.js'),
|
||||
import('butterchurn-presets/dist/extra.js'),
|
||||
import('butterchurn-presets/dist/md1.js'),
|
||||
]);
|
||||
|
||||
const presets = {
|
||||
...base.default,
|
||||
...extra.default,
|
||||
...md1.default,
|
||||
};
|
||||
|
||||
mergedPresetsCache = presets;
|
||||
|
||||
return presets;
|
||||
}
|
||||
|
||||
const VisualizerInner = () => {
|
||||
const { webAudio } = useWebAudio();
|
||||
const canvasRef = createRef<HTMLCanvasElement>();
|
||||
@@ -72,15 +97,14 @@ const VisualizerInner = () => {
|
||||
|
||||
const loadLibraries = async () => {
|
||||
try {
|
||||
const [butterchurnModule, presetsModule] = await Promise.all([
|
||||
const [butterchurnModule, mergedPresets] = await Promise.all([
|
||||
import('butterchurn'),
|
||||
import('butterchurn-presets'),
|
||||
loadAllButterchurnPresets(),
|
||||
]);
|
||||
|
||||
if (isMounted) {
|
||||
butterchurnRef.current = butterchurnModule.default;
|
||||
butterchurnPresetsRef.current = butterchurnPresetsRef.current =
|
||||
getButterchurnPresetOptions(presetsModule.default);
|
||||
butterchurnPresetsRef.current = getButterchurnPresetOptions(mergedPresets);
|
||||
|
||||
setLibrariesLoaded(true);
|
||||
}
|
||||
@@ -533,11 +557,9 @@ export const Visualizer = () => {
|
||||
|
||||
const loadPresets = async () => {
|
||||
try {
|
||||
const presetsModule = await import('butterchurn-presets');
|
||||
const mergedPresets = await loadAllButterchurnPresets();
|
||||
if (isMounted) {
|
||||
butterchurnPresetsRef.current = getButterchurnPresetOptions(
|
||||
presetsModule.default,
|
||||
);
|
||||
butterchurnPresetsRef.current = getButterchurnPresetOptions(mergedPresets);
|
||||
setPresetsLoaded(true);
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -581,6 +603,15 @@ export const Visualizer = () => {
|
||||
setStore({ expanded: false, visualizerExpanded: !visualizerExpanded });
|
||||
};
|
||||
|
||||
const pickRandomPreset = (presetList: string[], currentPresetName: string | undefined) => {
|
||||
const availablePresets =
|
||||
presetList.length > 1
|
||||
? presetList.filter((name) => name !== currentPresetName)
|
||||
: presetList;
|
||||
const randomIndex = Math.floor(Math.random() * availablePresets.length);
|
||||
return availablePresets[randomIndex];
|
||||
};
|
||||
|
||||
const handleNextPreset = () => {
|
||||
if (!presetsLoaded) return;
|
||||
|
||||
@@ -588,10 +619,16 @@ export const Visualizer = () => {
|
||||
if (presetList.length === 0) return;
|
||||
|
||||
const currentPresetName = useSettingsStore.getState().visualizer.butterchurn.currentPreset;
|
||||
const currentIndex = currentPresetName ? presetList.indexOf(currentPresetName) : -1;
|
||||
const nextIndex =
|
||||
currentIndex >= 0 && currentIndex < presetList.length - 1 ? currentIndex + 1 : 0;
|
||||
const nextPresetName = presetList[nextIndex];
|
||||
|
||||
let nextPresetName: string;
|
||||
if (butterchurnSettings.randomizeNextPreset) {
|
||||
nextPresetName = pickRandomPreset(presetList, currentPresetName);
|
||||
} else {
|
||||
const currentIndex = currentPresetName ? presetList.indexOf(currentPresetName) : -1;
|
||||
const nextIndex =
|
||||
currentIndex >= 0 && currentIndex < presetList.length - 1 ? currentIndex + 1 : 0;
|
||||
nextPresetName = presetList[nextIndex];
|
||||
}
|
||||
|
||||
setSettings({
|
||||
visualizer: {
|
||||
@@ -609,9 +646,15 @@ export const Visualizer = () => {
|
||||
if (presetList.length === 0) return;
|
||||
|
||||
const currentPresetName = useSettingsStore.getState().visualizer.butterchurn.currentPreset;
|
||||
const currentIndex = currentPresetName ? presetList.indexOf(currentPresetName) : -1;
|
||||
const prevIndex = currentIndex > 0 ? currentIndex - 1 : presetList.length - 1;
|
||||
const prevPresetName = presetList[prevIndex];
|
||||
|
||||
let prevPresetName: string;
|
||||
if (butterchurnSettings.randomizeNextPreset) {
|
||||
prevPresetName = pickRandomPreset(presetList, currentPresetName);
|
||||
} else {
|
||||
const currentIndex = currentPresetName ? presetList.indexOf(currentPresetName) : -1;
|
||||
const prevIndex = currentIndex > 0 ? currentIndex - 1 : presetList.length - 1;
|
||||
prevPresetName = presetList[prevIndex];
|
||||
}
|
||||
|
||||
setSettings({
|
||||
visualizer: {
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import { useFullScreenPlayerStore } from '/@/renderer/store/full-screen-player.store';
|
||||
|
||||
/**
|
||||
* Id applied to the expanded visualizer's inner container so that F11 can target it
|
||||
* directly. Fullscreening that element (rather than the whole document) promotes it to
|
||||
* the browser's top layer, so it covers the window bar and playerbar instead of being
|
||||
* boxed in by them.
|
||||
*/
|
||||
export const VISUALIZER_FULLSCREEN_TARGET_ID = 'visualizer-fullscreen-target';
|
||||
|
||||
export const useFullscreenToggle = () => {
|
||||
useEffect(() => {
|
||||
const toggleFullscreen = () => {
|
||||
// Already fullscreen: back out, regardless of what was fullscreened.
|
||||
if (document.fullscreenElement) {
|
||||
document.exitFullscreen().catch(() => {});
|
||||
return;
|
||||
}
|
||||
|
||||
const { visualizerExpanded } = useFullScreenPlayerStore.getState();
|
||||
|
||||
const visualizerTarget = visualizerExpanded
|
||||
? document.getElementById(VISUALIZER_FULLSCREEN_TARGET_ID)
|
||||
: null;
|
||||
|
||||
// Expanded visualizer -> fullscreen just the visualizer.
|
||||
// Anything else -> normal whole-window fullscreen.
|
||||
const target = visualizerTarget ?? document.documentElement;
|
||||
|
||||
target.requestFullscreen().catch(() => {});
|
||||
};
|
||||
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.key !== 'F11') return;
|
||||
|
||||
event.preventDefault();
|
||||
toggleFullscreen();
|
||||
};
|
||||
|
||||
// Capture phase so this still fires while a text input has focus.
|
||||
window.addEventListener('keydown', handleKeyDown, true);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('keydown', handleKeyDown, true);
|
||||
};
|
||||
}, []);
|
||||
};
|
||||
Reference in New Issue
Block a user