752 lines
24 KiB
TypeScript
752 lines
24 KiB
TypeScript
import React, { useEffect, useRef } from 'react';
|
|
import { ActivityBar, Sidebar, Editor, StatusBar, Panel, TabBar, ToastContainer, showToast, ResizablePanel, WindowTitleBar, AssistantSidebar } from './components';
|
|
import { useAppStore, PostData, MediaData, TaskProgress } from './store';
|
|
import { loadTabsForProject, saveTabsForProject } from './utils';
|
|
import { openSingletonToolTab } from './navigation/tabPolicy';
|
|
import { persistSiteValidationReport } from './navigation/siteValidationPersistence';
|
|
import { persistTranslationValidationReport } from './navigation/translationValidationPersistence';
|
|
import { persistDuplicatesResult } from './navigation/duplicatesPersistence';
|
|
import { executeActivityClick } from './navigation/activityExecution';
|
|
import { handleBlogmarkCreatedEvent } from './navigation/blogmarkHandling';
|
|
import {
|
|
buildBlogmarkTransformOutputEntries,
|
|
buildBlogmarkTransformToastNotifications,
|
|
parseBlogmarkCreatedEventPayload,
|
|
shouldAutoOpenPanelForOutputEntries,
|
|
} from './navigation/blogmarkTransformOutput';
|
|
import { createDeferredEventGate } from './navigation/deferredEventGate';
|
|
import { createAndFocusPost } from './navigation/postCreation';
|
|
import { ensureRendererPicoThemeStylesheet, getRendererPicoTheme } from './utils/picoTheme';
|
|
import { addWindowEventListener, BDS_EVENT_SCRIPTS_CHANGED } from './utils/windowEvents';
|
|
import { refreshPythonMacroSlugs, wirePythonMacroPreview, invalidatePythonMacroScriptCache } from './macros';
|
|
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,
|
|
toggleAssistantSidebar,
|
|
setSelectedPost,
|
|
setActiveProject,
|
|
setPicoTheme,
|
|
openTab,
|
|
restoreTabState,
|
|
appendPanelOutputEntry,
|
|
} = useAppStore();
|
|
const blogmarkEventGateRef = useRef(createDeferredEventGate<PostData>());
|
|
|
|
const processBlogmarkCreated = (created: PostData) => {
|
|
addPost(created);
|
|
const state = useAppStore.getState();
|
|
handleBlogmarkCreatedEvent(
|
|
{
|
|
activeView: state.activeView,
|
|
sidebarVisible: state.sidebarVisible,
|
|
},
|
|
created,
|
|
{
|
|
setActiveView: state.setActiveView,
|
|
toggleSidebar: state.toggleSidebar,
|
|
setSelectedPost: state.setSelectedPost,
|
|
openTab: state.openTab,
|
|
},
|
|
);
|
|
};
|
|
|
|
// 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[]);
|
|
}
|
|
|
|
// Load known Python macro slugs for editor detection
|
|
await refreshPythonMacroSlugs();
|
|
|
|
// Wire Python macro resolver/renderer for editor preview
|
|
wirePythonMacroPreview();
|
|
} catch (error) {
|
|
console.error('Failed to load initial data:', error);
|
|
} finally {
|
|
setLoading(false);
|
|
setTimeout(() => {
|
|
blogmarkEventGateRef.current.markReady(processBlogmarkCreated);
|
|
}, 0);
|
|
}
|
|
};
|
|
|
|
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);
|
|
}) || (() => {})
|
|
);
|
|
|
|
// Post translation events (refresh post to update availableLanguages for sidebar badges)
|
|
const handlePostTranslationChange = (data: unknown) => {
|
|
const translation = data as { translationFor?: string };
|
|
if (translation.translationFor) {
|
|
window.electronAPI?.posts.get(translation.translationFor).then((post) => {
|
|
if (post) {
|
|
updatePost((post as PostData).id, post as PostData);
|
|
}
|
|
});
|
|
}
|
|
};
|
|
unsubscribers.push(
|
|
window.electronAPI?.on('post:translationCreated', handlePostTranslationChange) || (() => {})
|
|
);
|
|
unsubscribers.push(
|
|
window.electronAPI?.on('post:translationUpdated', handlePostTranslationChange) || (() => {})
|
|
);
|
|
|
|
// 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 state = useAppStore.getState();
|
|
await createAndFocusPost({
|
|
createPost: async (input) => (await window.electronAPI?.posts.create(input)) as { id: string } | null | undefined,
|
|
setSelectedPost: state.setSelectedPost,
|
|
ensurePostsSidebar: () => {
|
|
const next = useAppStore.getState();
|
|
executeActivityClick(
|
|
{
|
|
activeView: next.activeView,
|
|
sidebarVisible: next.sidebarVisible,
|
|
tabs: next.tabs,
|
|
activeTabId: next.activeTabId,
|
|
},
|
|
'posts',
|
|
{
|
|
setActiveView: next.setActiveView,
|
|
toggleSidebar: next.toggleSidebar,
|
|
},
|
|
);
|
|
},
|
|
onError: (error) => {
|
|
console.error('Failed to create post:', error);
|
|
},
|
|
});
|
|
}) || (() => {})
|
|
);
|
|
|
|
unsubscribers.push(
|
|
window.electronAPI?.on('blogmark:created', (payload: unknown) => {
|
|
const parsedPayload = parseBlogmarkCreatedEventPayload(payload);
|
|
if (!parsedPayload) {
|
|
return;
|
|
}
|
|
|
|
const created = parsedPayload.post as PostData;
|
|
if (!created?.id) {
|
|
return;
|
|
}
|
|
|
|
const outputEntries = buildBlogmarkTransformOutputEntries(parsedPayload.transform, tr);
|
|
const toastNotifications = buildBlogmarkTransformToastNotifications(parsedPayload.transform, tr);
|
|
|
|
toastNotifications.forEach((notification) => {
|
|
if (notification.kind === 'error') {
|
|
showToast.error(notification.message);
|
|
return;
|
|
}
|
|
|
|
showToast.success(notification.message);
|
|
});
|
|
|
|
if (outputEntries.length > 0) {
|
|
const createdAt = new Date().toISOString();
|
|
outputEntries.forEach((entry, index) => {
|
|
appendPanelOutputEntry({
|
|
id: `blogmark-transform-${Date.now()}-${index}`,
|
|
createdAt,
|
|
message: entry.message,
|
|
kind: entry.kind,
|
|
});
|
|
});
|
|
|
|
if (shouldAutoOpenPanelForOutputEntries(outputEntries)) {
|
|
useAppStore.setState({
|
|
panelVisible: true,
|
|
panelActiveTab: 'output',
|
|
});
|
|
}
|
|
}
|
|
|
|
blogmarkEventGateRef.current.push(created, processBlogmarkCreated);
|
|
}) || (() => {})
|
|
);
|
|
|
|
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:toggleAssistantSidebar', () => {
|
|
toggleAssistantSidebar();
|
|
}) || (() => {})
|
|
);
|
|
|
|
unsubscribers.push(
|
|
window.electronAPI?.on('menu:viewPosts', () => {
|
|
const state = useAppStore.getState();
|
|
executeActivityClick(
|
|
{
|
|
activeView: state.activeView,
|
|
sidebarVisible: state.sidebarVisible,
|
|
tabs: state.tabs,
|
|
activeTabId: state.activeTabId,
|
|
},
|
|
'posts',
|
|
{
|
|
setActiveView: state.setActiveView,
|
|
toggleSidebar: state.toggleSidebar,
|
|
},
|
|
);
|
|
}) || (() => {})
|
|
);
|
|
|
|
unsubscribers.push(
|
|
window.electronAPI?.on('menu:viewMedia', () => {
|
|
const state = useAppStore.getState();
|
|
executeActivityClick(
|
|
{
|
|
activeView: state.activeView,
|
|
sidebarVisible: state.sidebarVisible,
|
|
tabs: state.tabs,
|
|
activeTabId: state.activeTabId,
|
|
},
|
|
'media',
|
|
{
|
|
setActiveView: state.setActiveView,
|
|
toggleSidebar: state.toggleSidebar,
|
|
},
|
|
);
|
|
}) || (() => {})
|
|
);
|
|
|
|
unsubscribers.push(
|
|
window.electronAPI?.on('menu:editPreferences', () => {
|
|
openSingletonToolTab(openTab, 'settings');
|
|
}) || (() => {})
|
|
);
|
|
|
|
unsubscribers.push(
|
|
window.electronAPI?.on('menu:editMenu', () => {
|
|
openSingletonToolTab(openTab, 'menu-editor');
|
|
}) || (() => {})
|
|
);
|
|
|
|
// 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(),
|
|
window.electronAPI?.scripts.rebuildFromFiles(),
|
|
window.electronAPI?.templates.rebuildFromFiles(),
|
|
]);
|
|
await window.electronAPI?.posts.rebuildLinks();
|
|
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', () => {
|
|
openSingletonToolTab(openTab, 'metadata-diff');
|
|
}) || (() => {})
|
|
);
|
|
|
|
unsubscribers.push(
|
|
window.electronAPI?.on('menu:findDuplicates', () => {
|
|
openSingletonToolTab(openTab, 'find-duplicates');
|
|
}) || (() => {})
|
|
);
|
|
|
|
unsubscribers.push(
|
|
window.electronAPI?.on('embeddings:duplicateSearchResult', (...args: unknown[]) => {
|
|
const pairs = args[0] as import('../main/shared/electronApi').DuplicatePair[];
|
|
const projectId = useAppStore.getState().activeProject?.id;
|
|
if (projectId && pairs) {
|
|
persistDuplicatesResult(projectId, pairs);
|
|
window.dispatchEvent(new CustomEvent('bds:duplicates-updated', {
|
|
detail: { projectId },
|
|
}));
|
|
}
|
|
}) || (() => {})
|
|
);
|
|
|
|
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:regenerateCalendar', async () => {
|
|
try {
|
|
await window.electronAPI?.blog.regenerateCalendar();
|
|
} catch (error) {
|
|
console.error('Calendar regeneration failed:', error);
|
|
showToast.error(tr('app.calendarRegenerationFailed'));
|
|
}
|
|
}) || (() => {})
|
|
);
|
|
|
|
unsubscribers.push(
|
|
window.electronAPI?.on('menu:validateSite', () => {
|
|
const validateAndOpen = async () => {
|
|
try {
|
|
const report = await window.electronAPI?.blog.validateSite();
|
|
const projectId = useAppStore.getState().activeProject?.id;
|
|
if (projectId && report) {
|
|
persistSiteValidationReport(projectId, report);
|
|
window.dispatchEvent(new CustomEvent('bds:site-validation-updated', {
|
|
detail: { projectId },
|
|
}));
|
|
}
|
|
openSingletonToolTab(openTab, 'site-validation');
|
|
} catch (error) {
|
|
console.error('Site validation failed:', error);
|
|
showToast.error(tr('siteValidation.error.validate'));
|
|
}
|
|
};
|
|
void validateAndOpen();
|
|
}) || (() => {})
|
|
);
|
|
|
|
unsubscribers.push(
|
|
window.electronAPI?.on('menu:validateTranslations', () => {
|
|
const validateAndOpen = async () => {
|
|
try {
|
|
const report = await window.electronAPI?.blog.validateTranslations();
|
|
const projectId = useAppStore.getState().activeProject?.id;
|
|
if (projectId && report) {
|
|
persistTranslationValidationReport(projectId, report);
|
|
window.dispatchEvent(new CustomEvent('bds:translation-validation-updated', {
|
|
detail: { projectId },
|
|
}));
|
|
}
|
|
openSingletonToolTab(openTab, 'translation-validation');
|
|
} catch (error) {
|
|
console.error('Translation validation failed:', error);
|
|
showToast.error(tr('translationValidation.error.validate'));
|
|
}
|
|
};
|
|
void validateAndOpen();
|
|
}) || (() => {})
|
|
);
|
|
|
|
unsubscribers.push(
|
|
window.electronAPI?.on('menu:fillMissingTranslations', () => {
|
|
const fillMissing = async () => {
|
|
try {
|
|
const result = await window.electronAPI?.blog.fillMissingTranslations();
|
|
if (result) {
|
|
if (!result.taskStarted) {
|
|
showToast.info(tr('blog.fillMissing.nothingToDo'));
|
|
} else {
|
|
showToast.success(tr('blog.fillMissing.started'));
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('Fill missing translations failed:', error);
|
|
showToast.error(tr('blog.fillMissing.error'));
|
|
}
|
|
};
|
|
void fillMissing();
|
|
}) || (() => {})
|
|
);
|
|
|
|
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:uploadSite', async () => {
|
|
try {
|
|
const prefs = await window.electronAPI?.meta.getPublishingPreferences();
|
|
if (!prefs) {
|
|
showToast.error(tr('app.uploadSiteNoCredentials'));
|
|
return;
|
|
}
|
|
if (!prefs.sshHost || !prefs.sshUser || !prefs.sshRemotePath) {
|
|
showToast.error(tr('app.uploadSiteNoCredentials'));
|
|
return;
|
|
}
|
|
await window.electronAPI?.publish.uploadSite(prefs);
|
|
} catch (error: any) {
|
|
console.error('Site upload failed:', error);
|
|
if (error?.message?.includes('Airplane mode')) {
|
|
useAppStore.getState().showErrorModal({ message: tr('app.uploadSiteOfflineMode') });
|
|
} else {
|
|
showToast.error(tr('app.uploadSiteFailed'));
|
|
}
|
|
}
|
|
}) || (() => {})
|
|
);
|
|
|
|
unsubscribers.push(
|
|
window.electronAPI?.on('menu:openDocumentation', () => {
|
|
openSingletonToolTab(openTab, 'documentation');
|
|
}) || (() => {})
|
|
);
|
|
|
|
unsubscribers.push(
|
|
window.electronAPI?.on('menu:openApiDocumentation', () => {
|
|
openSingletonToolTab(openTab, 'api-documentation');
|
|
}) || (() => {})
|
|
);
|
|
|
|
// 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 }));
|
|
}
|
|
}) || (() => {})
|
|
);
|
|
|
|
// Refresh Python macro slugs when scripts change
|
|
unsubscribers.push(
|
|
addWindowEventListener(BDS_EVENT_SCRIPTS_CHANGED, () => {
|
|
invalidatePythonMacroScriptCache();
|
|
void refreshPythonMacroSlugs();
|
|
})
|
|
);
|
|
|
|
void window.electronAPI?.app.notifyRendererReady?.().catch((error) => {
|
|
console.error('Failed to notify renderer readiness:', error);
|
|
});
|
|
|
|
return () => {
|
|
unsubscribers.forEach(unsub => unsub());
|
|
};
|
|
}, []);
|
|
|
|
// Subscribe to entity:changed events fired by the CLI NotificationWatcher.
|
|
// When the CLI mutates posts or media while the app is open, refresh the
|
|
// affected entry in the local store so the UI stays in sync.
|
|
useEffect(() => {
|
|
const unsub = window.electronAPI?.onEntityChanged(async ({ entity, entityId, action }) => {
|
|
if (entity === 'post') {
|
|
if (action === 'deleted') {
|
|
removePost(entityId);
|
|
useAppStore.getState().closeTab(entityId);
|
|
} else {
|
|
const post = await window.electronAPI?.posts.get(entityId);
|
|
if (post) {
|
|
const p = post as PostData;
|
|
action === 'created' ? addPost(p) : updatePost(p.id, p);
|
|
}
|
|
}
|
|
} else if (entity === 'media') {
|
|
if (action === 'deleted') {
|
|
removeMedia(entityId);
|
|
} else {
|
|
const media = await window.electronAPI?.media.get(entityId);
|
|
if (media) {
|
|
const m = media as MediaData;
|
|
action === 'created' ? addMedia(m) : updateMedia(m.id, m);
|
|
}
|
|
}
|
|
}
|
|
// script and template entities have no cached store state — they are
|
|
// loaded on demand and will reflect CLI changes on next navigation.
|
|
});
|
|
return () => unsub?.();
|
|
}, [addPost, updatePost, removePost, addMedia, updateMedia, removeMedia]);
|
|
|
|
const { sidebarVisible, assistantSidebarVisible } = useAppStore();
|
|
|
|
return (
|
|
<div className="app">
|
|
<WindowTitleBar />
|
|
<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>
|
|
{assistantSidebarVisible && (
|
|
<ResizablePanel
|
|
direction="horizontal"
|
|
initialSize={360}
|
|
minSize={280}
|
|
maxSize={640}
|
|
storageKey="assistant-sidebar-width"
|
|
resizerPosition="start"
|
|
>
|
|
<AssistantSidebar />
|
|
</ResizablePanel>
|
|
)}
|
|
</div>
|
|
<StatusBar />
|
|
<ToastContainer />
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default App;
|