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,
|
is_image_file,
|
||||||
make_thumbnail,
|
make_thumbnail,
|
||||||
)
|
)
|
||||||
|
from .specials import get_special_paths, special_name
|
||||||
|
from .models import Image as ImModel
|
||||||
|
|
||||||
|
|
||||||
def do_recursive_search(start_path, query):
|
def do_recursive_search(start_path, query):
|
||||||
@@ -63,7 +65,7 @@ def do_recursive_search(start_path, query):
|
|||||||
return subdirs, images
|
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
|
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.
|
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)
|
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:
|
try:
|
||||||
current_entries = [
|
current_entries = [
|
||||||
entry for entry in full_path.iterdir() if not entry.is_symlink()
|
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:
|
except Exception:
|
||||||
dir_text = ""
|
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(
|
back_url = gallery_url(
|
||||||
Path(dir_text) if dir_text != "" else None, True, query_state
|
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
|
# sort_label depends on SORT_LABELS in common; import lazily to avoid circulars
|
||||||
from .common import SORT_LABELS
|
from .common import SORT_LABELS
|
||||||
|
|
||||||
|
|||||||
@@ -31,9 +31,10 @@ from .common import (
|
|||||||
is_image_file,
|
is_image_file,
|
||||||
make_thumbnail,
|
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.
|
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)
|
image = Path("/imgs/").joinpath(path_text)
|
||||||
img_dir = full_path.parent
|
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:
|
try:
|
||||||
entries = [
|
entries = [
|
||||||
entry
|
entry
|
||||||
@@ -79,7 +87,7 @@ def render_image(request, path_text, full_path):
|
|||||||
# Sort siblings according to requested sort mode
|
# Sort siblings according to requested sort mode
|
||||||
images_sorted = sort_images(entries, sort_key)
|
images_sorted = sort_images(entries, sort_key)
|
||||||
|
|
||||||
# Find index of current image
|
# Find index of current image within the resolved list
|
||||||
try:
|
try:
|
||||||
index = next(i for i, p in enumerate(images_sorted) if p.name == full_path.name)
|
index = next(i for i, p in enumerate(images_sorted) if p.name == full_path.name)
|
||||||
except StopIteration:
|
except StopIteration:
|
||||||
@@ -122,6 +130,11 @@ def render_image(request, path_text, full_path):
|
|||||||
except Exception:
|
except Exception:
|
||||||
dir_text = ""
|
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(
|
back_url = gallery_url(
|
||||||
Path(dir_text) if dir_text != "" else None, True, query_state
|
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:
|
except Exception:
|
||||||
return None
|
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
|
# build_breadcrumbs expects a path_text representing directories only
|
||||||
dir_path_text = dir_text if dir_text != "" else ""
|
dir_path_text = dir_text if dir_text != "" else ""
|
||||||
breadcrumbs = build_breadcrumbs(dir_path_text, query_state)
|
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),
|
"sort_options": build_sort_options(request, search_text, sort_key, theme),
|
||||||
"theme_toggle_url": append_query("/gallery/toggle-settings/", theme_query),
|
"theme_toggle_url": append_query("/gallery/toggle-settings/", theme_query),
|
||||||
"path": path_text,
|
"path": path_text,
|
||||||
|
"is_special": special is not None,
|
||||||
}
|
}
|
||||||
|
|
||||||
from .common import SORT_LABELS
|
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>
|
<hr>
|
||||||
|
|
||||||
|
{% if not is_special %}
|
||||||
<form action="{{ search_action_url }}" method="get">
|
<form action="{{ search_action_url }}" method="get">
|
||||||
<div class="input-group input-group-sm">
|
<div class="input-group input-group-sm">
|
||||||
<input type="search" name="search" class="form-control search-input" placeholder="Search..." value="{{ search_text }}">
|
<input type="search" name="search" class="form-control search-input" placeholder="Search..." value="{{ search_text }}">
|
||||||
@@ -40,12 +41,13 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<hr>
|
<hr>
|
||||||
|
|
||||||
<a href="#" class="sidebar-link">Favorites</a>
|
<a href="/gallery/favorites/" class="sidebar-link {% if active_special == 'favorites' %}active-sort{% endif %}">Favorites</a>
|
||||||
<a href="#" class="sidebar-link">Most visited</a>
|
<a href="/gallery/most-visited/" class="sidebar-link {% if active_special == 'most-visited' %}active-sort{% endif %}">Most visited</a>
|
||||||
<a href="#" class="sidebar-link">Recently visited</a>
|
<a href="/gallery/recent/" class="sidebar-link {% if active_special == 'recent' %}active-sort{% endif %}">Recently visited</a>
|
||||||
|
|
||||||
{% if path != '' %}
|
{% if path != '' %}
|
||||||
<hr>
|
<hr>
|
||||||
@@ -112,18 +114,20 @@
|
|||||||
|
|
||||||
<hr>
|
<hr>
|
||||||
|
|
||||||
|
{% if not is_special %}
|
||||||
<form action="{{ search_action_url }}" method="get">
|
<form action="{{ search_action_url }}" method="get">
|
||||||
<div class="input-group input-group-sm">
|
<div class="input-group input-group-sm">
|
||||||
<input type="search" name="search" class="form-control search-input" placeholder="Search..." value="{{ search_text }}">
|
<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>
|
<span class="input-group-text search-addon"><i class="fa-solid fa-magnifying-glass"></i></span>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<hr>
|
<hr>
|
||||||
|
|
||||||
<a href="#" class="sidebar-link">Favorites</a>
|
<a href="/gallery/favorites/" class="sidebar-link">Favorites</a>
|
||||||
<a href="#" class="sidebar-link">Most visited</a>
|
<a href="/gallery/most-visited/" class="sidebar-link">Most visited</a>
|
||||||
<a href="#" class="sidebar-link">Recently visited</a>
|
<a href="/gallery/recent/" class="sidebar-link">Recently visited</a>
|
||||||
|
|
||||||
{% if path != '' %}
|
{% if path != '' %}
|
||||||
<hr>
|
<hr>
|
||||||
@@ -186,7 +190,11 @@
|
|||||||
{% if not forloop.first %}
|
{% if not forloop.first %}
|
||||||
<span class="crumb-sep">/</span>
|
<span class="crumb-sep">/</span>
|
||||||
{% endif %}
|
{% 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>
|
<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 %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -29,9 +29,9 @@
|
|||||||
|
|
||||||
<hr>
|
<hr>
|
||||||
|
|
||||||
<a href="#" class="sidebar-link">Favorites</a>
|
<a href="/gallery/favorites/" class="sidebar-link {% if is_special and breadcrumbs.0.label == 'Favorites' %}active-sort{% endif %}">Favorites</a>
|
||||||
<a href="#" class="sidebar-link">Most visited</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="#" class="sidebar-link">Recently visited</a>
|
<a href="/gallery/recent/" class="sidebar-link {% if is_special and breadcrumbs.0.label == 'Recent' %}active-sort{% endif %}">Recently visited</a>
|
||||||
|
|
||||||
<hr>
|
<hr>
|
||||||
|
|
||||||
@@ -110,9 +110,9 @@
|
|||||||
<span class="crumb-sep">/</span>
|
<span class="crumb-sep">/</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if crumb.path %}
|
{% 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 %}
|
{% 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 %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
# Django imports
|
# Django imports
|
||||||
from django.urls import path
|
from django.urls import path, re_path
|
||||||
|
|
||||||
# Module imports
|
# Module imports
|
||||||
from .views import gallery_view, toggle_settings
|
from .views import gallery_view, toggle_settings, special_gallery_view
|
||||||
|
|
||||||
###########################################################################################
|
###########################################################################################
|
||||||
# URL Patterns. #
|
# URL Patterns. #
|
||||||
@@ -13,5 +13,17 @@ urlpatterns = [
|
|||||||
# Views.
|
# Views.
|
||||||
path("", gallery_view, name="gallery_view_root"),
|
path("", gallery_view, name="gallery_view_root"),
|
||||||
path("toggle-settings/", toggle_settings, name="toggle_settings"),
|
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"),
|
path("<path:path>/", gallery_view, name="gallery_view_path"),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -20,6 +20,9 @@ from .image import render_image
|
|||||||
from .models import UserSettings
|
from .models import UserSettings
|
||||||
|
|
||||||
|
|
||||||
|
SPECIALS = ['favorites', 'most-visited', 'recent']
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def index(request):
|
def index(request):
|
||||||
return redirect("gallery_view_root")
|
return redirect("gallery_view_root")
|
||||||
@@ -51,6 +54,46 @@ def gallery_view(request, path=None):
|
|||||||
return render_image(request, path_text, candidate)
|
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
|
@login_required
|
||||||
def toggle_settings(request):
|
def toggle_settings(request):
|
||||||
"""Persist theme and/or sort for the current user and redirect back.
|
"""Persist theme and/or sort for the current user and redirect back.
|
||||||
|
|||||||
Reference in New Issue
Block a user