Skip to content

Commit d5de58e

Browse files
committed
New: Added option for user to Copy/Move photos
Upgrade: Added hierarchical folder ignoring on the source folder Major Bug Fix: Fixed race conditions introduced in get_locations Minor Bug Fix: Made small UI/UX improvements for better user experience ---
1 parent 492ccc9 commit d5de58e

20 files changed

Lines changed: 1350 additions & 325 deletions

.gitignore

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@ __pycache__/
33
*.py[cod]
44
*$py.class
55

6+
7+
# Helpers for developer
8+
.github/copilot-instructions.md
9+
610
# C extensions
711
*.so
812

@@ -209,4 +213,14 @@ cython_debug/
209213
/backend/presets/
210214

211215
# Ignore any log files that might be generated in the backend directory.
212-
/backend/*.log
216+
/backend/*.log
217+
218+
219+
# This devloper and I am sick of discarding exe again and again
220+
backend_server-x86_64-pc-windows-msvc.exe
221+
backend_server-i686-pc-windows-msvc.exe
222+
backend_server-x86_64-unknown-linux-gnu
223+
backend_server-aarch64-unknown-linux-gnu
224+
backend_server-aarch64-apple-darwin
225+
backend_server-x86_64-apple-darwin
226+
backend_server-i686-unknown-linux-gnu

README.md

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,53 @@ Local Lens uses a hybrid architecture combining the best of web and desktop tech
169169
3. Run the installer and follow the setup wizard
170170
4. Launch Local Lens from your applications menu
171171

172+
### Option 2: Running from Source (For Developers)
173+
174+
This method is for developers who want to run the latest code or contribute to the project. It enables hot-reloading for both the frontend and backend.
175+
176+
#### 1. Setup
177+
First, ensure you have all the [required software](#for-development) installed.
178+
179+
```bash
180+
# 1. Clone the repository
181+
git clone https://github.com/ashesbloom/LocalLens.git
182+
cd LocalLens
183+
184+
# 2. Set up the Python backend
185+
cd backend
186+
python -m venv venv
187+
# On Windows
188+
venv\Scripts\activate
189+
# On macOS/Linux
190+
# source venv/bin/activate
191+
pip install -r requirements.txt
192+
cd ..
193+
194+
# 3. Set up the Node.js frontend
195+
cd frontend
196+
npm install
197+
cd ..
198+
```
199+
200+
#### 2. Run the Application
201+
You will need two separate terminals to run the application in development mode.
202+
203+
**Terminal 1: Start the Backend Server**
204+
```bash
205+
cd backend
206+
# Activate your virtual environment if not already active
207+
venv\Scripts\activate
208+
# Start the server with hot-reloading
209+
uvicorn main:app --reload
210+
```
211+
The backend will be running on `http://127.0.0.1:8000`.
212+
213+
**Terminal 2: Start the Frontend Application**
214+
```bash
215+
cd frontend
216+
npm run tauri dev
217+
```
218+
This will open the Local Lens desktop application, which will automatically connect to your running backend server. Changes to the Python code will auto-reload the backend, and changes to the React code will auto-reload the frontend.
172219

173220
### For Development
174221

backend/exceptions.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
11
class OperationAbortedError(Exception):
2-
"""Custom exception to signal a user-initiated abort."""
3-
pass
2+
"""Custom exception for when a user cancels an operation."""
3+
4+
def __init__(self, message, manifest=None):
5+
super().__init__(message)
6+
self.manifest = manifest if manifest is not None else []

backend/main.py

Lines changed: 36 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ def get_app_data_dir():
7979
from organizer_logic import (
8080
process_photos, SUPPORTED_EXTENSIONS,
8181
load_face_encodings, find_and_group_photos, get_metadata_overview,
82-
initialize_libraries
82+
initialize_libraries, build_folder_tree
8383
)
8484
import organizer_logic
8585
from enrollment_logic import update_encodings
@@ -145,6 +145,7 @@ class SortRequest(BaseModel):
145145
destination_folder: str
146146
sorting_options: SortOptions
147147
ignore_list: Optional[List[str]] = []
148+
operation_mode: Optional[str] = 'move' # Add this line, default to 'move'
148149

149150
# NEW: Model for the 'Find & Group' feature configuration
150151
class FindGroupConfig(BaseModel):
@@ -160,6 +161,8 @@ class FindGroupRequest(BaseModel):
160161
destination_folder: str
161162
find_config: FindGroupConfig
162163
ignore_list: Optional[List[str]] = []
164+
# REMOVE: operation_mode is no longer needed here.
165+
# operation_mode: Optional[str] = 'copy'
163166

164167
class LastConfig(BaseModel):
165168
source_folder: Optional[str] = ""
@@ -168,6 +171,7 @@ class LastConfig(BaseModel):
168171
face_mode: Optional[str] = "balanced"
169172
maintain_hierarchy: Optional[bool] = False
170173
ignored_subfolders: Optional[List[str]] = []
174+
operation_mode: Optional[str] = "standard" # This is for UI mode, not file op
171175

172176
# NEW: Model for a single person's data in a batch.
173177
class PersonData(BaseModel):
@@ -191,6 +195,8 @@ class OpenEnrolledFolderRequest(BaseModel):
191195

192196
class SubfolderRequest(BaseModel):
193197
path: str
198+
# ADD THIS: The frontend will send the list of folders to ignore.
199+
ignore_list: Optional[List[str]] = []
194200

195201
class MetadataOverviewRequest(BaseModel):
196202
source_folder: str
@@ -366,23 +372,42 @@ async def stream_logs(request: Request):
366372

367373
@app.post("/api/list-subfolders")
368374
async def list_subfolders(request: SubfolderRequest):
369-
"""Lists subdirectories and counts files and folders in a given path."""
375+
"""
376+
MODIFIED: Lists subdirectories as a hierarchical tree and dynamically counts
377+
files and folders, respecting an ignore list of full paths.
378+
"""
370379
source_path = request.path
380+
# The ignore list now contains full paths.
381+
ignore_set = set(request.ignore_list or [])
382+
371383
if not source_path or not os.path.isdir(source_path):
372384
raise HTTPException(status_code=404, detail="Source path is not a valid directory.")
373385
try:
374-
subfolder_names = [d for d in os.listdir(source_path) if os.path.isdir(os.path.join(source_path, d))]
386+
# Build the hierarchical tree structure for the UI.
387+
folder_tree = build_folder_tree(source_path)
375388

376389
file_count = 0
377390
folder_count = 0
378391

379-
# Walk the entire directory to count files and folders
380-
for _, dirnames, filenames in os.walk(source_path):
381-
folder_count += len(dirnames)
392+
# CORRECTED LOGIC: Walk the entire directory tree to accurately count items.
393+
# This now matches the behavior of the core processing logic.
394+
for dirpath, dirnames, filenames in os.walk(source_path):
395+
# Count folders that are NOT in the ignore list.
396+
# We check this by iterating through the children of the current dirpath.
397+
for d in dirnames:
398+
if os.path.join(dirpath, d) not in ignore_set:
399+
folder_count += 1
400+
401+
# If the current directory itself is ignored, skip counting its files,
402+
# but allow os.walk to continue into its subdirectories (like 'B' inside 'A').
403+
if dirpath in ignore_set:
404+
continue
405+
406+
# If the directory is not ignored, count its supported files.
382407
file_count += len([f for f in filenames if f.lower().endswith(SUPPORTED_EXTENSIONS)])
383408

384409
return {
385-
"subfolders": subfolder_names,
410+
"subfolders": folder_tree, # Return the tree structure
386411
"stats": {
387412
"folder_count": folder_count,
388413
"file_count": file_count
@@ -398,7 +423,8 @@ async def start_sorting_endpoint(request: SortRequest, background_tasks: Backgro
398423
"source_folder": request.source_folder,
399424
"destination_folder": request.destination_folder,
400425
"sorting_options": request.sorting_options.dict(),
401-
"ignore_list": request.ignore_list or []
426+
"ignore_list": request.ignore_list or [],
427+
"operation_mode": request.operation_mode
402428
}
403429
background_tasks.add_task(run_organization_task, config)
404430
return {"message": "Organization process started successfully."}
@@ -412,7 +438,8 @@ async def start_find_group_endpoint(request: FindGroupRequest, background_tasks:
412438
"source_folder": request.source_folder,
413439
"destination_folder": request.destination_folder,
414440
"find_config": request.find_config.dict(),
415-
"ignore_list": request.ignore_list or []
441+
"ignore_list": request.ignore_list or [],
442+
# REMOVE: No longer passing operation_mode from here.
416443
}
417444
background_tasks.add_task(run_find_group_task, config)
418445
return {"message": "Find & Group process started successfully."}

0 commit comments

Comments
 (0)