Frontend code cleanup, remove dead code
This commit is contained in:
parent
e95547b4e0
commit
8d6446bdd6
16 changed files with 71 additions and 661 deletions
7
.gitignore
vendored
7
.gitignore
vendored
|
|
@ -11,6 +11,13 @@ output/
|
|||
|
||||
# Configuration with secrets
|
||||
config.toml
|
||||
.env
|
||||
|
||||
# ONNX models (large binaries)
|
||||
models/
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
|
||||
# Debug/temp artifacts
|
||||
*.dot
|
||||
|
|
|
|||
|
|
@ -1,330 +0,0 @@
|
|||
# Frontend Refactor - Priority 1 Fixes
|
||||
|
||||
## Summary
|
||||
|
||||
Completed all Priority 1 fixes from the code review:
|
||||
1. ✅ Extracted `sanitizeFolderName` to shared utility
|
||||
2. ✅ Extracted `formatSize` to shared utility
|
||||
3. ✅ Refactored folder state management (eliminated circular flow)
|
||||
|
||||
## Changes Made
|
||||
|
||||
### 1. Created Shared Utilities (`src/lib/utils.js`)
|
||||
|
||||
**New file:** `/frontend/src/lib/utils.js`
|
||||
|
||||
Extracted two functions that were duplicated across components:
|
||||
|
||||
- **`sanitizeFolderName(name, id)`** - Previously duplicated in:
|
||||
- App.svelte (lines 65-84)
|
||||
- ProcessingControls.svelte (lines 15-34)
|
||||
|
||||
- **`formatSize(bytes)`** - Previously duplicated in:
|
||||
- ProcessingControls.svelte (lines 113-117)
|
||||
- OutputManager.svelte (lines 84-88)
|
||||
- GalleryView.svelte (lines 123-127)
|
||||
|
||||
**Impact:**
|
||||
- Eliminated 5 instances of duplicated code (~50 lines total)
|
||||
- Single source of truth for these utilities
|
||||
- Easier maintenance and testing
|
||||
- Functions properly documented with JSDoc
|
||||
|
||||
### 2. Refactored Folder State Management
|
||||
|
||||
**Problem:** Circular data flow created complexity and potential race conditions:
|
||||
```
|
||||
OutputManager (loads) → App (stores) → ProcessingControls (consumes)
|
||||
```
|
||||
|
||||
**Solution:** Simplified to unidirectional flow:
|
||||
```
|
||||
App (loads & stores) → OutputManager (displays) + ProcessingControls (consumes)
|
||||
```
|
||||
|
||||
**Changes:**
|
||||
|
||||
#### App.svelte
|
||||
- **Added:** `loadOutputFolders()` function to fetch folder list
|
||||
- **Added:** `$effect` to load folders when connection established
|
||||
- **Modified:** `handleFolderDeleted()` now calls `loadOutputFolders()` to refresh
|
||||
- **Removed:** `outputFolderRefreshKey` state variable (no longer needed)
|
||||
- **Removed:** Duplicated `sanitizeFolderName()` function
|
||||
- **Simplified:** OutputManager props - now just passes `folders` directly
|
||||
|
||||
#### OutputManager.svelte
|
||||
- **Removed:** Internal `folders` state (now receives as prop)
|
||||
- **Removed:** `loading` and `error` state (no longer manages fetching)
|
||||
- **Removed:** `loadFolders()` function (parent handles this)
|
||||
- **Removed:** `refreshKey` effect (parent controls updates)
|
||||
- **Removed:** `onFoldersLoaded` callback (no longer needed)
|
||||
- **Removed:** Refresh button from UI (folders auto-refresh)
|
||||
- **Removed:** Unused CSS for refresh button and error states
|
||||
- **Simplified:** Component is now purely presentational
|
||||
|
||||
#### ProcessingControls.svelte
|
||||
- **Removed:** Duplicated `sanitizeFolderName()` function
|
||||
- **Removed:** Duplicated `formatSize()` function
|
||||
- **Added:** Import from shared utils
|
||||
|
||||
#### GalleryView.svelte
|
||||
- **Removed:** Duplicated `formatSize()` function
|
||||
- **Added:** Import from shared utils
|
||||
|
||||
### 3. Benefits
|
||||
|
||||
**Before:**
|
||||
- 5 copies of utility functions
|
||||
- Circular state flow between components
|
||||
- OutputManager managed its own loading state
|
||||
- Manual refresh button required
|
||||
- Multiple sources of truth for folder data
|
||||
|
||||
**After:**
|
||||
- Single copy of each utility in shared location
|
||||
- Clear unidirectional data flow (App → children)
|
||||
- OutputManager is a pure presentation component
|
||||
- Automatic refresh when folders change
|
||||
- Single source of truth (App state)
|
||||
|
||||
**Code Quality Improvements:**
|
||||
- **DRY:** No duplicated logic
|
||||
- **Maintainability:** Changes to utilities happen in one place
|
||||
- **Testability:** Utilities can be unit tested independently
|
||||
- **Clarity:** Clear ownership of state (App owns, children display/consume)
|
||||
- **Reliability:** No race conditions from multiple components fetching
|
||||
|
||||
### 4. Build Status
|
||||
|
||||
✅ **Frontend builds successfully** with no errors
|
||||
|
||||
Warnings (pre-existing, unrelated to refactor):
|
||||
- Accessibility warnings in SettingsPanel (label associations)
|
||||
- State reference warning in PeopleSelector (intended behavior)
|
||||
|
||||
### 5. Testing Recommendations
|
||||
|
||||
Manual testing should verify:
|
||||
1. ✅ Output folders list displays correctly
|
||||
2. ✅ Folders refresh after job completion
|
||||
3. ✅ Folders refresh after deletion
|
||||
4. ✅ Folder deletion warning shows correct size/count
|
||||
5. ✅ No console errors related to folder loading
|
||||
6. ✅ ProcessingControls shows "already exists" warning correctly
|
||||
|
||||
## Files Modified
|
||||
|
||||
- **Created:**
|
||||
- `frontend/src/lib/utils.js` (42 lines)
|
||||
|
||||
- **Modified:**
|
||||
- `frontend/src/App.svelte` (-28 lines, refactored state management)
|
||||
- `frontend/src/lib/components/ProcessingControls.svelte` (-29 lines)
|
||||
- `frontend/src/lib/components/OutputManager.svelte` (-63 lines, major simplification)
|
||||
- `frontend/src/lib/components/GalleryView.svelte` (-8 lines)
|
||||
|
||||
**Total:** ~120 lines of code removed, architecture significantly improved
|
||||
|
||||
---
|
||||
|
||||
# Frontend Refactor - Priority 2 Fixes
|
||||
|
||||
## Summary
|
||||
|
||||
Completed all Priority 2 fixes (2026-02-08):
|
||||
1. ✅ Moved global CSS out of App.svelte
|
||||
2. ✅ Created constants file for magic values
|
||||
3. ✅ Standardized error handling
|
||||
4. ✅ Added loading states to all async buttons
|
||||
|
||||
## Changes Made
|
||||
|
||||
### 1. Global CSS Organization
|
||||
|
||||
**Created:** `frontend/src/styles/global.css`
|
||||
- Moved global reset styles (box-sizing, margin, padding)
|
||||
- Moved body styles (font-family, background, color, line-height)
|
||||
|
||||
**Updated:** `frontend/src/main.js`
|
||||
- Added import for global CSS file
|
||||
|
||||
**Updated:** `frontend/src/App.svelte`
|
||||
- Removed `:global(*)` and `:global(body)` styles
|
||||
- Component-specific styles remain
|
||||
|
||||
**Benefits:**
|
||||
- Clear separation of global vs component styles
|
||||
- Single location for global style changes
|
||||
- Follows standard Svelte/Vite patterns
|
||||
|
||||
### 2. Constants Consolidation
|
||||
|
||||
**Created:** `frontend/src/lib/constants.js`
|
||||
|
||||
Centralized all magic values into organized exports:
|
||||
- `COLORS` - All color values (backgrounds, text, borders, status)
|
||||
- `SPACING` - Standard spacing scale
|
||||
- `RADIUS` - Border radius values
|
||||
- `TRANSITIONS` - Animation transitions
|
||||
- `FONT_SIZES` - Typography scale
|
||||
- `Z_INDEX` - Z-index layering system
|
||||
- `TIMING` - Timing values (timeouts, delays, durations)
|
||||
- `STORAGE_KEYS` - LocalStorage keys
|
||||
- `API` - API endpoint paths
|
||||
- `WS` - WebSocket configuration
|
||||
- `JOB_STATUS` - Job status constants
|
||||
- `DEFAULT_CONFIG` - Default configuration (moved from SettingsPanel)
|
||||
|
||||
**Components Updated:**
|
||||
- `App.svelte` - Uses STORAGE_KEYS, WS, JOB_STATUS
|
||||
- `ProcessingControls.svelte` - Uses JOB_STATUS
|
||||
- `SettingsPanel.svelte` - Uses DEFAULT_CONFIG, TIMING (removed 40+ lines of duplicate config)
|
||||
|
||||
**Benefits:**
|
||||
- Single source of truth for all magic values
|
||||
- Easy to update colors, spacing, timing globally
|
||||
- Better discoverability
|
||||
- Eliminates duplicate constant definitions
|
||||
|
||||
### 3. Standardized Error Handling
|
||||
|
||||
**Created:** `frontend/src/lib/errorHandler.js`
|
||||
|
||||
Utilities for consistent error handling:
|
||||
- `parseError()` - Parse errors from Response or Error objects
|
||||
- `handleError()` - Handle errors with console logging
|
||||
- `showErrorAlert()` - Display error in alert dialog
|
||||
- `fetchWithErrorHandling()` - Fetch wrapper with error handling
|
||||
- `safeJsonParse()` - Safe JSON parsing with fallback
|
||||
- `executeAsync()` - Generic async operation wrapper
|
||||
- `ErrorMode` enum - Error display modes
|
||||
|
||||
**Components Updated (all use standardized error handling):**
|
||||
- `App.svelte`
|
||||
- `ProcessingControls.svelte`
|
||||
- `SettingsPanel.svelte`
|
||||
- `OutputManager.svelte`
|
||||
- `GalleryView.svelte`
|
||||
- `ConnectionStatus.svelte`
|
||||
- `PeopleSelector.svelte`
|
||||
|
||||
**Error Handling Pattern:**
|
||||
|
||||
Before:
|
||||
```javascript
|
||||
try {
|
||||
const res = await fetch('/api/endpoint');
|
||||
if (!res.ok) {
|
||||
const data = await res.json().catch(() => ({}));
|
||||
throw new Error(data.message || 'Operation failed');
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Operation failed:', e);
|
||||
error = e.message;
|
||||
}
|
||||
```
|
||||
|
||||
After:
|
||||
```javascript
|
||||
try {
|
||||
const res = await fetch('/api/endpoint');
|
||||
if (!res.ok) throw res;
|
||||
} catch (e) {
|
||||
error = await handleError('Operation failed', e);
|
||||
}
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- Consistent error messages across the application
|
||||
- Automatic error logging
|
||||
- Flexible error display (alert, console, inline)
|
||||
- Proper parsing of Response objects with JSON error messages
|
||||
- Less boilerplate code
|
||||
|
||||
### 4. Async Button Loading States
|
||||
|
||||
**Created:** `frontend/src/lib/asyncState.js`
|
||||
- `createAsyncState()` - Async state manager using Svelte 5 runes
|
||||
- `getButtonText()` - Get button text based on loading state
|
||||
- `createAsyncHandler()` - Create async handler with loading state
|
||||
|
||||
**Components Updated:**
|
||||
- `ProcessingControls.svelte` - Added `starting` state, button shows "Starting..."
|
||||
- `SettingsPanel.svelte` - Already had `saving` state (verified working)
|
||||
- `OutputManager.svelte` - Already had `deleting` state (verified working)
|
||||
- `GalleryView.svelte` - Already had `deleting` and `compiling` states (verified working)
|
||||
|
||||
**Loading State Pattern:**
|
||||
```javascript
|
||||
let loading = $state(false);
|
||||
|
||||
async function doSomething() {
|
||||
loading = true;
|
||||
try {
|
||||
await someAsyncOperation();
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
// In template:
|
||||
<button onclick={doSomething} disabled={loading}>
|
||||
{loading ? 'Loading...' : 'Do Something'}
|
||||
</button>
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- Clear visual feedback during async operations
|
||||
- Prevents double-clicks and race conditions
|
||||
- Better user experience
|
||||
- Consistent loading patterns
|
||||
- Reusable utilities for future components
|
||||
|
||||
## File Structure
|
||||
|
||||
```
|
||||
frontend/src/
|
||||
├── styles/
|
||||
│ └── global.css # NEW: Global styles
|
||||
├── lib/
|
||||
│ ├── components/ # UPDATED: All components
|
||||
│ ├── constants.js # NEW: Application constants
|
||||
│ ├── errorHandler.js # NEW: Error handling utilities
|
||||
│ ├── asyncState.js # NEW: Async state management
|
||||
│ └── utils.js # EXISTING: Utility functions
|
||||
├── App.svelte # UPDATED: Uses constants
|
||||
└── main.js # UPDATED: Imports global CSS
|
||||
```
|
||||
|
||||
## Build Status
|
||||
|
||||
✅ **Frontend builds successfully** (`npm run build`)
|
||||
|
||||
All changes verified:
|
||||
- No runtime errors
|
||||
- All async operations have loading states
|
||||
- Error handling is consistent
|
||||
- Constants are properly imported and used
|
||||
|
||||
## Code Quality Improvements
|
||||
|
||||
**Before Priority 2:**
|
||||
- Global styles mixed with component styles
|
||||
- Magic values duplicated across files (colors, timing, etc.)
|
||||
- Inconsistent error handling (alert, console.error, inline)
|
||||
- Some async buttons lacked loading states
|
||||
|
||||
**After Priority 2:**
|
||||
- Global styles in dedicated file
|
||||
- All magic values in centralized constants file
|
||||
- Standardized error handling with utilities
|
||||
- All async buttons have proper loading states
|
||||
|
||||
## Future Improvements
|
||||
|
||||
Suggestions for further enhancement:
|
||||
1. **Toast Notifications** - Replace `alert()` calls with toast system
|
||||
2. **Error Boundary** - Add Svelte error boundary for component errors
|
||||
3. **Retry Logic** - Add exponential backoff retry for failed requests
|
||||
4. **Loading Skeletons** - Replace loading text with skeleton screens
|
||||
5. **CSS Variables** - Convert constants to CSS custom properties for theming
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
<script>
|
||||
import { onMount, onDestroy } from 'svelte';
|
||||
import { onDestroy } from 'svelte';
|
||||
import ConnectionStatus from './lib/components/ConnectionStatus.svelte';
|
||||
import GalleryView from './lib/components/GalleryView.svelte';
|
||||
import OutputManager from './lib/components/OutputManager.svelte';
|
||||
|
|
@ -42,8 +42,8 @@
|
|||
let initialSelectedPersonId = $state(loadPersistedPersonId());
|
||||
let jobStatus = $state('idle');
|
||||
let progress = $state({ completed: 0, total: 0, message: '' });
|
||||
let ws = $state(null);
|
||||
let wsReconnectTimeout = $state(null);
|
||||
let ws = null;
|
||||
let wsReconnectTimeout = null;
|
||||
|
||||
// View state management for gallery
|
||||
let currentView = $state('main'); // 'main' | 'gallery'
|
||||
|
|
@ -99,7 +99,7 @@
|
|||
galleryFolder = null;
|
||||
currentView = 'main';
|
||||
// Check for any running job (e.g., video compilation started from gallery)
|
||||
checkAndPollProgress();
|
||||
connectWebSocket();
|
||||
// Restore scroll position after DOM updates
|
||||
requestAnimationFrame(() => {
|
||||
window.scrollTo(0, savedScrollPosition);
|
||||
|
|
@ -110,7 +110,7 @@
|
|||
connectionOk = data.connected;
|
||||
// Check for running job when connection is established
|
||||
if (data.connected) {
|
||||
checkAndPollProgress();
|
||||
connectWebSocket();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -124,11 +124,6 @@
|
|||
progress = data;
|
||||
}
|
||||
|
||||
async function checkAndPollProgress() {
|
||||
// Connect WebSocket if not already connected
|
||||
connectWebSocket();
|
||||
}
|
||||
|
||||
function connectWebSocket() {
|
||||
if (ws && (ws.readyState === WebSocket.CONNECTING || ws.readyState === WebSocket.OPEN)) {
|
||||
return; // Already connected or connecting
|
||||
|
|
@ -198,14 +193,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
// Initial check will happen when connection is established
|
||||
// Load output folders initially and when connection is established
|
||||
if (connectionOk) {
|
||||
loadOutputFolders();
|
||||
}
|
||||
});
|
||||
|
||||
// Reload folders when connection is established
|
||||
$effect(() => {
|
||||
if (connectionOk) {
|
||||
|
|
|
|||
|
|
@ -1,116 +0,0 @@
|
|||
/**
|
||||
* Async operation state management utilities.
|
||||
* Helps manage loading, error, and success states for async operations.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Create an async state manager using Svelte 5 runes
|
||||
* @returns {Object} State manager with loading, error, and execute methods
|
||||
*
|
||||
* @example
|
||||
* const asyncOp = createAsyncState();
|
||||
* await asyncOp.execute(async () => {
|
||||
* const res = await fetch('/api/data');
|
||||
* return await res.json();
|
||||
* });
|
||||
* // Use asyncOp.loading, asyncOp.error in template
|
||||
*/
|
||||
export function createAsyncState() {
|
||||
let loading = $state(false);
|
||||
let error = $state(null);
|
||||
|
||||
async function execute(operation, options = {}) {
|
||||
const { onSuccess, onError } = options;
|
||||
|
||||
loading = true;
|
||||
error = null;
|
||||
|
||||
try {
|
||||
const result = await operation();
|
||||
onSuccess?.(result);
|
||||
return { success: true, data: result };
|
||||
} catch (err) {
|
||||
error = err.message || String(err);
|
||||
onError?.(error);
|
||||
return { success: false, error };
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function reset() {
|
||||
loading = false;
|
||||
error = null;
|
||||
}
|
||||
|
||||
return {
|
||||
get loading() {
|
||||
return loading;
|
||||
},
|
||||
get error() {
|
||||
return error;
|
||||
},
|
||||
execute,
|
||||
reset,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get button text based on loading state
|
||||
* @param {boolean} loading - Whether operation is in progress
|
||||
* @param {string} defaultText - Text when not loading
|
||||
* @param {string} loadingText - Text when loading (defaults to defaultText + "...")
|
||||
* @returns {string} Button text
|
||||
*/
|
||||
export function getButtonText(loading, defaultText, loadingText = null) {
|
||||
if (loading) {
|
||||
return loadingText || `${defaultText}...`;
|
||||
}
|
||||
return defaultText;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a standardized async operation handler with loading state
|
||||
* Useful for button click handlers
|
||||
*
|
||||
* @param {Function} operation - Async operation to execute
|
||||
* @param {Object} stateRefs - Object with loading state ref (for mutation)
|
||||
* @param {Object} options - Additional options
|
||||
* @returns {Function} Handler function
|
||||
*
|
||||
* @example
|
||||
* let saving = $state(false);
|
||||
* const handleSave = createAsyncHandler(
|
||||
* async () => { await saveData(); },
|
||||
* { loading: () => saving, setLoading: (val) => { saving = val; } }
|
||||
* );
|
||||
*/
|
||||
export function createAsyncHandler(operation, stateRefs, options = {}) {
|
||||
const { onSuccess, onError, confirmMessage } = options;
|
||||
|
||||
return async function handler(...args) {
|
||||
// Optional confirmation
|
||||
if (confirmMessage && !confirm(confirmMessage)) {
|
||||
return { success: false, cancelled: true };
|
||||
}
|
||||
|
||||
// Set loading state
|
||||
if (stateRefs.setLoading) {
|
||||
stateRefs.setLoading(true);
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await operation(...args);
|
||||
onSuccess?.(result);
|
||||
return { success: true, data: result };
|
||||
} catch (error) {
|
||||
const errorMessage = error.message || String(error);
|
||||
onError?.(errorMessage);
|
||||
return { success: false, error: errorMessage };
|
||||
} finally {
|
||||
if (stateRefs.setLoading) {
|
||||
stateRefs.setLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
@ -80,13 +80,13 @@
|
|||
}
|
||||
|
||||
.indicator.error {
|
||||
background: #ef4444;
|
||||
background: #dc2626;
|
||||
}
|
||||
|
||||
.retry {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #ef4444;
|
||||
color: #dc2626;
|
||||
cursor: pointer;
|
||||
font-size: inherit;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
<script>
|
||||
import { onMount } from 'svelte';
|
||||
import { formatSize } from '../utils.js';
|
||||
import { handleError, showErrorAlert } from '../errorHandler.js';
|
||||
import { API } from '../constants.js';
|
||||
|
||||
|
|
@ -207,11 +206,14 @@
|
|||
class="image-card"
|
||||
class:selected={selectedImages.has(image.filename)}
|
||||
>
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div class="checkbox-overlay" onclick={(e) => { e.stopPropagation(); toggleSelect(image.filename); }}>
|
||||
<button
|
||||
type="button"
|
||||
class="checkbox-overlay"
|
||||
aria-label={selectedImages.has(image.filename) ? `Deselect ${image.filename}` : `Select ${image.filename}`}
|
||||
onclick={(e) => { e.stopPropagation(); toggleSelect(image.filename); }}
|
||||
>
|
||||
<span class="checkbox">{selectedImages.has(image.filename) ? '✓' : ''}</span>
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="image-button"
|
||||
|
|
@ -255,29 +257,27 @@
|
|||
|
||||
{#if lightboxImage}
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div class="lightbox-overlay" onclick={closeLightbox} onkeydown={handleLightboxKeydown}>
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||
<div class="lightbox-overlay" onclick={closeLightbox} role="presentation">
|
||||
<button type="button" class="lightbox-close" onclick={closeLightbox}>×</button>
|
||||
|
||||
{#if lightboxIndex > 0}
|
||||
<button type="button" class="lightbox-nav lightbox-prev" onclick={(e) => { e.stopPropagation(); lightboxPrev(); }}>‹</button>
|
||||
{/if}
|
||||
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
|
||||
<img
|
||||
class="lightbox-img"
|
||||
src="/output/{encodeURIComponent(folderName)}/images/{encodeURIComponent(lightboxImage.filename)}?v={cacheBust}"
|
||||
alt={lightboxImage.filename}
|
||||
onclick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
<div class="lightbox-img-wrapper" role="presentation" onclick={(e) => e.stopPropagation()}>
|
||||
<img
|
||||
class="lightbox-img"
|
||||
src="/output/{encodeURIComponent(folderName)}/images/{encodeURIComponent(lightboxImage.filename)}?v={cacheBust}"
|
||||
alt={lightboxImage.filename}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{#if lightboxIndex < images.length - 1}
|
||||
<button type="button" class="lightbox-nav lightbox-next" onclick={(e) => { e.stopPropagation(); lightboxNext(); }}>›</button>
|
||||
{/if}
|
||||
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div class="lightbox-info" onclick={(e) => e.stopPropagation()}>
|
||||
<div class="lightbox-info" role="presentation" onclick={(e) => e.stopPropagation()}>
|
||||
<span>{lightboxImage.filename}</span>
|
||||
<span class="lightbox-counter">{lightboxIndex + 1} / {images.length}</span>
|
||||
</div>
|
||||
|
|
@ -431,6 +431,7 @@
|
|||
}
|
||||
|
||||
.checkbox-overlay {
|
||||
all: unset;
|
||||
position: absolute;
|
||||
top: 0.25rem;
|
||||
left: 0.25rem;
|
||||
|
|
@ -442,6 +443,7 @@
|
|||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.image-card.selected .checkbox-overlay {
|
||||
|
|
@ -599,6 +601,12 @@
|
|||
right: 1rem;
|
||||
}
|
||||
|
||||
.lightbox-img-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.lightbox-img {
|
||||
max-width: 90vw;
|
||||
max-height: 85vh;
|
||||
|
|
|
|||
|
|
@ -96,6 +96,7 @@
|
|||
<button
|
||||
type="button"
|
||||
class="delete-btn"
|
||||
aria-label="Delete folder {folder.name}"
|
||||
onclick={() => deleteFolder(folder.name)}
|
||||
disabled={disabled || deleting !== null}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -220,7 +220,7 @@
|
|||
}
|
||||
|
||||
.error {
|
||||
color: #ef4444;
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
.error button {
|
||||
|
|
|
|||
|
|
@ -87,15 +87,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
async function cancelProcessing() {
|
||||
try {
|
||||
const res = await fetch(API.cancel, { method: 'POST' });
|
||||
if (!res.ok) throw res;
|
||||
} catch (e) {
|
||||
await handleError('Cancel failed', e);
|
||||
}
|
||||
}
|
||||
|
||||
function handleStartClick() {
|
||||
if (existingFolder) {
|
||||
const folder = existingFolder;
|
||||
|
|
|
|||
|
|
@ -59,8 +59,8 @@
|
|||
if (progress.completed > savedCompleted) {
|
||||
savedCompleted = progress.completed;
|
||||
}
|
||||
} else if (jobStatus === 'idle') {
|
||||
// Clear saved stats when starting a new job
|
||||
} else if (jobStatus === 'idle' || jobStatus === 'error') {
|
||||
// Clear saved stats when returning to idle or on error
|
||||
savedSkipStats = {};
|
||||
savedCompleted = 0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@
|
|||
<video
|
||||
controls
|
||||
autoplay
|
||||
muted
|
||||
loop
|
||||
src={videoUrl}
|
||||
onerror={handleVideoError}
|
||||
|
|
|
|||
|
|
@ -114,9 +114,6 @@
|
|||
|
||||
function toggle() {
|
||||
isOpen = !isOpen;
|
||||
if (isOpen && loading) {
|
||||
loadConfig();
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
|
|
@ -303,6 +300,24 @@
|
|||
<span class="value">{config.processing.head_pose.max_pitch.toFixed(0)}°</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="setting-row sub-setting">
|
||||
<label for="max-roll">
|
||||
<span class="setting-label">Max Roll</span>
|
||||
<span class="setting-hint">Maximum head tilt angle</span>
|
||||
</label>
|
||||
<div class="setting-control">
|
||||
<input
|
||||
id="max-roll"
|
||||
type="range"
|
||||
bind:value={config.processing.head_pose.max_roll}
|
||||
min="5"
|
||||
max="90"
|
||||
step="5"
|
||||
/>
|
||||
<span class="value">{config.processing.head_pose.max_roll.toFixed(0)}°</span>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
|
|
@ -320,7 +335,7 @@
|
|||
<div class="setting-row sub-setting">
|
||||
<label for="min-ear">
|
||||
<span class="setting-label">Minimum eye opening</span>
|
||||
<span class="setting-hint">Discard image if Eye Aspect Ratio is bellow this value</span>
|
||||
<span class="setting-hint">Discard image if Eye Aspect Ratio is below this value</span>
|
||||
</label>
|
||||
<div class="setting-control-with-visual">
|
||||
<input
|
||||
|
|
@ -411,7 +426,7 @@
|
|||
min="1"
|
||||
max="10"
|
||||
/>
|
||||
<span class="setting-label">per</span>
|
||||
<span class="setting-label">photos per</span>
|
||||
<div class="toggle-group">
|
||||
{#each [['day', 'day'], ['week', 'week'], ['month', 'month']] as [value, label]}
|
||||
<button
|
||||
|
|
|
|||
|
|
@ -17,6 +17,9 @@
|
|||
const minX = $derived(minBrightness * width);
|
||||
const maxX = $derived(maxBrightness * width);
|
||||
|
||||
// Unique ID for SVG gradient to avoid collision with multiple instances
|
||||
const gradientId = `brightness-gradient-${Math.random().toString(36).slice(2, 9)}`;
|
||||
|
||||
let dragging = $state(null); // 'min' | 'max' | null
|
||||
let svgElement = $state(null);
|
||||
|
||||
|
|
@ -70,7 +73,7 @@
|
|||
>
|
||||
<!-- Define gradient from black to white -->
|
||||
<defs>
|
||||
<linearGradient id="brightness-gradient" x1="0%" y1="0%" x2="100%" y2="0%">
|
||||
<linearGradient id={gradientId} x1="0%" y1="0%" x2="100%" y2="0%">
|
||||
<stop offset="0%" style="stop-color:rgb(0,0,0);stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:rgb(255,255,255);stop-opacity:1" />
|
||||
</linearGradient>
|
||||
|
|
@ -82,7 +85,7 @@
|
|||
y={gradientBarY}
|
||||
width={width}
|
||||
height={gradientBarHeight}
|
||||
fill="url(#brightness-gradient)"
|
||||
fill="url(#{gradientId})"
|
||||
stroke="white"
|
||||
stroke-width="1.5"
|
||||
rx="3"
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@
|
|||
|
||||
// Create almond-shaped eye outline using a path
|
||||
// The path creates a pointed eye shape (yes, this is a Skyrim reference.)
|
||||
const eyePath = $derived(() => {
|
||||
const eyePath = $derived.by(() => {
|
||||
const halfWidth = eyeWidth / 2;
|
||||
const halfHeight = eyeHeight / 2;
|
||||
|
||||
|
|
@ -45,7 +45,7 @@
|
|||
>
|
||||
<!-- Eye outline (almond shape) -->
|
||||
<path
|
||||
d={eyePath()}
|
||||
d={eyePath}
|
||||
fill="none"
|
||||
stroke="white"
|
||||
stroke-width="2.5"
|
||||
|
|
|
|||
|
|
@ -1,89 +1,10 @@
|
|||
/**
|
||||
* Application constants and magic values.
|
||||
* Single source of truth for colors, spacing, timing, etc.
|
||||
*/
|
||||
|
||||
// ===== Colors =====
|
||||
export const COLORS = {
|
||||
// Backgrounds
|
||||
bgPrimary: '#0f0f0f',
|
||||
bgSecondary: '#1a1a1a',
|
||||
bgTertiary: '#252525',
|
||||
bgHover: '#333',
|
||||
bgHoverLight: '#444',
|
||||
|
||||
// Text
|
||||
textPrimary: '#fff',
|
||||
textSecondary: '#e0e0e0',
|
||||
textTertiary: '#888',
|
||||
textQuaternary: '#666',
|
||||
|
||||
// Borders
|
||||
border: '#333',
|
||||
borderLight: '#252525',
|
||||
|
||||
// Brand/Accent
|
||||
primary: '#4f46e5',
|
||||
primaryHover: '#4338ca',
|
||||
|
||||
// Status colors
|
||||
success: '#22c55e',
|
||||
error: '#dc2626',
|
||||
warning: '#f59e0b',
|
||||
info: '#3b82f6',
|
||||
};
|
||||
|
||||
// ===== Spacing =====
|
||||
export const SPACING = {
|
||||
xs: '0.25rem',
|
||||
sm: '0.5rem',
|
||||
md: '0.75rem',
|
||||
lg: '1rem',
|
||||
xl: '1.5rem',
|
||||
xxl: '2rem',
|
||||
xxxl: '3rem',
|
||||
};
|
||||
|
||||
// ===== Border Radius =====
|
||||
export const RADIUS = {
|
||||
sm: '4px',
|
||||
md: '6px',
|
||||
lg: '8px',
|
||||
};
|
||||
|
||||
// ===== Transitions =====
|
||||
export const TRANSITIONS = {
|
||||
default: 'all 0.15s ease',
|
||||
fast: 'all 0.1s ease',
|
||||
slow: 'all 0.3s ease',
|
||||
};
|
||||
|
||||
// ===== Font Sizes =====
|
||||
export const FONT_SIZES = {
|
||||
xs: '0.625rem',
|
||||
sm: '0.75rem',
|
||||
md: '0.875rem',
|
||||
base: '1rem',
|
||||
lg: '1.125rem',
|
||||
xl: '1.5rem',
|
||||
};
|
||||
|
||||
// ===== Z-Index Layers =====
|
||||
export const Z_INDEX = {
|
||||
base: 0,
|
||||
dropdown: 10,
|
||||
overlay: 100,
|
||||
modal: 1000,
|
||||
toast: 10000,
|
||||
};
|
||||
|
||||
// ===== Timing (milliseconds) =====
|
||||
export const TIMING = {
|
||||
messageDisplayDuration: 2000,
|
||||
wsReconnectDelay: 1000,
|
||||
debounceDefault: 300,
|
||||
animationFast: 150,
|
||||
animationDefault: 300,
|
||||
};
|
||||
|
||||
// ===== Local Storage Keys =====
|
||||
|
|
|
|||
|
|
@ -51,81 +51,3 @@ export async function showErrorAlert(context, error) {
|
|||
alert(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a fetch wrapper with standardized error handling
|
||||
* @param {string} url - Fetch URL
|
||||
* @param {RequestInit} options - Fetch options
|
||||
* @returns {Promise<Response>} Response object if ok, throws on error
|
||||
*/
|
||||
export async function fetchWithErrorHandling(url, options = {}) {
|
||||
try {
|
||||
const response = await fetch(url, options);
|
||||
if (!response.ok) {
|
||||
throw response;
|
||||
}
|
||||
return response;
|
||||
} catch (error) {
|
||||
// Re-throw for caller to handle
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Safely parse JSON from response, with fallback
|
||||
* @param {Response} response - Fetch response
|
||||
* @param {any} fallback - Fallback value if parsing fails
|
||||
* @returns {Promise<any>} Parsed JSON or fallback
|
||||
*/
|
||||
export async function safeJsonParse(response, fallback = {}) {
|
||||
try {
|
||||
return await response.json();
|
||||
} catch {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Error display modes
|
||||
*/
|
||||
export const ErrorMode = {
|
||||
ALERT: 'alert',
|
||||
CONSOLE: 'console',
|
||||
INLINE: 'inline',
|
||||
};
|
||||
|
||||
/**
|
||||
* Generic async operation wrapper with error handling
|
||||
* @param {Function} operation - Async operation to execute
|
||||
* @param {Object} options - Options
|
||||
* @param {string} options.context - Error context message
|
||||
* @param {string} options.mode - Error display mode (alert, console, inline)
|
||||
* @param {Function} options.onError - Custom error handler
|
||||
* @param {Function} options.onSuccess - Success callback
|
||||
* @returns {Promise<{success: boolean, data?: any, error?: string}>}
|
||||
*/
|
||||
export async function executeAsync(operation, options = {}) {
|
||||
const {
|
||||
context = 'Operation failed',
|
||||
mode = ErrorMode.CONSOLE,
|
||||
onError,
|
||||
onSuccess,
|
||||
} = options;
|
||||
|
||||
try {
|
||||
const result = await operation();
|
||||
onSuccess?.();
|
||||
return { success: true, data: result };
|
||||
} catch (error) {
|
||||
const errorMessage = await handleError(context, error);
|
||||
|
||||
// Display error based on mode
|
||||
if (mode === ErrorMode.ALERT) {
|
||||
alert(errorMessage);
|
||||
}
|
||||
|
||||
// Call custom error handler if provided
|
||||
onError?.(errorMessage);
|
||||
|
||||
return { success: false, error: errorMessage };
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue