// src/stores/uiStore.ts
import { createStore, produce } from 'solid-js/store';
import type { AudioSource } from '~/audio/AudioCapture';
export type Aspect = 'shugo' | 'gunshi' | 'kantoku' | 'sensei';
export type Density = 'comfortable' | 'compact';
interface UiState {
panel: 'none' | 'editor' | 'library' | 'save' | 'settings' | 'cinema';
aspect: Aspect;
density: Density;
audio: {
active: boolean;
permission: 'idle' | 'requesting' | 'granted' | 'denied';
/** Which capture path the next toggle-on should use. */
source: AudioSource;
/** Live 0..1 levels, updated by the Canvas RAF loop when audio is active. */
levels: { bass: number; mid: number; high: number };
};
recording: { active: boolean; startedAt: number | null };
showFps: boolean;
/** True when the viewport is narrow enough that the editor should go full-width. */
isNarrow: boolean;
respawnNonce: number;
paused: boolean;
/** Incremented to request a one-off preview snapshot of the canvas. */
snapshotNonce: number;
/** Incremented to request a full-resolution PNG download. */
exportNonce: number;
/** Short-lived status banner after the Share button is pressed. */
shareNotice: 'idle' | 'copied' | 'failed' | 'unsupported';
}
const [uiState, setUiState] = createStore<UiState>({
panel: 'none',
aspect: 'shugo',
density: 'comfortable',
audio: {
active: false,
permission: 'idle',
source: 'mic',
levels: { bass: 0, mid: 0, high: 0 },
},
recording: { active: false, startedAt: null },
showFps: true,
isNarrow: typeof window !== 'undefined' ? window.innerWidth < 600 : false,
respawnNonce: 0,
paused: false,
snapshotNonce: 0,
exportNonce: 0,
shareNotice: 'idle',
});
export { uiState };
export function openPanel(panel: UiState['panel']): void {
setUiState('panel', panel);
}
export function closePanel(): void {
setUiState('panel', 'none');
}
export function setAspect(aspect: Aspect): void {
setUiState('aspect', aspect);
}
export function setDensity(density: Density): void {
setUiState('density', density);
}
export function setAudioActive(active: boolean): void {
setUiState(
produce((s) => {
s.audio.active = active;
}),
);
}
export function setAudioPermission(p: UiState['audio']['permission']): void {
setUiState(
produce((s) => {
s.audio.permission = p;
}),
);
}
export function setAudioSource(src: AudioSource): void {
setUiState(
produce((s) => {
s.audio.source = src;
}),
);
}
export function setAudioLevels(bass: number, mid: number, high: number): void {
setUiState(
produce((s) => {
s.audio.levels.bass = bass;
s.audio.levels.mid = mid;
s.audio.levels.high = high;
}),
);
}
export function startRecording(): void {
setUiState(
produce((s) => {
s.recording.active = true;
s.recording.startedAt = Date.now();
}),
);
}
export function stopRecording(): void {
setUiState(
produce((s) => {
s.recording.active = false;
s.recording.startedAt = null;
}),
);
}
export function toggleFps(): void {
setUiState('showFps', (v) => !v);
}
export function setIsNarrow(v: boolean): void {
setUiState('isNarrow', v);
}
export function triggerRespawn(): void {
setUiState('respawnNonce', (n) => n + 1);
}
export function togglePaused(): void {
setUiState('paused', (v) => !v);
}
export function requestSnapshot(): void {
setUiState('snapshotNonce', (n) => n + 1);
}
export function requestExport(): void {
setUiState('exportNonce', (n) => n + 1);
}
let shareNoticeTimer: ReturnType<typeof setTimeout> | null = null;
export function flashShareNotice(kind: Exclude<UiState['shareNotice'], 'idle'>): void {
setUiState('shareNotice', kind);
if (shareNoticeTimer !== null) clearTimeout(shareNoticeTimer);
shareNoticeTimer = setTimeout(() => {
setUiState('shareNotice', 'idle');
shareNoticeTimer = null;
}, 2500);
}