diff --git a/viewer/.views.py.kate-swp b/viewer/.views.py.kate-swp new file mode 100644 index 0000000..23c4271 Binary files /dev/null and b/viewer/.views.py.kate-swp differ diff --git a/viewer/directory.py b/viewer/directory.py index ddd19d9..3788cec 100644 --- a/viewer/directory.py +++ b/viewer/directory.py @@ -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,31 +83,42 @@ def render_directory(request, path_text, full_path): ) query_state = build_query(search_text) - try: - current_entries = [ - entry for entry in full_path.iterdir() if not entry.is_symlink() - ] - except OSError: - return HttpResponseNotFound("Not found") - - current_subdirs = sorted( - [entry for entry in current_entries if entry.is_dir()], - key=lambda item: ( - item.name.lower(), - str(item.relative_to(settings.GALLERY_ROOT)).lower(), - ), - ) - - if search_text == "": - images = [ - entry - for entry in current_entries - if entry.is_file() and is_image_file(entry) - ] + # 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: - _, images = do_recursive_search(full_path, search_text) + try: + current_entries = [ + entry for entry in full_path.iterdir() if not entry.is_symlink() + ] + except OSError: + return HttpResponseNotFound("Not found") - images = sort_images(images, sort_key) + current_subdirs = sorted( + [entry for entry in current_entries if entry.is_dir()], + key=lambda item: ( + item.name.lower(), + str(item.relative_to(settings.GALLERY_ROOT)).lower(), + ), + ) + + if search_text == "": + images = [ + entry + for entry in current_entries + if entry.is_file() and is_image_file(entry) + ] + else: + _, images = do_recursive_search(full_path, search_text) + + images = sort_images(images, sort_key) image_data = [] for image in images: @@ -162,10 +175,15 @@ def render_directory(request, path_text, full_path): except Exception: dir_text = "" - back_url = gallery_url( - Path(dir_text) if dir_text != "" else None, True, query_state - ) - back_thumb = get_first_image_thumbnail_url(full_path.parent) + # 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 + ) + back_thumb = get_first_image_thumbnail_url(full_path.parent) home_url = gallery_url(None, True, query_state) home_thumb = get_first_image_thumbnail_url(settings.GALLERY_ROOT) @@ -189,11 +207,25 @@ def render_directory(request, path_text, full_path): "search_action_url": gallery_url( Path(path_text) if path_text != "" else None, True, search_action_query ), - "clear_search_url": gallery_url( + "clear_search_url": gallery_url( Path(path_text) if path_text != "" else None, True, None ), } + # 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 diff --git a/viewer/image.py b/viewer/image.py index 2ef98d1..abe40ee 100644 --- a/viewer/image.py +++ b/viewer/image.py @@ -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,19 +68,26 @@ def render_image(request, path_text, full_path): image = Path("/imgs/").joinpath(path_text) img_dir = full_path.parent - try: - entries = [ - entry - for entry in img_dir.iterdir() - if entry.is_file() and not entry.is_symlink() and is_image_file(entry) - ] - except OSError: - return HttpResponseNotFound("Not found") + 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 + for entry in img_dir.iterdir() + if entry.is_file() and not entry.is_symlink() and is_image_file(entry) + ] + except OSError: + return HttpResponseNotFound("Not found") - # Sort siblings according to requested sort mode - images_sorted = sort_images(entries, sort_key) + # 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,10 +130,15 @@ def render_image(request, path_text, full_path): except Exception: dir_text = "" - back_url = gallery_url( - Path(dir_text) if dir_text != "" else None, True, query_state - ) - back_thumb = get_first_image_thumbnail_url(full_path.parent) + # 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 + ) + back_thumb = get_first_image_thumbnail_url(full_path.parent) home_url = gallery_url(None, True, query_state) home_thumb = get_first_image_thumbnail_url(settings.GALLERY_ROOT) @@ -169,11 +182,19 @@ def render_image(request, path_text, full_path): except Exception: return None - # Breadcrumbs: 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) - breadcrumbs.append({"label": full_path.name, "path": None}) + # 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) + breadcrumbs.append({"label": full_path.name, "path": None}) next_theme = "light" if theme == "dark" else "dark" theme_query = { @@ -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 diff --git a/viewer/specials.py b/viewer/specials.py new file mode 100644 index 0000000..d6be536 --- /dev/null +++ b/viewer/specials.py @@ -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 diff --git a/viewer/templates/gallery_view.html b/viewer/templates/gallery_view.html index 7da9b18..467e47e 100644 --- a/viewer/templates/gallery_view.html +++ b/viewer/templates/gallery_view.html @@ -29,6 +29,7 @@