feat: allow projects to have external data path for posts and media
This commit is contained in:
@@ -3,7 +3,8 @@
|
|||||||
"allow": [
|
"allow": [
|
||||||
"Bash(npm run build:*)",
|
"Bash(npm run build:*)",
|
||||||
"Bash(npx tsc:*)",
|
"Bash(npx tsc:*)",
|
||||||
"Bash(node ./node_modules/typescript/bin/tsc:*)"
|
"Bash(node ./node_modules/typescript/bin/tsc:*)",
|
||||||
|
"Bash(npm run build:main:*)"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ export class DatabaseConnection {
|
|||||||
private localDb: DrizzleDB | null = null;
|
private localDb: DrizzleDB | null = null;
|
||||||
private localClient: Client | null = null;
|
private localClient: Client | null = null;
|
||||||
private config: DatabaseConfig;
|
private config: DatabaseConfig;
|
||||||
|
private _closing = false;
|
||||||
|
|
||||||
constructor(config?: Partial<DatabaseConfig>) {
|
constructor(config?: Partial<DatabaseConfig>) {
|
||||||
const userDataPath = app.getPath('userData');
|
const userDataPath = app.getPath('userData');
|
||||||
@@ -58,9 +59,15 @@ export class DatabaseConnection {
|
|||||||
return this.localDb;
|
return this.localDb;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get isClosing(): boolean {
|
||||||
|
return this._closing;
|
||||||
|
}
|
||||||
|
|
||||||
getLocal(): DrizzleDB {
|
getLocal(): DrizzleDB {
|
||||||
if (!this.localDb) {
|
if (!this.localDb) {
|
||||||
throw new Error('Local database not initialized. Call initializeLocal() first.');
|
throw new Error(this._closing
|
||||||
|
? 'Database is closing'
|
||||||
|
: 'Local database not initialized. Call initializeLocal() first.');
|
||||||
}
|
}
|
||||||
return this.localDb;
|
return this.localDb;
|
||||||
}
|
}
|
||||||
@@ -449,6 +456,7 @@ export class DatabaseConnection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async close(): Promise<void> {
|
async close(): Promise<void> {
|
||||||
|
this._closing = true;
|
||||||
if (this.localClient) {
|
if (this.localClient) {
|
||||||
this.localClient.close();
|
this.localClient.close();
|
||||||
this.localClient = null;
|
this.localClient = null;
|
||||||
|
|||||||
@@ -49,20 +49,28 @@ export interface MediaMetadata {
|
|||||||
|
|
||||||
export class MediaEngine extends EventEmitter {
|
export class MediaEngine extends EventEmitter {
|
||||||
private currentProjectId: string = 'default';
|
private currentProjectId: string = 'default';
|
||||||
private projectBaseDir: string | null = null;
|
private dataDir: string | null = null; // For media files (may be external)
|
||||||
|
private internalDir: string | null = null; // For thumbnails (always local)
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
private getProjectBaseDir(): string {
|
private getDefaultBaseDir(): string {
|
||||||
if (this.projectBaseDir) return this.projectBaseDir;
|
|
||||||
const userDataPath = app.getPath('userData');
|
const userDataPath = app.getPath('userData');
|
||||||
return path.join(userDataPath, 'projects', this.currentProjectId);
|
return path.join(userDataPath, 'projects', this.currentProjectId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getDataDir(): string {
|
||||||
|
return this.dataDir || this.getDefaultBaseDir();
|
||||||
|
}
|
||||||
|
|
||||||
|
private getInternalDir(): string {
|
||||||
|
return this.internalDir || this.getDefaultBaseDir();
|
||||||
|
}
|
||||||
|
|
||||||
private getMediaBaseDir(): string {
|
private getMediaBaseDir(): string {
|
||||||
return path.join(this.getProjectBaseDir(), 'media');
|
return path.join(this.getDataDir(), 'media');
|
||||||
}
|
}
|
||||||
|
|
||||||
private getMediaDir(): string {
|
private getMediaDir(): string {
|
||||||
@@ -91,9 +99,11 @@ export class MediaEngine extends EventEmitter {
|
|||||||
return path.join(dir, `${id}${extension}`);
|
return path.join(dir, `${id}${extension}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
setProjectContext(projectId: string, baseDir?: string): void {
|
setProjectContext(projectId: string, dataDir?: string, internalDir?: string): void {
|
||||||
this.currentProjectId = projectId;
|
this.currentProjectId = projectId;
|
||||||
this.projectBaseDir = baseDir || null;
|
this.dataDir = dataDir || null;
|
||||||
|
this.internalDir = internalDir || null;
|
||||||
|
console.log(`[MediaEngine] setProjectContext: projectId=${projectId}, dataDir=${this.dataDir}, internalDir=${this.internalDir}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
getProjectContext(): string {
|
getProjectContext(): string {
|
||||||
@@ -108,7 +118,7 @@ export class MediaEngine extends EventEmitter {
|
|||||||
* Get the thumbnails directory for the current project
|
* Get the thumbnails directory for the current project
|
||||||
*/
|
*/
|
||||||
private getThumbnailsDir(): string {
|
private getThumbnailsDir(): string {
|
||||||
return path.join(this.getProjectBaseDir(), 'thumbnails');
|
return path.join(this.getInternalDir(), 'thumbnails');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -543,6 +553,7 @@ export class MediaEngine extends EventEmitter {
|
|||||||
|
|
||||||
async rebuildDatabaseFromFiles(): Promise<void> {
|
async rebuildDatabaseFromFiles(): Promise<void> {
|
||||||
const mediaBaseDir = this.getMediaBaseDir();
|
const mediaBaseDir = this.getMediaBaseDir();
|
||||||
|
console.log(`[MediaEngine] rebuildDatabaseFromFiles: scanning mediaBaseDir=${mediaBaseDir}`);
|
||||||
const task: Task<void> = {
|
const task: Task<void> = {
|
||||||
id: uuidv4(),
|
id: uuidv4(),
|
||||||
name: 'Rebuild database from media files',
|
name: 'Rebuild database from media files',
|
||||||
|
|||||||
@@ -32,7 +32,6 @@ export const DEFAULT_CATEGORIES = ['article', 'picture', 'aside', 'page'];
|
|||||||
*/
|
*/
|
||||||
export class MetaEngine extends EventEmitter {
|
export class MetaEngine extends EventEmitter {
|
||||||
private currentProjectId: string = 'default';
|
private currentProjectId: string = 'default';
|
||||||
private projectBaseDir: string | null = null;
|
|
||||||
private tags: Set<string> = new Set();
|
private tags: Set<string> = new Set();
|
||||||
private categories: Set<string> = new Set();
|
private categories: Set<string> = new Set();
|
||||||
private projectMetadata: ProjectMetadata | null = null;
|
private projectMetadata: ProjectMetadata | null = null;
|
||||||
@@ -42,18 +41,21 @@ export class MetaEngine extends EventEmitter {
|
|||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
private getProjectBaseDir(): string {
|
/**
|
||||||
if (this.projectBaseDir) return this.projectBaseDir;
|
* Always returns the internal project directory (in userData).
|
||||||
|
* Meta files never live in an external dataPath.
|
||||||
|
*/
|
||||||
|
private getInternalBaseDir(): string {
|
||||||
const userDataPath = app.getPath('userData');
|
const userDataPath = app.getPath('userData');
|
||||||
return path.join(userDataPath, 'projects', this.currentProjectId);
|
return path.join(userDataPath, 'projects', this.currentProjectId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the meta directory path for the current project.
|
* Get the meta directory path for the current project.
|
||||||
* Format: {baseDir}/meta/
|
* Always in the internal directory (userData), never external.
|
||||||
*/
|
*/
|
||||||
getMetaDir(): string {
|
getMetaDir(): string {
|
||||||
return path.join(this.getProjectBaseDir(), 'meta');
|
return path.join(this.getInternalBaseDir(), 'meta');
|
||||||
}
|
}
|
||||||
|
|
||||||
private getTagsFilePath(): string {
|
private getTagsFilePath(): string {
|
||||||
@@ -68,9 +70,8 @@ export class MetaEngine extends EventEmitter {
|
|||||||
return path.join(this.getMetaDir(), 'project.json');
|
return path.join(this.getMetaDir(), 'project.json');
|
||||||
}
|
}
|
||||||
|
|
||||||
setProjectContext(projectId: string, baseDir?: string): void {
|
setProjectContext(projectId: string): void {
|
||||||
this.currentProjectId = projectId;
|
this.currentProjectId = projectId;
|
||||||
this.projectBaseDir = baseDir || null;
|
|
||||||
// Reset in-memory cache when project changes
|
// Reset in-memory cache when project changes
|
||||||
this.tags.clear();
|
this.tags.clear();
|
||||||
this.categories.clear();
|
this.categories.clear();
|
||||||
|
|||||||
@@ -142,16 +142,16 @@ export class PostEngine extends EventEmitter {
|
|||||||
await client.execute({ sql: 'DELETE FROM posts_fts WHERE id = ?', args: [id] });
|
await client.execute({ sql: 'DELETE FROM posts_fts WHERE id = ?', args: [id] });
|
||||||
}
|
}
|
||||||
|
|
||||||
private projectBaseDir: string | null = null;
|
private dataDir: string | null = null;
|
||||||
|
|
||||||
private getProjectBaseDir(): string {
|
private getDataDir(): string {
|
||||||
if (this.projectBaseDir) return this.projectBaseDir;
|
if (this.dataDir) return this.dataDir;
|
||||||
const userDataPath = app.getPath('userData');
|
const userDataPath = app.getPath('userData');
|
||||||
return path.join(userDataPath, 'projects', this.currentProjectId);
|
return path.join(userDataPath, 'projects', this.currentProjectId);
|
||||||
}
|
}
|
||||||
|
|
||||||
private getPostsBaseDir(): string {
|
private getPostsBaseDir(): string {
|
||||||
return path.join(this.getProjectBaseDir(), 'posts');
|
return path.join(this.getDataDir(), 'posts');
|
||||||
}
|
}
|
||||||
|
|
||||||
private getPostsDir(): string {
|
private getPostsDir(): string {
|
||||||
@@ -179,9 +179,9 @@ export class PostEngine extends EventEmitter {
|
|||||||
return path.join(dir, `${slug}.md`);
|
return path.join(dir, `${slug}.md`);
|
||||||
}
|
}
|
||||||
|
|
||||||
setProjectContext(projectId: string, baseDir?: string): void {
|
setProjectContext(projectId: string, dataDir?: string): void {
|
||||||
this.currentProjectId = projectId;
|
this.currentProjectId = projectId;
|
||||||
this.projectBaseDir = baseDir || null;
|
this.dataDir = dataDir || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
getProjectContext(): string {
|
getProjectContext(): string {
|
||||||
|
|||||||
@@ -31,37 +31,42 @@ export class ProjectEngine extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the base directory for a project's data.
|
* Get the internal base directory for a project (always in userData).
|
||||||
* If the project has a custom dataPath, use that; otherwise use the default.
|
* This is where meta, thumbnails, tags, and project.json live.
|
||||||
*/
|
*/
|
||||||
getProjectBaseDir(projectId: string, dataPath?: string | null): string {
|
getInternalBaseDir(projectId: string): string {
|
||||||
if (dataPath) {
|
|
||||||
return dataPath;
|
|
||||||
}
|
|
||||||
const userDataPath = app.getPath('userData');
|
const userDataPath = app.getPath('userData');
|
||||||
return path.join(userDataPath, 'projects', projectId);
|
return path.join(userDataPath, 'projects', projectId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the default base directory (in userData) for a project.
|
* Get the data directory for posts and media.
|
||||||
|
* If a custom dataPath is set, posts/media live there; otherwise in the internal dir.
|
||||||
|
*/
|
||||||
|
getDataDir(projectId: string, dataPath?: string | null): string {
|
||||||
|
if (dataPath) {
|
||||||
|
return dataPath;
|
||||||
|
}
|
||||||
|
return this.getInternalBaseDir(projectId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Alias kept for backward compatibility — returns the internal base dir.
|
||||||
*/
|
*/
|
||||||
getDefaultProjectBaseDir(projectId: string): string {
|
getDefaultProjectBaseDir(projectId: string): string {
|
||||||
const userDataPath = app.getPath('userData');
|
return this.getInternalBaseDir(projectId);
|
||||||
return path.join(userDataPath, 'projects', projectId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async ensureProjectDirectories(projectId: string, dataPath?: string | null): Promise<void> {
|
private async ensureProjectDirectories(projectId: string, dataPath?: string | null): Promise<void> {
|
||||||
const projectDir = this.getProjectBaseDir(projectId, dataPath);
|
// Internal directories (always in userData)
|
||||||
const postsDir = path.join(projectDir, 'posts');
|
const internalDir = this.getInternalBaseDir(projectId);
|
||||||
const mediaDir = path.join(projectDir, 'media');
|
await fs.mkdir(path.join(internalDir, 'thumbnails'), { recursive: true });
|
||||||
const thumbnailsDir = path.join(projectDir, 'thumbnails');
|
await fs.mkdir(path.join(internalDir, 'meta'), { recursive: true });
|
||||||
const metaDir = path.join(projectDir, 'meta');
|
|
||||||
|
|
||||||
await fs.mkdir(projectDir, { recursive: true });
|
// Data directories (may be external)
|
||||||
await fs.mkdir(postsDir, { recursive: true });
|
const dataDir = this.getDataDir(projectId, dataPath);
|
||||||
await fs.mkdir(mediaDir, { recursive: true });
|
await fs.mkdir(path.join(dataDir, 'posts'), { recursive: true });
|
||||||
await fs.mkdir(thumbnailsDir, { recursive: true });
|
await fs.mkdir(path.join(dataDir, 'media'), { recursive: true });
|
||||||
await fs.mkdir(metaDir, { recursive: true });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async createProject(data: { name: string; description?: string; slug?: string; dataPath?: string }): Promise<ProjectData> {
|
async createProject(data: { name: string; description?: string; slug?: string; dataPath?: string }): Promise<ProjectData> {
|
||||||
@@ -194,14 +199,13 @@ export class ProjectEngine extends EventEmitter {
|
|||||||
// Delete associated media from database
|
// Delete associated media from database
|
||||||
await db.delete(media).where(eq(media.projectId, id));
|
await db.delete(media).where(eq(media.projectId, id));
|
||||||
|
|
||||||
// Delete project files and directories
|
// Delete the internal project directory (meta, thumbnails, and posts/media if stored internally).
|
||||||
const paths = this.getProjectPaths(id);
|
// If a custom dataPath is set, external posts/media are NOT deleted — the user manages that storage.
|
||||||
|
const internalDir = this.getInternalBaseDir(id);
|
||||||
try {
|
try {
|
||||||
// Delete posts directory
|
await fs.rm(internalDir, { recursive: true, force: true });
|
||||||
await fs.rm(path.dirname(paths.posts), { recursive: true, force: true });
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Directory may not exist, that's okay
|
console.warn(`Could not delete internal project directory for ${id}:`, error);
|
||||||
console.warn(`Could not delete project directory for ${id}:`, error);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete project from database
|
// Delete project from database
|
||||||
@@ -287,10 +291,10 @@ export class ProjectEngine extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getProjectPaths(projectId: string, dataPath?: string | null): { posts: string; media: string } {
|
getProjectPaths(projectId: string, dataPath?: string | null): { posts: string; media: string } {
|
||||||
const baseDir = this.getProjectBaseDir(projectId, dataPath);
|
const dataDir = this.getDataDir(projectId, dataPath);
|
||||||
return {
|
return {
|
||||||
posts: path.join(baseDir, 'posts'),
|
posts: path.join(dataDir, 'posts'),
|
||||||
media: path.join(baseDir, 'media'),
|
media: path.join(dataDir, 'media'),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -110,14 +110,16 @@ function isValidHexColor(color: string): boolean {
|
|||||||
*/
|
*/
|
||||||
export class TagEngine extends EventEmitter {
|
export class TagEngine extends EventEmitter {
|
||||||
private currentProjectId: string = 'default';
|
private currentProjectId: string = 'default';
|
||||||
private projectBaseDir: string | null = null;
|
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
private getProjectBaseDir(): string {
|
/**
|
||||||
if (this.projectBaseDir) return this.projectBaseDir;
|
* Always returns the internal project directory (in userData).
|
||||||
|
* Tag metadata never lives in an external dataPath.
|
||||||
|
*/
|
||||||
|
private getInternalBaseDir(): string {
|
||||||
const userDataPath = app.getPath('userData');
|
const userDataPath = app.getPath('userData');
|
||||||
return path.join(userDataPath, 'projects', this.currentProjectId);
|
return path.join(userDataPath, 'projects', this.currentProjectId);
|
||||||
}
|
}
|
||||||
@@ -125,9 +127,8 @@ export class TagEngine extends EventEmitter {
|
|||||||
/**
|
/**
|
||||||
* Set the current project context
|
* Set the current project context
|
||||||
*/
|
*/
|
||||||
setProjectContext(projectId: string, baseDir?: string): void {
|
setProjectContext(projectId: string): void {
|
||||||
this.currentProjectId = projectId;
|
this.currentProjectId = projectId;
|
||||||
this.projectBaseDir = baseDir || null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -141,7 +142,7 @@ export class TagEngine extends EventEmitter {
|
|||||||
* Get the tags file path for filesystem persistence
|
* Get the tags file path for filesystem persistence
|
||||||
*/
|
*/
|
||||||
private getTagsFilePath(): string {
|
private getTagsFilePath(): string {
|
||||||
return path.join(this.getProjectBaseDir(), 'meta', 'tags-metadata.json');
|
return path.join(this.getInternalBaseDir(), 'meta', 'tags-metadata.json');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -11,54 +11,72 @@ import { taskManager, TaskProgress } from '../engine/TaskManager';
|
|||||||
import { getDatabase } from '../database';
|
import { getDatabase } from '../database';
|
||||||
import { media } from '../database/schema';
|
import { media } from '../database/schema';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrap an IPC handler so that "Database is closing" errors during shutdown
|
||||||
|
* are silently swallowed instead of being logged as scary red error messages.
|
||||||
|
*/
|
||||||
|
function safeHandle(channel: string, handler: (...args: any[]) => Promise<any>): void {
|
||||||
|
ipcMain.handle(channel, async (...args) => {
|
||||||
|
try {
|
||||||
|
return await handler(...args);
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error?.message === 'Database is closing') {
|
||||||
|
return null; // Silently ignore during shutdown
|
||||||
|
}
|
||||||
|
throw error; // Re-throw all other errors
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function registerIpcHandlers(): void {
|
export function registerIpcHandlers(): void {
|
||||||
// ============ Project Handlers ============
|
// ============ Project Handlers ============
|
||||||
|
|
||||||
ipcMain.handle('projects:create', async (_, data: { name: string; description?: string; slug?: string }) => {
|
safeHandle('projects:create', async (_, data: { name: string; description?: string; slug?: string }) => {
|
||||||
const engine = getProjectEngine();
|
const engine = getProjectEngine();
|
||||||
return engine.createProject(data);
|
return engine.createProject(data);
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('projects:update', async (_, id: string, data: Partial<ProjectData>) => {
|
safeHandle('projects:update', async (_, id: string, data: Partial<ProjectData>) => {
|
||||||
const engine = getProjectEngine();
|
const engine = getProjectEngine();
|
||||||
return engine.updateProject(id, data);
|
return engine.updateProject(id, data);
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('projects:delete', async (_, id: string) => {
|
safeHandle('projects:delete', async (_, id: string) => {
|
||||||
const engine = getProjectEngine();
|
const engine = getProjectEngine();
|
||||||
return engine.deleteProject(id);
|
return engine.deleteProject(id);
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('projects:deleteWithData', async (_, id: string) => {
|
safeHandle('projects:deleteWithData', async (_, id: string) => {
|
||||||
const engine = getProjectEngine();
|
const engine = getProjectEngine();
|
||||||
return engine.deleteProjectWithData(id);
|
return engine.deleteProjectWithData(id);
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('projects:get', async (_, id: string) => {
|
safeHandle('projects:get', async (_, id: string) => {
|
||||||
const engine = getProjectEngine();
|
const engine = getProjectEngine();
|
||||||
return engine.getProject(id);
|
return engine.getProject(id);
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('projects:getAll', async () => {
|
safeHandle('projects:getAll', async () => {
|
||||||
const engine = getProjectEngine();
|
const engine = getProjectEngine();
|
||||||
return engine.getAllProjects();
|
return engine.getAllProjects();
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('projects:getActive', async () => {
|
safeHandle('projects:getActive', async () => {
|
||||||
const projectEngine = getProjectEngine();
|
const projectEngine = getProjectEngine();
|
||||||
const project = await projectEngine.getActiveProject();
|
const project = await projectEngine.getActiveProject();
|
||||||
|
|
||||||
// Ensure all engines have the correct project context
|
// Ensure all engines have the correct project context
|
||||||
if (project) {
|
if (project) {
|
||||||
const baseDir = projectEngine.getProjectBaseDir(project.id, project.dataPath);
|
const internalDir = projectEngine.getInternalBaseDir(project.id);
|
||||||
|
const dataDir = projectEngine.getDataDir(project.id, project.dataPath);
|
||||||
const postEngine = getPostEngine();
|
const postEngine = getPostEngine();
|
||||||
const mediaEngine = getMediaEngine();
|
const mediaEngine = getMediaEngine();
|
||||||
const metaEngine = getMetaEngine();
|
const metaEngine = getMetaEngine();
|
||||||
const tagEngine = getTagEngine();
|
const tagEngine = getTagEngine();
|
||||||
postEngine.setProjectContext(project.id, baseDir);
|
postEngine.setProjectContext(project.id, dataDir);
|
||||||
mediaEngine.setProjectContext(project.id, baseDir);
|
mediaEngine.setProjectContext(project.id, dataDir, internalDir);
|
||||||
metaEngine.setProjectContext(project.id, baseDir);
|
metaEngine.setProjectContext(project.id);
|
||||||
tagEngine.setProjectContext(project.id, baseDir);
|
tagEngine.setProjectContext(project.id);
|
||||||
|
|
||||||
// Sync meta on startup
|
// Sync meta on startup
|
||||||
await metaEngine.syncOnStartup();
|
await metaEngine.syncOnStartup();
|
||||||
@@ -67,21 +85,22 @@ export function registerIpcHandlers(): void {
|
|||||||
return project;
|
return project;
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('projects:setActive', async (_, id: string) => {
|
safeHandle('projects:setActive', async (_, id: string) => {
|
||||||
const projectEngine = getProjectEngine();
|
const projectEngine = getProjectEngine();
|
||||||
const project = await projectEngine.setActiveProject(id);
|
const project = await projectEngine.setActiveProject(id);
|
||||||
|
|
||||||
// Update all engines to use the new project context
|
// Update all engines to use the new project context
|
||||||
if (project) {
|
if (project) {
|
||||||
const baseDir = projectEngine.getProjectBaseDir(project.id, project.dataPath);
|
const internalDir = projectEngine.getInternalBaseDir(project.id);
|
||||||
|
const dataDir = projectEngine.getDataDir(project.id, project.dataPath);
|
||||||
const postEngine = getPostEngine();
|
const postEngine = getPostEngine();
|
||||||
const mediaEngine = getMediaEngine();
|
const mediaEngine = getMediaEngine();
|
||||||
const metaEngine = getMetaEngine();
|
const metaEngine = getMetaEngine();
|
||||||
const tagEngine = getTagEngine();
|
const tagEngine = getTagEngine();
|
||||||
postEngine.setProjectContext(project.id, baseDir);
|
postEngine.setProjectContext(project.id, dataDir);
|
||||||
mediaEngine.setProjectContext(project.id, baseDir);
|
mediaEngine.setProjectContext(project.id, dataDir, internalDir);
|
||||||
metaEngine.setProjectContext(project.id, baseDir);
|
metaEngine.setProjectContext(project.id);
|
||||||
tagEngine.setProjectContext(project.id, baseDir);
|
tagEngine.setProjectContext(project.id);
|
||||||
|
|
||||||
// Sync meta on project switch
|
// Sync meta on project switch
|
||||||
await metaEngine.syncOnStartup();
|
await metaEngine.syncOnStartup();
|
||||||
@@ -92,68 +111,69 @@ export function registerIpcHandlers(): void {
|
|||||||
|
|
||||||
// ============ Post Handlers ============
|
// ============ Post Handlers ============
|
||||||
|
|
||||||
ipcMain.handle('posts:create', async (_, data: Partial<PostData>) => {
|
safeHandle('posts:create', async (_, data: Partial<PostData>) => {
|
||||||
const engine = getPostEngine();
|
const engine = getPostEngine();
|
||||||
return engine.createPost(data);
|
return engine.createPost(data);
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('posts:isSlugAvailable', async (_, slug: string, excludePostId?: string) => {
|
safeHandle('posts:isSlugAvailable', async (_, slug: string, excludePostId?: string) => {
|
||||||
const engine = getPostEngine();
|
const engine = getPostEngine();
|
||||||
return engine.isSlugAvailable(slug, excludePostId);
|
return engine.isSlugAvailable(slug, excludePostId);
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('posts:generateUniqueSlug', async (_, title: string, excludePostId?: string) => {
|
safeHandle('posts:generateUniqueSlug', async (_, title: string, excludePostId?: string) => {
|
||||||
const engine = getPostEngine();
|
const engine = getPostEngine();
|
||||||
return engine.generateUniqueSlug(title, excludePostId);
|
return engine.generateUniqueSlug(title, excludePostId);
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('posts:update', async (_, id: string, data: Partial<PostData>) => {
|
safeHandle('posts:update', async (_, id: string, data: Partial<PostData>) => {
|
||||||
const engine = getPostEngine();
|
const engine = getPostEngine();
|
||||||
return engine.updatePost(id, data);
|
return engine.updatePost(id, data);
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('posts:delete', async (_, id: string) => {
|
safeHandle('posts:delete', async (_, id: string) => {
|
||||||
const engine = getPostEngine();
|
const engine = getPostEngine();
|
||||||
return engine.deletePost(id);
|
return engine.deletePost(id);
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('posts:get', async (_, id: string) => {
|
safeHandle('posts:get', async (_, id: string) => {
|
||||||
const engine = getPostEngine();
|
const engine = getPostEngine();
|
||||||
return engine.getPost(id);
|
return engine.getPost(id);
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('posts:getAll', async (_, options?: PaginationOptions) => {
|
safeHandle('posts:getAll', async (_, options?: PaginationOptions) => {
|
||||||
const engine = getPostEngine();
|
const engine = getPostEngine();
|
||||||
return engine.getAllPosts(options);
|
return engine.getAllPosts(options);
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('posts:getByStatus', async (_, status: 'draft' | 'published' | 'archived') => {
|
safeHandle('posts:getByStatus', async (_, status: 'draft' | 'published' | 'archived') => {
|
||||||
const engine = getPostEngine();
|
const engine = getPostEngine();
|
||||||
return engine.getPostsByStatus(status);
|
return engine.getPostsByStatus(status);
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('posts:publish', async (_, id: string) => {
|
safeHandle('posts:publish', async (_, id: string) => {
|
||||||
const engine = getPostEngine();
|
const engine = getPostEngine();
|
||||||
return engine.publishPost(id);
|
return engine.publishPost(id);
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('posts:discard', async (_, id: string) => {
|
safeHandle('posts:discard', async (_, id: string) => {
|
||||||
const engine = getPostEngine();
|
const engine = getPostEngine();
|
||||||
return engine.discardChanges(id);
|
return engine.discardChanges(id);
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('posts:hasPublishedVersion', async (_, id: string) => {
|
safeHandle('posts:hasPublishedVersion', async (_, id: string) => {
|
||||||
const engine = getPostEngine();
|
const engine = getPostEngine();
|
||||||
return engine.hasPublishedVersion(id);
|
return engine.hasPublishedVersion(id);
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('posts:rebuildFromFiles', async () => {
|
safeHandle('posts:rebuildFromFiles', async () => {
|
||||||
// Ensure project context is current before rebuilding
|
// Ensure project context is current before rebuilding
|
||||||
const projectEngine = getProjectEngine();
|
const projectEngine = getProjectEngine();
|
||||||
const project = await projectEngine.getActiveProject();
|
const project = await projectEngine.getActiveProject();
|
||||||
const engine = getPostEngine();
|
const engine = getPostEngine();
|
||||||
if (project) {
|
if (project) {
|
||||||
engine.setProjectContext(project.id);
|
const dataDir = projectEngine.getDataDir(project.id, project.dataPath);
|
||||||
|
engine.setProjectContext(project.id, dataDir);
|
||||||
}
|
}
|
||||||
// Fire and forget - don't await, let it run in background
|
// Fire and forget - don't await, let it run in background
|
||||||
engine.rebuildDatabaseFromFiles().catch(err => {
|
engine.rebuildDatabaseFromFiles().catch(err => {
|
||||||
@@ -161,67 +181,68 @@ export function registerIpcHandlers(): void {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('posts:search', async (_, query: string) => {
|
safeHandle('posts:search', async (_, query: string) => {
|
||||||
const engine = getPostEngine();
|
const engine = getPostEngine();
|
||||||
return engine.searchPosts(query);
|
return engine.searchPosts(query);
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('posts:filter', async (_, filter: PostFilter) => {
|
safeHandle('posts:filter', async (_, filter: PostFilter) => {
|
||||||
const engine = getPostEngine();
|
const engine = getPostEngine();
|
||||||
return engine.getPostsFiltered(filter);
|
return engine.getPostsFiltered(filter);
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('posts:getTags', async () => {
|
safeHandle('posts:getTags', async () => {
|
||||||
const engine = getPostEngine();
|
const engine = getPostEngine();
|
||||||
return engine.getAvailableTags();
|
return engine.getAvailableTags();
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('posts:getCategories', async () => {
|
safeHandle('posts:getCategories', async () => {
|
||||||
const engine = getPostEngine();
|
const engine = getPostEngine();
|
||||||
return engine.getAvailableCategories();
|
return engine.getAvailableCategories();
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('posts:getByYearMonth', async () => {
|
safeHandle('posts:getByYearMonth', async () => {
|
||||||
const engine = getPostEngine();
|
const engine = getPostEngine();
|
||||||
return engine.getPostsByYearMonth();
|
return engine.getPostsByYearMonth();
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('posts:getTagsWithCounts', async () => {
|
safeHandle('posts:getTagsWithCounts', async () => {
|
||||||
const engine = getPostEngine();
|
const engine = getPostEngine();
|
||||||
return engine.getTagsWithCounts();
|
return engine.getTagsWithCounts();
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('posts:getCategoriesWithCounts', async () => {
|
safeHandle('posts:getCategoriesWithCounts', async () => {
|
||||||
const engine = getPostEngine();
|
const engine = getPostEngine();
|
||||||
return engine.getCategoriesWithCounts();
|
return engine.getCategoriesWithCounts();
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('posts:getDashboardStats', async () => {
|
safeHandle('posts:getDashboardStats', async () => {
|
||||||
const engine = getPostEngine();
|
const engine = getPostEngine();
|
||||||
return engine.getDashboardStats();
|
return engine.getDashboardStats();
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('posts:getLinksTo', async (_, id: string) => {
|
safeHandle('posts:getLinksTo', async (_, id: string) => {
|
||||||
const engine = getPostEngine();
|
const engine = getPostEngine();
|
||||||
return engine.getLinksTo(id);
|
return engine.getLinksTo(id);
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('posts:getLinkedBy', async (_, id: string) => {
|
safeHandle('posts:getLinkedBy', async (_, id: string) => {
|
||||||
const engine = getPostEngine();
|
const engine = getPostEngine();
|
||||||
return engine.getLinkedBy(id);
|
return engine.getLinkedBy(id);
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('posts:rebuildLinks', async () => {
|
safeHandle('posts:rebuildLinks', async () => {
|
||||||
const engine = getPostEngine();
|
const engine = getPostEngine();
|
||||||
return engine.rebuildAllPostLinks();
|
return engine.rebuildAllPostLinks();
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('posts:reindexText', async () => {
|
safeHandle('posts:reindexText', async () => {
|
||||||
const projectEngine = getProjectEngine();
|
const projectEngine = getProjectEngine();
|
||||||
const project = await projectEngine.getActiveProject();
|
const project = await projectEngine.getActiveProject();
|
||||||
const engine = getPostEngine();
|
const engine = getPostEngine();
|
||||||
if (project) {
|
if (project) {
|
||||||
engine.setProjectContext(project.id);
|
const dataDir = projectEngine.getDataDir(project.id, project.dataPath);
|
||||||
|
engine.setProjectContext(project.id, dataDir);
|
||||||
}
|
}
|
||||||
// Fire and forget - let it run as a background task
|
// Fire and forget - let it run as a background task
|
||||||
engine.reindexText().catch(err => {
|
engine.reindexText().catch(err => {
|
||||||
@@ -231,12 +252,12 @@ export function registerIpcHandlers(): void {
|
|||||||
|
|
||||||
// ============ Media Handlers ============
|
// ============ Media Handlers ============
|
||||||
|
|
||||||
ipcMain.handle('media:import', async (_, sourcePath: string, metadata?: Partial<MediaData>) => {
|
safeHandle('media:import', async (_, sourcePath: string, metadata?: Partial<MediaData>) => {
|
||||||
const engine = getMediaEngine();
|
const engine = getMediaEngine();
|
||||||
return engine.importMedia(sourcePath, metadata);
|
return engine.importMedia(sourcePath, metadata);
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('media:importDialog', async () => {
|
safeHandle('media:importDialog', async () => {
|
||||||
const result = await dialog.showOpenDialog({
|
const result = await dialog.showOpenDialog({
|
||||||
title: 'Import Media',
|
title: 'Import Media',
|
||||||
filters: [
|
filters: [
|
||||||
@@ -265,45 +286,47 @@ export function registerIpcHandlers(): void {
|
|||||||
return imported;
|
return imported;
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('media:update', async (_, id: string, data: Partial<MediaData>) => {
|
safeHandle('media:update', async (_, id: string, data: Partial<MediaData>) => {
|
||||||
const engine = getMediaEngine();
|
const engine = getMediaEngine();
|
||||||
return engine.updateMedia(id, data);
|
return engine.updateMedia(id, data);
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('media:delete', async (_, id: string) => {
|
safeHandle('media:delete', async (_, id: string) => {
|
||||||
const engine = getMediaEngine();
|
const engine = getMediaEngine();
|
||||||
return engine.deleteMedia(id);
|
return engine.deleteMedia(id);
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('media:get', async (_, id: string) => {
|
safeHandle('media:get', async (_, id: string) => {
|
||||||
const engine = getMediaEngine();
|
const engine = getMediaEngine();
|
||||||
return engine.getMedia(id);
|
return engine.getMedia(id);
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('media:getUrl', async (_, id: string) => {
|
safeHandle('media:getUrl', async (_, id: string) => {
|
||||||
// Returns the bds-media:// protocol URL for a media item
|
// Returns the bds-media:// protocol URL for a media item
|
||||||
return `bds-media://${id}`;
|
return `bds-media://${id}`;
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('media:getFilePath', async (_, id: string) => {
|
safeHandle('media:getFilePath', async (_, id: string) => {
|
||||||
// Returns the actual file path for a media item (for debugging/advanced use)
|
// Returns the actual file path for a media item (for debugging/advanced use)
|
||||||
const db = getDatabase().getLocal();
|
const db = getDatabase().getLocal();
|
||||||
const mediaItem = await db.select().from(media).where(eq(media.id, id)).get();
|
const mediaItem = await db.select().from(media).where(eq(media.id, id)).get();
|
||||||
return mediaItem?.filePath ?? null;
|
return mediaItem?.filePath ?? null;
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('media:getAll', async () => {
|
safeHandle('media:getAll', async () => {
|
||||||
const engine = getMediaEngine();
|
const engine = getMediaEngine();
|
||||||
return engine.getAllMedia();
|
return engine.getAllMedia();
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('media:rebuildFromFiles', async () => {
|
safeHandle('media:rebuildFromFiles', async () => {
|
||||||
// Ensure project context is current before rebuilding
|
// Ensure project context is current before rebuilding
|
||||||
const projectEngine = getProjectEngine();
|
const projectEngine = getProjectEngine();
|
||||||
const project = await projectEngine.getActiveProject();
|
const project = await projectEngine.getActiveProject();
|
||||||
const engine = getMediaEngine();
|
const engine = getMediaEngine();
|
||||||
if (project) {
|
if (project) {
|
||||||
engine.setProjectContext(project.id);
|
const internalDir = projectEngine.getInternalBaseDir(project.id);
|
||||||
|
const dataDir = projectEngine.getDataDir(project.id, project.dataPath);
|
||||||
|
engine.setProjectContext(project.id, dataDir, internalDir);
|
||||||
}
|
}
|
||||||
// Fire and forget - don't await, let it run in background
|
// Fire and forget - don't await, let it run in background
|
||||||
engine.rebuildDatabaseFromFiles().catch(err => {
|
engine.rebuildDatabaseFromFiles().catch(err => {
|
||||||
@@ -311,12 +334,12 @@ export function registerIpcHandlers(): void {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('media:getThumbnail', async (_, id: string, size?: 'small' | 'medium' | 'large') => {
|
safeHandle('media:getThumbnail', async (_, id: string, size?: 'small' | 'medium' | 'large') => {
|
||||||
const engine = getMediaEngine();
|
const engine = getMediaEngine();
|
||||||
return engine.getThumbnailDataUrl(id, size || 'small');
|
return engine.getThumbnailDataUrl(id, size || 'small');
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('media:regenerateThumbnails', async (_, id: string) => {
|
safeHandle('media:regenerateThumbnails', async (_, id: string) => {
|
||||||
const engine = getMediaEngine();
|
const engine = getMediaEngine();
|
||||||
const mediaItem = await engine.getMedia(id);
|
const mediaItem = await engine.getMedia(id);
|
||||||
if (mediaItem && mediaItem.mimeType.startsWith('image/')) {
|
if (mediaItem && mediaItem.mimeType.startsWith('image/')) {
|
||||||
@@ -329,56 +352,58 @@ export function registerIpcHandlers(): void {
|
|||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('media:regenerateMissingThumbnails', async () => {
|
safeHandle('media:regenerateMissingThumbnails', async () => {
|
||||||
const projectEngine = getProjectEngine();
|
const projectEngine = getProjectEngine();
|
||||||
const project = await projectEngine.getActiveProject();
|
const project = await projectEngine.getActiveProject();
|
||||||
const engine = getMediaEngine();
|
const engine = getMediaEngine();
|
||||||
if (project) {
|
if (project) {
|
||||||
engine.setProjectContext(project.id);
|
const internalDir = projectEngine.getInternalBaseDir(project.id);
|
||||||
|
const dataDir = projectEngine.getDataDir(project.id, project.dataPath);
|
||||||
|
engine.setProjectContext(project.id, dataDir, internalDir);
|
||||||
}
|
}
|
||||||
return engine.regenerateMissingThumbnails();
|
return engine.regenerateMissingThumbnails();
|
||||||
});
|
});
|
||||||
|
|
||||||
// ============ Sync Handlers ============
|
// ============ Sync Handlers ============
|
||||||
|
|
||||||
ipcMain.handle('sync:configure', async (_, config: SyncConfig) => {
|
safeHandle('sync:configure', async (_, config: SyncConfig) => {
|
||||||
const engine = getSyncEngine();
|
const engine = getSyncEngine();
|
||||||
return engine.configure(config);
|
return engine.configure(config);
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('sync:start', async (_, direction: SyncDirection = 'bidirectional') => {
|
safeHandle('sync:start', async (_, direction: SyncDirection = 'bidirectional') => {
|
||||||
const engine = getSyncEngine();
|
const engine = getSyncEngine();
|
||||||
return engine.fullSync(direction);
|
return engine.fullSync(direction);
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('sync:getStatus', async () => {
|
safeHandle('sync:getStatus', async () => {
|
||||||
const engine = getSyncEngine();
|
const engine = getSyncEngine();
|
||||||
return engine.getSyncStatus();
|
return engine.getSyncStatus();
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('sync:isConfigured', async () => {
|
safeHandle('sync:isConfigured', async () => {
|
||||||
const engine = getSyncEngine();
|
const engine = getSyncEngine();
|
||||||
return engine.isConfigured();
|
return engine.isConfigured();
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('sync:getPendingCount', async () => {
|
safeHandle('sync:getPendingCount', async () => {
|
||||||
const engine = getSyncEngine();
|
const engine = getSyncEngine();
|
||||||
return engine.getPendingChangesCount();
|
return engine.getPendingChangesCount();
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('sync:getLog', async (_, limit?: number) => {
|
safeHandle('sync:getLog', async (_, limit?: number) => {
|
||||||
const engine = getSyncEngine();
|
const engine = getSyncEngine();
|
||||||
return engine.getSyncLog(limit);
|
return engine.getSyncLog(limit);
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('sync:stopAutoSync', async () => {
|
safeHandle('sync:stopAutoSync', async () => {
|
||||||
const engine = getSyncEngine();
|
const engine = getSyncEngine();
|
||||||
return engine.stopAutoSync();
|
return engine.stopAutoSync();
|
||||||
});
|
});
|
||||||
|
|
||||||
// ============ Dropbox Sync Handlers ============
|
// ============ Dropbox Sync Handlers ============
|
||||||
|
|
||||||
ipcMain.handle('dropbox:configure', async (_, config: Partial<DropboxSyncConfig>) => {
|
safeHandle('dropbox:configure', async (_, config: Partial<DropboxSyncConfig>) => {
|
||||||
const engine = getDropboxSyncEngine();
|
const engine = getDropboxSyncEngine();
|
||||||
|
|
||||||
// Inject local project paths so the engine knows where files live
|
// Inject local project paths so the engine knows where files live
|
||||||
@@ -402,47 +427,47 @@ export function registerIpcHandlers(): void {
|
|||||||
return engine.configure(fullConfig);
|
return engine.configure(fullConfig);
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('dropbox:isConfigured', async () => {
|
safeHandle('dropbox:isConfigured', async () => {
|
||||||
const engine = getDropboxSyncEngine();
|
const engine = getDropboxSyncEngine();
|
||||||
return engine.isConfigured();
|
return engine.isConfigured();
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('dropbox:getStatus', async () => {
|
safeHandle('dropbox:getStatus', async () => {
|
||||||
const engine = getDropboxSyncEngine();
|
const engine = getDropboxSyncEngine();
|
||||||
return engine.getStatus();
|
return engine.getStatus();
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('dropbox:syncAll', async () => {
|
safeHandle('dropbox:syncAll', async () => {
|
||||||
const engine = getDropboxSyncEngine();
|
const engine = getDropboxSyncEngine();
|
||||||
return engine.syncAll();
|
return engine.syncAll();
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('dropbox:startWatching', async () => {
|
safeHandle('dropbox:startWatching', async () => {
|
||||||
const engine = getDropboxSyncEngine();
|
const engine = getDropboxSyncEngine();
|
||||||
engine.startWatching();
|
engine.startWatching();
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('dropbox:stopWatching', async () => {
|
safeHandle('dropbox:stopWatching', async () => {
|
||||||
const engine = getDropboxSyncEngine();
|
const engine = getDropboxSyncEngine();
|
||||||
engine.stopWatching();
|
engine.stopWatching();
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('dropbox:startPolling', async () => {
|
safeHandle('dropbox:startPolling', async () => {
|
||||||
const engine = getDropboxSyncEngine();
|
const engine = getDropboxSyncEngine();
|
||||||
engine.startPolling();
|
engine.startPolling();
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('dropbox:stopPolling', async () => {
|
safeHandle('dropbox:stopPolling', async () => {
|
||||||
const engine = getDropboxSyncEngine();
|
const engine = getDropboxSyncEngine();
|
||||||
engine.stopPolling();
|
engine.stopPolling();
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('dropbox:getConflicts', async () => {
|
safeHandle('dropbox:getConflicts', async () => {
|
||||||
const engine = getDropboxSyncEngine();
|
const engine = getDropboxSyncEngine();
|
||||||
return engine.getPendingConflicts();
|
return engine.getPendingConflicts();
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('dropbox:resolveConflict', async (_, conflictId: string, resolution: ConflictResolution) => {
|
safeHandle('dropbox:resolveConflict', async (_, conflictId: string, resolution: ConflictResolution) => {
|
||||||
const engine = getDropboxSyncEngine();
|
const engine = getDropboxSyncEngine();
|
||||||
const conflicts = engine.getPendingConflicts();
|
const conflicts = engine.getPendingConflicts();
|
||||||
const conflict = conflicts.find(c => c.id === conflictId);
|
const conflict = conflicts.find(c => c.id === conflictId);
|
||||||
@@ -452,32 +477,32 @@ export function registerIpcHandlers(): void {
|
|||||||
return engine.resolveConflict(conflict, resolution);
|
return engine.resolveConflict(conflict, resolution);
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('dropbox:getLastSyncTime', async () => {
|
safeHandle('dropbox:getLastSyncTime', async () => {
|
||||||
const engine = getDropboxSyncEngine();
|
const engine = getDropboxSyncEngine();
|
||||||
return engine.getLastSyncTime();
|
return engine.getLastSyncTime();
|
||||||
});
|
});
|
||||||
|
|
||||||
// ============ Task Handlers ============
|
// ============ Task Handlers ============
|
||||||
|
|
||||||
ipcMain.handle('tasks:getAll', async () => {
|
safeHandle('tasks:getAll', async () => {
|
||||||
return taskManager.getAllTasks();
|
return taskManager.getAllTasks();
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('tasks:getRunning', async () => {
|
safeHandle('tasks:getRunning', async () => {
|
||||||
return taskManager.getRunningTasks();
|
return taskManager.getRunningTasks();
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('tasks:cancel', async (_, taskId: string) => {
|
safeHandle('tasks:cancel', async (_, taskId: string) => {
|
||||||
return taskManager.cancelTask(taskId);
|
return taskManager.cancelTask(taskId);
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('tasks:clearCompleted', async () => {
|
safeHandle('tasks:clearCompleted', async () => {
|
||||||
return taskManager.clearCompletedTasks();
|
return taskManager.clearCompletedTasks();
|
||||||
});
|
});
|
||||||
|
|
||||||
// ============ App Handlers ============
|
// ============ App Handlers ============
|
||||||
|
|
||||||
ipcMain.handle('app:getDataPaths', async () => {
|
safeHandle('app:getDataPaths', async () => {
|
||||||
// Get paths for the active project
|
// Get paths for the active project
|
||||||
const projectEngine = getProjectEngine();
|
const projectEngine = getProjectEngine();
|
||||||
const activeProject = await projectEngine.getActiveProject();
|
const activeProject = await projectEngine.getActiveProject();
|
||||||
@@ -490,11 +515,11 @@ export function registerIpcHandlers(): void {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('app:openFolder', async (_, folderPath: string) => {
|
safeHandle('app:openFolder', async (_, folderPath: string) => {
|
||||||
return shell.openPath(folderPath);
|
return shell.openPath(folderPath);
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('app:selectFolder', async (_, title?: string) => {
|
safeHandle('app:selectFolder', async (_, title?: string) => {
|
||||||
const result = await dialog.showOpenDialog({
|
const result = await dialog.showOpenDialog({
|
||||||
title: title || 'Select Folder',
|
title: title || 'Select Folder',
|
||||||
properties: ['openDirectory', 'createDirectory'],
|
properties: ['openDirectory', 'createDirectory'],
|
||||||
@@ -505,52 +530,52 @@ export function registerIpcHandlers(): void {
|
|||||||
return result.filePaths[0];
|
return result.filePaths[0];
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('app:getDefaultProjectPath', async (_, projectId: string) => {
|
safeHandle('app:getDefaultProjectPath', async (_, projectId: string) => {
|
||||||
const projectEngine = getProjectEngine();
|
const projectEngine = getProjectEngine();
|
||||||
return projectEngine.getDefaultProjectBaseDir(projectId);
|
return projectEngine.getDefaultProjectBaseDir(projectId);
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('app:showItemInFolder', async (_, itemPath: string) => {
|
safeHandle('app:showItemInFolder', async (_, itemPath: string) => {
|
||||||
return shell.showItemInFolder(itemPath);
|
return shell.showItemInFolder(itemPath);
|
||||||
});
|
});
|
||||||
|
|
||||||
// ============ Meta Handlers ============
|
// ============ Meta Handlers ============
|
||||||
|
|
||||||
ipcMain.handle('meta:getTags', async () => {
|
safeHandle('meta:getTags', async () => {
|
||||||
const engine = getMetaEngine();
|
const engine = getMetaEngine();
|
||||||
return engine.getTags();
|
return engine.getTags();
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('meta:getCategories', async () => {
|
safeHandle('meta:getCategories', async () => {
|
||||||
const engine = getMetaEngine();
|
const engine = getMetaEngine();
|
||||||
return engine.getCategories();
|
return engine.getCategories();
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('meta:addTag', async (_, tag: string) => {
|
safeHandle('meta:addTag', async (_, tag: string) => {
|
||||||
const engine = getMetaEngine();
|
const engine = getMetaEngine();
|
||||||
await engine.addTag(tag);
|
await engine.addTag(tag);
|
||||||
return engine.getTags();
|
return engine.getTags();
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('meta:removeTag', async (_, tag: string) => {
|
safeHandle('meta:removeTag', async (_, tag: string) => {
|
||||||
const engine = getMetaEngine();
|
const engine = getMetaEngine();
|
||||||
await engine.removeTag(tag);
|
await engine.removeTag(tag);
|
||||||
return engine.getTags();
|
return engine.getTags();
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('meta:addCategory', async (_, category: string) => {
|
safeHandle('meta:addCategory', async (_, category: string) => {
|
||||||
const engine = getMetaEngine();
|
const engine = getMetaEngine();
|
||||||
await engine.addCategory(category);
|
await engine.addCategory(category);
|
||||||
return engine.getCategories();
|
return engine.getCategories();
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('meta:removeCategory', async (_, category: string) => {
|
safeHandle('meta:removeCategory', async (_, category: string) => {
|
||||||
const engine = getMetaEngine();
|
const engine = getMetaEngine();
|
||||||
await engine.removeCategory(category);
|
await engine.removeCategory(category);
|
||||||
return engine.getCategories();
|
return engine.getCategories();
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('meta:syncOnStartup', async () => {
|
safeHandle('meta:syncOnStartup', async () => {
|
||||||
const engine = getMetaEngine();
|
const engine = getMetaEngine();
|
||||||
await engine.syncOnStartup();
|
await engine.syncOnStartup();
|
||||||
return {
|
return {
|
||||||
@@ -560,18 +585,18 @@ export function registerIpcHandlers(): void {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('meta:getProjectMetadata', async () => {
|
safeHandle('meta:getProjectMetadata', async () => {
|
||||||
const engine = getMetaEngine();
|
const engine = getMetaEngine();
|
||||||
return engine.getProjectMetadata();
|
return engine.getProjectMetadata();
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('meta:setProjectMetadata', async (_, metadata: { name: string; description?: string }) => {
|
safeHandle('meta:setProjectMetadata', async (_, metadata: { name: string; description?: string }) => {
|
||||||
const engine = getMetaEngine();
|
const engine = getMetaEngine();
|
||||||
await engine.setProjectMetadata(metadata);
|
await engine.setProjectMetadata(metadata);
|
||||||
return engine.getProjectMetadata();
|
return engine.getProjectMetadata();
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('meta:updateProjectMetadata', async (_, updates: { name?: string; description?: string; dataPath?: string }) => {
|
safeHandle('meta:updateProjectMetadata', async (_, updates: { name?: string; description?: string; dataPath?: string }) => {
|
||||||
const engine = getMetaEngine();
|
const engine = getMetaEngine();
|
||||||
await engine.updateProjectMetadata(updates);
|
await engine.updateProjectMetadata(updates);
|
||||||
return engine.getProjectMetadata();
|
return engine.getProjectMetadata();
|
||||||
@@ -579,57 +604,57 @@ export function registerIpcHandlers(): void {
|
|||||||
|
|
||||||
// ============ Tag Management Handlers ============
|
// ============ Tag Management Handlers ============
|
||||||
|
|
||||||
ipcMain.handle('tags:getAll', async () => {
|
safeHandle('tags:getAll', async () => {
|
||||||
const engine = getTagEngine();
|
const engine = getTagEngine();
|
||||||
return engine.getAllTags();
|
return engine.getAllTags();
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('tags:getWithCounts', async () => {
|
safeHandle('tags:getWithCounts', async () => {
|
||||||
const engine = getTagEngine();
|
const engine = getTagEngine();
|
||||||
return engine.getTagsWithCounts();
|
return engine.getTagsWithCounts();
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('tags:get', async (_, id: string) => {
|
safeHandle('tags:get', async (_, id: string) => {
|
||||||
const engine = getTagEngine();
|
const engine = getTagEngine();
|
||||||
return engine.getTag(id);
|
return engine.getTag(id);
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('tags:getByName', async (_, name: string) => {
|
safeHandle('tags:getByName', async (_, name: string) => {
|
||||||
const engine = getTagEngine();
|
const engine = getTagEngine();
|
||||||
return engine.getTagByName(name);
|
return engine.getTagByName(name);
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('tags:create', async (_, data: { name: string; color?: string }) => {
|
safeHandle('tags:create', async (_, data: { name: string; color?: string }) => {
|
||||||
const engine = getTagEngine();
|
const engine = getTagEngine();
|
||||||
return engine.createTag(data);
|
return engine.createTag(data);
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('tags:update', async (_, id: string, data: { name?: string; color?: string | null }) => {
|
safeHandle('tags:update', async (_, id: string, data: { name?: string; color?: string | null }) => {
|
||||||
const engine = getTagEngine();
|
const engine = getTagEngine();
|
||||||
return engine.updateTag(id, data);
|
return engine.updateTag(id, data);
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('tags:delete', async (_, id: string) => {
|
safeHandle('tags:delete', async (_, id: string) => {
|
||||||
const engine = getTagEngine();
|
const engine = getTagEngine();
|
||||||
return engine.deleteTag(id);
|
return engine.deleteTag(id);
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('tags:merge', async (_, sourceTagIds: string[], targetTagId: string) => {
|
safeHandle('tags:merge', async (_, sourceTagIds: string[], targetTagId: string) => {
|
||||||
const engine = getTagEngine();
|
const engine = getTagEngine();
|
||||||
return engine.mergeTags(sourceTagIds, targetTagId);
|
return engine.mergeTags(sourceTagIds, targetTagId);
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('tags:rename', async (_, id: string, newName: string) => {
|
safeHandle('tags:rename', async (_, id: string, newName: string) => {
|
||||||
const engine = getTagEngine();
|
const engine = getTagEngine();
|
||||||
return engine.renameTag(id, newName);
|
return engine.renameTag(id, newName);
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('tags:getPostsWithTag', async (_, tagId: string) => {
|
safeHandle('tags:getPostsWithTag', async (_, tagId: string) => {
|
||||||
const engine = getTagEngine();
|
const engine = getTagEngine();
|
||||||
return engine.getPostsWithTag(tagId);
|
return engine.getPostsWithTag(tagId);
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('tags:syncFromPosts', async () => {
|
safeHandle('tags:syncFromPosts', async () => {
|
||||||
const engine = getTagEngine();
|
const engine = getTagEngine();
|
||||||
return engine.syncTagsFromPosts();
|
return engine.syncTagsFromPosts();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -381,14 +381,20 @@ async function initialize(): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (mediaItem && mediaItem.filePath) {
|
if (mediaItem && mediaItem.filePath) {
|
||||||
|
// Check if file exists before attempting to fetch
|
||||||
|
if (!fs.existsSync(mediaItem.filePath)) {
|
||||||
|
console.error(`[bds-media] File not found at path: ${mediaItem.filePath} (media id: ${mediaIdentifier})`);
|
||||||
|
return new Response(`Media file not found at: ${mediaItem.filePath}`, { status: 404 });
|
||||||
|
}
|
||||||
// Use net.fetch to get the file - this handles the file protocol properly
|
// Use net.fetch to get the file - this handles the file protocol properly
|
||||||
return net.fetch(`file://${mediaItem.filePath}`);
|
return net.fetch(`file://${mediaItem.filePath}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return a 404 response if media not found
|
// Return a 404 response if media not found
|
||||||
return new Response('Media not found', { status: 404 });
|
console.error(`[bds-media] No media record found for identifier: ${mediaIdentifier}`);
|
||||||
|
return new Response('Media not found in database', { status: 404 });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error serving media:', error);
|
console.error('[bds-media] Error serving media:', error);
|
||||||
return new Response('Internal server error', { status: 500 });
|
return new Response('Internal server error', { status: 500 });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -404,7 +410,13 @@ async function initialize(): Promise<void> {
|
|||||||
const thumbnails = await engine.getThumbnailPaths(mediaId);
|
const thumbnails = await engine.getThumbnailPaths(mediaId);
|
||||||
|
|
||||||
if (thumbnails.small) {
|
if (thumbnails.small) {
|
||||||
return net.fetch(`file://${thumbnails.small}`);
|
// Check if thumbnail file exists
|
||||||
|
if (!fs.existsSync(thumbnails.small)) {
|
||||||
|
console.error(`[bds-thumb] Thumbnail not found at path: ${thumbnails.small} (media id: ${mediaId})`);
|
||||||
|
// Fall through to try full image
|
||||||
|
} else {
|
||||||
|
return net.fetch(`file://${thumbnails.small}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback to full image if thumbnail doesn't exist
|
// Fallback to full image if thumbnail doesn't exist
|
||||||
@@ -416,12 +428,18 @@ async function initialize(): Promise<void> {
|
|||||||
.get();
|
.get();
|
||||||
|
|
||||||
if (mediaItem && mediaItem.filePath) {
|
if (mediaItem && mediaItem.filePath) {
|
||||||
|
// Check if file exists before attempting to fetch
|
||||||
|
if (!fs.existsSync(mediaItem.filePath)) {
|
||||||
|
console.error(`[bds-thumb] Media file not found at path: ${mediaItem.filePath} (media id: ${mediaId})`);
|
||||||
|
return new Response(`Media file not found at: ${mediaItem.filePath}`, { status: 404 });
|
||||||
|
}
|
||||||
return net.fetch(`file://${mediaItem.filePath}`);
|
return net.fetch(`file://${mediaItem.filePath}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.error(`[bds-thumb] No media record found for id: ${mediaId}`);
|
||||||
return new Response('Thumbnail not found', { status: 404 });
|
return new Response('Thumbnail not found', { status: 404 });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error serving thumbnail:', error);
|
console.error('[bds-thumb] Error serving thumbnail:', error);
|
||||||
return new Response('Internal server error', { status: 500 });
|
return new Response('Internal server error', { status: 500 });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -77,9 +77,9 @@ export const ChatPanel: React.FC<ChatPanelProps> = ({ conversationId }) => {
|
|||||||
const unsubToolCall = window.electronAPI?.chat.onToolCall((data) => {
|
const unsubToolCall = window.electronAPI?.chat.onToolCall((data) => {
|
||||||
console.log('[ChatPanel] Tool call received:', data);
|
console.log('[ChatPanel] Tool call received:', data);
|
||||||
if (data.conversationId === conversationId) {
|
if (data.conversationId === conversationId) {
|
||||||
const toolCall = data.toolCall as { name: string; args: unknown };
|
const toolCall = data.toolCall as { name: string; arguments: Record<string, unknown> };
|
||||||
toolEventsRef.current.push({ name: toolCall.name, args: toolCall.args });
|
toolEventsRef.current.push({ name: toolCall.name, args: toolCall.arguments });
|
||||||
setToolEvents(prev => [...prev, { type: 'call', name: toolCall.name, args: toolCall.args, timestamp: Date.now() }]);
|
setToolEvents(prev => [...prev, { type: 'call', name: toolCall.name, args: toolCall.arguments, timestamp: Date.now() }]);
|
||||||
scrollToBottom();
|
scrollToBottom();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user