import React, { useEffect } from 'react'; import { ActivityBar, Sidebar, Editor, StatusBar, Panel, TabBar, ToastContainer, showToast, ResizablePanel, WindowTitleBar } from './components'; import { useAppStore, PostData, MediaData, TaskProgress } from './store'; import { loadTabsForProject, saveTabsForProject } from './utils'; import { ensureRendererPicoThemeStylesheet, getRendererPicoTheme } from './utils/picoTheme'; import { useI18n } from './i18n'; import './App.css'; const App: React.FC = () => { const { t: tr } = useI18n(); const { setPosts, setMedia, addPost, updatePost, removePost, addMedia, updateMedia, removeMedia, setTasks, updateTask, setLoading, toggleSidebar, togglePanel, setActiveView, setSelectedPost, setActiveProject, setPicoTheme, 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(); if (activeProject) { setActiveProject(activeProject as import('./store').ProjectData); const metadata = await window.electronAPI?.meta.getProjectMetadata(); setPicoTheme(metadata?.picoTheme); const resolvedTheme = getRendererPicoTheme(metadata?.picoTheme); await ensureRendererPicoThemeStylesheet(resolvedTheme); } // 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); } } // 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(); saveTabsForProject(projectId, tabState); } }; window.addEventListener('beforeunload', saveTabsOnUnload); return () => window.removeEventListener('beforeunload', saveTabsOnUnload); }, [tr]); // 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); useAppStore.getState().closeTab(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); }) || (() => {}) ); // Task events unsubscribers.push( window.electronAPI?.on('task:created', (task: unknown) => { const t = task as TaskProgress; updateTask(t.taskId, t); }) || (() => {}) ); 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(tr('app.taskCompleted', { message: t.message })); }) || (() => {}) ); unsubscribers.push( window.electronAPI?.on('task:failed', (task: unknown) => { const t = task as TaskProgress; updateTask(t.taskId, t); showToast.error(tr('app.taskFailed', { message: 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'); }) || (() => {}) ); // 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', async () => { try { await Promise.all([ window.electronAPI?.posts.rebuildFromFiles(), window.electronAPI?.media.rebuildFromFiles(), ]); await window.electronAPI?.media.regenerateMissingThumbnails(); } catch (error) { console.error('Database rebuild failed:', error); showToast.error(tr('app.databaseRebuildFailed')); } }) || (() => {}) ); unsubscribers.push( window.electronAPI?.on('menu:reindexText', async () => { try { await Promise.all([ window.electronAPI?.posts.reindexText(), window.electronAPI?.media.reindexText(), ]); } catch (error) { console.error('Text reindex failed:', error); showToast.error(tr('app.textReindexFailed')); } }) || (() => {}) ); unsubscribers.push( window.electronAPI?.on('menu:metadataDiff', () => { // Open metadata diff tool tab openTab({ id: 'metadata-diff', type: 'metadata-diff', title: tr('app.metadataDiff') }); }) || (() => {}) ); unsubscribers.push( window.electronAPI?.on('menu:generateSitemap', async () => { try { await window.electronAPI?.blog.generateSitemap(); } catch (error) { console.error('Sitemap generation failed:', error); showToast.error(tr('app.sitemapGenerationFailed')); } }) || (() => {}) ); unsubscribers.push( window.electronAPI?.on('menu:validateSite', () => { openTab({ id: 'site-validation-report', type: 'site-validation', isTransient: true }); }) || (() => {}) ); unsubscribers.push( window.electronAPI?.on('menu:previewPost', async () => { try { const selectedPostId = useAppStore.getState().selectedPostId; if (!selectedPostId) { return; } const previewUrl = await window.electronAPI?.posts.getPreviewUrl(selectedPostId); if (typeof previewUrl === 'string' && previewUrl.length > 0) { window.open(previewUrl, '_blank', 'noopener'); } } catch (error) { console.error('Failed to open selected post preview:', error); showToast.error(tr('app.previewOpenFailed')); } }) || (() => {}) ); unsubscribers.push( window.electronAPI?.on('menu:openDocumentation', () => { openTab({ id: 'documentation', type: 'documentation', isTransient: false }); }) || (() => {}) ); // Import completion event - refresh posts and media stores unsubscribers.push( window.electronAPI?.import.onComplete(async (data) => { // Refresh posts store if any posts were imported if (data.posts.imported > 0 || data.pages.imported > 0) { 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); } } // Refresh media store if any media was imported if (data.media.imported > 0) { const mediaResult = await window.electronAPI?.media.getAll(); if (mediaResult) { setMedia(mediaResult as MediaData[]); } } // Show success toast const importedCount = data.posts.imported + data.pages.imported; const importedMedia = data.media.imported; if (data.success) { showToast.success(tr('app.importComplete', { posts: importedCount, media: importedMedia })); } }) || (() => {}) ); return () => { unsubscribers.forEach(unsub => unsub()); }; }, []); const { sidebarVisible } = useAppStore(); return (
{sidebarVisible && ( )}
); }; export default App;