diff --git a/packages/ui/src/components/desktop/OpenInAppButton.tsx b/packages/ui/src/components/desktop/OpenInAppButton.tsx index 92411d1e..82a0def0 100644 --- a/packages/ui/src/components/desktop/OpenInAppButton.tsx +++ b/packages/ui/src/components/desktop/OpenInAppButton.tsx @@ -7,46 +7,28 @@ import { DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'; import { toast } from '@/components/ui'; -import { updateDesktopSettings } from '@/lib/persistence'; import { copyTextToClipboard } from '@/lib/clipboard'; import { cn } from '@/lib/utils'; -import { fetchDesktopInstalledApps, isDesktopLocalOriginActive, isTauriShell, openDesktopPath, openDesktopProjectInApp, type DesktopSettings, type InstalledDesktopAppInfo } from '@/lib/desktop'; -import { DEFAULT_OPEN_IN_APP_ID, OPEN_IN_APPS, getOpenInAppById, type OpenInApp } from '@/lib/openInApps'; +import { isDesktopLocalOriginActive, isTauriShell, openDesktopPath, openDesktopProjectInApp } from '@/lib/desktop'; +import { DEFAULT_OPEN_IN_APP_ID, OPEN_IN_APPS } from '@/lib/openInApps'; +import { useOpenInAppsStore, type OpenInAppOption } from '@/stores/useOpenInAppsStore'; import { RiArrowDownSLine, RiCheckLine, RiFileCopyLine, RiRefreshLine } from '@remixicon/react'; const FINDER_DEFAULT_ICON_DATA_URL = 'data:image/png;base64,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'; const TERMINAL_DEFAULT_ICON_DATA_URL = 'data:image/png;base64,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'; -type OpenInAppOption = { - id: OpenInApp['id']; - label: OpenInApp['label']; - appName: OpenInApp['appName']; +type OpenInAppOptionWithFallback = OpenInAppOption & { fallbackIconDataUrl?: string; - iconDataUrl?: string; }; -const OPEN_IN_APP_OPTIONS: OpenInAppOption[] = OPEN_IN_APPS.map((app) => ({ +const withFallbackIcon = (app: OpenInAppOption): OpenInAppOptionWithFallback => ({ ...app, fallbackIconDataUrl: app.id === 'finder' ? FINDER_DEFAULT_ICON_DATA_URL : app.id === 'terminal' ? TERMINAL_DEFAULT_ICON_DATA_URL : undefined, -})); - -const ALWAYS_AVAILABLE_APP_IDS = new Set(['finder', 'terminal']); -const getAlwaysAvailableApps = () => OPEN_IN_APP_OPTIONS.filter((app) => ALWAYS_AVAILABLE_APP_IDS.has(app.id)); - -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; -}; +}); const AppIcon = ({ label, @@ -92,179 +74,29 @@ type OpenInAppButtonProps = { }; export const OpenInAppButton = ({ directory, activeFilePath, className }: OpenInAppButtonProps) => { - const [selectedAppId, setSelectedAppId] = React.useState(getStoredAppId); - const [availableApps, setAvailableApps] = React.useState(getAlwaysAvailableApps); - const [hasLoadedApps, setHasLoadedApps] = React.useState(false); - const [isCacheStale, setIsCacheStale] = React.useState(false); - const [isScanning, setIsScanning] = React.useState(false); - const isMountedRef = React.useRef(true); - const isLoadingRef = React.useRef(false); - const keepScanningRef = React.useRef(false); - const hasLoadedAppsRef = React.useRef(false); - const retryTimeoutRef = React.useRef | null>(null); - const retryAttemptRef = React.useRef(0); + const selectedAppId = useOpenInAppsStore((state) => state.selectedAppId); + const availableApps = useOpenInAppsStore((state) => state.availableApps); + const isCacheStale = useOpenInAppsStore((state) => state.isCacheStale); + const isScanning = useOpenInAppsStore((state) => state.isScanning); + const initialize = useOpenInAppsStore((state) => state.initialize); + const loadInstalledApps = useOpenInAppsStore((state) => state.loadInstalledApps); + const selectApp = useOpenInAppsStore((state) => state.selectApp); React.useEffect(() => { - if (typeof window === 'undefined') return; - const handler = (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); - setSelectedAppId(nextId); - }; - window.addEventListener('openchamber:settings-synced', handler); - return () => window.removeEventListener('openchamber:settings-synced', handler); - }, []); - - React.useEffect(() => { - return () => { - isMountedRef.current = false; - if (retryTimeoutRef.current) { - clearTimeout(retryTimeoutRef.current); - } - }; - }, []); - - const setLoadedState = React.useCallback((value: boolean) => { - hasLoadedAppsRef.current = value; - setHasLoadedApps(value); - }, []); + initialize(); + }, [initialize]); const isDesktopLocal = isTauriShell() && isDesktopLocalOriginActive(); - const applyInstalledApps = React.useCallback((installed: InstalledDesktopAppInfo[]) => { - if (installed.length === 0) { - setAvailableApps(getAlwaysAvailableApps()); - setLoadedState(false); - 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_APP_OPTIONS.filter( - (app) => allowed.has(app.appName) || ALWAYS_AVAILABLE_APP_IDS.has(app.id) - ); - const withIcons = filtered.map((app) => ({ - ...app, - iconDataUrl: iconMap.get(app.appName), - })); - setAvailableApps(withIcons); - setLoadedState(true); - }, [setLoadedState]); - - const loadInstalledApps = React.useCallback(async (force?: boolean) => { - if (isLoadingRef.current) return; - if (hasLoadedApps && !force) return; - const appNames = OPEN_IN_APP_OPTIONS.map((app) => app.appName); - if (retryTimeoutRef.current) { - clearTimeout(retryTimeoutRef.current); - retryTimeoutRef.current = null; - } - if (force) { - setLoadedState(false); - } - isLoadingRef.current = true; - setIsScanning(true); - keepScanningRef.current = false; - try { - const { - apps: installed, - success, - hasCache, - isCacheStale: nextCacheStale, - } = await fetchDesktopInstalledApps(appNames, force); - if (!isMountedRef.current) return; - setIsCacheStale(hasCache ? nextCacheStale : false); - applyInstalledApps(installed); - if (success) { - if (!hasCache && installed.length === 0 && retryAttemptRef.current < 3) { - const delays = [800, 1600, 3200]; - const delay = delays[retryAttemptRef.current] ?? 3200; - retryAttemptRef.current += 1; - keepScanningRef.current = true; - retryTimeoutRef.current = setTimeout(() => { - void loadInstalledApps(); - }, delay); - return; - } - retryAttemptRef.current = 0; - keepScanningRef.current = false; - return; - } - if (retryAttemptRef.current < 3) { - const delays = [1000, 3000, 7000]; - const delay = delays[retryAttemptRef.current] ?? 7000; - retryAttemptRef.current += 1; - keepScanningRef.current = true; - retryTimeoutRef.current = setTimeout(() => { - void loadInstalledApps(); - }, delay); - } - } finally { - isLoadingRef.current = false; - if (!keepScanningRef.current) { - setIsScanning(false); - } - } - }, [applyInstalledApps, hasLoadedApps, setLoadedState]); - - React.useEffect(() => { - if (!isDesktopLocal) return; - if (typeof window === 'undefined') return; - void loadInstalledApps(); - const handler = () => { - void loadInstalledApps(); - }; - window.addEventListener('openchamber:app-ready', handler); - const updateHandler = (event: Event) => { - const detail = (event as CustomEvent).detail; - if (Array.isArray(detail)) { - retryAttemptRef.current = 3; - keepScanningRef.current = false; - setIsScanning(false); - setIsCacheStale(false); - applyInstalledApps(detail); - } - }; - window.addEventListener('openchamber:installed-apps-updated', updateHandler); - const flag = (window as unknown as { __openchamberAppReady?: boolean }).__openchamberAppReady; - if (flag) { - void loadInstalledApps(); - } - return () => { - window.removeEventListener('openchamber:app-ready', handler); - window.removeEventListener('openchamber:installed-apps-updated', updateHandler); - }; - }, [applyInstalledApps, isDesktopLocal, loadInstalledApps]); - - React.useEffect(() => { - if (!isDesktopLocal) return; - if (typeof window === 'undefined') return; - const fallbackTimer = window.setTimeout(() => { - if (!hasLoadedAppsRef.current) { - void loadInstalledApps(); - } - }, 5000); - return () => window.clearTimeout(fallbackTimer); - }, [isDesktopLocal, loadInstalledApps]); - const selectedApp = React.useMemo(() => { - const known = OPEN_IN_APP_OPTIONS.find((app) => app.id === selectedAppId) - ?? OPEN_IN_APP_OPTIONS.find((app) => app.id === DEFAULT_OPEN_IN_APP_ID) - ?? OPEN_IN_APP_OPTIONS[0]; + const known = availableApps.find((app) => app.id === selectedAppId) + ?? availableApps.find((app) => app.id === DEFAULT_OPEN_IN_APP_ID) + ?? availableApps[0] + ?? OPEN_IN_APPS[0]; if (known) { - const iconDataUrl = availableApps.find((app) => app.appName === known.appName)?.iconDataUrl; - return iconDataUrl ? { ...known, iconDataUrl } : known; + return withFallbackIcon(known); } - return availableApps[0]; + return withFallbackIcon(OPEN_IN_APPS[0]); }, [availableApps, selectedAppId]); if (!isDesktopLocal || !directory) { @@ -283,11 +115,7 @@ export const OpenInAppButton = ({ directory, activeFilePath, className }: OpenIn }; const handleSelect = async (app: OpenInAppOption) => { - setSelectedAppId(app.id); - if (typeof window !== 'undefined') { - window.localStorage.setItem('openInAppId', app.id); - } - await updateDesktopSettings({ openInAppId: app.id }); + await selectApp(app.id); await handleOpen(app); }; @@ -350,23 +178,26 @@ export const OpenInAppButton = ({ directory, activeFilePath, className }: OpenIn Copy Path - {availableApps.map((app) => ( - void handleSelect(app)} - > - - {app.label} - {selectedApp.id === app.id ? ( - - ) : null} - - ))} + {availableApps.map((app) => { + const appWithFallback = withFallbackIcon(app); + return ( + void handleSelect(app)} + > + + {app.label} + {selectedApp.id === app.id ? ( + + ) : null} + + ); + })} {isCacheStale ? ( { - const stored = typeof window !== 'undefined' ? window.localStorage.getItem('openInAppId') : null; - const selected = getOpenInAppById(stored); - if (selected) { - return selected; - } - return getDefaultOpenInApp(); -}; - const getParentDirectoryPath = (path: string): string => { const normalized = normalizePath(path); if (!normalized) return ''; @@ -121,6 +113,33 @@ const getParentDirectoryPath = (path: string): string => { return parent; }; +const OpenInAppListIcon = ({ label, iconDataUrl }: { label: string; iconDataUrl?: string }) => { + const [failed, setFailed] = React.useState(false); + const initial = label.trim().slice(0, 1).toUpperCase() || '?'; + + if (iconDataUrl && !failed) { + return ( + setFailed(true)} + /> + ); + } + + return ( + + {initial} + + ); +}; + const sortNodes = (items: FileNode[]) => items.slice().sort((a, b) => { if (a.type !== b.type) { @@ -566,6 +585,14 @@ export const FilesView: React.FC = ({ mode = 'full' }) => { const canRename = Boolean(files.rename); const canDelete = Boolean(files.delete); const canReveal = Boolean(files.revealPath); + const openInApps = useOpenInAppsStore((state) => state.availableApps); + const openInCacheStale = useOpenInAppsStore((state) => state.isCacheStale); + const initializeOpenInApps = useOpenInAppsStore((state) => state.initialize); + const loadOpenInApps = useOpenInAppsStore((state) => state.loadInstalledApps); + + React.useEffect(() => { + initializeOpenInApps(); + }, [initializeOpenInApps]); const handleRevealPath = React.useCallback((targetPath: string) => { if (!files.revealPath) return; @@ -574,35 +601,34 @@ export const FilesView: React.FC = ({ mode = 'full' }) => { }); }, [files]); - const handleOpenInSelectedApp = React.useCallback(async () => { + const handleOpenInApp = React.useCallback(async (app: { id: string; appName: string }) => { if (!selectedFile?.path || !root) { return; } - const selectedApp = getSelectedOpenInApp(); const fileDirectory = getParentDirectoryPath(selectedFile.path) || root; - if (OPEN_DIRECTORY_APP_IDS.has(selectedApp.id)) { - const openedDirectory = await openDesktopPath(fileDirectory, selectedApp.appName); + if (OPEN_DIRECTORY_APP_IDS.has(app.id)) { + const openedDirectory = await openDesktopPath(fileDirectory, app.appName); if (!openedDirectory) { - toast.error(`Failed to open in ${selectedApp.appName}`); + toast.error(`Failed to open in ${app.appName}`); } return; } - const openedInApp = await openDesktopProjectInApp(root, selectedApp.id, selectedApp.appName, selectedFile.path); + const openedInApp = await openDesktopProjectInApp(root, app.id, app.appName, selectedFile.path); if (openedInApp) { return; } - const openedFile = await openDesktopPath(selectedFile.path, selectedApp.appName); + const openedFile = await openDesktopPath(selectedFile.path, app.appName); if (openedFile) { return; } - const openedDirectory = await openDesktopPath(fileDirectory, selectedApp.appName); + const openedDirectory = await openDesktopPath(fileDirectory, app.appName); if (!openedDirectory) { - toast.error(`Failed to open in ${selectedApp.appName}`); + toast.error(`Failed to open in ${app.appName}`); } }, [root, selectedFile?.path]); @@ -2256,16 +2282,40 @@ export const FilesView: React.FC = ({ mode = 'full' }) => { )} - + + + + + + {openInApps.map((app) => ( + void handleOpenInApp(app)} + > + + {app.label} + + ))} + {openInCacheStale ? ( + void loadOpenInApps(true)} + > + + Refresh Apps + + ) : null} + + {canEdit && !isSelectedImage && (