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
This commit is contained in:
WithoutPants 2025-06-17 11:00:00 +10:00 committed by GitHub
parent a145576f39
commit 7d692232ed
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 94 additions and 34 deletions

View file

@ -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 <LoadingIndicator />;
}
if (error) {
return (
<ErrorMessage
message={
<FormattedMessage
id="errors.loading_type"
values={{ type: "items" }}
/>
}
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 <LoadingIndicator />;
}
if (result.error) {
return (
<ErrorMessage
message={
<FormattedMessage
id="errors.loading_type"
values={{ type: "items" }}
/>
}
error={result.error.message}
/>
);
}
return (
<>
<LoadedContent loading={result.loading} error={result.error}>
{children}
{!!pages && (
<>
@ -89,7 +98,7 @@ export const PagedList: React.FC<
{pagination}
</>
)}
</>
</LoadedContent>
);
}, [
result.loading,

View file

@ -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<{
<Overlay
target={currentPageCtrl.current}
show={showSelectPage}
placement="bottom"
placement={pagePopupPlacement}
rootClose
onHide={() => 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<IPaginationProps> = ({
currentPage,
totalItems,
onChangePage,
pagePopupPlacement,
}) => {
const intl = useIntl();
const totalPages = useMemo(
@ -168,6 +178,7 @@ export const Pagination: React.FC<IPaginationProps> = ({
totalPages={totalPages}
currentPage={currentPage}
onChangePage={onChangePage}
pagePopupPlacement={pagePopupPlacement}
/>
);
@ -183,7 +194,7 @@ export const Pagination: React.FC<IPaginationProps> = ({
<FormattedNumber value={page} />
</Button>
));
}, [totalPages, currentPage, onChangePage]);
}, [totalPages, currentPage, onChangePage, pagePopupPlacement]);
if (totalPages <= 1) return <div />;
@ -227,6 +238,7 @@ export const Pagination: React.FC<IPaginationProps> = ({
};
export const PaginationIndex: React.FC<IPaginationIndexProps> = ({
loading,
itemsPerPage,
currentPage,
totalItems,
@ -234,6 +246,8 @@ export const PaginationIndex: React.FC<IPaginationIndexProps> = ({
}) => {
const intl = useIntl();
if (loading) return null;
// Build the pagination index string
const firstItemCount: number = Math.min(
(currentPage - 1) * itemsPerPage + 1,

View file

@ -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;
}
}

View file

@ -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()}
/>
<PagedList
result={result}
cachedResult={cachedResult}
filter={filter}
totalCount={totalCount}
onChangePage={setPage}
<PaginationIndex
loading={cachedResult.loading}
itemsPerPage={filter.itemsPerPage}
currentPage={filter.currentPage}
totalItems={totalCount}
metadataByline={metadataByline}
>
/>
<LoadedContent loading={result.loading} error={result.error}>
<SceneList
filter={effectiveFilter}
scenes={items}
@ -503,7 +505,20 @@ export const FilteredSceneList = (props: IFilteredScenes) => {
onSelectChange={onSelectChange}
fromGroupId={fromGroupId}
/>
</PagedList>
</LoadedContent>
{totalCount > filter.itemsPerPage && (
<div className="pagination-footer">
<Pagination
itemsPerPage={filter.itemsPerPage}
currentPage={filter.currentPage}
totalItems={totalCount}
metadataByline={metadataByline}
onChangePage={setPage}
pagePopupPlacement="top"
/>
</div>
)}
</div>
</SidebarPane>
</div>

View file

@ -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,