Skip to content

Commit c6e6643

Browse files
committed
Adding new feature with new version release
1 parent 01e7897 commit c6e6643

11 files changed

Lines changed: 287 additions & 42 deletions

File tree

.github/workflows/release.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,6 @@ jobs:
7474
copy backend\dist\backend_server-x86_64-pc-windows-msvc.exe frontend\src-tauri\
7575
7676
77-
7877
# Build the final application
7978
- name: Build Tauri application
8079
env:

CHANGELOG.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,21 @@ All notable changes to Local Lens will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [2.0.6] - 2025-12-04
9+
10+
### Added
11+
12+
- Delete button for saved presets in the preset manager
13+
14+
### Fixed
15+
16+
- Fixed crash when selecting a preset with deleted/missing folder paths (now shows error dialog)
17+
- Fixed 'Find & Group' result dialog showing incorrect information after operation completion
18+
19+
### Changed
20+
21+
- 'Find & Group' mode now always copies files (removed Copy/Move toggle to prevent data loss)
22+
823
## [2.0.2] - 2025-12-03
924

1025
### Fixed

backend/main.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -630,6 +630,20 @@ async def save_path_preset(preset: PathPreset):
630630
except IOError:
631631
raise HTTPException(status_code=500, detail="Failed to save preset file.")
632632

633+
@app.delete("/api/presets/paths/{preset_name}")
634+
async def delete_path_preset(preset_name: str):
635+
"""Deletes a path preset by name."""
636+
try:
637+
presets = await get_path_presets()
638+
if preset_name not in presets:
639+
raise HTTPException(status_code=404, detail=f"Preset '{preset_name}' not found.")
640+
del presets[preset_name]
641+
with open(PATH_PRESETS_FILE, 'w') as f:
642+
json.dump(presets, f, indent=4)
643+
return {"status": "success", "message": f"Preset '{preset_name}' deleted."}
644+
except IOError:
645+
raise HTTPException(status_code=500, detail="Failed to delete preset.")
646+
633647
@app.post("/api/open-enrolled-folder")
634648
async def open_enrolled_folder(request: OpenEnrolledFolderRequest):
635649
"""Opens the folder for a specific enrolled person."""

frontend/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "local-lens",
33
"private": true,
4-
"version": "2.0.2",
4+
"version": "2.0.6",
55
"type": "module",
66
"scripts": {
77
"dev:pre": "node -e \"const fs = require('fs'); const path = 'src-tauri/backend_server-x86_64-pc-windows-msvc.exe'; if (!fs.existsSync(path)) { fs.writeFileSync(path, ''); }\"",

frontend/src-tauri/Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

frontend/src-tauri/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "LocalLens"
3-
version = "2.0.2"
3+
version = "2.0.6"
44
description = "Application to organize photos using AI with face recognition and object detection."
55
authors = ["you"]
66
edition = "2021"

frontend/src-tauri/tauri.conf.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"$schema": "https://schema.tauri.app/config/2",
33
"productName": "Local Lens",
4-
"version": "2.0.2",
4+
"version": "2.0.6",
55
"identifier": "ashes.locallens",
66
"build": {
77
"beforeDevCommand": "npm run dev:pre && npm run dev",

frontend/src/App.jsx

Lines changed: 165 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,12 @@ function App() {
118118
// --- NEW State for Reload Confirmation Modal ---
119119
const [showReloadModal, setShowReloadModal] = useState(false);
120120

121+
// --- NEW State for Invalid Preset Error Modal ---
122+
const [invalidPresetError, setInvalidPresetError] = useState({ show: false, presetName: '', invalidPaths: [] });
123+
124+
// --- NEW State for Delete Preset Confirmation Modal ---
125+
const [deletePresetConfirm, setDeletePresetConfirm] = useState({ show: false, presetName: '' });
126+
121127
// --- NEW State for Move Complete Modal ---
122128
const [showMoveCompleteModal, setShowMoveCompleteModal] = useState(false);
123129

@@ -385,12 +391,16 @@ function App() {
385391
const result = await response.json();
386392
if (!response.ok) {
387393
// Refined: Check for 'detail' from FastAPI's HTTPException first.
388-
throw new Error(result.detail || result.message || `API call to ${endpoint} failed: ${response.statusText}`);
394+
// Create an error with additional info for callers to distinguish error types
395+
const error = new Error(result.detail || result.message || `API call to ${endpoint} failed: ${response.statusText}`);
396+
error.status = response.status;
397+
error.isHttpError = true;
398+
throw error;
389399
}
390400
return result;
391401
} catch (error) {
392-
// This error is typically a network failure (e.g., "Failed to fetch")
393-
if (isBackendConnected) {
402+
// Only mark backend as disconnected for actual network failures, not HTTP errors (4xx, 5xx)
403+
if (!error.isHttpError && isBackendConnected) {
394404
setIsBackendConnected(false);
395405
logToConsole(`Backend connection lost. Will try to reconnect automatically.`, 'error');
396406
}
@@ -404,6 +414,20 @@ function App() {
404414
setLogs(prevLogs => [...prevLogs, { message, type, time: new Date().toLocaleTimeString() }]);
405415
};
406416

417+
// Helper function to validate if a path exists on the filesystem
418+
const validatePath = async (path) => {
419+
if (!path) return false;
420+
try {
421+
await apiCall('/api/validate-path', {
422+
method: 'POST',
423+
body: JSON.stringify({ path }),
424+
});
425+
return true;
426+
} catch (error) {
427+
return false;
428+
}
429+
};
430+
407431
// --- UI Handlers & Backend Integrations ---
408432

409433
const runStartupChecks = async () => {
@@ -637,8 +661,23 @@ function App() {
637661
try {
638662
const config = await apiCall('/api/config/load');
639663
if (Object.keys(config).length > 0) {
640-
if (config.source_folder) setSourceFolder(config.source_folder);
641-
if (config.destination_folder) setDestinationFolder(config.destination_folder);
664+
// Validate folder paths before setting them to prevent crashes
665+
if (config.source_folder) {
666+
const sourceValid = await validatePath(config.source_folder);
667+
if (sourceValid) {
668+
setSourceFolder(config.source_folder);
669+
} else {
670+
logToConsole(`Previous source folder no longer exists: ${config.source_folder}`, "warning");
671+
}
672+
}
673+
if (config.destination_folder) {
674+
const destValid = await validatePath(config.destination_folder);
675+
if (destValid) {
676+
setDestinationFolder(config.destination_folder);
677+
} else {
678+
logToConsole(`Previous destination folder no longer exists: ${config.destination_folder}`, "warning");
679+
}
680+
}
642681
if (config.sort_method) setSortMethod(config.sort_method);
643682
if (config.face_mode) setFaceMode(config.face_mode);
644683
if (config.file_operation_mode) setFileOperationMode(config.file_operation_mode); // <-- ADD THIS
@@ -701,13 +740,85 @@ function App() {
701740
}
702741
};
703742

743+
const handleDeletePreset = async (presetName) => {
744+
try {
745+
await apiCall(`/api/presets/paths/${encodeURIComponent(presetName)}`, {
746+
method: 'DELETE',
747+
});
748+
logToConsole(`Preset '${presetName}' deleted successfully.`, 'success');
749+
// Clear selection if the deleted preset was selected
750+
if (selectedPreset === presetName) {
751+
setSelectedPreset('');
752+
}
753+
await loadPresets();
754+
} catch (error) {
755+
logToConsole(`Failed to delete preset '${presetName}': ${error.message}`, 'error');
756+
}
757+
};
758+
759+
// Track if we're currently validating a preset to prevent multiple simultaneous validations
760+
const [isValidatingPreset, setIsValidatingPreset] = useState(false);
761+
704762
const handlePresetChange = (e) => {
705763
const presetName = e.target.value;
706-
setSelectedPreset(presetName);
707-
if (presets[presetName]) {
708-
setSourceFolder(presets[presetName].source);
709-
setDestinationFolder(presets[presetName].destination);
764+
765+
if (!presets[presetName]) {
766+
setSelectedPreset(presetName);
767+
return;
710768
}
769+
770+
// Prevent selecting while validation is in progress
771+
if (isValidatingPreset) {
772+
return;
773+
}
774+
775+
const sourcePath = presets[presetName].source;
776+
const destPath = presets[presetName].destination;
777+
778+
// Set validating state to show feedback
779+
setIsValidatingPreset(true);
780+
781+
// Wrap async validation in an IIFE to properly handle the promise
782+
(async () => {
783+
try {
784+
// Validate both paths exist before setting them
785+
const [sourceValid, destValid] = await Promise.all([
786+
validatePath(sourcePath),
787+
validatePath(destPath)
788+
]);
789+
790+
const invalidPaths = [];
791+
if (!sourceValid) invalidPaths.push(`Source: ${sourcePath}`);
792+
if (!destValid) invalidPaths.push(`Destination: ${destPath}`);
793+
794+
if (invalidPaths.length > 0) {
795+
// Show error modal using state (more reliable than message() from IIFE)
796+
setInvalidPresetError({
797+
show: true,
798+
presetName: presetName,
799+
invalidPaths: invalidPaths
800+
});
801+
setIsValidatingPreset(false);
802+
return;
803+
}
804+
805+
// Only update state if validation passes
806+
setSelectedPreset(presetName);
807+
setSourceFolder(sourcePath);
808+
setDestinationFolder(destPath);
809+
logToConsole(`Loaded preset '${presetName}' successfully.`, 'success');
810+
} catch (err) {
811+
console.error('Error validating preset paths:', err);
812+
// Show error modal for backend connection issues
813+
setInvalidPresetError({
814+
show: true,
815+
presetName: presetName,
816+
invalidPaths: [`Backend error: ${err.message}`]
817+
});
818+
} finally {
819+
setIsValidatingPreset(false);
820+
}
821+
})();
711822
};
712823

713824
const resetUi = () => {
@@ -1108,6 +1219,50 @@ function App() {
11081219
</p>
11091220
</ConfirmationModal>
11101221

1222+
{/* Invalid Preset Error Modal */}
1223+
<ConfirmationModal
1224+
isVisible={invalidPresetError.show}
1225+
title="Invalid Preset Paths"
1226+
onCancel={() => setInvalidPresetError({ show: false, presetName: '', invalidPaths: [] })}
1227+
onConfirm={async () => {
1228+
await handleDeletePreset(invalidPresetError.presetName);
1229+
setInvalidPresetError({ show: false, presetName: '', invalidPaths: [] });
1230+
}}
1231+
confirmText="Delete Preset"
1232+
cancelText="Keep Preset"
1233+
>
1234+
<p>
1235+
<strong>The preset '{invalidPresetError.presetName}' contains folder paths that no longer exist:</strong>
1236+
<br /><br />
1237+
<span style={{ fontFamily: 'monospace', fontSize: '0.9em', wordBreak: 'break-all' }}>
1238+
{invalidPresetError.invalidPaths.map((path, idx) => (
1239+
<span key={idx}>{path}<br /></span>
1240+
))}
1241+
</span>
1242+
<br />
1243+
Would you like to delete this preset or keep it for later?
1244+
</p>
1245+
</ConfirmationModal>
1246+
1247+
{/* Delete Preset Confirmation Modal */}
1248+
<ConfirmationModal
1249+
isVisible={deletePresetConfirm.show}
1250+
title="Delete Preset"
1251+
onCancel={() => setDeletePresetConfirm({ show: false, presetName: '' })}
1252+
onConfirm={async () => {
1253+
await handleDeletePreset(deletePresetConfirm.presetName);
1254+
setDeletePresetConfirm({ show: false, presetName: '' });
1255+
}}
1256+
confirmText="Delete"
1257+
cancelText="Cancel"
1258+
>
1259+
<p>
1260+
Are you sure you want to delete the preset <strong>'{deletePresetConfirm.presetName}'</strong>?
1261+
<br /><br />
1262+
This action cannot be undone.
1263+
</p>
1264+
</ConfirmationModal>
1265+
11111266
<main className="app-main">
11121267
<div className="setup-grid">
11131268
<div className="setup-column">
@@ -1126,6 +1281,7 @@ function App() {
11261281
selectedPreset={selectedPreset}
11271282
handleSelectPreset={handlePresetChange}
11281283
handleSavePreset={handleSavePreset}
1284+
onRequestDelete={(presetName) => setDeletePresetConfirm({ show: true, presetName })}
11291285
showSaveButton={showSaveButton}
11301286
/>
11311287
{/* --- ADD THIS COMPONENT --- */}

frontend/src/components/ConfirmationModal.jsx

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import React from 'react';
22
import './ConfirmationModal.css';
33

4-
const ConfirmationModal = ({ isVisible, title, onConfirm, onCancel, confirmText = "Confirm", children }) => {
4+
const ConfirmationModal = ({ isVisible, title, onConfirm, onCancel, confirmText = "Confirm", cancelText = "Cancel", hideCancelButton = false, children }) => {
55
if (!isVisible) {
66
return null;
77
}
@@ -23,9 +23,11 @@ const ConfirmationModal = ({ isVisible, title, onConfirm, onCancel, confirmText
2323
{children}
2424
</div>
2525
<div className="confirm-modal-actions">
26-
<button className="btn-cancel" onClick={onCancel}>
27-
Cancel
28-
</button>
26+
{!hideCancelButton && (
27+
<button className="btn-cancel" onClick={onCancel}>
28+
{cancelText}
29+
</button>
30+
)}
2931
<button className="btn-confirm" onClick={onConfirm}>
3032
{confirmText}
3133
</button>
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/* Preset Manager Styles */
2+
3+
.preset-dropdown-container {
4+
display: flex;
5+
align-items: center;
6+
gap: 0.5rem;
7+
flex: 1;
8+
}
9+
10+
.preset-dropdown-container .select-field {
11+
flex: 1;
12+
}
13+
14+
.btn-delete-preset {
15+
display: flex;
16+
align-items: center;
17+
justify-content: center;
18+
padding: 0.5rem;
19+
background: transparent;
20+
border: 1px solid var(--border-color, #3a3a3a);
21+
border-radius: 6px;
22+
color: var(--text-secondary, #888);
23+
cursor: pointer;
24+
transition: all 0.2s ease;
25+
min-width: 36px;
26+
height: 36px;
27+
}
28+
29+
.btn-delete-preset:hover {
30+
background: rgba(220, 53, 69, 0.1);
31+
border-color: #dc3545;
32+
color: #dc3545;
33+
}
34+
35+
.btn-delete-preset svg {
36+
width: 16px;
37+
height: 16px;
38+
}

0 commit comments

Comments
 (0)