From 7d692232eda9c4416aecae445a8659c0e30529ce Mon Sep 17 00:00:00 2001
From: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
Date: Tue, 17 Jun 2025 11:00:00 +1000
Subject: [PATCH] Move pagination to a sticky bottom toolbar on scenes page
(#5924)
* Adjust main padding to be the same as navbar height
* Add LoadedContent component for loading and error display
* Add option for pagination popup placement
* Show results summary at top only. Add sticky bottom pagination
---
ui/v2.5/src/components/List/PagedList.tsx | 49 ++++++++++++---------
ui/v2.5/src/components/List/Pagination.tsx | 20 +++++++--
ui/v2.5/src/components/List/styles.scss | 16 +++++++
ui/v2.5/src/components/Scenes/SceneList.tsx | 33 ++++++++++----
ui/v2.5/src/index.scss | 10 ++++-
5 files changed, 94 insertions(+), 34 deletions(-)
diff --git a/ui/v2.5/src/components/List/PagedList.tsx b/ui/v2.5/src/components/List/PagedList.tsx
index ab27e2697..e42eebf06 100644
--- a/ui/v2.5/src/components/List/PagedList.tsx
+++ b/ui/v2.5/src/components/List/PagedList.tsx
@@ -1,11 +1,37 @@
import React, { PropsWithChildren, useMemo } from "react";
-import { QueryResult } from "@apollo/client";
+import { ApolloError, QueryResult } from "@apollo/client";
import { ListFilterModel } from "src/models/list-filter/filter";
import { Pagination, PaginationIndex } from "./Pagination";
import { LoadingIndicator } from "../Shared/LoadingIndicator";
import { ErrorMessage } from "../Shared/ErrorMessage";
import { FormattedMessage } from "react-intl";
+export const LoadedContent: React.FC<
+ PropsWithChildren<{
+ loading?: boolean;
+ error?: ApolloError;
+ }>
+> = ({ loading, error, children }) => {
+ if (loading) {
+ return ;
+ }
+ if (error) {
+ return (
+
+ }
+ error={error.message}
+ />
+ );
+ }
+
+ return <>{children}>;
+};
+
export const PagedList: React.FC<
PropsWithChildren<{
result: QueryResult;
@@ -63,25 +89,8 @@ export const PagedList: React.FC<
]);
const content = useMemo(() => {
- if (result.loading) {
- return ;
- }
- if (result.error) {
- return (
-
- }
- error={result.error.message}
- />
- );
- }
-
return (
- <>
+
{children}
{!!pages && (
<>
@@ -89,7 +98,7 @@ export const PagedList: React.FC<
{pagination}
>
)}
- >
+
);
}, [
result.loading,
diff --git a/ui/v2.5/src/components/List/Pagination.tsx b/ui/v2.5/src/components/List/Pagination.tsx
index 6ee08680b..e5cc89b40 100644
--- a/ui/v2.5/src/components/List/Pagination.tsx
+++ b/ui/v2.5/src/components/List/Pagination.tsx
@@ -13,12 +13,19 @@ import useFocus from "src/utils/focus";
import { Icon } from "../Shared/Icon";
import { faCheck, faChevronDown } from "@fortawesome/free-solid-svg-icons";
import { useStopWheelScroll } from "src/utils/form";
+import { Placement } from "react-bootstrap/esm/Overlay";
const PageCount: React.FC<{
totalPages: number;
currentPage: number;
onChangePage: (page: number) => void;
-}> = ({ totalPages, currentPage, onChangePage }) => {
+ pagePopupPlacement?: Placement;
+}> = ({
+ totalPages,
+ currentPage,
+ onChangePage,
+ pagePopupPlacement = "bottom",
+}) => {
const intl = useIntl();
const currentPageCtrl = useRef(null);
const [pageInput, pageFocus] = useFocus();
@@ -94,7 +101,7 @@ const PageCount: React.FC<{
setShowSelectPage(false)}
>
@@ -138,9 +145,11 @@ interface IPaginationProps {
totalItems: number;
metadataByline?: React.ReactNode;
onChangePage: (page: number) => void;
+ pagePopupPlacement?: Placement;
}
interface IPaginationIndexProps {
+ loading?: boolean;
itemsPerPage: number;
currentPage: number;
totalItems: number;
@@ -154,6 +163,7 @@ export const Pagination: React.FC = ({
currentPage,
totalItems,
onChangePage,
+ pagePopupPlacement,
}) => {
const intl = useIntl();
const totalPages = useMemo(
@@ -168,6 +178,7 @@ export const Pagination: React.FC = ({
totalPages={totalPages}
currentPage={currentPage}
onChangePage={onChangePage}
+ pagePopupPlacement={pagePopupPlacement}
/>
);
@@ -183,7 +194,7 @@ export const Pagination: React.FC = ({
));
- }, [totalPages, currentPage, onChangePage]);
+ }, [totalPages, currentPage, onChangePage, pagePopupPlacement]);
if (totalPages <= 1) return ;
@@ -227,6 +238,7 @@ export const Pagination: React.FC = ({
};
export const PaginationIndex: React.FC = ({
+ loading,
itemsPerPage,
currentPage,
totalItems,
@@ -234,6 +246,8 @@ export const PaginationIndex: React.FC = ({
}) => {
const intl = useIntl();
+ if (loading) return null;
+
// Build the pagination index string
const firstItemCount: number = Math.min(
(currentPage - 1) * itemsPerPage + 1,
diff --git a/ui/v2.5/src/components/List/styles.scss b/ui/v2.5/src/components/List/styles.scss
index 3372ee2f3..9f78354a6 100644
--- a/ui/v2.5/src/components/List/styles.scss
+++ b/ui/v2.5/src/components/List/styles.scss
@@ -942,3 +942,19 @@ input[type="range"].zoom-slider {
margin-right: 0.5rem;
}
}
+
+.pagination-footer {
+ background-color: $body-bg;
+ bottom: $navbar-height;
+ padding: 0.5rem 1rem;
+ position: sticky;
+ z-index: 10;
+
+ @include media-breakpoint-up(sm) {
+ bottom: 0;
+ }
+
+ .pagination {
+ margin-bottom: 0;
+ }
+}
diff --git a/ui/v2.5/src/components/Scenes/SceneList.tsx b/ui/v2.5/src/components/Scenes/SceneList.tsx
index fd2ad1657..3f9bf6315 100644
--- a/ui/v2.5/src/components/Scenes/SceneList.tsx
+++ b/ui/v2.5/src/components/Scenes/SceneList.tsx
@@ -25,7 +25,7 @@ import { objectTitle } from "src/core/files";
import TextUtils from "src/utils/text";
import { View } from "../List/views";
import { FileSize } from "../Shared/FileSize";
-import { PagedList } from "../List/PagedList";
+import { LoadedContent } from "../List/PagedList";
import { useCloseEditDelete, useFilterOperations } from "../List/util";
import { IListFilterOperation } from "../List/ListOperationButtons";
import { FilteredListToolbar } from "../List/FilteredListToolbar";
@@ -48,6 +48,7 @@ import {
useFilteredSidebarKeybinds,
} from "../List/Filters/FilterSidebar";
import { PatchContainerComponent } from "src/patch";
+import { Pagination, PaginationIndex } from "../List/Pagination";
function renderMetadataByline(result: GQL.FindScenesQueryResult) {
const duration = result?.data?.findScenes?.duration;
@@ -488,14 +489,15 @@ export const FilteredSceneList = (props: IFilteredScenes) => {
onRemoveAll={() => clearAllCriteria()}
/>
-
+ />
+
+
{
onSelectChange={onSelectChange}
fromGroupId={fromGroupId}
/>
-
+
+
+ {totalCount > filter.itemsPerPage && (
+
+ )}
diff --git a/ui/v2.5/src/index.scss b/ui/v2.5/src/index.scss
index 730ad9eb2..9393397ed 100755
--- a/ui/v2.5/src/index.scss
+++ b/ui/v2.5/src/index.scss
@@ -51,15 +51,21 @@ body {
-webkit-font-smoothing: antialiased;
margin: 0;
overflow-x: hidden;
- padding: 4rem 0 0 0;
+ padding: $navbar-height 0 0 0;
@include media-breakpoint-down(xs) {
@media (orientation: portrait) {
- padding: 1rem 0 5rem;
+ padding: 1rem 0 $navbar-height;
}
}
}
+.main {
+ @include media-breakpoint-up(sm) {
+ padding-top: 0.5rem;
+ }
+}
+
#group-page,
#performer-page,
#studio-page,