diff --git a/ui/src/stores/developerModeSlice.ts b/ui/src/stores/developerModeSlice.ts new file mode 100644 index 00000000..5b794b61 --- /dev/null +++ b/ui/src/stores/developerModeSlice.ts @@ -0,0 +1,33 @@ +import { loadDeveloperMode, saveDeveloperMode } from '../lib/developerMode' +import type { MediaFilter } from '../types' + +export type DeveloperModeSlice = { + developerMode: boolean + setDeveloperMode: (enabled: boolean) => void +} + +// Runtime get() is the full AppState. The setter may also write mediaFilter +// when leaving auditdev; that field stays owned by the gallery store. +type DeveloperModeWriteState = DeveloperModeSlice & { + mediaFilter?: MediaFilter +} + +type SetDeveloperModeState = ( + partial: Partial | ((state: DeveloperModeWriteState) => Partial), +) => void + +type GetDeveloperModeState = () => DeveloperModeWriteState + +export function createDeveloperModeSlice(set: SetDeveloperModeState, get: GetDeveloperModeState): DeveloperModeSlice { + return { + developerMode: loadDeveloperMode(), + setDeveloperMode: enabled => { + saveDeveloperMode(enabled) + const { mediaFilter } = get() + set({ + developerMode: enabled, + mediaFilter: !enabled && mediaFilter === 'auditdev' ? 'all' : mediaFilter, + }) + }, + } +} diff --git a/ui/src/stores/useStore.ts b/ui/src/stores/useStore.ts index 3f5f04fc..b3391c9b 100644 --- a/ui/src/stores/useStore.ts +++ b/ui/src/stores/useStore.ts @@ -3,7 +3,6 @@ import type { GenerateParams, OutputFile, MediaFilter, AspectRatio, ResolutionPr import { DEFAULT_DIRECT_VIDEO_MASTER_PROMPT } from '../types' import * as api from '../api/client' import { type FamilyId, type ThemeMode, type ThemePrefs } from '../lib/theme' -import { loadDeveloperMode, saveDeveloperMode } from '../lib/developerMode' import { splitPromptSchedule } from '../lib/promptScheduler' import { DEFAULT_PRODUCTION_PROFILE, productionImageModelType, resolveSupportedVideoFormat } from '../lib/productionProfile' import { createKeyedWriteSequencer } from '../lib/keyedWriteSequencer' @@ -11,6 +10,7 @@ import { createActivityPublicationGate } from '../lib/activityPublication' import { isGenerationJobActive } from '../lib/generationJobState' import { mapDirectorClipImages } from '../lib/directorClipImages' import { llmActivityPreview } from '../lib/llmActivityPreview' +import { createDeveloperModeSlice } from './developerModeSlice' import { createDirectorSlice } from './directorSlice' import { createSettingsSlice } from './settingsSlice' import { createThemeSlice } from './themeSlice' @@ -2330,6 +2330,10 @@ export const useStore = create((set, get) => ({ partial => set(partial as never), () => get(), ), + ...createDeveloperModeSlice( + partial => set(partial as never), + () => get(), + ), // Generation mode generationMode: 'video', editSubMode: 'retake' as import('../types').EditSubMode, @@ -3041,15 +3045,6 @@ export const useStore = create((set, get) => ({ toggleSidebar: () => set(s => ({ sidebarOpen: !s.sidebarOpen })), setSidebarOpen: (open) => set({ sidebarOpen: open }), - developerMode: loadDeveloperMode(), - setDeveloperMode: (enabled) => { - saveDeveloperMode(enabled) - set(s => ({ - developerMode: enabled, - mediaFilter: !enabled && s.mediaFilter === 'auditdev' ? 'all' : s.mediaFilter, - })) - }, - // CivitAI LoRA Browser // Director Pipeline Dashboard retakeDialogOpen: false, diff --git a/ui/tests/architectureSlices.test.mjs b/ui/tests/architectureSlices.test.mjs index f8d488b1..82986f2d 100644 --- a/ui/tests/architectureSlices.test.mjs +++ b/ui/tests/architectureSlices.test.mjs @@ -119,6 +119,49 @@ test('settings slice toggles open and tab through the public facade', async () = assert.equal(useStore.getState().settingsOpen, false) }) +test('developer-mode slice persists the local flag through the public facade', async () => { + if (!globalThis.localStorage) { + const { JSDOM } = await import('jsdom') + const dom = new JSDOM('', { url: 'http://localhost/' }) + Object.assign(globalThis, { + window: dom.window, + document: dom.window.document, + localStorage: dom.window.localStorage, + }) + } + globalThis.localStorage.removeItem('hocuspocus-developer-mode-v1') + + const { createDeveloperModeSlice } = await import('../src/stores/developerModeSlice.ts') + let state + const set = update => { + const partial = typeof update === 'function' ? update(state) : update + state = { ...state, ...partial } + } + state = createDeveloperModeSlice(set, () => state) + assert.equal(state.developerMode, false) + state.setDeveloperMode(true) + assert.equal(state.developerMode, true) + assert.equal(globalThis.localStorage.getItem('hocuspocus-developer-mode-v1'), '1') + + const { useStore } = await import('../src/stores/useStore.ts') + useStore.getState().setDeveloperMode(true) + assert.equal(useStore.getState().developerMode, true) + assert.equal(globalThis.localStorage.getItem('hocuspocus-developer-mode-v1'), '1') + useStore.getState().setDeveloperMode(false) + assert.equal(useStore.getState().developerMode, false) + assert.equal(globalThis.localStorage.getItem('hocuspocus-developer-mode-v1'), null) + + useStore.setState({ mediaFilter: 'auditdev', developerMode: true }) + useStore.getState().setDeveloperMode(false) + assert.equal(useStore.getState().developerMode, false) + assert.equal(useStore.getState().mediaFilter, 'all') + + useStore.setState({ mediaFilter: 'videos' }) + useStore.getState().setDeveloperMode(true) + assert.equal(useStore.getState().developerMode, true) + assert.equal(useStore.getState().mediaFilter, 'videos') +}) + test('useStore keeps Director actions available through its existing public facade', async () => { const { useStore } = await import('../src/stores/useStore.ts') useStore.setState({ directorAutoMode: true, directorSeamless: true })