import { useSyncExternalStore } from 'react'; import type { AutomationScript } from '@lib/script-types'; import { scriptApi } from '@lib/script-api'; import { resolveChannel } from '@constant/channel'; import { CONFIG_KEYS, IPC_EVENTS } from '../lib/constants'; import { invokeIpc } from '../lib/host-api'; export interface ChannelItem { id: string; channelName: string; channelUrl: string; } export interface ChannelStoreState { initialized: boolean; loading: boolean; selectedChannels: ChannelItem[]; availableChannels: ChannelItem[]; error: string | null; } const listeners = new Set<() => void>(); let initPromise: Promise | null = null; let state: ChannelStoreState = { initialized: false, loading: false, selectedChannels: [], availableChannels: [], error: null, }; function emit(): void { for (const listener of listeners) { listener(); } } function patchState(patch: Partial): ChannelStoreState { state = { ...state, ...patch }; emit(); return state; } function normalizeChannelItem(item: Partial | null | undefined): ChannelItem | null { const channelUrl = String(item?.channelUrl ?? '').trim(); if (!channelUrl) return null; const channelName = String(item?.channelName ?? channelUrl).trim() || channelUrl; const id = String(item?.id ?? channelUrl).trim() || channelUrl; return { id, channelName, channelUrl, }; } function dedupeChannels(items: Array | null | undefined>): ChannelItem[] { const channelMap = new Map(); for (const item of items) { const normalized = normalizeChannelItem(item); if (!normalized || channelMap.has(normalized.channelUrl)) continue; channelMap.set(normalized.channelUrl, normalized); } return Array.from(channelMap.values()); } function mapScriptsToChannels(scripts: AutomationScript[]): ChannelItem[] { const items: ChannelItem[] = []; for (const script of scripts) { if (!script.channel) continue; const resolved = resolveChannel(script.channel); const channelUrl = String(resolved.url ?? '').trim(); if (!channelUrl) continue; items.push({ id: channelUrl, channelName: String(resolved.name ?? channelUrl).trim() || channelUrl, channelUrl, }); } return dedupeChannels(items); } async function loadSelectedChannels(): Promise { const saved = await invokeIpc(IPC_EVENTS.GET_CONFIG, CONFIG_KEYS.SELECTED_CHANNELS); return Array.isArray(saved) ? dedupeChannels(saved) : []; } async function loadAvailableChannels(): Promise { const scripts = await scriptApi.list(); return mapScriptsToChannels(Array.isArray(scripts) ? scripts : []); } async function hydrate(): Promise { patchState({ loading: true, error: null }); try { const [selectedChannels, availableChannels] = await Promise.all([ loadSelectedChannels(), loadAvailableChannels(), ]); patchState({ initialized: true, loading: false, selectedChannels, availableChannels, error: null, }); } catch (error) { patchState({ initialized: true, loading: false, selectedChannels: [], availableChannels: [], error: error instanceof Error ? error.message : String(error), }); } } async function refreshAvailableChannels(): Promise { patchState({ loading: true, error: null }); try { const availableChannels = await loadAvailableChannels(); patchState({ loading: false, availableChannels, error: null, }); return availableChannels; } catch (error) { const message = error instanceof Error ? error.message : String(error); patchState({ loading: false, error: message, }); throw error; } } async function saveSelectedChannels(items: ChannelItem[]): Promise { const nextItems = dedupeChannels(items); patchState({ selectedChannels: nextItems, error: null }); await invokeIpc(IPC_EVENTS.SET_CONFIG, CONFIG_KEYS.SELECTED_CHANNELS, nextItems); return nextItems; } function setSelectedChannels(items: ChannelItem[]): ChannelItem[] { const nextItems = dedupeChannels(items); patchState({ selectedChannels: nextItems }); return nextItems; } function addSelectedChannel(item: ChannelItem): ChannelItem[] { return setSelectedChannels([...state.selectedChannels, item]); } function removeSelectedChannel(id: string): ChannelItem[] { return setSelectedChannels(state.selectedChannels.filter((item) => item.id !== id)); } function subscribe(listener: () => void): () => void { listeners.add(listener); return () => listeners.delete(listener); } function getSnapshot(): ChannelStoreState { return state; } async function initChannelStore(): Promise { if (!initPromise) { initPromise = hydrate(); } await initPromise; } export const channelStore = { subscribe, getSnapshot, getState: () => state, init: initChannelStore, loadSelectedChannels, refreshAvailableChannels, saveSelectedChannels, setSelectedChannels, addSelectedChannel, removeSelectedChannel, }; export function useChannelStore(): ChannelStoreState { return useSyncExternalStore(channelStore.subscribe, channelStore.getSnapshot, channelStore.getSnapshot); }