Skip to content

Commit 38666e0

Browse files
authored
Feat: Add Settings, Theme Persistence, and Download Management (#75)
* feat: setting page, dark theme and download path * docs: arc guide, component guide & developer workflow * fix: version update
1 parent a446052 commit 38666e0

16 files changed

Lines changed: 776 additions & 52 deletions

docs/ARCHITECTURE.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,29 @@ The main process runs Node.js and manages the application lifecycle. It's organi
5757
- `loopi:saveTree` → TreeStore: Save/update automation
5858
- `loopi:loadExample` → TreeStore: Load example from `docs/examples/`
5959
- `loopi:deleteTree` → TreeStore: Delete automation file from disk
60+
- `loopi:loadSettings` → SettingsStore: Load app settings
61+
- `loopi:saveSettings` → SettingsStore: Save app settings
62+
- `dialog:selectFolder` → Dialog: Open folder picker for download path
63+
64+
#### SettingsStore
65+
- **Purpose**: Persist application settings
66+
- **Storage**: `~/.config/loopi/settings.json`
67+
- **Manages**:
68+
- Theme preference (light, dark, system)
69+
- Notifications toggle
70+
- Download path configuration
71+
- **API**:
72+
- `loadSettings()`: Returns AppSettings with defaults if file doesn't exist
73+
- `saveSettings(settings)`: Persists to disk, triggers download handler update
74+
75+
#### DownloadManager
76+
- **Purpose**: Handle file downloads with configurable save path
77+
- **Features**:
78+
- Auto-creates download directory if it doesn't exist
79+
- Listens to `will-download` events on default session
80+
- Sets full file path (directory + filename) for each download
81+
- Falls back to system Downloads folder if custom path not set
82+
- **Integration**: Called on app startup and when settings change
6083

6184
### Renderer Process (`src/`)
6285

docs/COMPONENT_GUIDE.md

Lines changed: 200 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ Detailed guide for understanding and extending React components in Loopi.
66

77
```
88
App (src/app.tsx)
9-
├── Router → Dashboard | AutomationBuilder | Credentials
9+
├── Router → Dashboard | AutomationBuilder | Settings
1010
1111
├── Dashboard (src/components/Dashboard.tsx)
1212
│ ├── Tabs: "Your Automations" | "Examples"
@@ -19,34 +19,40 @@ App (src/app.tsx)
1919
│ └── Lists 7 example automations
2020
│ └── "Load Example" button for each
2121
22-
└── AutomationBuilder (src/components/AutomationBuilder.tsx)
23-
├── BuilderHeader
24-
│ ├── Title & Description
25-
│ ├── Run/Stop buttons
26-
│ └── Settings dialog
27-
28-
├── BuilderCanvas (ReactFlow)
29-
│ ├── AutomationNode (visual node)
30-
│ ├── AddStepPopup (step type picker)
31-
│ └── Edge connections
32-
33-
└── NodeDetails (right sidebar)
34-
├── NodeHeader (title + delete)
35-
36-
├── StepEditor (routes to specific step editor)
37-
│ ├── ClickStep (src/components/.../stepTypes/ClickStep.tsx)
38-
│ ├── TypeStep
39-
│ ├── ExtractStep
40-
│ ├── ApiCallStep
41-
│ ├── NavigateStep
42-
│ ├── SetVariableStep
43-
│ ├── ModifyVariableStep
44-
│ └── ... other steps
45-
46-
└── ConditionEditor (if conditional node)
47-
├── Element existence conditions
48-
├── Value comparison conditions
49-
└── Post-processing options
22+
├── AutomationBuilder (src/components/AutomationBuilder.tsx)
23+
│ ├── BuilderHeader
24+
│ │ ├── Title & Description
25+
│ │ ├── Run/Stop buttons
26+
│ │ └── Settings dialog
27+
│ │
28+
│ ├── BuilderCanvas (ReactFlow)
29+
│ │ ├── AutomationNode (visual node)
30+
│ │ ├── AddStepPopup (step type picker)
31+
│ │ └── Edge connections
32+
│ │
33+
│ └── NodeDetails (right sidebar)
34+
│ ├── NodeHeader (title + delete)
35+
│ │
36+
│ ├── StepEditor (routes to specific step editor)
37+
│ │ ├── ClickStep (src/components/.../stepTypes/ClickStep.tsx)
38+
│ │ ├── TypeStep
39+
│ │ ├── ExtractStep
40+
│ │ ├── ApiCallStep
41+
│ │ ├── NavigateStep
42+
│ │ ├── SetVariableStep
43+
│ │ ├── ModifyVariableStep
44+
│ │ └── ... other steps
45+
│ │
46+
│ └── ConditionEditor (if conditional node)
47+
│ ├── Element existence conditions
48+
│ ├── Value comparison conditions
49+
│ └── Post-processing options
50+
51+
└── Settings (src/components/Settings.tsx)
52+
├── Appearance (Theme selector: Light, Dark, System)
53+
├── Downloads (Download path configuration with folder picker)
54+
├── Notifications (Enable/Disable toggle)
55+
└── About (App info and GitHub link)
5056
```
5157

5258
### Key Components
@@ -422,6 +428,13 @@ Routes IPC messages to appropriate services.
422428
- `loopi:loadExample` → TreeStore.loadExample()
423429
- `loopi:deleteTree` → TreeStore.deleteAutomation()
424430

431+
**Settings Handlers:**
432+
- `loopi:loadSettings` → SettingsStore.loadSettings() - Retrieves saved app settings
433+
- `loopi:saveSettings` → SettingsStore.saveSettings() - Persists settings and re-setup download handler
434+
435+
**File Dialog Handlers:**
436+
- `dialog:selectFolder` → electron.dialog.showOpenDialog() - Opens native folder picker
437+
425438
**Type Definitions (src/types/globals.d.ts):**
426439
```typescript
427440
interface ElectronAPI {
@@ -432,6 +445,11 @@ interface ElectronAPI {
432445
loadExample: (fileName: string) => Promise<StoredAutomation>;
433446
delete: (automationId: string) => Promise<boolean>;
434447
};
448+
settings: {
449+
load: () => Promise<AppSettings>;
450+
save: (settings: AppSettings) => Promise<void>;
451+
};
452+
selectFolder: () => Promise<string | null>;
435453
}
436454

437455
declare global {
@@ -441,6 +459,159 @@ declare global {
441459
}
442460
```
443461

462+
### Settings Component (src/components/Settings.tsx)
463+
464+
App-wide preferences and configuration interface.
465+
466+
**Purpose:**
467+
Allows users to customize theme, manage download location, and configure notifications. All settings persist to disk via Electron storage and apply immediately without a save button.
468+
469+
**Features:**
470+
- **Theme Selection**: Light, Dark, or System preference
471+
- Stores selection in `~/.config/[AppName]/settings.json`
472+
- Applies theme by toggling `dark` class on document root
473+
- Respects system preference when "System" mode selected
474+
- No page flicker: theme loads synchronously from storage before render
475+
476+
- **Download Path Configuration**: Custom download location
477+
- Folder picker via native file dialog
478+
- Stores full path in settings
479+
- Auto-creates directory if doesn't exist
480+
- Used by DownloadManager when processing downloads
481+
482+
- **Notifications Toggle**: Enable/disable app notifications
483+
- Stores preference in settings
484+
- Can be extended for toast notification control
485+
486+
- **About Section**: App info and links
487+
- GitHub repository link
488+
- App version and description
489+
490+
**Implementation:**
491+
```typescript
492+
export function Settings() {
493+
const [settings, setSettings] = useState<AppSettings>({
494+
theme: "light",
495+
enableNotifications: true,
496+
});
497+
498+
// Load settings on mount
499+
useEffect(() => {
500+
window.electronAPI.settings.load().then(setSettings);
501+
}, []);
502+
503+
// Auto-save on change
504+
useEffect(() => {
505+
window.electronAPI.settings.save(settings);
506+
}, [settings]);
507+
508+
// Theme application
509+
useEffect(() => {
510+
if (settings.theme === "system") {
511+
const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
512+
document.documentElement.classList.toggle("dark", prefersDark);
513+
} else {
514+
document.documentElement.classList.toggle("dark", settings.theme === "dark");
515+
}
516+
}, [settings.theme]);
517+
518+
const handleSelectFolder = async () => {
519+
const path = await window.electronAPI.selectFolder();
520+
if (path) {
521+
setSettings(prev => ({ ...prev, downloadPath: path }));
522+
}
523+
};
524+
525+
return (
526+
<div className="p-6 space-y-6">
527+
{/* Theme Section */}
528+
<div className="space-y-2">
529+
<Label>Theme</Label>
530+
<Select value={settings.theme} onValueChange={...}>
531+
<SelectTrigger>
532+
<SelectValue />
533+
</SelectTrigger>
534+
<SelectContent>
535+
<SelectItem value="light">Light</SelectItem>
536+
<SelectItem value="dark">Dark</SelectItem>
537+
<SelectItem value="system">System</SelectItem>
538+
</SelectContent>
539+
</Select>
540+
</div>
541+
542+
{/* Download Path Section */}
543+
<div className="space-y-2">
544+
<Label>Download Location</Label>
545+
<div className="flex gap-2">
546+
<Input
547+
value={settings.downloadPath || ""}
548+
readOnly
549+
className="text-xs"
550+
/>
551+
<Button onClick={handleSelectFolder} variant="outline">
552+
Browse
553+
</Button>
554+
</div>
555+
</div>
556+
557+
{/* Notifications Section */}
558+
<div className="flex items-center justify-between">
559+
<Label>Enable Notifications</Label>
560+
<Switch
561+
checked={settings.enableNotifications}
562+
onCheckedChange={...}
563+
/>
564+
</div>
565+
</div>
566+
);
567+
}
568+
```
569+
570+
**Type Definition (src/types/globals.d.ts):**
571+
```typescript
572+
interface AppSettings {
573+
theme: "light" | "dark" | "system";
574+
enableNotifications: boolean;
575+
downloadPath?: string;
576+
}
577+
578+
interface ElectronAPI {
579+
settings: {
580+
load: () => Promise<AppSettings>;
581+
save: (settings: AppSettings) => Promise<void>;
582+
};
583+
selectFolder: () => Promise<string | null>;
584+
// ... other APIs
585+
}
586+
```
587+
588+
**Backend Integration:**
589+
- Loads from `SettingsStore` (src/main/settingsStore.ts)
590+
- Saves to disk via IPC handler `loopi:saveSettings`
591+
- Load handler: `loopi:loadSettings`
592+
- Folder picker handler: `dialog:selectFolder`
593+
- Download handler re-setup after settings save
594+
595+
**Dark Theme System:**
596+
The dark theme uses CSS class toggling and Tailwind's dark mode:
597+
```css
598+
.dark .react-flow {
599+
background: #1f2937;
600+
}
601+
602+
.dark .react-flow__controls {
603+
background: #111827;
604+
color: #f3f4f6;
605+
}
606+
```
607+
608+
Tailwind classes automatically respect `.dark` class:
609+
```jsx
610+
<div className="bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100">
611+
Content
612+
</div>
613+
```
614+
444615
### UI Component Patterns
445616

446617
#### Form Fields

docs/DEVELOPMENT_WORKFLOWS.md

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,79 @@ pnpm start # Development mode with live reload
154154
155155
---
156156
157+
### Working with IPC and Main Process
158+
159+
**When to add IPC handlers:**
160+
- Need to access file system (read/write files)
161+
- Need to access Electron APIs (dialog, session, etc.)
162+
- Need cross-process communication between renderer and main process
163+
164+
**Adding a new IPC handler:**
165+
166+
1. **Define the IPC handler** in `src/main/ipcHandlers.ts`:
167+
```typescript
168+
ipcMain.handle("channel:name", async (event, arg) => {
169+
// Handler code here
170+
return result;
171+
});
172+
```
173+
174+
2. **Expose in preload** in `src/preload.ts`:
175+
```typescript
176+
const electronAPI = {
177+
yourAPI: {
178+
method: () => ipcRenderer.invoke("channel:name"),
179+
},
180+
};
181+
```
182+
183+
3. **Update types** in `src/types/globals.d.ts`:
184+
```typescript
185+
interface ElectronAPI {
186+
yourAPI: {
187+
method: () => Promise<ReturnType>;
188+
};
189+
}
190+
```
191+
192+
4. **Use from React** in components:
193+
```typescript
194+
const result = await window.electronAPI.yourAPI.method();
195+
```
196+
197+
**Working with Settings:**
198+
- Stored in `~/.config/[AppName]/settings.json`
199+
- Loaded/saved via `SettingsStore` service
200+
- Auto-persisted from Settings component
201+
- Loaded on app startup in `src/app.tsx`
202+
203+
**Example - Adding a new setting:**
204+
```typescript
205+
// 1. Update AppSettings interface
206+
interface AppSettings {
207+
theme: "light" | "dark" | "system";
208+
enableNotifications: boolean;
209+
downloadPath?: string;
210+
newSetting?: string; // Add this
211+
}
212+
213+
// 2. Add UI in Settings.tsx
214+
<Input
215+
value={settings.newSetting}
216+
onChange={(e) => setSettings(prev => ({
217+
...prev,
218+
newSetting: e.target.value
219+
}))}
220+
/>
221+
222+
// 3. Auto-save via useEffect (already in component)
223+
useEffect(() => {
224+
window.electronAPI.settings.save(settings);
225+
}, [settings]);
226+
```
227+
228+
---
229+
157230
### Testing Your Changes
158231

159232
**Manual Testing Checklist:**

0 commit comments

Comments
 (0)