special galleries: helper module, renderers, templates & urls; hide back in special views; highlight active special
This commit is contained in:
BIN
viewer/.views.py.kate-swp
Normal file
BIN
viewer/.views.py.kate-swp
Normal file
Binary file not shown.
@@ -24,6 +24,8 @@ from .common import (
|
||||
is_image_file,
|
||||
make_thumbnail,
|
||||
)
|
||||
from .specials import get_special_paths, special_name
|
||||
from .models import Image as ImModel
|
||||
|
||||
|
||||
def do_recursive_search(start_path, query):
|
||||
@@ -63,7 +65,7 @@ def do_recursive_search(start_path, query):
|
||||
return subdirs, images
|
||||
|
||||
|
||||
def render_directory(request, path_text, full_path):
|
||||
def render_directory(request, path_text, full_path, special=None):
|
||||
"""
|
||||
Renders the gallery view related to directories, be it the contents of an actual directory
|
||||
in the file system, or logical gallery directories like search result pages.
|
||||
@@ -81,6 +83,17 @@ def render_directory(request, path_text, full_path):
|
||||
)
|
||||
query_state = build_query(search_text)
|
||||
|
||||
# If rendering a logical special gallery, obtain the image list from the
|
||||
# Image model and skip filesystem subdir enumeration.
|
||||
if special is not None:
|
||||
# build images using the helper that returns Path objects
|
||||
images = get_special_paths(request.user, special)
|
||||
# Respect the client's sort_key only if the special is favorites
|
||||
# otherwise the ordering comes from the DB query (most-visited/recent).
|
||||
if special == "favorites":
|
||||
images = sort_images(images, sort_key)
|
||||
current_subdirs = []
|
||||
else:
|
||||
try:
|
||||
current_entries = [
|
||||
entry for entry in full_path.iterdir() if not entry.is_symlink()
|
||||
@@ -162,6 +175,11 @@ def render_directory(request, path_text, full_path):
|
||||
except Exception:
|
||||
dir_text = ""
|
||||
|
||||
# For special galleries hide the Back control but still present Home
|
||||
if special is not None:
|
||||
back_url = None
|
||||
back_thumb = None
|
||||
else:
|
||||
back_url = gallery_url(
|
||||
Path(dir_text) if dir_text != "" else None, True, query_state
|
||||
)
|
||||
@@ -194,6 +212,20 @@ def render_directory(request, path_text, full_path):
|
||||
),
|
||||
}
|
||||
|
||||
# When rendering a special gallery adjust breadcrumbs and hide search
|
||||
# and back controls. Use `is_special` and `special_name` in the template.
|
||||
if special is not None:
|
||||
context["is_special"] = True
|
||||
context["special_name"] = special_name(special)
|
||||
# expose which special is active so templates can highlight it
|
||||
context["active_special"] = special
|
||||
# Override breadcrumbs for special galleries to be a single label
|
||||
context["breadcrumbs"] = [{"label": context["special_name"], "path": None}]
|
||||
# Hide the search box (templates use `is_special` to decide)
|
||||
context["search_text"] = ""
|
||||
context["search_action_url"] = ""
|
||||
context["clear_search_url"] = ""
|
||||
|
||||
# sort_label depends on SORT_LABELS in common; import lazily to avoid circulars
|
||||
from .common import SORT_LABELS
|
||||
|
||||
|
||||
@@ -31,9 +31,10 @@ from .common import (
|
||||
is_image_file,
|
||||
make_thumbnail,
|
||||
)
|
||||
from .specials import get_special_paths, special_name
|
||||
|
||||
|
||||
def render_image(request, path_text, full_path):
|
||||
def render_image(request, path_text, full_path, special=None):
|
||||
"""
|
||||
Renders the view corresponding to an image file.
|
||||
"""
|
||||
@@ -67,6 +68,13 @@ def render_image(request, path_text, full_path):
|
||||
image = Path("/imgs/").joinpath(path_text)
|
||||
img_dir = full_path.parent
|
||||
|
||||
if special is not None:
|
||||
# Use the special gallery ordering for prev/next navigation
|
||||
images_sorted = get_special_paths(request.user, special)
|
||||
# If favorites, respect client sort; otherwise keep DB ordering
|
||||
if special == "favorites":
|
||||
images_sorted = sort_images(images_sorted, sort_key)
|
||||
else:
|
||||
try:
|
||||
entries = [
|
||||
entry
|
||||
@@ -79,7 +87,7 @@ def render_image(request, path_text, full_path):
|
||||
# Sort siblings according to requested sort mode
|
||||
images_sorted = sort_images(entries, sort_key)
|
||||
|
||||
# Find index of current image
|
||||
# Find index of current image within the resolved list
|
||||
try:
|
||||
index = next(i for i, p in enumerate(images_sorted) if p.name == full_path.name)
|
||||
except StopIteration:
|
||||
@@ -122,6 +130,11 @@ def render_image(request, path_text, full_path):
|
||||
except Exception:
|
||||
dir_text = ""
|
||||
|
||||
# For special galleries hide the Back control but still present Home
|
||||
if special is not None:
|
||||
back_url = None
|
||||
back_thumb = None
|
||||
else:
|
||||
back_url = gallery_url(
|
||||
Path(dir_text) if dir_text != "" else None, True, query_state
|
||||
)
|
||||
@@ -169,7 +182,15 @@ def render_image(request, path_text, full_path):
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
# Breadcrumbs: include directory breadcrumbs then append file name as label-only
|
||||
# Breadcrumbs
|
||||
if special is not None:
|
||||
# SPECIAL NAME / IMAGE
|
||||
breadcrumbs = [
|
||||
{"label": special_name(special), "path": None},
|
||||
{"label": full_path.name, "path": None},
|
||||
]
|
||||
else:
|
||||
# include directory breadcrumbs then append file name as label-only
|
||||
# build_breadcrumbs expects a path_text representing directories only
|
||||
dir_path_text = dir_text if dir_text != "" else ""
|
||||
breadcrumbs = build_breadcrumbs(dir_path_text, query_state)
|
||||
@@ -216,6 +237,7 @@ def render_image(request, path_text, full_path):
|
||||
"sort_options": build_sort_options(request, search_text, sort_key, theme),
|
||||
"theme_toggle_url": append_query("/gallery/toggle-settings/", theme_query),
|
||||
"path": path_text,
|
||||
"is_special": special is not None,
|
||||
}
|
||||
|
||||
from .common import SORT_LABELS
|
||||
|
||||
71
viewer/specials.py
Normal file
71
viewer/specials.py
Normal file
@@ -0,0 +1,71 @@
|
||||
"""
|
||||
Helpers for assembling "special" galleries (favorites, most-visited, recent).
|
||||
These functions return filesystem Path objects suitable for use by the
|
||||
existing directory and image renderers.
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
from .models import Image as ImModel
|
||||
|
||||
|
||||
SPECIALS = {
|
||||
"favorites": "Favorites",
|
||||
"most-visited": "Most Visited",
|
||||
"recent": "Recent",
|
||||
}
|
||||
|
||||
|
||||
def special_name(key):
|
||||
return SPECIALS.get(key, key)
|
||||
|
||||
|
||||
def special_root_url(key, query_dict=None):
|
||||
# Build a URL for the special gallery root (e.g. /gallery/favorites/)
|
||||
base = f"/gallery/{key}/"
|
||||
if not query_dict:
|
||||
return base
|
||||
from urllib.parse import urlencode
|
||||
|
||||
return base + "?" + urlencode(query_dict)
|
||||
|
||||
|
||||
def get_special_paths(user, key, limit=100):
|
||||
"""Return a list of pathlib.Path objects for the requested special gallery.
|
||||
|
||||
Only include files that actually exist on disk. The returned list is
|
||||
ordered according to the special gallery semantics.
|
||||
"""
|
||||
|
||||
qs = ImModel.objects.filter(user=user)
|
||||
|
||||
if key == "favorites":
|
||||
qs = qs.filter(favorite=True).order_by("-last_visited")
|
||||
elif key == "most-visited":
|
||||
qs = qs.order_by("-visits")
|
||||
elif key == "recent":
|
||||
qs = qs.order_by("-last_visited")
|
||||
else:
|
||||
return []
|
||||
|
||||
if key in ("most-visited", "recent"):
|
||||
qs = qs[:limit]
|
||||
|
||||
paths = []
|
||||
for row in qs:
|
||||
try:
|
||||
p = Path(row.path)
|
||||
# ensure the stored path is inside GALLERY_ROOT for safety
|
||||
try:
|
||||
p.relative_to(settings.GALLERY_ROOT)
|
||||
except Exception:
|
||||
# skip paths outside the gallery root
|
||||
continue
|
||||
if p.exists() and p.is_file():
|
||||
paths.append(p)
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
return paths
|
||||
@@ -29,6 +29,7 @@
|
||||
|
||||
<hr>
|
||||
|
||||
{% if not is_special %}
|
||||
<form action="{{ search_action_url }}" method="get">
|
||||
<div class="input-group input-group-sm">
|
||||
<input type="search" name="search" class="form-control search-input" placeholder="Search..." value="{{ search_text }}">
|
||||
@@ -40,12 +41,13 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
</form>
|
||||
{% endif %}
|
||||
|
||||
<hr>
|
||||
|
||||
<a href="#" class="sidebar-link">Favorites</a>
|
||||
<a href="#" class="sidebar-link">Most visited</a>
|
||||
<a href="#" class="sidebar-link">Recently visited</a>
|
||||
<a href="/gallery/favorites/" class="sidebar-link {% if active_special == 'favorites' %}active-sort{% endif %}">Favorites</a>
|
||||
<a href="/gallery/most-visited/" class="sidebar-link {% if active_special == 'most-visited' %}active-sort{% endif %}">Most visited</a>
|
||||
<a href="/gallery/recent/" class="sidebar-link {% if active_special == 'recent' %}active-sort{% endif %}">Recently visited</a>
|
||||
|
||||
{% if path != '' %}
|
||||
<hr>
|
||||
@@ -112,18 +114,20 @@
|
||||
|
||||
<hr>
|
||||
|
||||
{% if not is_special %}
|
||||
<form action="{{ search_action_url }}" method="get">
|
||||
<div class="input-group input-group-sm">
|
||||
<input type="search" name="search" class="form-control search-input" placeholder="Search..." value="{{ search_text }}">
|
||||
<span class="input-group-text search-addon"><i class="fa-solid fa-magnifying-glass"></i></span>
|
||||
</div>
|
||||
</form>
|
||||
{% endif %}
|
||||
|
||||
<hr>
|
||||
|
||||
<a href="#" class="sidebar-link">Favorites</a>
|
||||
<a href="#" class="sidebar-link">Most visited</a>
|
||||
<a href="#" class="sidebar-link">Recently visited</a>
|
||||
<a href="/gallery/favorites/" class="sidebar-link">Favorites</a>
|
||||
<a href="/gallery/most-visited/" class="sidebar-link">Most visited</a>
|
||||
<a href="/gallery/recent/" class="sidebar-link">Recently visited</a>
|
||||
|
||||
{% if path != '' %}
|
||||
<hr>
|
||||
@@ -186,7 +190,11 @@
|
||||
{% if not forloop.first %}
|
||||
<span class="crumb-sep">/</span>
|
||||
{% endif %}
|
||||
{% if crumb.path %}
|
||||
<a href="{{ crumb.path }}" class="{% if not forloop.last %}crumb-link{% else %}crumb-link-last{% endif %}">{{ crumb.label|truncatechars:45 }}</a>
|
||||
{% else %}
|
||||
<span class="{% if not forloop.last %}crumb-link{% else %}crumb-link-last{% endif %}">{{ crumb.label|truncatechars:45 }}</span>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -29,9 +29,9 @@
|
||||
|
||||
<hr>
|
||||
|
||||
<a href="#" class="sidebar-link">Favorites</a>
|
||||
<a href="#" class="sidebar-link">Most visited</a>
|
||||
<a href="#" class="sidebar-link">Recently visited</a>
|
||||
<a href="/gallery/favorites/" class="sidebar-link {% if is_special and breadcrumbs.0.label == 'Favorites' %}active-sort{% endif %}">Favorites</a>
|
||||
<a href="/gallery/most-visited/" class="sidebar-link {% if is_special and breadcrumbs.0.label == 'Most Visited' %}active-sort{% endif %}">Most visited</a>
|
||||
<a href="/gallery/recent/" class="sidebar-link {% if is_special and breadcrumbs.0.label == 'Recent' %}active-sort{% endif %}">Recently visited</a>
|
||||
|
||||
<hr>
|
||||
|
||||
@@ -110,9 +110,9 @@
|
||||
<span class="crumb-sep">/</span>
|
||||
{% endif %}
|
||||
{% if crumb.path %}
|
||||
<a href="{{ crumb.path }}" class="crumb-link">{{ crumb.label }}</a>
|
||||
<a href="{{ crumb.path }}" class="{% if not forloop.last %}crumb-link{% else %}crumb-link-last{% endif %}">{{ crumb.label }}</a>
|
||||
{% else %}
|
||||
<span class="crumb-link-full">{{ crumb.label }}</span>
|
||||
<span class="{% if not forloop.last %}crumb-link{% else %}crumb-link-last{% endif %}">{{ crumb.label }}</span>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# Django imports
|
||||
from django.urls import path
|
||||
from django.urls import path, re_path
|
||||
|
||||
# Module imports
|
||||
from .views import gallery_view, toggle_settings
|
||||
from .views import gallery_view, toggle_settings, special_gallery_view
|
||||
|
||||
###########################################################################################
|
||||
# URL Patterns. #
|
||||
@@ -13,5 +13,17 @@ urlpatterns = [
|
||||
# Views.
|
||||
path("", gallery_view, name="gallery_view_root"),
|
||||
path("toggle-settings/", toggle_settings, name="toggle_settings"),
|
||||
# Special gallery routes (explicit list to avoid clashing with regular paths)
|
||||
re_path(
|
||||
r"^(?P<gallery>favorites|most-visited|recent)/(?P<path>.*)/$",
|
||||
special_gallery_view,
|
||||
name="special_gallery_view_path",
|
||||
),
|
||||
re_path(
|
||||
r"^(?P<gallery>favorites|most-visited|recent)/$",
|
||||
special_gallery_view,
|
||||
name="special_gallery_view_root",
|
||||
),
|
||||
# Generic gallery path (catch-all for filesystem paths)
|
||||
path("<path:path>/", gallery_view, name="gallery_view_path"),
|
||||
]
|
||||
|
||||
@@ -20,6 +20,9 @@ from .image import render_image
|
||||
from .models import UserSettings
|
||||
|
||||
|
||||
SPECIALS = ['favorites', 'most-visited', 'recent']
|
||||
|
||||
|
||||
@login_required
|
||||
def index(request):
|
||||
return redirect("gallery_view_root")
|
||||
@@ -51,6 +54,46 @@ def gallery_view(request, path=None):
|
||||
return render_image(request, path_text, candidate)
|
||||
|
||||
|
||||
@login_required
|
||||
def special_gallery_view(request, gallery, path=None):
|
||||
"""
|
||||
Shows a list of subdirectories and image files inside the given special gallery.
|
||||
Available special galleries are: (1) favorites; (2) most visited; (3) recently visited.
|
||||
"""
|
||||
|
||||
root = settings.GALLERY_ROOT.resolve()
|
||||
|
||||
if gallery not in SPECIALS:
|
||||
return HttpResponseNotFound("Not found")
|
||||
|
||||
try:
|
||||
candidate = root.joinpath(path).resolve() if path is not None else root
|
||||
candidate.relative_to(root)
|
||||
except Exception:
|
||||
return HttpResponseNotFound("Not found")
|
||||
|
||||
path_text = path if path is not None else ""
|
||||
|
||||
if path is not None:
|
||||
# Special galleries only have a logical root directory.
|
||||
# When there is a valid sub-directory path inside the gallery root
|
||||
# in the request then redirect to the base special gallery.
|
||||
if candidate.is_dir():
|
||||
return redirect('special_gallery_view_path', gallery, None, permanent=True)
|
||||
|
||||
else:
|
||||
# When there is no path to render, then go to the corresponding special gallery root.
|
||||
return render_directory(request, path_text, candidate, gallery)
|
||||
|
||||
# Fail if the requested image doesn' exist.
|
||||
if not candidate.exists():
|
||||
return HttpResponseNotFound("Not found")
|
||||
|
||||
# Images are rendered normally, with a control to ensure the back, next and previous buttons
|
||||
# use the special gallery.
|
||||
return render_image(request, path_text, candidate, gallery)
|
||||
|
||||
|
||||
@login_required
|
||||
def toggle_settings(request):
|
||||
"""Persist theme and/or sort for the current user and redirect back.
|
||||
|
||||
Reference in New Issue
Block a user