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 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 { ListFilterModel } from "src/models/list-filter/filter";
import { Pagination, PaginationIndex } from "./Pagination"; import { Pagination, PaginationIndex } from "./Pagination";
import { LoadingIndicator } from "../Shared/LoadingIndicator"; import { LoadingIndicator } from "../Shared/LoadingIndicator";
import { ErrorMessage } from "../Shared/ErrorMessage"; import { ErrorMessage } from "../Shared/ErrorMessage";
import { FormattedMessage } from "react-intl"; 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< export const PagedList: React.FC<
PropsWithChildren<{ PropsWithChildren<{
result: QueryResult; result: QueryResult;
@ -63,25 +89,8 @@ export const PagedList: React.FC<
]); ]);
const content = useMemo(() => { 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 ( return (
<> <LoadedContent loading={result.loading} error={result.error}>
{children} {children}
{!!pages && ( {!!pages && (
<> <>
@ -89,7 +98,7 @@ export const PagedList: React.FC<
{pagination} {pagination}
</> </>
)} )}
</> </LoadedContent>
); );
}, [ }, [
result.loading, result.loading,

View file

@ -13,12 +13,19 @@ import useFocus from "src/utils/focus";
import { Icon } from "../Shared/Icon"; import { Icon } from "../Shared/Icon";
import { faCheck, faChevronDown } from "@fortawesome/free-solid-svg-icons"; import { faCheck, faChevronDown } from "@fortawesome/free-solid-svg-icons";
import { useStopWheelScroll } from "src/utils/form"; import { useStopWheelScroll } from "src/utils/form";
import { Placement } from "react-bootstrap/esm/Overlay";
const PageCount: React.FC<{ const PageCount: React.FC<{
totalPages: number; totalPages: number;
currentPage: number; currentPage: number;
onChangePage: (page: number) => void; onChangePage: (page: number) => void;
}> = ({ totalPages, currentPage, onChangePage }) => { pagePopupPlacement?: Placement;
}> = ({
totalPages,
currentPage,
onChangePage,
pagePopupPlacement = "bottom",
}) => {
const intl = useIntl(); const intl = useIntl();
const currentPageCtrl = useRef(null); const currentPageCtrl = useRef(null);
const [pageInput, pageFocus] = useFocus(); const [pageInput, pageFocus] = useFocus();
@ -94,7 +101,7 @@ const PageCount: React.FC<{
<Overlay <Overlay
target={currentPageCtrl.current} target={currentPageCtrl.current}
show={showSelectPage} show={showSelectPage}
placement="bottom" placement={pagePopupPlacement}
rootClose rootClose
onHide={() => setShowSelectPage(false)} onHide={() => setShowSelectPage(false)}
> >
@ -138,9 +145,11 @@ interface IPaginationProps {
totalItems: number; totalItems: number;
metadataByline?: React.ReactNode; metadataByline?: React.ReactNode;
onChangePage: (page: number) => void; onChangePage: (page: number) => void;
pagePopupPlacement?: Placement;
} }
interface IPaginationIndexProps { interface IPaginationIndexProps {
loading?: boolean;
itemsPerPage: number; itemsPerPage: number;
currentPage: number; currentPage: number;
totalItems: number; totalItems: number;
@ -154,6 +163,7 @@ export const Pagination: React.FC<IPaginationProps> = ({
currentPage, currentPage,
totalItems, totalItems,
onChangePage, onChangePage,
pagePopupPlacement,
}) => { }) => {
const intl = useIntl(); const intl = useIntl();
const totalPages = useMemo( const totalPages = useMemo(
@ -168,6 +178,7 @@ export const Pagination: React.FC<IPaginationProps> = ({
totalPages={totalPages} totalPages={totalPages}
currentPage={currentPage} currentPage={currentPage}
onChangePage={onChangePage} onChangePage={onChangePage}
pagePopupPlacement={pagePopupPlacement}
/> />
); );
@ -183,7 +194,7 @@ export const Pagination: React.FC<IPaginationProps> = ({
<FormattedNumber value={page} /> <FormattedNumber value={page} />
</Button> </Button>
)); ));
}, [totalPages, currentPage, onChangePage]); }, [totalPages, currentPage, onChangePage, pagePopupPlacement]);
if (totalPages <= 1) return <div />; if (totalPages <= 1) return <div />;
@ -227,6 +238,7 @@ export const Pagination: React.FC<IPaginationProps> = ({
}; };
export const PaginationIndex: React.FC<IPaginationIndexProps> = ({ export const PaginationIndex: React.FC<IPaginationIndexProps> = ({
loading,
itemsPerPage, itemsPerPage,
currentPage, currentPage,
totalItems, totalItems,
@ -234,6 +246,8 @@ export const PaginationIndex: React.FC<IPaginationIndexProps> = ({
}) => { }) => {
const intl = useIntl(); const intl = useIntl();
if (loading) return null;
// Build the pagination index string // Build the pagination index string
const firstItemCount: number = Math.min( const firstItemCount: number = Math.min(
(currentPage - 1) * itemsPerPage + 1, (currentPage - 1) * itemsPerPage + 1,

View file

@ -942,3 +942,19 @@ input[type="range"].zoom-slider {
margin-right: 0.5rem; 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 TextUtils from "src/utils/text";
import { View } from "../List/views"; import { View } from "../List/views";
import { FileSize } from "../Shared/FileSize"; import { FileSize } from "../Shared/FileSize";
import { PagedList } from "../List/PagedList"; import { LoadedContent } from "../List/PagedList";
import { useCloseEditDelete, useFilterOperations } from "../List/util"; import { useCloseEditDelete, useFilterOperations } from "../List/util";
import { IListFilterOperation } from "../List/ListOperationButtons"; import { IListFilterOperation } from "../List/ListOperationButtons";
import { FilteredListToolbar } from "../List/FilteredListToolbar"; import { FilteredListToolbar } from "../List/FilteredListToolbar";
@ -48,6 +48,7 @@ import {
useFilteredSidebarKeybinds, useFilteredSidebarKeybinds,
} from "../List/Filters/FilterSidebar"; } from "../List/Filters/FilterSidebar";
import { PatchContainerComponent } from "src/patch"; import { PatchContainerComponent } from "src/patch";
import { Pagination, PaginationIndex } from "../List/Pagination";
function renderMetadataByline(result: GQL.FindScenesQueryResult) { function renderMetadataByline(result: GQL.FindScenesQueryResult) {
const duration = result?.data?.findScenes?.duration; const duration = result?.data?.findScenes?.duration;
@ -488,14 +489,15 @@ export const FilteredSceneList = (props: IFilteredScenes) => {
onRemoveAll={() => clearAllCriteria()} onRemoveAll={() => clearAllCriteria()}
/> />
<PagedList <PaginationIndex
result={result} loading={cachedResult.loading}
cachedResult={cachedResult} itemsPerPage={filter.itemsPerPage}
filter={filter} currentPage={filter.currentPage}
totalCount={totalCount} totalItems={totalCount}
onChangePage={setPage}
metadataByline={metadataByline} metadataByline={metadataByline}
> />
<LoadedContent loading={result.loading} error={result.error}>
<SceneList <SceneList
filter={effectiveFilter} filter={effectiveFilter}
scenes={items} scenes={items}
@ -503,7 +505,20 @@ export const FilteredSceneList = (props: IFilteredScenes) => {
onSelectChange={onSelectChange} onSelectChange={onSelectChange}
fromGroupId={fromGroupId} 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> </div>
</SidebarPane> </SidebarPane>
</div> </div>

View file

@ -51,15 +51,21 @@ body {
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
margin: 0; margin: 0;
overflow-x: hidden; overflow-x: hidden;
padding: 4rem 0 0 0; padding: $navbar-height 0 0 0;
@include media-breakpoint-down(xs) { @include media-breakpoint-down(xs) {
@media (orientation: portrait) { @media (orientation: portrait) {
padding: 1rem 0 5rem; padding: 1rem 0 $navbar-height;
} }
} }
} }
.main {
@include media-breakpoint-up(sm) {
padding-top: 0.5rem;
}
}
#group-page, #group-page,
#performer-page, #performer-page,
#studio-page, #studio-page,