feat(viewer): persist theme/sort via toggle view; use middleware-provided theme/sort in views and templates

This commit is contained in:
Miguel Astor
2026-03-25 04:43:35 -04:00
parent 8719be3227
commit 73b538b698
5 changed files with 107 additions and 24 deletions

View File

@@ -170,16 +170,22 @@ def build_sort_options(request, search_text, sort_key, theme):
options = [] options = []
for option_key, label in SORT_OPTIONS: for option_key, label in SORT_OPTIONS:
query = {"sort": option_key, "theme": theme} # Build a URL that points to the settings toggle endpoint which will
# persist the chosen sort (and optionally theme) in the user's
# UserSettings and then redirect back to the current view. Include
# the current full path as the `next` parameter so the toggle view
# can return to the same page.
query = {"next": request.get_full_path(), "sort": option_key, "theme": theme}
if search_text != "": if search_text != "":
# Include search top-level so templates/tests can assert its presence
query["search"] = search_text query["search"] = search_text
options.append( options.append(
{ {
"key": option_key, "key": option_key,
"label": label, "label": label,
"url": append_query(request.path, query), "url": append_query("/gallery/toggle-settings/", query),
"is_active": option_key == sort_key, "is_active": option_key == sort_key,
} }
) )

View File

@@ -16,6 +16,7 @@ from .common import (
normalize_theme, normalize_theme,
build_query, build_query,
gallery_url, gallery_url,
append_query,
sort_images, sort_images,
build_sort_options, build_sort_options,
build_breadcrumbs, build_breadcrumbs,
@@ -68,9 +69,16 @@ def render_directory(request, path_text, full_path):
in the file system, or logical gallery directories like search result pages. in the file system, or logical gallery directories like search result pages.
""" """
# Search remains a GET parameter. For sort and theme prefer explicit
# GET parameters when present (so query-preserving links behave as
# callers expect), otherwise fall back to middleware-provided settings.
search_text = request.GET.get("search", "").strip() search_text = request.GET.get("search", "").strip()
sort_key = normalize_sort(request.GET.get("sort", "abc")) sort_key = normalize_sort(
theme = normalize_theme(request.GET.get("theme", "dark")) request.GET.get("sort") or getattr(request, "sort", None) or "abc"
)
theme = normalize_theme(
request.GET.get("theme") or getattr(request, "theme", None) or "dark"
)
query_state = build_query(search_text, sort_key, theme) query_state = build_query(search_text, sort_key, theme)
try: try:
@@ -130,10 +138,18 @@ def render_directory(request, path_text, full_path):
) )
next_theme = "light" if theme == "dark" else "dark" next_theme = "light" if theme == "dark" else "dark"
theme_query = {"sort": sort_key, "theme": next_theme}
# The theme toggle button now calls the persistent settings endpoint.
# Include `next` (for redirect back) and also surface `sort` and `search`
# as top-level query parameters so templates/tests can inspect them easily.
theme_toggle_query = {
"next": request.get_full_path(),
"theme": next_theme,
"sort": sort_key,
}
if search_text != "": if search_text != "":
theme_query["search"] = search_text theme_toggle_query["search"] = search_text
search_action_query = {"sort": sort_key, "theme": theme} search_action_query = {"sort": sort_key, "theme": theme}
@@ -147,8 +163,8 @@ def render_directory(request, path_text, full_path):
"breadcrumbs": build_breadcrumbs(path_text, query_state), "breadcrumbs": build_breadcrumbs(path_text, query_state),
"images": image_data, "images": image_data,
"subdirs": subdir_data, "subdirs": subdir_data,
"theme_toggle_url": gallery_url( "theme_toggle_url": append_query(
Path(path_text) if path_text != "" else None, True, theme_query "/gallery/toggle-settings/", theme_toggle_query
), ),
"search_action_url": gallery_url( "search_action_url": gallery_url(
Path(path_text) if path_text != "" else None, True, search_action_query Path(path_text) if path_text != "" else None, True, search_action_query

View File

@@ -23,6 +23,7 @@ from .common import (
normalize_theme, normalize_theme,
build_query, build_query,
gallery_url, gallery_url,
append_query,
sort_images, sort_images,
build_sort_options, build_sort_options,
build_breadcrumbs, build_breadcrumbs,
@@ -40,7 +41,7 @@ def render_image(request, path_text, full_path):
try: try:
img = Im.objects.get(path=full_path, user=request.user) img = Im.objects.get(path=full_path, user=request.user)
if request.method == 'POST': if request.method == "POST":
img.favorite = not img.favorite img.favorite = not img.favorite
except Im.DoesNotExist: except Im.DoesNotExist:
@@ -51,10 +52,16 @@ def render_image(request, path_text, full_path):
img.visits = img.visits + 1 img.visits = img.visits + 1
img.save() img.save()
# Preserve query state (sort, search, theme) similar to gallery view # Search remains a GET parameter. For sort and theme prefer explicit
# GET parameters when present, otherwise fall back to middleware-provided
# settings on the request.
search_text = request.GET.get("search", "").strip() search_text = request.GET.get("search", "").strip()
sort_key = normalize_sort(request.GET.get("sort", "abc")) sort_key = normalize_sort(
theme = normalize_theme(request.GET.get("theme", "dark")) request.GET.get("sort") or getattr(request, "sort", None) or "abc"
)
theme = normalize_theme(
request.GET.get("theme") or getattr(request, "theme", None) or "dark"
)
query_state = build_query(search_text, sort_key, theme) query_state = build_query(search_text, sort_key, theme)
image = Path("/imgs/").joinpath(path_text) image = Path("/imgs/").joinpath(path_text)
@@ -169,9 +176,13 @@ def render_image(request, path_text, full_path):
breadcrumbs.append({"label": full_path.name, "path": None}) breadcrumbs.append({"label": full_path.name, "path": None})
next_theme = "light" if theme == "dark" else "dark" next_theme = "light" if theme == "dark" else "dark"
theme_query = {"sort": sort_key, "theme": next_theme} theme_query = {
"next": request.get_full_path(),
"sort": sort_key,
"theme": next_theme,
}
if search_text != "": if search_text != "":
theme_query["search"] = search_text theme_query["next"] = request.get_full_path()
context = { context = {
"image_path": image, "image_path": image,
@@ -196,16 +207,14 @@ def render_image(request, path_text, full_path):
"modified": fmt_ts(modified_ts), "modified": fmt_ts(modified_ts),
"visits": img.visits, "visits": img.visits,
"visited": fmt_ts(img.last_visited.timestamp()), "visited": fmt_ts(img.last_visited.timestamp()),
"favorite": img.favorite "favorite": img.favorite,
}, },
"breadcrumbs": breadcrumbs, "breadcrumbs": breadcrumbs,
"theme": theme, "theme": theme,
"sort_key": sort_key, "sort_key": sort_key,
"sort_label": "", "sort_label": "",
"sort_options": build_sort_options(request, search_text, sort_key, theme), "sort_options": build_sort_options(request, search_text, sort_key, theme),
"theme_toggle_url": gallery_url( "theme_toggle_url": append_query("/gallery/toggle-settings/", theme_query),
Path(dir_path_text) if dir_path_text != "" else None, True, theme_query
),
"path": path_text, "path": path_text,
} }

View File

@@ -2,9 +2,7 @@
from django.urls import path from django.urls import path
# Module imports # Module imports
from .views import ( from .views import gallery_view, toggle_settings
gallery_view
)
########################################################################################### ###########################################################################################
# URL Patterns. # # URL Patterns. #
@@ -13,6 +11,7 @@ from .views import (
urlpatterns = [ urlpatterns = [
# Views. # Views.
path('', gallery_view, name = 'gallery_view_root'), path("", gallery_view, name="gallery_view_root"),
path('<path:path>/', gallery_view, name = 'gallery_view_path'), path("toggle-settings/", toggle_settings, name="toggle_settings"),
path("<path:path>/", gallery_view, name="gallery_view_path"),
] ]

View File

@@ -1,19 +1,24 @@
"""Top-level views module. """Top-level views module.
After refactor this file only keeps the minimal public view entry points and imports After refactor this file only keeps the minimal public view entry points and imports
helpers from the new `directory` and `image` modules. helpers from the new `directory` and `image` modules. Also provides the
`toggle_settings` view used by template buttons to persist theme/sort.
""" """
# Django imports. # Django imports.
from urllib.parse import urlparse
from django.http import HttpResponseNotFound from django.http import HttpResponseNotFound
from django.conf import settings from django.conf import settings
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.shortcuts import redirect from django.shortcuts import redirect
from django.core.exceptions import MultipleObjectsReturned
# Local helpers split into modules # Local helpers split into modules
from .directory import render_directory from .directory import render_directory
from .image import render_image from .image import render_image
from .models import UserSettings
@login_required @login_required
def index(request): def index(request):
@@ -44,3 +49,51 @@ def gallery_view(request, path=None):
return render_directory(request, path_text, candidate) return render_directory(request, path_text, candidate)
return render_image(request, path_text, candidate) return render_image(request, path_text, candidate)
@login_required
def toggle_settings(request):
"""Persist theme and/or sort for the current user and redirect back.
Expected query params:
- next: the URL to redirect back to (optional)
- theme: optional, 'light' or 'dark'
- sort: optional, one of allowed sort keys
The view will obtain or create the UserSettings row for the user and set
any provided values. If multiple UserSettings rows exist (shouldn't
normally happen) the first is used.
"""
next_url = request.GET.get("next") or "/gallery/"
# Only allow in-site redirects for safety
parsed = urlparse(next_url)
if parsed.netloc and parsed.netloc != "":
next_url = "/gallery/"
user = getattr(request, "user", None)
if not user or not getattr(user, "is_authenticated", False):
return redirect(next_url)
# Obtain or create the settings row
try:
settings_obj = UserSettings.objects.get(user=user)
except UserSettings.DoesNotExist:
settings_obj = UserSettings(user=user)
except MultipleObjectsReturned:
settings_obj = UserSettings.objects.filter(user=user).first()
# Apply provided values
theme = request.GET.get("theme")
sort = request.GET.get("sort")
if theme in ("light", "dark"):
settings_obj.theme = theme
if sort:
settings_obj.sort = sort
settings_obj.save()
return redirect(next_url)