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 = () => ( ); // 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 }) => (

{description}

{children}
); // 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 (

{title}

{description &&

{description}

}
{children}
); }; export const SettingsView: React.FC = () => { const { preferredEditorMode, setPreferredEditorMode, activeProject, setActiveProject } = useAppStore(); const [searchQuery, setSearchQuery] = useState(''); const [credentials, setCredentials] = useState(defaultCredentials); const [showSecrets, setShowSecrets] = useState(false); const [dropboxConfigured, setDropboxConfigured] = useState(false); const [dropboxLastSync, setDropboxLastSync] = useState(null); const contentRef = useRef(null); // Project settings const [projectName, setProjectName] = useState(''); const [projectDescription, setProjectDescription] = useState(''); // Post categories management const [postCategories, setPostCategories] = useState(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 = () => (