| import { create } from "zustand"; |
| import { persist } from "zustand/middleware"; |
| const DEFAULT_SETTINGS = { |
| sceneDuration: 0, |
| transitionDuration: 800, |
| autoHideDelay: 3000, |
| narrationPosition: "bottom", |
| narrationSize: "medium", |
| showSceneNumber: true, |
| pauseOnEnd: true, |
| sagaMode: true, |
| autoGenerateScenes: true, |
| }; |
| const initialState = { |
| isActive: false, |
| isFullscreen: false, |
| sessionId: null, |
| storyTitle: "", |
| sagaId: null, |
| chapterNumber: 1, |
| isLoadingNextChapter: false, |
| isPlaying: false, |
| currentSceneIndex: 0, |
| scenes: [], |
| isPrefetching: false, |
| prefetchError: null, |
| maxScenes: 24, |
| isStoryComplete: false, |
| controlsVisible: true, |
| showSettings: false, |
| showEndScreen: false, |
| playbackError: null, |
| settings: DEFAULT_SETTINGS, |
| }; |
| export const useTVModeStore = create()(persist((set, get) => ({ |
| ...initialState, |
| |
| enterTVMode: (sessionId, storyTitle, scenes, startIndex = 0) => { |
| |
| const tvScenes = scenes.map((scene) => ({ |
| ...scene, |
| status: "ready", |
| |
| imageStatus: (scene.image_url || scene.image) ? "ready" : "pending", |
| })); |
| set({ |
| isActive: true, |
| sessionId, |
| storyTitle, |
| sagaId: sessionId, |
| chapterNumber: 1, |
| isLoadingNextChapter: false, |
| scenes: tvScenes, |
| currentSceneIndex: startIndex, |
| isPlaying: true, |
| controlsVisible: true, |
| showEndScreen: false, |
| playbackError: null, |
| prefetchError: null, |
| isStoryComplete: false, |
| }); |
| }, |
| exitTVMode: () => { |
| set({ |
| isActive: false, |
| isFullscreen: false, |
| isPlaying: false, |
| controlsVisible: true, |
| showSettings: false, |
| showEndScreen: false, |
| }); |
| }, |
| setFullscreen: (fullscreen) => set({ isFullscreen: fullscreen }), |
| |
| play: () => set({ isPlaying: true }), |
| pause: () => set({ isPlaying: false }), |
| togglePlay: () => set((state) => ({ isPlaying: !state.isPlaying })), |
| |
| nextScene: () => { |
| const { currentSceneIndex, scenes, settings } = get(); |
| if (currentSceneIndex < scenes.length - 1) { |
| set({ |
| currentSceneIndex: currentSceneIndex + 1, |
| showEndScreen: false, |
| }); |
| } |
| else if (settings.pauseOnEnd) { |
| set({ isPlaying: false, showEndScreen: true }); |
| } |
| }, |
| prevScene: () => { |
| const { currentSceneIndex } = get(); |
| if (currentSceneIndex > 0) { |
| set({ |
| currentSceneIndex: currentSceneIndex - 1, |
| showEndScreen: false, |
| }); |
| } |
| }, |
| goToScene: (index) => { |
| const { scenes } = get(); |
| if (index >= 0 && index < scenes.length) { |
| set({ |
| currentSceneIndex: index, |
| showEndScreen: false, |
| }); |
| } |
| }, |
| |
| addScene: (scene) => { |
| set((state) => ({ |
| scenes: [...state.scenes, { |
| ...scene, |
| status: "ready", |
| |
| imageStatus: (scene.image_url || scene.image) ? "ready" : "generating", |
| }], |
| })); |
| }, |
| updateSceneStatus: (index, status, error) => { |
| set((state) => ({ |
| scenes: state.scenes.map((s, i) => i === index ? { ...s, status } : s), |
| playbackError: error || state.playbackError, |
| })); |
| }, |
| updateSceneImage: (index, imageUrl) => { |
| set((state) => ({ |
| scenes: state.scenes.map((s, i) => i === index ? { ...s, image_url: imageUrl, imageStatus: "ready" } : s), |
| })); |
| }, |
| setSceneImageStatus: (index, status) => { |
| set((state) => ({ |
| scenes: state.scenes.map((s, i) => i === index ? { ...s, imageStatus: status } : s), |
| })); |
| }, |
| |
| updateSceneImageByIdx: (sceneIdx, imageUrl) => { |
| set((state) => ({ |
| scenes: state.scenes.map((s) => s.idx === sceneIdx |
| ? { ...s, image_url: imageUrl, imageStatus: "ready" } |
| : s), |
| })); |
| }, |
| setSceneImageStatusByIdx: (sceneIdx, status) => { |
| set((state) => ({ |
| scenes: state.scenes.map((s) => s.idx === sceneIdx ? { ...s, imageStatus: status } : s), |
| })); |
| }, |
| |
| setPrefetching: (isPrefetching) => set({ isPrefetching }), |
| setPrefetchError: (error) => set({ prefetchError: error }), |
| setStoryComplete: (complete) => set({ isStoryComplete: complete }), |
| |
| setLoadingNextChapter: (loading) => set({ isLoadingNextChapter: loading }), |
| startNextChapter: (sessionId, title, scenes, chapterNumber) => { |
| |
| const tvScenes = scenes.map((scene) => ({ |
| ...scene, |
| status: "ready", |
| imageStatus: (scene.image_url || scene.image) ? "ready" : "pending", |
| })); |
| set({ |
| sessionId, |
| storyTitle: title, |
| chapterNumber, |
| isLoadingNextChapter: false, |
| scenes: tvScenes, |
| currentSceneIndex: 0, |
| isPlaying: true, |
| isStoryComplete: false, |
| prefetchError: null, |
| showEndScreen: false, |
| }); |
| }, |
| |
| showControls: () => set({ controlsVisible: true }), |
| hideControls: () => set({ controlsVisible: false }), |
| setShowSettings: (show) => set({ showSettings: show }), |
| setShowEndScreen: (show) => set({ showEndScreen: show }), |
| setPlaybackError: (error) => set({ playbackError: error }), |
| |
| updateSettings: (newSettings) => set((state) => ({ |
| settings: { ...state.settings, ...newSettings }, |
| })), |
| resetSettings: () => set({ settings: DEFAULT_SETTINGS }), |
| }), { |
| name: "tv-mode-storage", |
| partialize: (state) => ({ |
| settings: state.settings, |
| }), |
| })); |
| |
| export const selectCurrentScene = (state) => state.scenes[state.currentSceneIndex]; |
| export const selectNextScene = (state) => state.scenes[state.currentSceneIndex + 1]; |
| export const selectIsLastScene = (state) => state.currentSceneIndex >= state.scenes.length - 1; |
| export const selectCanPrefetch = (state) => state.currentSceneIndex < state.maxScenes - 1 && |
| !state.isPrefetching && |
| !state.scenes[state.currentSceneIndex + 1]; |
| export const selectSceneDuration = (state) => { |
| const scene = selectCurrentScene(state); |
| if (state.settings.sceneDuration > 0) { |
| return state.settings.sceneDuration * 1000; |
| } |
| return (scene?.duration_s || 8) * 1000; |
| }; |
| |
| export const selectCurrentSceneImageReady = (state) => { |
| const scene = selectCurrentScene(state); |
| if (!scene) |
| return false; |
| |
| const hasImage = Boolean(scene.image_url || scene.image); |
| return hasImage || scene.imageStatus === "ready"; |
| }; |
| |
| export const selectNextSceneReady = (state) => { |
| const nextScene = selectNextScene(state); |
| if (!nextScene) |
| return false; |
| |
| if (nextScene.status !== "ready") |
| return false; |
| |
| const hasImage = Boolean(nextScene.image_url || nextScene.image); |
| return hasImage || nextScene.imageStatus === "ready"; |
| }; |
| |
| export const selectIsWaitingForImage = (state) => { |
| const scene = selectCurrentScene(state); |
| if (!scene) |
| return false; |
| |
| const hasImage = Boolean(scene.image_url || scene.image); |
| return !hasImage && (scene.imageStatus === "generating" || scene.imageStatus === "pending"); |
| }; |
|
|