Skip to content

Commit 9c32a99

Browse files
committed
Merge branch 'fix/1293-production-zoom-scroll' into test-release/pr1295-workflow
2 parents 511af91 + 3950d16 commit 9c32a99

25 files changed

Lines changed: 2040 additions & 958 deletions

File tree

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
name: Close stale merge-conflicted PRs
2+
3+
on:
4+
schedule:
5+
- cron: "17 2 * * *"
6+
workflow_dispatch:
7+
8+
permissions:
9+
contents: read
10+
issues: write
11+
pull-requests: write
12+
13+
jobs:
14+
stale-merge-conflicts:
15+
runs-on: ubuntu-latest
16+
17+
steps:
18+
- name: Warn and close inactive merge-conflicted PRs
19+
uses: actions/stale@v10
20+
with:
21+
repo-token: ${{ secrets.GITHUB_TOKEN }}
22+
23+
# Do not process issues.
24+
days-before-issue-stale: -1
25+
days-before-issue-close: -1
26+
27+
# Only process PRs that still have unresolved merge conflicts.
28+
only-pr-labels: "PR has merge conflicts"
29+
30+
# Warn after 30 inactive days, then close after 5 more inactive days.
31+
days-before-pr-stale: 30
32+
days-before-pr-close: 5
33+
34+
# Use a separate stale label so the original merge-conflict label keeps its meaning.
35+
stale-pr-label: "stale merge conflict"
36+
close-pr-label: "closed stale merge conflict"
37+
38+
# If the contributor comments or updates the PR, remove stale state and restart the timer.
39+
remove-pr-stale-when-updated: true
40+
41+
# Help process larger backlogs across daily runs.
42+
operations-per-run: 100
43+
44+
stale-pr-message: |
45+
⚠️ **This PR still has unresolved merge conflicts and has been inactive for the past 30 days.**
46+
47+
The `PR has merge conflicts` label is still applied, and there has been no recent activity on this PR.
48+
49+
Please resolve the merge conflicts or leave a comment if you are still actively working on it.
50+
51+
If there is no further activity within the next 5 days, this PR may be automatically closed to help keep the PR backlog manageable.
52+
53+
close-pr-message: |
54+
🔒 **Closing this PR due to unresolved merge conflicts and prolonged inactivity.**
55+
56+
This PR still had unresolved merge conflicts, and there was no activity within 5 days of the stale warning being posted.
57+
58+
You may reopen this PR or submit a new one after resolving the merge conflicts.

backend/app/database/images.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
# Standard library imports
22
import sqlite3
33
from typing import Any, List, Mapping, Tuple, TypedDict, Union, Optional
4+
import json
5+
46
from datetime import datetime
57

68
# App-specific imports
@@ -456,6 +458,41 @@ def db_toggle_image_favourite_status(image_id: str) -> bool:
456458
conn.close()
457459

458460

461+
def db_get_image_by_id(image_id: str) -> Optional[dict]:
462+
"""
463+
Get a single image by ID with its favorite status.
464+
"""
465+
conn = _connect()
466+
cursor = conn.cursor()
467+
try:
468+
cursor.execute(
469+
"""
470+
SELECT id, path, folder_id, thumbnailPath, metadata, isTagged, isFavourite
471+
FROM images
472+
WHERE id = ?
473+
""",
474+
(image_id,),
475+
)
476+
row = cursor.fetchone()
477+
if not row:
478+
return None
479+
try:
480+
metadata = json.loads(row[4]) if row[4] else {}
481+
except json.JSONDecodeError:
482+
metadata = {}
483+
return {
484+
"id": row[0],
485+
"path": row[1],
486+
"folder_id": row[2],
487+
"thumbnailPath": row[3],
488+
"metadata": metadata,
489+
"isTagged": bool(row[5]),
490+
"isFavourite": bool(row[6]),
491+
}
492+
finally:
493+
conn.close()
494+
495+
459496
# ============================================================================
460497
# MEMORIES FEATURE - Location and Time-based Queries
461498
# ============================================================================

backend/app/logging/setup_logging.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -237,7 +237,9 @@ def emit(self, record: logging.LogRecord) -> None:
237237
"""
238238
# Get the appropriate module name
239239
module_name = record.name
240-
if "." in module_name:
240+
if module_name.startswith("uvicorn"):
241+
module_name = "uvicorn"
242+
elif "." in module_name:
241243
module_name = module_name.split(".")[-1]
242244

243245
# Create a message that includes the original module in the format

backend/app/routes/images.py

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@
44
from app.schemas.images import ErrorResponse
55
from app.utils.images import image_util_parse_metadata
66
from pydantic import BaseModel
7-
from app.database.images import db_toggle_image_favourite_status
7+
from app.database.images import db_toggle_image_favourite_status, db_get_image_by_id
88
from app.logging.setup_logging import get_logger
99

10+
1011
# Initialize logger
1112
logger = get_logger(__name__)
1213
router = APIRouter()
@@ -97,26 +98,37 @@ class ToggleFavouriteRequest(BaseModel):
9798

9899
@router.post("/toggle-favourite")
99100
def toggle_favourite(req: ToggleFavouriteRequest):
101+
"""
102+
Toggle the favorite status of an image.
103+
"""
100104
image_id = req.image_id
101105
try:
102106
success = db_toggle_image_favourite_status(image_id)
103107
if not success:
104108
raise HTTPException(
105-
status_code=404, detail="Image not found or failed to toggle"
109+
status_code=status.HTTP_404_NOT_FOUND,
110+
detail="Image not found or failed to toggle",
106111
)
107112
# Fetch updated status to return
108-
image = next(
109-
(img for img in db_get_all_images() if img["id"] == image_id), None
110-
)
113+
image = db_get_image_by_id(image_id)
114+
if not image:
115+
raise HTTPException(
116+
status_code=status.HTTP_404_NOT_FOUND,
117+
detail="Image not found after toggle",
118+
)
111119
return {
112120
"success": True,
113121
"image_id": image_id,
114122
"isFavourite": image.get("isFavourite", False),
115123
}
116-
124+
except HTTPException:
125+
raise # Re-raise HTTPExceptions to preserve status codes
117126
except Exception as e:
118127
logger.error(f"error in /toggle-favourite route: {e}")
119-
raise HTTPException(status_code=500, detail=f"Internal server error: {e}")
128+
raise HTTPException(
129+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
130+
detail=f"Internal server error: {e}",
131+
)
120132

121133

122134
class ImageInfoResponse(BaseModel):

docs/backend/backend_python/openapi.json

Lines changed: 0 additions & 113 deletions
Original file line numberDiff line numberDiff line change
@@ -2073,119 +2073,6 @@
20732073
],
20742074
"title": "DeleteFoldersResponse"
20752075
},
2076-
"app__schemas__folders__ErrorResponse": {
2077-
"properties": {
2078-
"success": {
2079-
"type": "boolean",
2080-
"title": "Success",
2081-
"default": false
2082-
},
2083-
"message": {
2084-
"anyOf": [
2085-
{
2086-
"type": "string"
2087-
},
2088-
{
2089-
"type": "null"
2090-
}
2091-
],
2092-
"title": "Message"
2093-
},
2094-
"error": {
2095-
"anyOf": [
2096-
{
2097-
"type": "string"
2098-
},
2099-
{
2100-
"type": "null"
2101-
}
2102-
],
2103-
"title": "Error"
2104-
}
2105-
},
2106-
"type": "object",
2107-
"title": "ErrorResponse"
2108-
},
2109-
"app__schemas__face_clusters__ErrorResponse": {
2110-
"properties": {
2111-
"success": {
2112-
"type": "boolean",
2113-
"title": "Success",
2114-
"default": false
2115-
},
2116-
"message": {
2117-
"anyOf": [
2118-
{
2119-
"type": "string"
2120-
},
2121-
{
2122-
"type": "null"
2123-
}
2124-
],
2125-
"title": "Message"
2126-
},
2127-
"error": {
2128-
"anyOf": [
2129-
{
2130-
"type": "string"
2131-
},
2132-
{
2133-
"type": "null"
2134-
}
2135-
],
2136-
"title": "Error"
2137-
}
2138-
},
2139-
"type": "object",
2140-
"title": "ErrorResponse"
2141-
},
2142-
"app__schemas__images__ErrorResponse": {
2143-
"properties": {
2144-
"success": {
2145-
"type": "boolean",
2146-
"title": "Success",
2147-
"default": false
2148-
},
2149-
"message": {
2150-
"type": "string",
2151-
"title": "Message"
2152-
},
2153-
"error": {
2154-
"type": "string",
2155-
"title": "Error"
2156-
}
2157-
},
2158-
"type": "object",
2159-
"required": [
2160-
"message",
2161-
"error"
2162-
],
2163-
"title": "ErrorResponse"
2164-
},
2165-
"app__schemas__user_preferences__ErrorResponse": {
2166-
"properties": {
2167-
"success": {
2168-
"type": "boolean",
2169-
"title": "Success"
2170-
},
2171-
"error": {
2172-
"type": "string",
2173-
"title": "Error"
2174-
},
2175-
"message": {
2176-
"type": "string",
2177-
"title": "Message"
2178-
}
2179-
},
2180-
"type": "object",
2181-
"required": [
2182-
"success",
2183-
"error",
2184-
"message"
2185-
],
2186-
"title": "ErrorResponse",
2187-
"description": "Error response model"
2188-
},
21892076
"FaceSearchRequest": {
21902077
"properties": {
21912078
"path": {

docs/frontend/gallery-view.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# Gallery View
2+
3+
The gallery view is the main way users browse and interact with their photos in PictoPy. It presents media in a responsive grid, organized by date, with filtering and AI-backed organization.
4+
5+
## Overview
6+
7+
The main gallery shows photos and videos in a grid layout. Images are grouped by date, and users can filter by tags, favourites, and other criteria. The gallery connects to the [state management](state-management.md) layer for images and folders, and uses shared [UI components](ui-components.md) for layout and controls.
8+
9+
## Layout and Behavior
10+
11+
- **Grid layout** – Media is displayed in a responsive grid that adapts to window size.
12+
- **Date grouping** – Items are organized chronologically (by capture or file date) for quick scanning.
13+
- **Filtering** – Users can filter by tagged status, favourites, and other attributes exposed in the UI.
14+
- **Thumbnails** – Each item is shown as a thumbnail with optional overlays (e.g. favourite icon, AI tags).
15+
16+
## Key Components
17+
18+
The gallery experience is built from several frontend components:
19+
20+
- **ChronologicalGallery** – Renders the date-grouped grid of media.
21+
- **ImageCard** – Renders a single thumbnail, metadata hints, and interaction targets (e.g. select, favourite).
22+
- **MediaView** / **ImageViewer** – Full-size view and navigation when a photo or video is opened.
23+
- **MediaThumbnails** – Thumbnail strip or grid used in the lightbox/viewer.
24+
- **MediaInfoPanel** – Shows metadata, tags, and location for the currently viewed item.
25+
- **MediaViewControls**, **ZoomControls**, **NavigationButtons** – Control zoom, next/previous, and other viewer actions.
26+
27+
## Screenshots
28+
29+
For visual examples of the main gallery, AI tagging, and related screens, see [Screenshots](screenshots.md).
30+
31+
## Related Documentation
32+
33+
- [State Management](state-management.md) – How images and folders are stored and updated in the Redux store.
34+
- [Screenshots](screenshots.md) – Screenshots of the main gallery and other views.
35+
- [UI Components](ui-components.md) – Shared components used in the gallery and across the app.

0 commit comments

Comments
 (0)