803 lines
26 KiB
TypeScript
803 lines
26 KiB
TypeScript
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
|
import { useAppStore } from '../../store';
|
|
import { showToast } from '../Toast';
|
|
import './SettingsView.css';
|
|
|
|
// Export category IDs for sidebar navigation
|
|
export type SettingsCategory = 'project' | 'editor' | 'content' | 'sync' | 'publishing' | 'data';
|
|
|
|
// Scroll to a settings section by category ID
|
|
export const scrollToSettingsSection = (category: SettingsCategory) => {
|
|
const element = document.getElementById(`settings-section-${category}`);
|
|
if (element) {
|
|
element.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
}
|
|
};
|
|
|
|
// Settings categories
|
|
|
|
interface Credentials {
|
|
// Dropbox File Sync
|
|
dropboxAccessToken: string;
|
|
dropboxAppKey: string;
|
|
dropboxRemotePath: string;
|
|
// FTP Publishing
|
|
ftpHost: string;
|
|
ftpUser: string;
|
|
ftpPassword: string;
|
|
// SSH Publishing
|
|
sshHost: string;
|
|
sshUser: string;
|
|
sshKeyPath: string;
|
|
}
|
|
|
|
const defaultCredentials: Credentials = {
|
|
dropboxAccessToken: '',
|
|
dropboxAppKey: '',
|
|
dropboxRemotePath: '/blog',
|
|
ftpHost: '',
|
|
ftpUser: '',
|
|
ftpPassword: '',
|
|
sshHost: '',
|
|
sshUser: '',
|
|
sshKeyPath: '',
|
|
};
|
|
|
|
// Search icon for the search bar
|
|
const SearchIcon = () => (
|
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
|
|
<path d="M15.25 0a.75.75 0 0 1 .53.22.75.75 0 0 1 0 1.06l-3.25 3.25A6.5 6.5 0 1 1 11.47 3.47l3.25-3.25A.75.75 0 0 1 15.25 0zM6.5 12a5.5 5.5 0 1 0 0-11 5.5 5.5 0 0 0 0 11z"/>
|
|
</svg>
|
|
);
|
|
|
|
// Default post categories based on VISION.md
|
|
const DEFAULT_POST_CATEGORIES = ['article', 'picture', 'aside', 'page'];
|
|
|
|
// Standard categories that cannot be deleted
|
|
const PROTECTED_CATEGORIES = ['article', 'aside', 'page', 'picture'];
|
|
|
|
// Individual setting row component (VS Code style)
|
|
const SettingRow: React.FC<{
|
|
id: string;
|
|
label: string;
|
|
description: string;
|
|
children: React.ReactNode;
|
|
}> = ({ id, label, description, children }) => (
|
|
<div className="setting-row" id={`setting-${id}`}>
|
|
<div className="setting-info">
|
|
<label className="setting-label" htmlFor={id}>{label}</label>
|
|
<p className="setting-description">{description}</p>
|
|
</div>
|
|
<div className="setting-control">
|
|
{children}
|
|
</div>
|
|
</div>
|
|
);
|
|
|
|
// Section header component with optional ID for scrolling
|
|
const SettingSection: React.FC<{
|
|
id?: string;
|
|
title: string;
|
|
description?: string;
|
|
children: React.ReactNode;
|
|
hidden?: boolean;
|
|
}> = ({ id, title, description, children, hidden }) => {
|
|
if (hidden) return null;
|
|
return (
|
|
<div className="setting-section" id={id}>
|
|
<div className="setting-section-header">
|
|
<h3>{title}</h3>
|
|
{description && <p className="setting-section-description">{description}</p>}
|
|
</div>
|
|
<div className="setting-section-content">
|
|
{children}
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export const SettingsView: React.FC = () => {
|
|
const { preferredEditorMode, setPreferredEditorMode, activeProject, setActiveProject } = useAppStore();
|
|
const [searchQuery, setSearchQuery] = useState('');
|
|
const [credentials, setCredentials] = useState<Credentials>(defaultCredentials);
|
|
const [showSecrets, setShowSecrets] = useState(false);
|
|
const [dropboxConfigured, setDropboxConfigured] = useState(false);
|
|
const [dropboxLastSync, setDropboxLastSync] = useState<string | null>(null);
|
|
const contentRef = useRef<HTMLDivElement>(null);
|
|
|
|
// Project settings
|
|
const [projectName, setProjectName] = useState('');
|
|
const [projectDescription, setProjectDescription] = useState('');
|
|
|
|
// Post categories management
|
|
const [postCategories, setPostCategories] = useState<string[]>(DEFAULT_POST_CATEGORIES);
|
|
const [newCategoryInput, setNewCategoryInput] = useState('');
|
|
|
|
// Check if a section has any matching settings
|
|
const sectionHasMatches = useCallback((sectionKeywords: string[]) => {
|
|
if (!searchQuery) return true;
|
|
return sectionKeywords.some(keyword =>
|
|
keyword.toLowerCase().includes(searchQuery.toLowerCase())
|
|
);
|
|
}, [searchQuery]);
|
|
|
|
// Sync project fields from active project
|
|
useEffect(() => {
|
|
if (activeProject) {
|
|
setProjectName(activeProject.name);
|
|
setProjectDescription(activeProject.description || '');
|
|
}
|
|
}, [activeProject]);
|
|
|
|
// Load saved credentials and categories
|
|
useEffect(() => {
|
|
const loadSettings = async () => {
|
|
try {
|
|
const savedCreds = localStorage.getItem('bds-credentials');
|
|
if (savedCreds) {
|
|
setCredentials({ ...defaultCredentials, ...JSON.parse(savedCreds) });
|
|
}
|
|
|
|
// Load saved post categories
|
|
const savedCategories = localStorage.getItem('bds-categories');
|
|
if (savedCategories) {
|
|
const categories = JSON.parse(savedCategories);
|
|
if (Array.isArray(categories) && categories.length > 0) {
|
|
setPostCategories(categories);
|
|
}
|
|
}
|
|
|
|
// Check Dropbox status
|
|
const dbxConfigured = await window.electronAPI?.dropbox?.isConfigured();
|
|
setDropboxConfigured(dbxConfigured || false);
|
|
|
|
if (dbxConfigured) {
|
|
const lastSync = await window.electronAPI?.dropbox?.getLastSyncTime();
|
|
setDropboxLastSync(lastSync || null);
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to load settings:', error);
|
|
}
|
|
};
|
|
loadSettings();
|
|
}, []);
|
|
|
|
const handleSaveDropbox = async () => {
|
|
try {
|
|
localStorage.setItem('bds-credentials', JSON.stringify(credentials));
|
|
|
|
if (credentials.dropboxAccessToken && credentials.dropboxAppKey) {
|
|
await window.electronAPI?.dropbox?.configure({
|
|
accessToken: credentials.dropboxAccessToken,
|
|
appKey: credentials.dropboxAppKey,
|
|
remotePath: credentials.dropboxRemotePath || '/blog',
|
|
});
|
|
setDropboxConfigured(true);
|
|
showToast.success('Dropbox sync configured');
|
|
} else {
|
|
showToast.success('Credentials saved');
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to save Dropbox credentials:', error);
|
|
showToast.error('Failed to configure Dropbox sync');
|
|
}
|
|
};
|
|
|
|
const handleSavePublishing = async () => {
|
|
try {
|
|
localStorage.setItem('bds-credentials', JSON.stringify(credentials));
|
|
showToast.success('Publishing credentials saved');
|
|
} catch (error) {
|
|
console.error('Failed to save publishing credentials:', error);
|
|
showToast.error('Failed to save credentials');
|
|
}
|
|
};
|
|
|
|
const handleClearCredentials = (type: 'dropbox' | 'ftp' | 'ssh') => {
|
|
const newCreds = { ...credentials };
|
|
switch (type) {
|
|
case 'dropbox':
|
|
newCreds.dropboxAccessToken = '';
|
|
newCreds.dropboxAppKey = '';
|
|
newCreds.dropboxRemotePath = '/blog';
|
|
break;
|
|
case 'ftp':
|
|
newCreds.ftpHost = '';
|
|
newCreds.ftpUser = '';
|
|
newCreds.ftpPassword = '';
|
|
break;
|
|
case 'ssh':
|
|
newCreds.sshHost = '';
|
|
newCreds.sshUser = '';
|
|
newCreds.sshKeyPath = '';
|
|
break;
|
|
}
|
|
setCredentials(newCreds);
|
|
localStorage.setItem('bds-credentials', JSON.stringify(newCreds));
|
|
showToast.success(`${type.charAt(0).toUpperCase() + type.slice(1)} credentials cleared`);
|
|
};
|
|
|
|
const handleDropboxSync = async () => {
|
|
try {
|
|
showToast.loading('Starting Dropbox sync...');
|
|
await window.electronAPI?.dropbox?.syncAll();
|
|
showToast.dismiss();
|
|
showToast.success('Dropbox sync completed');
|
|
const lastSync = await window.electronAPI?.dropbox?.getLastSyncTime();
|
|
setDropboxLastSync(lastSync || null);
|
|
} catch (error) {
|
|
showToast.dismiss();
|
|
showToast.error('Dropbox sync failed');
|
|
}
|
|
};
|
|
|
|
const handleTestDropboxConnection = async () => {
|
|
showToast.loading('Testing Dropbox connection...');
|
|
try {
|
|
const status = await window.electronAPI?.dropbox?.getStatus();
|
|
showToast.dismiss();
|
|
if (status) {
|
|
showToast.success('Dropbox connection active');
|
|
} else {
|
|
showToast.error('Dropbox connection failed');
|
|
}
|
|
} catch {
|
|
showToast.dismiss();
|
|
showToast.error('Dropbox connection failed');
|
|
}
|
|
};
|
|
|
|
// Save project settings
|
|
const handleSaveProject = async () => {
|
|
if (!activeProject) return;
|
|
try {
|
|
const updated = await window.electronAPI?.projects.update(activeProject.id, {
|
|
name: projectName.trim() || activeProject.name,
|
|
description: projectDescription.trim(),
|
|
});
|
|
if (updated) {
|
|
setActiveProject(updated as any);
|
|
useAppStore.getState().updateProject(activeProject.id, updated as any);
|
|
}
|
|
showToast.success('Project settings saved');
|
|
} catch (error) {
|
|
console.error('Failed to save project settings:', error);
|
|
showToast.error('Failed to save project settings');
|
|
}
|
|
};
|
|
|
|
// Keywords for each section for search filtering
|
|
const projectKeywords = ['project', 'name', 'description', 'blog', 'site'];
|
|
const editorKeywords = ['editor', 'mode', 'wysiwyg', 'markdown', 'preview', 'visual'];
|
|
const contentKeywords = ['content', 'categories', 'post', 'article', 'picture', 'aside', 'page'];
|
|
const syncKeywords = ['sync', 'dropbox', 'file', 'backup', 'token', 'remote'];
|
|
const publishingKeywords = ['publishing', 'ftp', 'ssh', 'deploy', 'server', 'host', 'upload'];
|
|
const dataKeywords = ['data', 'database', 'rebuild', 'maintenance', 'posts', 'media', 'links', 'folder', 'filesystem'];
|
|
|
|
const renderProjectSettings = () => (
|
|
<SettingSection
|
|
id="settings-section-project"
|
|
title="Project"
|
|
description="General settings for the active blog project."
|
|
hidden={!sectionHasMatches(projectKeywords)}
|
|
>
|
|
<SettingRow
|
|
id="project-name"
|
|
label="Project Name"
|
|
description="The display name of your blog project."
|
|
>
|
|
<input
|
|
id="project-name"
|
|
type="text"
|
|
placeholder="My Blog"
|
|
value={projectName}
|
|
onChange={(e) => setProjectName(e.target.value)}
|
|
/>
|
|
</SettingRow>
|
|
|
|
<SettingRow
|
|
id="project-description"
|
|
label="Description"
|
|
description="A short description of your blog. This can be used in templates and metadata."
|
|
>
|
|
<textarea
|
|
id="project-description"
|
|
placeholder="A blog about..."
|
|
value={projectDescription}
|
|
onChange={(e) => setProjectDescription(e.target.value)}
|
|
rows={3}
|
|
/>
|
|
</SettingRow>
|
|
|
|
<div className="setting-actions">
|
|
<button className="primary" onClick={handleSaveProject}>
|
|
Save Project Settings
|
|
</button>
|
|
</div>
|
|
</SettingSection>
|
|
);
|
|
|
|
const renderEditorSettings = () => (
|
|
<SettingSection
|
|
id="settings-section-editor"
|
|
title="Editor"
|
|
description="Configure the blog post editor behavior and appearance."
|
|
hidden={!sectionHasMatches(editorKeywords)}
|
|
>
|
|
<SettingRow
|
|
id="editor-mode"
|
|
label="Default Editor Mode"
|
|
description="Choose the default mode when opening posts. You can switch modes at any time using the editor toolbar."
|
|
>
|
|
<select
|
|
id="editor-mode"
|
|
value={preferredEditorMode}
|
|
onChange={(e) => setPreferredEditorMode(e.target.value as 'wysiwyg' | 'markdown' | 'preview')}
|
|
>
|
|
<option value="wysiwyg">WYSIWYG (Visual Editor)</option>
|
|
<option value="markdown">Markdown (Source)</option>
|
|
<option value="preview">Preview (Read-only)</option>
|
|
</select>
|
|
</SettingRow>
|
|
</SettingSection>
|
|
);
|
|
|
|
// Handlers for post categories management
|
|
const handleAddCategory = () => {
|
|
const trimmed = newCategoryInput.trim().toLowerCase();
|
|
if (trimmed && !postCategories.includes(trimmed)) {
|
|
const updated = [...postCategories, trimmed];
|
|
setPostCategories(updated);
|
|
localStorage.setItem('bds-categories', JSON.stringify(updated));
|
|
setNewCategoryInput('');
|
|
showToast.success(`Category "${trimmed}" added`);
|
|
} else if (postCategories.includes(trimmed)) {
|
|
showToast.error('Category already exists');
|
|
}
|
|
};
|
|
|
|
const handleRemoveCategory = (categoryToRemove: string) => {
|
|
if (PROTECTED_CATEGORIES.includes(categoryToRemove)) {
|
|
showToast.error(`Cannot delete standard category "${categoryToRemove}"`);
|
|
return;
|
|
}
|
|
if (postCategories.length <= 1) {
|
|
showToast.error('Must have at least one category');
|
|
return;
|
|
}
|
|
const updated = postCategories.filter(c => c !== categoryToRemove);
|
|
setPostCategories(updated);
|
|
localStorage.setItem('bds-categories', JSON.stringify(updated));
|
|
showToast.success(`Category "${categoryToRemove}" removed`);
|
|
};
|
|
|
|
const handleResetCategories = () => {
|
|
setPostCategories(DEFAULT_POST_CATEGORIES);
|
|
localStorage.setItem('bds-categories', JSON.stringify(DEFAULT_POST_CATEGORIES));
|
|
showToast.success('Categories reset to defaults');
|
|
};
|
|
|
|
const renderContentSettings = () => (
|
|
<SettingSection
|
|
id="settings-section-content"
|
|
title="Post Categories"
|
|
description="Manage the available categories for blog posts. Each post can have one category that determines its display template."
|
|
hidden={!sectionHasMatches(contentKeywords)}
|
|
>
|
|
<div className="categories-list">
|
|
{postCategories.map((cat) => {
|
|
const isProtected = PROTECTED_CATEGORIES.includes(cat);
|
|
return (
|
|
<div key={cat} className="category-item">
|
|
<span className="category-name">{cat}{isProtected && ' (standard)'}</span>
|
|
{!isProtected && (
|
|
<button
|
|
className="category-remove"
|
|
onClick={() => handleRemoveCategory(cat)}
|
|
title={`Remove "${cat}" category`}
|
|
>
|
|
✕
|
|
</button>
|
|
)}
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
|
|
<div className="category-add-form">
|
|
<input
|
|
type="text"
|
|
placeholder="New category name..."
|
|
value={newCategoryInput}
|
|
onChange={(e) => setNewCategoryInput(e.target.value)}
|
|
onKeyDown={(e) => {
|
|
if (e.key === 'Enter') {
|
|
e.preventDefault();
|
|
handleAddCategory();
|
|
}
|
|
}}
|
|
/>
|
|
<button className="primary" onClick={handleAddCategory}>
|
|
Add Category
|
|
</button>
|
|
</div>
|
|
|
|
<div className="setting-actions">
|
|
<button className="secondary" onClick={handleResetCategories}>
|
|
Reset to Defaults
|
|
</button>
|
|
</div>
|
|
</SettingSection>
|
|
);
|
|
|
|
const renderSyncSettings = () => (
|
|
<>
|
|
<SettingSection
|
|
id="settings-section-sync"
|
|
title="File Sync — Dropbox"
|
|
description="Synchronize your blog files (posts and media) to Dropbox for backup and cross-device access."
|
|
hidden={!sectionHasMatches(syncKeywords)}
|
|
>
|
|
<SettingRow
|
|
id="dropbox-token"
|
|
label="Access Token"
|
|
description="Your Dropbox API access token. Generate one from the Dropbox App Console."
|
|
>
|
|
<div className="setting-input-group">
|
|
<input
|
|
id="dropbox-token"
|
|
type={showSecrets ? 'text' : 'password'}
|
|
placeholder="Your Dropbox access token"
|
|
value={credentials.dropboxAccessToken}
|
|
onChange={(e) => setCredentials({ ...credentials, dropboxAccessToken: e.target.value })}
|
|
/>
|
|
<button
|
|
className="setting-toggle-visibility"
|
|
onClick={() => setShowSecrets(!showSecrets)}
|
|
title={showSecrets ? 'Hide secrets' : 'Show secrets'}
|
|
>
|
|
{showSecrets ? '🔒' : '👁'}
|
|
</button>
|
|
</div>
|
|
</SettingRow>
|
|
|
|
<SettingRow
|
|
id="dropbox-appkey"
|
|
label="App Key"
|
|
description="The App Key from your Dropbox developer application."
|
|
>
|
|
<input
|
|
id="dropbox-appkey"
|
|
type="text"
|
|
placeholder="Your Dropbox App Key"
|
|
value={credentials.dropboxAppKey}
|
|
onChange={(e) => setCredentials({ ...credentials, dropboxAppKey: e.target.value })}
|
|
/>
|
|
</SettingRow>
|
|
|
|
<SettingRow
|
|
id="dropbox-path"
|
|
label="Remote Path"
|
|
description="The folder path in Dropbox where blog files will be synced. Default: /blog"
|
|
>
|
|
<input
|
|
id="dropbox-path"
|
|
type="text"
|
|
placeholder="/blog"
|
|
value={credentials.dropboxRemotePath}
|
|
onChange={(e) => setCredentials({ ...credentials, dropboxRemotePath: e.target.value })}
|
|
/>
|
|
</SettingRow>
|
|
|
|
<div className="setting-actions">
|
|
<button className="primary" onClick={handleSaveDropbox}>
|
|
{dropboxConfigured ? 'Update Configuration' : 'Enable Dropbox Sync'}
|
|
</button>
|
|
<button className="secondary" onClick={handleTestDropboxConnection}>
|
|
Test Connection
|
|
</button>
|
|
{dropboxConfigured && (
|
|
<button className="secondary" onClick={handleDropboxSync}>
|
|
Sync Now
|
|
</button>
|
|
)}
|
|
<button className="secondary danger" onClick={() => handleClearCredentials('dropbox')}>
|
|
Clear
|
|
</button>
|
|
</div>
|
|
|
|
{dropboxConfigured && (
|
|
<div className="setting-status success">
|
|
<span className="status-icon">✓</span>
|
|
<span>
|
|
Dropbox sync is configured
|
|
{dropboxLastSync && (
|
|
<span className="status-detail"> · Last sync: {new Date(dropboxLastSync).toLocaleString()}</span>
|
|
)}
|
|
</span>
|
|
</div>
|
|
)}
|
|
</SettingSection>
|
|
</>
|
|
);
|
|
|
|
const renderPublishingSettings = () => (
|
|
<>
|
|
<SettingSection
|
|
id="settings-section-publishing"
|
|
title="FTP Publishing"
|
|
description="Configure FTP credentials for publishing your blog to a web server."
|
|
hidden={!sectionHasMatches(publishingKeywords)}
|
|
>
|
|
<SettingRow
|
|
id="ftp-host"
|
|
label="Host"
|
|
description="The FTP server hostname or IP address."
|
|
>
|
|
<input
|
|
id="ftp-host"
|
|
type="text"
|
|
placeholder="ftp.example.com"
|
|
value={credentials.ftpHost}
|
|
onChange={(e) => setCredentials({ ...credentials, ftpHost: e.target.value })}
|
|
/>
|
|
</SettingRow>
|
|
|
|
<SettingRow
|
|
id="ftp-user"
|
|
label="Username"
|
|
description="Your FTP account username."
|
|
>
|
|
<input
|
|
id="ftp-user"
|
|
type="text"
|
|
placeholder="ftp-user"
|
|
value={credentials.ftpUser}
|
|
onChange={(e) => setCredentials({ ...credentials, ftpUser: e.target.value })}
|
|
/>
|
|
</SettingRow>
|
|
|
|
<SettingRow
|
|
id="ftp-password"
|
|
label="Password"
|
|
description="Your FTP account password."
|
|
>
|
|
<input
|
|
id="ftp-password"
|
|
type={showSecrets ? 'text' : 'password'}
|
|
placeholder="Password"
|
|
value={credentials.ftpPassword}
|
|
onChange={(e) => setCredentials({ ...credentials, ftpPassword: e.target.value })}
|
|
/>
|
|
</SettingRow>
|
|
|
|
<div className="setting-actions">
|
|
<button className="primary" onClick={handleSavePublishing}>Save</button>
|
|
<button className="secondary danger" onClick={() => handleClearCredentials('ftp')}>Clear</button>
|
|
</div>
|
|
</SettingSection>
|
|
|
|
<SettingSection
|
|
title="SSH Publishing"
|
|
description="Configure SSH credentials for secure deployment to your server."
|
|
hidden={!sectionHasMatches(publishingKeywords)}
|
|
>
|
|
<SettingRow
|
|
id="ssh-host"
|
|
label="Host"
|
|
description="The SSH server hostname or IP address."
|
|
>
|
|
<input
|
|
id="ssh-host"
|
|
type="text"
|
|
placeholder="server.example.com"
|
|
value={credentials.sshHost}
|
|
onChange={(e) => setCredentials({ ...credentials, sshHost: e.target.value })}
|
|
/>
|
|
</SettingRow>
|
|
|
|
<SettingRow
|
|
id="ssh-user"
|
|
label="Username"
|
|
description="Your SSH account username."
|
|
>
|
|
<input
|
|
id="ssh-user"
|
|
type="text"
|
|
placeholder="ssh-user"
|
|
value={credentials.sshUser}
|
|
onChange={(e) => setCredentials({ ...credentials, sshUser: e.target.value })}
|
|
/>
|
|
</SettingRow>
|
|
|
|
<SettingRow
|
|
id="ssh-keypath"
|
|
label="SSH Key Path"
|
|
description="Path to your SSH private key file."
|
|
>
|
|
<input
|
|
id="ssh-keypath"
|
|
type="text"
|
|
placeholder="~/.ssh/id_rsa"
|
|
value={credentials.sshKeyPath}
|
|
onChange={(e) => setCredentials({ ...credentials, sshKeyPath: e.target.value })}
|
|
/>
|
|
</SettingRow>
|
|
|
|
<div className="setting-actions">
|
|
<button className="primary" onClick={handleSavePublishing}>Save</button>
|
|
<button className="secondary danger" onClick={() => handleClearCredentials('ssh')}>Clear</button>
|
|
</div>
|
|
</SettingSection>
|
|
</>
|
|
);
|
|
|
|
const renderDataSettings = () => (
|
|
<>
|
|
<SettingSection
|
|
id="settings-section-data"
|
|
title="Database Maintenance"
|
|
description="Rebuild the local database index from source files. Useful if post or media files were edited externally."
|
|
hidden={!sectionHasMatches(dataKeywords)}
|
|
>
|
|
<SettingRow
|
|
id="rebuild-posts"
|
|
label="Rebuild Posts Database"
|
|
description="Re-scan all post markdown files and rebuild the database index."
|
|
>
|
|
<button
|
|
className="secondary"
|
|
onClick={async () => {
|
|
showToast.loading('Rebuilding posts database...');
|
|
try {
|
|
await window.electronAPI?.posts.rebuildFromFiles();
|
|
const postsResult = await window.electronAPI?.posts.getAll({ limit: 500, offset: 0 });
|
|
if (postsResult) {
|
|
useAppStore.getState().setPosts(postsResult.items, postsResult.hasMore, postsResult.total);
|
|
}
|
|
showToast.dismiss();
|
|
showToast.success('Posts database rebuilt');
|
|
} catch {
|
|
showToast.dismiss();
|
|
showToast.error('Failed to rebuild posts database');
|
|
}
|
|
}}
|
|
>
|
|
Rebuild Posts
|
|
</button>
|
|
</SettingRow>
|
|
|
|
<SettingRow
|
|
id="rebuild-media"
|
|
label="Rebuild Media Database"
|
|
description="Re-scan all media files and sidecar metadata. Regenerates missing entries."
|
|
>
|
|
<button
|
|
className="secondary"
|
|
onClick={async () => {
|
|
showToast.loading('Rebuilding media database...');
|
|
try {
|
|
await window.electronAPI?.media.rebuildFromFiles();
|
|
const media = await window.electronAPI?.media.getAll();
|
|
if (media) {
|
|
useAppStore.getState().setMedia(media as any[]);
|
|
}
|
|
showToast.dismiss();
|
|
showToast.success('Media database rebuilt');
|
|
} catch {
|
|
showToast.dismiss();
|
|
showToast.error('Failed to rebuild media database');
|
|
}
|
|
}}
|
|
>
|
|
Rebuild Media
|
|
</button>
|
|
</SettingRow>
|
|
|
|
<SettingRow
|
|
id="rebuild-links"
|
|
label="Rebuild Post Links"
|
|
description="Re-scan all posts and rebuild the internal link graph between posts."
|
|
>
|
|
<button
|
|
className="secondary"
|
|
onClick={async () => {
|
|
showToast.loading('Rebuilding post links...');
|
|
try {
|
|
await window.electronAPI?.posts.rebuildLinks();
|
|
showToast.dismiss();
|
|
showToast.success('Post links rebuilt');
|
|
} catch {
|
|
showToast.dismiss();
|
|
showToast.error('Failed to rebuild post links');
|
|
}
|
|
}}
|
|
>
|
|
Rebuild Links
|
|
</button>
|
|
</SettingRow>
|
|
</SettingSection>
|
|
|
|
<SettingSection
|
|
title="File System"
|
|
description="Access project data files and directories."
|
|
hidden={!sectionHasMatches(dataKeywords)}
|
|
>
|
|
<SettingRow
|
|
id="open-data"
|
|
label="Open Data Folder"
|
|
description="Open the project data folder containing posts, media, and database files."
|
|
>
|
|
<button
|
|
className="secondary"
|
|
onClick={async () => {
|
|
const paths = await window.electronAPI?.app.getDataPaths();
|
|
if (paths) {
|
|
window.electronAPI?.app.openFolder(paths.posts);
|
|
}
|
|
}}
|
|
>
|
|
Open Folder
|
|
</button>
|
|
</SettingRow>
|
|
</SettingSection>
|
|
</>
|
|
);
|
|
|
|
// Check if any results match the search
|
|
const hasAnyMatches = !searchQuery ||
|
|
sectionHasMatches(projectKeywords) ||
|
|
sectionHasMatches(editorKeywords) ||
|
|
sectionHasMatches(contentKeywords) ||
|
|
sectionHasMatches(syncKeywords) ||
|
|
sectionHasMatches(publishingKeywords) ||
|
|
sectionHasMatches(dataKeywords);
|
|
|
|
return (
|
|
<div className="settings-view">
|
|
{/* Header with search */}
|
|
<div className="settings-header">
|
|
<h2>Settings</h2>
|
|
<div className="settings-search">
|
|
<span className="settings-search-icon"><SearchIcon /></span>
|
|
<input
|
|
type="text"
|
|
placeholder="Search settings..."
|
|
value={searchQuery}
|
|
onChange={(e) => setSearchQuery(e.target.value)}
|
|
/>
|
|
{searchQuery && (
|
|
<button
|
|
className="settings-search-clear"
|
|
onClick={() => setSearchQuery('')}
|
|
>
|
|
✕
|
|
</button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Settings content - all sections in scrollable list */}
|
|
<div className="settings-content" ref={contentRef}>
|
|
{hasAnyMatches ? (
|
|
<>
|
|
{renderProjectSettings()}
|
|
{renderEditorSettings()}
|
|
{renderContentSettings()}
|
|
{renderSyncSettings()}
|
|
{renderPublishingSettings()}
|
|
{renderDataSettings()}
|
|
</>
|
|
) : (
|
|
<div className="settings-no-results">
|
|
<p>No settings found matching "{searchQuery}"</p>
|
|
<button onClick={() => setSearchQuery('')}>Clear search</button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default SettingsView;
|