From 85d196e5980fa6eb69749735993ed4dcca7520b4 Mon Sep 17 00:00:00 2001 From: hugo Date: Thu, 12 Feb 2026 15:00:37 +0100 Subject: [PATCH] feat: added dataPath for projects --- src/main/database/connection.ts | 8 +++ src/main/database/schema.ts | 1 + src/main/engine/MediaEngine.ts | 16 ++++-- src/main/engine/MetaEngine.ts | 35 +++++++++--- src/main/engine/PostEngine.ts | 14 ++++- src/main/engine/ProjectEngine.ts | 55 ++++++++++++++++--- src/main/engine/TagEngine.ts | 13 ++++- src/main/ipc/handlers.ts | 52 ++++++++++++------ src/main/main.ts | 43 +++++++++++++++ src/main/preload.ts | 6 +- .../components/SettingsView/SettingsView.tsx | 53 +++++++++++++++++- src/renderer/components/Sidebar/Sidebar.tsx | 2 +- src/renderer/index.html | 2 +- src/renderer/store/appStore.ts | 6 +- src/renderer/types/electron.d.ts | 6 +- 15 files changed, 263 insertions(+), 49 deletions(-) diff --git a/src/main/database/connection.ts b/src/main/database/connection.ts index 6006cd3..8818913 100644 --- a/src/main/database/connection.ts +++ b/src/main/database/connection.ts @@ -401,6 +401,14 @@ export class DatabaseConnection { console.log('Tags table created successfully'); } + // Migration: Add data_path column to projects table + const dataPathCol = await this.localClient.execute( + "SELECT name FROM pragma_table_info('projects') WHERE name = 'data_path'" + ); + if (dataPathCol.rows.length === 0) { + await this.localClient.execute("ALTER TABLE projects ADD COLUMN data_path TEXT"); + } + // Create default project if none exists const existingProjects = await this.localClient.execute('SELECT COUNT(*) as count FROM projects'); if (existingProjects.rows[0] && (existingProjects.rows[0].count as number) === 0) { diff --git a/src/main/database/schema.ts b/src/main/database/schema.ts index 39e4ad9..2ec8b73 100644 --- a/src/main/database/schema.ts +++ b/src/main/database/schema.ts @@ -6,6 +6,7 @@ export const projects = sqliteTable('projects', { name: text('name').notNull(), slug: text('slug').notNull().unique(), description: text('description'), + dataPath: text('data_path'), // Custom path for project data (null = default userData/projects/{id}) createdAt: integer('created_at', { mode: 'timestamp' }).notNull(), updatedAt: integer('updated_at', { mode: 'timestamp' }).notNull(), isActive: integer('is_active', { mode: 'boolean' }).notNull().default(false), diff --git a/src/main/engine/MediaEngine.ts b/src/main/engine/MediaEngine.ts index 30b371c..7fbb436 100644 --- a/src/main/engine/MediaEngine.ts +++ b/src/main/engine/MediaEngine.ts @@ -49,14 +49,20 @@ export interface MediaMetadata { export class MediaEngine extends EventEmitter { private currentProjectId: string = 'default'; + private projectBaseDir: string | null = null; constructor() { super(); } - private getMediaBaseDir(): string { + private getProjectBaseDir(): string { + if (this.projectBaseDir) return this.projectBaseDir; const userDataPath = app.getPath('userData'); - return path.join(userDataPath, 'projects', this.currentProjectId, 'media'); + return path.join(userDataPath, 'projects', this.currentProjectId); + } + + private getMediaBaseDir(): string { + return path.join(this.getProjectBaseDir(), 'media'); } private getMediaDir(): string { @@ -85,8 +91,9 @@ export class MediaEngine extends EventEmitter { return path.join(dir, `${id}${extension}`); } - setProjectContext(projectId: string): void { + setProjectContext(projectId: string, baseDir?: string): void { this.currentProjectId = projectId; + this.projectBaseDir = baseDir || null; } getProjectContext(): string { @@ -101,8 +108,7 @@ export class MediaEngine extends EventEmitter { * Get the thumbnails directory for the current project */ private getThumbnailsDir(): string { - const userDataPath = app.getPath('userData'); - return path.join(userDataPath, 'projects', this.currentProjectId, 'thumbnails'); + return path.join(this.getProjectBaseDir(), 'thumbnails'); } /** diff --git a/src/main/engine/MetaEngine.ts b/src/main/engine/MetaEngine.ts index ccb1d66..3335445 100644 --- a/src/main/engine/MetaEngine.ts +++ b/src/main/engine/MetaEngine.ts @@ -12,6 +12,7 @@ import { posts, projects } from '../database/schema'; export interface ProjectMetadata { name: string; description?: string; + dataPath?: string; // Custom path for project data } /** @@ -31,6 +32,7 @@ export const DEFAULT_CATEGORIES = ['article', 'picture', 'aside', 'page']; */ export class MetaEngine extends EventEmitter { private currentProjectId: string = 'default'; + private projectBaseDir: string | null = null; private tags: Set = new Set(); private categories: Set = new Set(); private projectMetadata: ProjectMetadata | null = null; @@ -40,13 +42,18 @@ export class MetaEngine extends EventEmitter { super(); } + private getProjectBaseDir(): string { + if (this.projectBaseDir) return this.projectBaseDir; + const userDataPath = app.getPath('userData'); + return path.join(userDataPath, 'projects', this.currentProjectId); + } + /** * Get the meta directory path for the current project. - * Format: {userData}/projects/{projectId}/meta/ + * Format: {baseDir}/meta/ */ getMetaDir(): string { - const userDataPath = app.getPath('userData'); - return path.join(userDataPath, 'projects', this.currentProjectId, 'meta'); + return path.join(this.getProjectBaseDir(), 'meta'); } private getTagsFilePath(): string { @@ -61,8 +68,9 @@ export class MetaEngine extends EventEmitter { return path.join(this.getMetaDir(), 'project.json'); } - setProjectContext(projectId: string): void { + setProjectContext(projectId: string, baseDir?: string): void { this.currentProjectId = projectId; + this.projectBaseDir = baseDir || null; // Reset in-memory cache when project changes this.tags.clear(); this.categories.clear(); @@ -334,14 +342,14 @@ export class MetaEngine extends EventEmitter { /** * Fetch the current project's data from the database. */ - private async fetchProjectFromDatabase(): Promise<{ name: string; description: string | null } | null> { + private async fetchProjectFromDatabase(): Promise<{ name: string; description: string | null; dataPath: string | null } | null> { const db = getDatabase().getLocal(); const project = await db - .select({ name: projects.name, description: projects.description }) + .select({ name: projects.name, description: projects.description, dataPath: projects.dataPath }) .from(projects) .where(eq(projects.id, this.currentProjectId)) .get(); - + return project || null; } @@ -459,6 +467,18 @@ export class MetaEngine extends EventEmitter { // Handle project metadata if (projectMetadataFileExists) { await this.loadProjectMetadata(); + + // If project.json has a dataPath, sync it back to the database + if (this.projectMetadata?.dataPath !== undefined) { + const projectData = await this.fetchProjectFromDatabase(); + if (projectData && projectData.dataPath !== this.projectMetadata.dataPath) { + const db = getDatabase().getLocal(); + await db.update(projects) + .set({ dataPath: this.projectMetadata.dataPath || null }) + .where(eq(projects.id, this.currentProjectId)); + console.log(`[MetaEngine] Synced dataPath from project.json to database: ${this.projectMetadata.dataPath || '(default)'}`); + } + } } else { // No file exists, fetch project data from database and create file const projectData = await this.fetchProjectFromDatabase(); @@ -468,6 +488,7 @@ export class MetaEngine extends EventEmitter { this.projectMetadata = { name: projectData.name, description: projectData.description || undefined, + dataPath: projectData.dataPath || undefined, }; await this.saveProjectMetadata(); } diff --git a/src/main/engine/PostEngine.ts b/src/main/engine/PostEngine.ts index 4dc6a96..48d48fb 100644 --- a/src/main/engine/PostEngine.ts +++ b/src/main/engine/PostEngine.ts @@ -142,9 +142,16 @@ export class PostEngine extends EventEmitter { await client.execute({ sql: 'DELETE FROM posts_fts WHERE id = ?', args: [id] }); } - private getPostsBaseDir(): string { + private projectBaseDir: string | null = null; + + private getProjectBaseDir(): string { + if (this.projectBaseDir) return this.projectBaseDir; const userDataPath = app.getPath('userData'); - return path.join(userDataPath, 'projects', this.currentProjectId, 'posts'); + return path.join(userDataPath, 'projects', this.currentProjectId); + } + + private getPostsBaseDir(): string { + return path.join(this.getProjectBaseDir(), 'posts'); } private getPostsDir(): string { @@ -172,8 +179,9 @@ export class PostEngine extends EventEmitter { return path.join(dir, `${slug}.md`); } - setProjectContext(projectId: string): void { + setProjectContext(projectId: string, baseDir?: string): void { this.currentProjectId = projectId; + this.projectBaseDir = baseDir || null; } getProjectContext(): string { diff --git a/src/main/engine/ProjectEngine.ts b/src/main/engine/ProjectEngine.ts index d977dc1..f6e5e3a 100644 --- a/src/main/engine/ProjectEngine.ts +++ b/src/main/engine/ProjectEngine.ts @@ -12,6 +12,7 @@ export interface ProjectData { name: string; slug: string; description?: string; + dataPath?: string; // Custom path for project data (undefined = default) createdAt: Date; updatedAt: Date; isActive: boolean; @@ -29,18 +30,41 @@ export class ProjectEngine extends EventEmitter { .replace(/^-|-$/g, ''); } - private async ensureProjectDirectories(projectId: string): Promise { + /** + * Get the base directory for a project's data. + * If the project has a custom dataPath, use that; otherwise use the default. + */ + getProjectBaseDir(projectId: string, dataPath?: string | null): string { + if (dataPath) { + return dataPath; + } const userDataPath = app.getPath('userData'); - const projectDir = path.join(userDataPath, 'projects', projectId); + return path.join(userDataPath, 'projects', projectId); + } + + /** + * Get the default base directory (in userData) for a project. + */ + getDefaultProjectBaseDir(projectId: string): string { + const userDataPath = app.getPath('userData'); + return path.join(userDataPath, 'projects', projectId); + } + + private async ensureProjectDirectories(projectId: string, dataPath?: string | null): Promise { + const projectDir = this.getProjectBaseDir(projectId, dataPath); const postsDir = path.join(projectDir, 'posts'); const mediaDir = path.join(projectDir, 'media'); + const thumbnailsDir = path.join(projectDir, 'thumbnails'); + const metaDir = path.join(projectDir, 'meta'); await fs.mkdir(projectDir, { recursive: true }); await fs.mkdir(postsDir, { recursive: true }); await fs.mkdir(mediaDir, { recursive: true }); + await fs.mkdir(thumbnailsDir, { recursive: true }); + await fs.mkdir(metaDir, { recursive: true }); } - async createProject(data: { name: string; description?: string; slug?: string }): Promise { + async createProject(data: { name: string; description?: string; slug?: string; dataPath?: string }): Promise { const db = getDatabase().getLocal(); const now = new Date(); const id = uuidv4(); @@ -61,13 +85,14 @@ export class ProjectEngine extends EventEmitter { name: data.name, slug: finalSlug, description: data.description, + dataPath: data.dataPath, createdAt: now, updatedAt: now, isActive: false, }; // Create directories using project ID (not slug) - await this.ensureProjectDirectories(id); + await this.ensureProjectDirectories(id, data.dataPath); // Insert into database const dbProject: NewProject = { @@ -75,6 +100,7 @@ export class ProjectEngine extends EventEmitter { name: project.name, slug: project.slug, description: project.description, + dataPath: project.dataPath, createdAt: project.createdAt, updatedAt: project.updatedAt, isActive: project.isActive, @@ -106,6 +132,7 @@ export class ProjectEngine extends EventEmitter { name: updated.name, slug: updated.slug, description: updated.description, + dataPath: updated.dataPath, updatedAt: updated.updatedAt, isActive: updated.isActive, }) @@ -116,6 +143,7 @@ export class ProjectEngine extends EventEmitter { name: updated.name, slug: updated.slug, description: updated.description || undefined, + dataPath: updated.dataPath || undefined, createdAt: updated.createdAt, updatedAt: updated.updatedAt, isActive: updated.isActive ?? false, @@ -196,6 +224,7 @@ export class ProjectEngine extends EventEmitter { name: dbProject.name, slug: dbProject.slug, description: dbProject.description || undefined, + dataPath: dbProject.dataPath || undefined, createdAt: dbProject.createdAt, updatedAt: dbProject.updatedAt, isActive: dbProject.isActive ?? false, @@ -211,6 +240,7 @@ export class ProjectEngine extends EventEmitter { name: p.name, slug: p.slug, description: p.description || undefined, + dataPath: p.dataPath || undefined, createdAt: p.createdAt, updatedAt: p.updatedAt, isActive: p.isActive ?? false, @@ -231,6 +261,7 @@ export class ProjectEngine extends EventEmitter { name: dbProject.name, slug: dbProject.slug, description: dbProject.description || undefined, + dataPath: dbProject.dataPath || undefined, createdAt: dbProject.createdAt, updatedAt: dbProject.updatedAt, isActive: dbProject.isActive ?? false, @@ -255,13 +286,21 @@ export class ProjectEngine extends EventEmitter { return project; } - getProjectPaths(projectId: string): { posts: string; media: string } { - const userDataPath = app.getPath('userData'); + getProjectPaths(projectId: string, dataPath?: string | null): { posts: string; media: string } { + const baseDir = this.getProjectBaseDir(projectId, dataPath); return { - posts: path.join(userDataPath, 'projects', projectId, 'posts'), - media: path.join(userDataPath, 'projects', projectId, 'media'), + posts: path.join(baseDir, 'posts'), + media: path.join(baseDir, 'media'), }; } + + /** + * Get project paths by looking up the project's dataPath from the database. + */ + async getProjectPathsResolved(projectId: string): Promise<{ posts: string; media: string }> { + const project = await this.getProject(projectId); + return this.getProjectPaths(projectId, project?.dataPath); + } } // Singleton instance diff --git a/src/main/engine/TagEngine.ts b/src/main/engine/TagEngine.ts index 37c74a2..a719dbf 100644 --- a/src/main/engine/TagEngine.ts +++ b/src/main/engine/TagEngine.ts @@ -110,16 +110,24 @@ function isValidHexColor(color: string): boolean { */ export class TagEngine extends EventEmitter { private currentProjectId: string = 'default'; + private projectBaseDir: string | null = null; constructor() { super(); } + private getProjectBaseDir(): string { + if (this.projectBaseDir) return this.projectBaseDir; + const userDataPath = app.getPath('userData'); + return path.join(userDataPath, 'projects', this.currentProjectId); + } + /** * Set the current project context */ - setProjectContext(projectId: string): void { + setProjectContext(projectId: string, baseDir?: string): void { this.currentProjectId = projectId; + this.projectBaseDir = baseDir || null; } /** @@ -133,8 +141,7 @@ export class TagEngine extends EventEmitter { * Get the tags file path for filesystem persistence */ private getTagsFilePath(): string { - const userDataPath = app.getPath('userData'); - return path.join(userDataPath, 'projects', this.currentProjectId, 'meta', 'tags-metadata.json'); + return path.join(this.getProjectBaseDir(), 'meta', 'tags-metadata.json'); } /** diff --git a/src/main/ipc/handlers.ts b/src/main/ipc/handlers.ts index ea32814..3e49fbe 100644 --- a/src/main/ipc/handlers.ts +++ b/src/main/ipc/handlers.ts @@ -47,44 +47,46 @@ export function registerIpcHandlers(): void { ipcMain.handle('projects:getActive', async () => { const projectEngine = getProjectEngine(); const project = await projectEngine.getActiveProject(); - + // Ensure all engines have the correct project context if (project) { + const baseDir = projectEngine.getProjectBaseDir(project.id, project.dataPath); const postEngine = getPostEngine(); const mediaEngine = getMediaEngine(); const metaEngine = getMetaEngine(); const tagEngine = getTagEngine(); - postEngine.setProjectContext(project.id); - mediaEngine.setProjectContext(project.id); - metaEngine.setProjectContext(project.id); - tagEngine.setProjectContext(project.id); - + postEngine.setProjectContext(project.id, baseDir); + mediaEngine.setProjectContext(project.id, baseDir); + metaEngine.setProjectContext(project.id, baseDir); + tagEngine.setProjectContext(project.id, baseDir); + // Sync meta on startup await metaEngine.syncOnStartup(); } - + return project; }); ipcMain.handle('projects:setActive', async (_, id: string) => { const projectEngine = getProjectEngine(); const project = await projectEngine.setActiveProject(id); - + // Update all engines to use the new project context if (project) { + const baseDir = projectEngine.getProjectBaseDir(project.id, project.dataPath); const postEngine = getPostEngine(); const mediaEngine = getMediaEngine(); const metaEngine = getMetaEngine(); const tagEngine = getTagEngine(); - postEngine.setProjectContext(project.id); - mediaEngine.setProjectContext(project.id); - metaEngine.setProjectContext(project.id); - tagEngine.setProjectContext(project.id); - + postEngine.setProjectContext(project.id, baseDir); + mediaEngine.setProjectContext(project.id, baseDir); + metaEngine.setProjectContext(project.id, baseDir); + tagEngine.setProjectContext(project.id, baseDir); + // Sync meta on project switch await metaEngine.syncOnStartup(); } - + return project; }); @@ -383,7 +385,7 @@ export function registerIpcHandlers(): void { const projectEngine = getProjectEngine(); const activeProject = await projectEngine.getActiveProject(); const projectId = activeProject?.id || 'default'; - const paths = projectEngine.getProjectPaths(projectId); + const paths = projectEngine.getProjectPaths(projectId, activeProject?.dataPath); const fullConfig: DropboxSyncConfig = { accessToken: config.accessToken, @@ -480,7 +482,7 @@ export function registerIpcHandlers(): void { const projectEngine = getProjectEngine(); const activeProject = await projectEngine.getActiveProject(); const projectId = activeProject?.id || 'default'; - const paths = projectEngine.getProjectPaths(projectId); + const paths = projectEngine.getProjectPaths(projectId, activeProject?.dataPath); return { database: getDatabase().getDataPaths().database, posts: paths.posts, @@ -492,6 +494,22 @@ export function registerIpcHandlers(): void { return shell.openPath(folderPath); }); + ipcMain.handle('app:selectFolder', async (_, title?: string) => { + const result = await dialog.showOpenDialog({ + title: title || 'Select Folder', + properties: ['openDirectory', 'createDirectory'], + }); + if (result.canceled || result.filePaths.length === 0) { + return null; + } + return result.filePaths[0]; + }); + + ipcMain.handle('app:getDefaultProjectPath', async (_, projectId: string) => { + const projectEngine = getProjectEngine(); + return projectEngine.getDefaultProjectBaseDir(projectId); + }); + ipcMain.handle('app:showItemInFolder', async (_, itemPath: string) => { return shell.showItemInFolder(itemPath); }); @@ -553,7 +571,7 @@ export function registerIpcHandlers(): void { return engine.getProjectMetadata(); }); - ipcMain.handle('meta:updateProjectMetadata', async (_, updates: { name?: string; description?: string }) => { + ipcMain.handle('meta:updateProjectMetadata', async (_, updates: { name?: string; description?: string; dataPath?: string }) => { const engine = getMetaEngine(); await engine.updateProjectMetadata(updates); return engine.getProjectMetadata(); diff --git a/src/main/main.ts b/src/main/main.ts index 4233e6d..450ecf3 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -5,6 +5,7 @@ import { getDatabase } from './database'; import { registerIpcHandlers, registerChatHandlers, initializeChatHandlers, cleanupChatHandlers } from './ipc'; import { media } from './database/schema'; import { eq } from 'drizzle-orm'; +import { getMediaEngine } from './engine/MediaEngine'; let mainWindow: BrowserWindow | null = null; @@ -22,6 +23,15 @@ protocol.registerSchemesAsPrivileged([ corsEnabled: true, }, }, + { + scheme: 'bds-thumb', + privileges: { + standard: true, + secure: true, + supportFetchAPI: true, + corsEnabled: true, + }, + }, ]); function createWindow(): void { @@ -383,6 +393,39 @@ async function initialize(): Promise { } }); + // Register custom protocol for serving thumbnail images + // URLs like bds-thumb://media-id will serve the small thumbnail webp + protocol.handle('bds-thumb', async (request) => { + try { + const url = new URL(request.url); + const mediaId = url.hostname; + + const engine = getMediaEngine(); + const thumbnails = await engine.getThumbnailPaths(mediaId); + + if (thumbnails.small) { + return net.fetch(`file://${thumbnails.small}`); + } + + // Fallback to full image if thumbnail doesn't exist + const database = getDatabase().getLocal(); + const mediaItem = await database + .select() + .from(media) + .where(eq(media.id, mediaId)) + .get(); + + if (mediaItem && mediaItem.filePath) { + return net.fetch(`file://${mediaItem.filePath}`); + } + + return new Response('Thumbnail not found', { status: 404 }); + } catch (error) { + console.error('Error serving thumbnail:', error); + return new Response('Internal server error', { status: 500 }); + } + }); + // Register IPC handlers registerIpcHandlers(); diff --git a/src/main/preload.ts b/src/main/preload.ts index a85ca85..570044e 100644 --- a/src/main/preload.ts +++ b/src/main/preload.ts @@ -99,6 +99,8 @@ contextBridge.exposeInMainWorld('electronAPI', { getDataPaths: () => ipcRenderer.invoke('app:getDataPaths'), openFolder: (folderPath: string) => ipcRenderer.invoke('app:openFolder', folderPath), showItemInFolder: (itemPath: string) => ipcRenderer.invoke('app:showItemInFolder', itemPath), + selectFolder: (title?: string) => ipcRenderer.invoke('app:selectFolder', title), + getDefaultProjectPath: (projectId: string) => ipcRenderer.invoke('app:getDefaultProjectPath', projectId), }, // Meta (tags, categories, and project metadata) @@ -112,7 +114,7 @@ contextBridge.exposeInMainWorld('electronAPI', { syncOnStartup: () => ipcRenderer.invoke('meta:syncOnStartup'), getProjectMetadata: () => ipcRenderer.invoke('meta:getProjectMetadata'), setProjectMetadata: (metadata: { name: string; description?: string }) => ipcRenderer.invoke('meta:setProjectMetadata', metadata), - updateProjectMetadata: (updates: { name?: string; description?: string }) => ipcRenderer.invoke('meta:updateProjectMetadata', updates), + updateProjectMetadata: (updates: { name?: string; description?: string; dataPath?: string }) => ipcRenderer.invoke('meta:updateProjectMetadata', updates), }, // Tag Management (advanced tag operations) @@ -267,6 +269,8 @@ export interface ElectronAPI { getDataPaths: () => Promise<{ database: string; posts: string; media: string }>; openFolder: (folderPath: string) => Promise; showItemInFolder: (itemPath: string) => Promise; + selectFolder: (title?: string) => Promise; + getDefaultProjectPath: (projectId: string) => Promise; }; meta: { getTags: () => Promise; diff --git a/src/renderer/components/SettingsView/SettingsView.tsx b/src/renderer/components/SettingsView/SettingsView.tsx index 1f8f699..b00ffe4 100644 --- a/src/renderer/components/SettingsView/SettingsView.tsx +++ b/src/renderer/components/SettingsView/SettingsView.tsx @@ -108,6 +108,8 @@ export const SettingsView: React.FC = () => { // Project settings const [projectName, setProjectName] = useState(''); const [projectDescription, setProjectDescription] = useState(''); + const [projectDataPath, setProjectDataPath] = useState(''); + const [defaultProjectPath, setDefaultProjectPath] = useState(''); // Post categories management const [postCategories, setPostCategories] = useState(DEFAULT_POST_CATEGORIES); @@ -135,6 +137,12 @@ export const SettingsView: React.FC = () => { if (activeProject) { setProjectName(activeProject.name); setProjectDescription(activeProject.description || ''); + setProjectDataPath(activeProject.dataPath || ''); + + // Load the default path for reference + window.electronAPI?.app.getDefaultProjectPath(activeProject.id).then(path => { + setDefaultProjectPath(path); + }); } }, [activeProject]); @@ -285,10 +293,18 @@ export const SettingsView: React.FC = () => { const updated = await window.electronAPI?.projects.update(activeProject.id, { name: projectName.trim() || activeProject.name, description: projectDescription.trim(), + dataPath: projectDataPath.trim() || undefined, }); if (updated) { setActiveProject(updated as any); useAppStore.getState().updateProject(activeProject.id, updated as any); + + // Also update project.json to keep dataPath in sync + await window.electronAPI?.meta.updateProjectMetadata({ + name: projectName.trim() || activeProject.name, + description: projectDescription.trim(), + dataPath: projectDataPath.trim() || undefined, + } as any); } showToast.success('Project settings saved'); } catch (error) { @@ -297,8 +313,19 @@ export const SettingsView: React.FC = () => { } }; + const handleBrowseDataPath = async () => { + const selected = await window.electronAPI?.app.selectFolder('Select Project Data Folder'); + if (selected) { + setProjectDataPath(selected); + } + }; + + const handleResetDataPath = () => { + setProjectDataPath(''); + }; + // Keywords for each section for search filtering - const projectKeywords = ['project', 'name', 'description', 'blog', 'site']; + const projectKeywords = ['project', 'name', 'description', 'blog', 'site', 'path', 'folder', 'location', 'data']; const editorKeywords = ['editor', 'mode', 'wysiwyg', 'markdown', 'preview', 'visual']; const contentKeywords = ['content', 'categories', 'post', 'article', 'picture', 'aside', 'page']; const aiKeywords = ['ai', 'assistant', 'chat', 'model', 'prompt', 'system', 'api', 'key', 'claude', 'gpt', 'opencode']; @@ -341,6 +368,30 @@ export const SettingsView: React.FC = () => { /> + +
+ setProjectDataPath(e.target.value)} + /> + + {projectDataPath && ( + + )} +
+
+