diff --git a/viewer/common.py b/viewer/common.py index b2a57b9..a150015 100644 --- a/viewer/common.py +++ b/viewer/common.py @@ -170,16 +170,22 @@ def build_sort_options(request, search_text, sort_key, theme): 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 != "": + # Include search top-level so templates/tests can assert its presence query["search"] = search_text options.append( { "key": option_key, "label": label, - "url": append_query(request.path, query), + "url": append_query("/gallery/toggle-settings/", query), "is_active": option_key == sort_key, } ) diff --git a/viewer/directory.py b/viewer/directory.py index 7bdd841..5221814 100644 --- a/viewer/directory.py +++ b/viewer/directory.py @@ -16,6 +16,7 @@ from .common import ( normalize_theme, build_query, gallery_url, + append_query, sort_images, build_sort_options, 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. """ + # 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() - sort_key = normalize_sort(request.GET.get("sort", "abc")) - theme = normalize_theme(request.GET.get("theme", "dark")) + sort_key = normalize_sort( + 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) try: @@ -130,10 +138,18 @@ def render_directory(request, path_text, full_path): ) 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 != "": - theme_query["search"] = search_text + theme_toggle_query["search"] = search_text 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), "images": image_data, "subdirs": subdir_data, - "theme_toggle_url": gallery_url( - Path(path_text) if path_text != "" else None, True, theme_query + "theme_toggle_url": append_query( + "/gallery/toggle-settings/", theme_toggle_query ), "search_action_url": gallery_url( Path(path_text) if path_text != "" else None, True, search_action_query diff --git a/viewer/image.py b/viewer/image.py index 4b97bbf..75ed9f1 100644 --- a/viewer/image.py +++ b/viewer/image.py @@ -23,6 +23,7 @@ from .common import ( normalize_theme, build_query, gallery_url, + append_query, sort_images, build_sort_options, build_breadcrumbs, @@ -40,7 +41,7 @@ def render_image(request, path_text, full_path): try: img = Im.objects.get(path=full_path, user=request.user) - if request.method == 'POST': + if request.method == "POST": img.favorite = not img.favorite except Im.DoesNotExist: @@ -51,10 +52,16 @@ def render_image(request, path_text, full_path): img.visits = img.visits + 1 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() - sort_key = normalize_sort(request.GET.get("sort", "abc")) - theme = normalize_theme(request.GET.get("theme", "dark")) + sort_key = normalize_sort( + 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) 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}) 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 != "": - theme_query["search"] = search_text + theme_query["next"] = request.get_full_path() context = { "image_path": image, @@ -196,16 +207,14 @@ def render_image(request, path_text, full_path): "modified": fmt_ts(modified_ts), "visits": img.visits, "visited": fmt_ts(img.last_visited.timestamp()), - "favorite": img.favorite + "favorite": img.favorite, }, "breadcrumbs": breadcrumbs, "theme": theme, "sort_key": sort_key, "sort_label": "", "sort_options": build_sort_options(request, search_text, sort_key, theme), - "theme_toggle_url": gallery_url( - Path(dir_path_text) if dir_path_text != "" else None, True, theme_query - ), + "theme_toggle_url": append_query("/gallery/toggle-settings/", theme_query), "path": path_text, } diff --git a/viewer/urls.py b/viewer/urls.py index dbf9907..ab6a97a 100644 --- a/viewer/urls.py +++ b/viewer/urls.py @@ -2,9 +2,7 @@ from django.urls import path # Module imports -from .views import ( - gallery_view -) +from .views import gallery_view, toggle_settings ########################################################################################### # URL Patterns. # @@ -13,6 +11,7 @@ from .views import ( urlpatterns = [ # Views. - path('', gallery_view, name = 'gallery_view_root'), - path('/', gallery_view, name = 'gallery_view_path'), + path("", gallery_view, name="gallery_view_root"), + path("toggle-settings/", toggle_settings, name="toggle_settings"), + path("/", gallery_view, name="gallery_view_path"), ] diff --git a/viewer/views.py b/viewer/views.py index 5908024..b14c9cd 100644 --- a/viewer/views.py +++ b/viewer/views.py @@ -1,19 +1,24 @@ """Top-level views module. 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. +from urllib.parse import urlparse from django.http import HttpResponseNotFound from django.conf import settings from django.contrib.auth.decorators import login_required from django.shortcuts import redirect +from django.core.exceptions import MultipleObjectsReturned # Local helpers split into modules from .directory import render_directory from .image import render_image +from .models import UserSettings + @login_required def index(request): @@ -44,3 +49,51 @@ def gallery_view(request, path=None): return render_directory(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)