Zum Inhalt

src/stores/uiStore.ts

// 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);
}