import { create } from 'zustand'; import { fetchDesktopInstalledApps, isDesktopLocalOriginActive, isDesktopShell, type DesktopSettings, type InstalledDesktopAppInfo } from '@/lib/desktop'; import { OPEN_IN_APPS, DEFAULT_OPEN_IN_APP_ID, OPEN_IN_ALWAYS_AVAILABLE_APP_IDS, getOpenInAppById, getPlatformOpenInApp, type OpenInApp } from '@/lib/openInApps'; import { updateDesktopSettings } from '@/lib/persistence'; export type OpenInAppOption = OpenInApp & { iconDataUrl?: string; }; type OpenInAppsState = { selectedAppId: string; availableApps: OpenInAppOption[]; hasLoadedApps: boolean; isCacheStale: boolean; isScanning: boolean; initialize: () => void; loadInstalledApps: (force?: boolean) => Promise; selectApp: (appId: string) => Promise; }; const getAlwaysAvailableApps = (): OpenInAppOption[] => { return OPEN_IN_APPS .filter((app) => OPEN_IN_ALWAYS_AVAILABLE_APP_IDS.has(app.id)) .map((app) => ({ ...getPlatformOpenInApp(app) })); }; const getStoredAppId = (): string => { if (typeof window === 'undefined') { return DEFAULT_OPEN_IN_APP_ID; } const stored = window.localStorage.getItem('openInAppId'); if (stored && getOpenInAppById(stored)) { return stored; } return DEFAULT_OPEN_IN_APP_ID; }; let initialized = false; let loading = false; let keepScanning = false; let retryAttempt = 0; let retryTimeout: ReturnType | null = null; const clearRetryTimeout = () => { if (retryTimeout) { clearTimeout(retryTimeout); retryTimeout = null; } }; export const useOpenInAppsStore = create()((set, get) => ({ selectedAppId: getStoredAppId(), availableApps: getAlwaysAvailableApps(), hasLoadedApps: false, isCacheStale: false, isScanning: false, initialize: () => { if (initialized || typeof window === 'undefined') { return; } initialized = true; const applyInstalledApps = (installed: InstalledDesktopAppInfo[], hasLoadedApps = true) => { if (!Array.isArray(installed) || installed.length === 0) { set({ availableApps: getAlwaysAvailableApps(), hasLoadedApps }); return; } const allowed = new Set(installed.map((app) => app.name)); const iconMap = new Map(installed.map((app) => [app.name, app.iconDataUrl ?? undefined])); const filtered = OPEN_IN_APPS.filter( (app) => allowed.has(app.appName) || OPEN_IN_ALWAYS_AVAILABLE_APP_IDS.has(app.id) ); const withIcons = filtered.map((app) => ({ ...getPlatformOpenInApp(app), iconDataUrl: iconMap.get(app.appName), })); set({ availableApps: withIcons, hasLoadedApps: true }); }; const loadInstalledApps = async (force?: boolean) => { if (!isDesktopShell() || !isDesktopLocalOriginActive()) { return; } if (loading) { return; } const state = get(); if (state.hasLoadedApps && !force) { return; } const appNames = OPEN_IN_APPS.map((app) => app.appName); clearRetryTimeout(); if (force) { set({ hasLoadedApps: false }); } loading = true; keepScanning = false; set({ isScanning: true }); try { const { apps: installed, success, hasCache, isCacheStale, } = await fetchDesktopInstalledApps(appNames, force); const shouldRetryEmptyScan = success && !hasCache && installed.length === 0 && retryAttempt < 3; set({ isCacheStale: hasCache ? isCacheStale : false }); applyInstalledApps(installed, success ? !shouldRetryEmptyScan : false); if (success) { if (shouldRetryEmptyScan) { const delays = [800, 1600, 3200]; const delay = delays[retryAttempt] ?? 3200; retryAttempt += 1; keepScanning = true; retryTimeout = setTimeout(() => { void loadInstalledApps(); }, delay); return; } retryAttempt = 0; keepScanning = false; return; } if (retryAttempt < 3) { const delays = [1000, 3000, 7000]; const delay = delays[retryAttempt] ?? 7000; retryAttempt += 1; keepScanning = true; retryTimeout = setTimeout(() => { void loadInstalledApps(); }, delay); } } finally { loading = false; if (!keepScanning) { set({ isScanning: false }); } } }; void loadInstalledApps(); const settingsHandler = (event: Event) => { const detail = (event as CustomEvent).detail; const nextId = detail && typeof detail.openInAppId === 'string' && detail.openInAppId.length > 0 && getOpenInAppById(detail.openInAppId) ? detail.openInAppId : null; if (!nextId) { return; } window.localStorage.setItem('openInAppId', nextId); set({ selectedAppId: nextId }); }; const appReadyHandler = () => { void loadInstalledApps(); }; const updateHandler = (event: Event) => { const detail = (event as CustomEvent).detail; if (!Array.isArray(detail)) { return; } retryAttempt = 3; keepScanning = false; set({ isScanning: false, isCacheStale: false }); applyInstalledApps(detail); }; window.addEventListener('openchamber:settings-synced', settingsHandler); window.addEventListener('openchamber:app-ready', appReadyHandler); window.addEventListener('openchamber:installed-apps-updated', updateHandler); const appReady = (window as unknown as { __openchamberAppReady?: boolean }).__openchamberAppReady; if (appReady) { void loadInstalledApps(); } window.setTimeout(() => { if (!get().hasLoadedApps) { void loadInstalledApps(); } }, 5000); }, loadInstalledApps: async (force?: boolean) => { if (!initialized) { get().initialize(); } if (!isDesktopShell() || !isDesktopLocalOriginActive()) { return; } if (loading) { return; } const state = get(); if (state.hasLoadedApps && !force) { return; } const appNames = OPEN_IN_APPS.map((app) => app.appName); clearRetryTimeout(); if (force) { set({ hasLoadedApps: false }); } loading = true; keepScanning = false; set({ isScanning: true }); try { const { apps: installed, hasCache, isCacheStale, } = await fetchDesktopInstalledApps(appNames, force); const allowed = new Set(installed.map((app) => app.name)); const iconMap = new Map(installed.map((app) => [app.name, app.iconDataUrl ?? undefined])); const filtered = OPEN_IN_APPS.filter( (app) => allowed.has(app.appName) || OPEN_IN_ALWAYS_AVAILABLE_APP_IDS.has(app.id) ); const withIcons = filtered.map((app) => ({ ...app, iconDataUrl: iconMap.get(app.appName), })); set({ availableApps: withIcons.length > 0 ? withIcons : getAlwaysAvailableApps(), hasLoadedApps: withIcons.length > 0, isCacheStale: hasCache ? isCacheStale : false, }); } finally { loading = false; if (!keepScanning) { set({ isScanning: false }); } } }, selectApp: async (appId: string) => { if (!getOpenInAppById(appId)) { return; } set({ selectedAppId: appId }); if (typeof window !== 'undefined') { window.localStorage.setItem('openInAppId', appId); } await updateDesktopSettings({ openInAppId: appId }); }, }));