Shell shortcuts: /, D, [ ], Ctrl+K palette + neighbors endpoint
Backend:
- GET /api/files/{md5}/neighbors/ returns previous/next items in the
current ordering (name, size, created_at) for keyboard navigation
Frontend:
- / focuses the library search input
- D downloads the file on the detail page
- [ and ] navigate to the previous/next item; hint shown on the page
- Ctrl/Cmd+K opens a command palette (navigation, toggle filters,
focus search, log in/out); Escape closes it
- Sort order now persists alongside the other library filters so
prev/next stays consistent
This commit is contained in:
@@ -13,6 +13,8 @@ from . import services
|
||||
from .models import MediaItem
|
||||
from .serializers import MediaItemSerializer
|
||||
|
||||
LIST_ORDERINGS = {"name", "-name", "size", "-size", "created_at", "-created_at"}
|
||||
|
||||
|
||||
class MediaItemViewSet(
|
||||
mixins.ListModelMixin,
|
||||
@@ -39,8 +41,9 @@ class MediaItemViewSet(
|
||||
if ratings:
|
||||
queryset = queryset.filter(rating__in=ratings)
|
||||
ordering = self.request.query_params.get("ordering", "").strip()
|
||||
allowed = {"name", "-name", "size", "-size", "created_at", "-created_at"}
|
||||
queryset = queryset.order_by(ordering if ordering in allowed else "-created_at")
|
||||
queryset = queryset.order_by(
|
||||
ordering if ordering in LIST_ORDERINGS else "-created_at"
|
||||
)
|
||||
return queryset.distinct()
|
||||
|
||||
@action(detail=True, methods=["get"])
|
||||
@@ -76,6 +79,47 @@ class MediaItemViewSet(
|
||||
return services.serve_file(request, thumbnail)
|
||||
return services.serve_file(request, path)
|
||||
|
||||
@action(detail=True, methods=["get"])
|
||||
def neighbors(self, request, md5=None):
|
||||
"""Previous/next items in the ordered list, for keyboard navigation."""
|
||||
item = self.get_object()
|
||||
ordering = request.query_params.get("ordering", "-created_at").strip()
|
||||
if ordering not in LIST_ORDERINGS:
|
||||
ordering = "-created_at"
|
||||
field = ordering.lstrip("-")
|
||||
descending = ordering.startswith("-")
|
||||
value = {
|
||||
"size": item.size,
|
||||
"created_at": item.created_at,
|
||||
"name": item.name,
|
||||
}[field]
|
||||
|
||||
queryset = self.get_queryset().exclude(pk=item.pk)
|
||||
if descending:
|
||||
previous_query = queryset.filter(**{f"{field}__gt": value}).order_by(field)
|
||||
next_query = queryset.filter(**{f"{field}__lt": value}).order_by(f"-{field}")
|
||||
else:
|
||||
previous_query = queryset.filter(**{f"{field}__lt": value}).order_by(
|
||||
f"-{field}"
|
||||
)
|
||||
next_query = queryset.filter(**{f"{field}__gt": value}).order_by(field)
|
||||
|
||||
def brief(obj):
|
||||
location = obj.locations.first()
|
||||
return {
|
||||
"md5": obj.md5,
|
||||
"filename": Path(location.rel_path).name if location else obj.md5,
|
||||
}
|
||||
|
||||
previous = previous_query.first()
|
||||
next_item = next_query.first()
|
||||
return Response(
|
||||
{
|
||||
"previous": brief(previous) if previous else None,
|
||||
"next": brief(next_item) if next_item else None,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class UploadView(APIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
Reference in New Issue
Block a user