Files
bDS/src/renderer/App.tsx
2026-02-14 10:12:37 +01:00

376 lines
10 KiB
TypeScript

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 (
<div className="app">
<div className="app-main">
<ActivityBar />
{sidebarVisible && (
<ResizablePanel
direction="horizontal"
initialSize={280}
minSize={200}
maxSize={500}
storageKey="sidebar-width"
resizerPosition="end"
>
<Sidebar />
</ResizablePanel>
)}
<div className="app-content">
<TabBar />
<Editor />
<Panel />
</div>
</div>
<StatusBar />
<ToastContainer />
</div>
);
};
export default App;