import React, { useEffect } from 'react'; import { ActivityBar, Sidebar, Editor, StatusBar, Panel, TabBar, ToastContainer, showToast, ResizablePanel } from './components'; import { useAppStore, PostData, MediaData, TaskProgress, TabState } from './store'; import './App.css'; // Helper to load tabs for a project from localStorage const TAB_STATE_PREFIX = 'bds-tabs-'; const loadTabsForProject = (projectId: string): TabState | null => { try { const stored = localStorage.getItem(`${TAB_STATE_PREFIX}${projectId}`); if (stored) { return JSON.parse(stored) as TabState; } } catch (error) { console.error('Failed to load tab state:', error); } return null; }; const App: React.FC = () => { const { setPosts, setMedia, addPost, updatePost, removePost, addMedia, updateMedia, removeMedia, setTasks, updateTask, setSyncStatus, setSyncConfigured, setPendingChanges, setLoading, toggleSidebar, togglePanel, setActiveView, setSelectedPost, openTab, restoreTabState, } = useAppStore(); // Load initial data useEffect(() => { const loadData = async () => { setLoading(true); try { // First, get active project to set the correct context in backend engines const activeProject = await window.electronAPI?.projects.getActive(); // Load posts (now with correct project context, limited to 500) const postsResult = await window.electronAPI?.posts.getAll({ limit: 500, offset: 0 }); if (postsResult) { const { items, hasMore, total } = postsResult as { items: PostData[]; hasMore: boolean; total: number }; setPosts(items, hasMore, total); } // Load media const media = await window.electronAPI?.media.getAll(); if (media) { setMedia(media as MediaData[]); } // Restore tabs for the active project if (activeProject && (activeProject as { id: string }).id) { const savedTabState = loadTabsForProject((activeProject as { id: string }).id); if (savedTabState) { restoreTabState(savedTabState); } } // Check sync status const syncConfigured = await window.electronAPI?.sync.isConfigured(); setSyncConfigured(syncConfigured || false); // Get pending changes count const pending = await window.electronAPI?.sync.getPendingCount(); if (pending) { setPendingChanges(pending); } // Load tasks const tasks = await window.electronAPI?.tasks.getAll(); if (tasks) { setTasks(tasks as TaskProgress[]); } } catch (error) { console.error('Failed to load initial data:', error); } finally { setLoading(false); } }; loadData(); }, []); // Save tabs when window closes useEffect(() => { const saveTabsOnUnload = () => { const state = useAppStore.getState(); const projectId = state.activeProject?.id; if (projectId) { const tabState = state.getTabState(); try { localStorage.setItem(`${TAB_STATE_PREFIX}${projectId}`, JSON.stringify(tabState)); } catch (error) { console.error('Failed to save tab state on unload:', error); } } }; window.addEventListener('beforeunload', saveTabsOnUnload); return () => window.removeEventListener('beforeunload', saveTabsOnUnload); }, []); // Set up event listeners for real-time updates useEffect(() => { const unsubscribers: Array<() => void> = []; // Post events unsubscribers.push( window.electronAPI?.on('post:created', (post: unknown) => { addPost(post as PostData); }) || (() => {}) ); unsubscribers.push( window.electronAPI?.on('post:updated', (post: unknown) => { const p = post as PostData; updatePost(p.id, p); }) || (() => {}) ); unsubscribers.push( window.electronAPI?.on('post:deleted', (id: unknown) => { removePost(id as string); }) || (() => {}) ); // Media events unsubscribers.push( window.electronAPI?.on('media:imported', (media: unknown) => { addMedia(media as MediaData); }) || (() => {}) ); unsubscribers.push( window.electronAPI?.on('media:updated', (media: unknown) => { const m = media as MediaData; updateMedia(m.id, m); }) || (() => {}) ); unsubscribers.push( window.electronAPI?.on('media:deleted', (id: unknown) => { removeMedia(id as string); }) || (() => {}) ); // Sync events unsubscribers.push( window.electronAPI?.on('sync:started', () => { setSyncStatus('syncing'); showToast.loading('Syncing...'); }) || (() => {}) ); unsubscribers.push( window.electronAPI?.on('sync:completed', async () => { setSyncStatus('idle'); showToast.dismiss(); showToast.success('Sync completed'); const pending = await window.electronAPI?.sync.getPendingCount(); if (pending) { setPendingChanges(pending); } }) || (() => {}) ); unsubscribers.push( window.electronAPI?.on('sync:failed', (errorMsg: unknown) => { setSyncStatus('error'); showToast.dismiss(); const message = typeof errorMsg === 'string' && errorMsg ? errorMsg : 'Unknown error'; showToast.error(`Sync failed: ${message}`); console.error('Sync failed:', message); }) || (() => {}) ); // Task events unsubscribers.push( window.electronAPI?.on('task:started', (task: unknown) => { const t = task as TaskProgress; updateTask(t.taskId, t); }) || (() => {}) ); unsubscribers.push( window.electronAPI?.on('task:progress', (task: unknown) => { const t = task as TaskProgress; updateTask(t.taskId, t); }) || (() => {}) ); unsubscribers.push( window.electronAPI?.on('task:completed', (task: unknown) => { const t = task as TaskProgress; updateTask(t.taskId, t); showToast.success(`Task completed: ${t.message}`); }) || (() => {}) ); unsubscribers.push( window.electronAPI?.on('task:failed', (task: unknown) => { const t = task as TaskProgress; updateTask(t.taskId, t); showToast.error(`Task failed: ${t.error || t.message}`); }) || (() => {}) ); // Menu events unsubscribers.push( window.electronAPI?.on('menu:newPost', async () => { const post = await window.electronAPI?.posts.create({ title: '', content: '', }); if (post) { setSelectedPost((post as PostData).id); setActiveView('posts'); } }) || (() => {}) ); unsubscribers.push( window.electronAPI?.on('menu:importMedia', () => { window.electronAPI?.media.importDialog(); }) || (() => {}) ); unsubscribers.push( window.electronAPI?.on('menu:toggleSidebar', () => { toggleSidebar(); }) || (() => {}) ); unsubscribers.push( window.electronAPI?.on('menu:togglePanel', () => { togglePanel(); }) || (() => {}) ); unsubscribers.push( window.electronAPI?.on('menu:viewPosts', () => { setActiveView('posts'); }) || (() => {}) ); unsubscribers.push( window.electronAPI?.on('menu:viewMedia', () => { setActiveView('media'); }) || (() => {}) ); unsubscribers.push( window.electronAPI?.on('menu:syncNow', () => { window.electronAPI?.sync.start('bidirectional'); }) || (() => {}) ); unsubscribers.push( window.electronAPI?.on('menu:pushChanges', () => { window.electronAPI?.sync.start('push'); }) || (() => {}) ); unsubscribers.push( window.electronAPI?.on('menu:pullChanges', () => { window.electronAPI?.sync.start('pull'); }) || (() => {}) ); unsubscribers.push( window.electronAPI?.on('menu:configureSync', () => { openTab({ type: 'settings', id: 'settings', isTransient: false }); }) || (() => {}) ); // Rebuild events - clear store on start, reload on complete unsubscribers.push( window.electronAPI?.on('posts:rebuildStarted', () => { setPosts([], false, 0); setSelectedPost(null); }) || (() => {}) ); unsubscribers.push( window.electronAPI?.on('posts:databaseRebuilt', async () => { const postsResult = await window.electronAPI?.posts.getAll({ limit: 500, offset: 0 }); if (postsResult) { const { items, hasMore, total } = postsResult as { items: PostData[]; hasMore: boolean; total: number }; setPosts(items, hasMore, total); } }) || (() => {}) ); unsubscribers.push( window.electronAPI?.on('media:rebuildStarted', () => { setMedia([]); }) || (() => {}) ); unsubscribers.push( window.electronAPI?.on('media:databaseRebuilt', async () => { const mediaResult = await window.electronAPI?.media.getAll(); if (mediaResult) { setMedia(mediaResult as MediaData[]); } }) || (() => {}) ); unsubscribers.push( window.electronAPI?.on('menu:rebuildDatabase', () => { // Fire and forget - the handlers return immediately now window.electronAPI?.posts.rebuildFromFiles(); window.electronAPI?.media.rebuildFromFiles(); // Also regenerate missing thumbnails after media rebuild window.electronAPI?.media.regenerateMissingThumbnails(); }) || (() => {}) ); unsubscribers.push( window.electronAPI?.on('menu:reindexText', () => { // Fire and forget - runs as a background task window.electronAPI?.posts.reindexText(); }) || (() => {}) ); return () => { unsubscribers.forEach(unsub => unsub()); }; }, []); const { sidebarVisible } = useAppStore(); return (