feat(viewer): persist theme/sort via toggle view; use middleware-provided theme/sort in views and templates
This commit is contained in:
@@ -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,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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"),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user