Scene list cleanup (#6104)

* Generalise and cleanup list toolbar
* Generalise ListResultsHeader
* Fix padding on sub-pages
This commit is contained in:
WithoutPants
2025-10-06 07:45:36 +11:00
committed by GitHub
parent af76f4a24a
commit c5bad48ece
9 changed files with 560 additions and 583 deletions

View File

@@ -23,8 +23,6 @@ import {
faPencil,
faPlay,
faPlus,
faSliders,
faTimes,
faTrash,
} from "@fortawesome/free-solid-svg-icons";
import { SceneMergeModal } from "./SceneMergeDialog";
@@ -39,7 +37,6 @@ import {
OperationDropdownItem,
} from "../List/ListOperationButtons";
import { useFilteredItemList } from "../List/ItemList";
import { FilterTags } from "../List/FilterTags";
import { Sidebar, SidebarPane, useSidebarState } from "../Shared/Sidebar";
import { SidebarPerformersFilter } from "../List/Filters/PerformersFilter";
import { SidebarStudiosFilter } from "../List/Filters/StudiosFilter";
@@ -57,18 +54,16 @@ import {
useFilteredSidebarKeybinds,
} from "../List/Filters/FilterSidebar";
import { PatchContainerComponent } from "src/patch";
import { Pagination, PaginationIndex } from "../List/Pagination";
import { Button, ButtonGroup, ButtonToolbar } from "react-bootstrap";
import { FilterButton } from "../List/Filters/FilterButton";
import { Pagination } from "../List/Pagination";
import { Button, ButtonGroup } from "react-bootstrap";
import { Icon } from "../Shared/Icon";
import { ListViewOptions } from "../List/ListViewOptions";
import {
PageSizeSelector,
SearchTermInput,
SortBySelect,
} from "../List/ListFilter";
import { Criterion } from "src/models/list-filter/criteria/criterion";
import useFocus from "src/utils/focus";
import {
FilteredListToolbar2,
ToolbarFilterSection,
ToolbarSelectionSection,
} from "../List/ListToolbar";
import { ListResultsHeader } from "../List/ListResultsHeader";
function renderMetadataByline(result: GQL.FindScenesQueryResult) {
const duration = result?.data?.findScenes?.duration;
@@ -340,38 +335,18 @@ interface IOperations {
className?: string;
}
const ListToolbarContent: React.FC<{
filter: ListFilterModel;
items: GQL.SlimSceneDataFragment[];
selectedIds: Set<string>;
const SceneListOperations: React.FC<{
items: number;
hasSelection: boolean;
operations: IOperations[];
onToggleSidebar: () => void;
onSetFilter: (filter: ListFilterModel) => void;
onEditCriterion: (c?: Criterion) => void;
onRemoveCriterion: (criterion: Criterion, valueIndex?: number) => void;
onRemoveAllCriterion: () => void;
onEditSearchTerm: () => void;
onRemoveSearchTerm: () => void;
onSelectAll: () => void;
onSelectNone: () => void;
onEdit: () => void;
onDelete: () => void;
onPlay: () => void;
onCreateNew: () => void;
}> = ({
filter,
items,
selectedIds,
hasSelection,
operations,
onToggleSidebar,
onSetFilter,
onEditCriterion,
onRemoveCriterion,
onRemoveAllCriterion,
onEditSearchTerm,
onRemoveSearchTerm,
onSelectAll,
onSelectNone,
onEdit,
onDelete,
onPlay,
@@ -379,174 +354,66 @@ const ListToolbarContent: React.FC<{
}) => {
const intl = useIntl();
const { criteria, searchTerm } = filter;
const hasSelection = selectedIds.size > 0;
const sidebarToggle = (
<Button
className="minimal sidebar-toggle-button ignore-sidebar-outside-click"
variant="secondary"
onClick={() => onToggleSidebar()}
title={intl.formatMessage({ id: "actions.sidebar.toggle" })}
>
<Icon icon={faSliders} />
</Button>
);
return (
<>
{!hasSelection && (
<>
<div className="search-container">
<SearchTermInput filter={filter} onFilterUpdate={onSetFilter} />
</div>
<div className="filter-section">
<FilterButton
onClick={() => onEditCriterion()}
count={criteria.length}
/>
<FilterTags
searchTerm={searchTerm}
criteria={criteria}
onEditCriterion={onEditCriterion}
onRemoveCriterion={onRemoveCriterion}
onRemoveAll={onRemoveAllCriterion}
onEditSearchTerm={onEditSearchTerm}
onRemoveSearchTerm={onRemoveSearchTerm}
truncateOnOverflow
/>
{sidebarToggle}
</div>
</>
)}
{hasSelection && (
<div className="selected-items-info">
<div>
<ButtonGroup>
{!!items && (
<Button
className="play-button"
variant="secondary"
className="minimal"
onClick={() => onSelectNone()}
title={intl.formatMessage({ id: "actions.select_none" })}
onClick={() => onPlay()}
title={intl.formatMessage({ id: "actions.play" })}
>
<Icon icon={faTimes} />
<Icon icon={faPlay} />
</Button>
<span>{selectedIds.size} selected</span>
<Button variant="link" onClick={() => onSelectAll()}>
<FormattedMessage id="actions.select_all" />
)}
{!hasSelection && (
<Button
className="create-new-button"
variant="secondary"
onClick={() => onCreateNew()}
title={intl.formatMessage(
{ id: "actions.create_entity" },
{ entityType: intl.formatMessage({ id: "scene" }) }
)}
>
<Icon icon={faPlus} />
</Button>
{sidebarToggle}
</div>
)}
<div>
<ButtonGroup>
{!!items.length && (
<Button
className="play-button"
variant="secondary"
onClick={() => onPlay()}
title={intl.formatMessage({ id: "actions.play" })}
>
<Icon icon={faPlay} />
)}
{hasSelection && (
<>
<Button variant="secondary" onClick={() => onEdit()}>
<Icon icon={faPencil} />
</Button>
)}
{!hasSelection && (
<Button
className="create-new-button"
variant="secondary"
onClick={() => onCreateNew()}
title={intl.formatMessage(
{ id: "actions.create_entity" },
{ entityType: intl.formatMessage({ id: "scene" }) }
)}
variant="danger"
className="btn-danger-minimal"
onClick={() => onDelete()}
>
<Icon icon={faPlus} />
<Icon icon={faTrash} />
</Button>
)}
</>
)}
{hasSelection && (
<>
<Button variant="secondary" onClick={() => onEdit()}>
<Icon icon={faPencil} />
</Button>
<Button
variant="danger"
className="btn-danger-minimal"
onClick={() => onDelete()}
>
<Icon icon={faTrash} />
</Button>
</>
)}
<OperationDropdown className="scene-list-operations">
{operations.map((o) => {
if (o.isDisplayed && !o.isDisplayed()) {
return null;
}
<OperationDropdown className="scene-list-operations">
{operations.map((o) => {
if (o.isDisplayed && !o.isDisplayed()) {
return null;
}
return (
<OperationDropdownItem
key={o.text}
onClick={o.onClick}
text={o.text}
className={o.className}
/>
);
})}
</OperationDropdown>
</ButtonGroup>
</div>
</>
);
};
const ListResultsHeader: React.FC<{
loading: boolean;
filter: ListFilterModel;
totalCount: number;
metadataByline?: React.ReactNode;
onChangeFilter: (filter: ListFilterModel) => void;
}> = ({ loading, filter, totalCount, metadataByline, onChangeFilter }) => {
return (
<ButtonToolbar className="scene-list-header">
<div>
<PaginationIndex
loading={loading}
itemsPerPage={filter.itemsPerPage}
currentPage={filter.currentPage}
totalItems={totalCount}
metadataByline={metadataByline}
/>
</div>
<div>
<SortBySelect
options={filter.options.sortByOptions}
sortBy={filter.sortBy}
sortDirection={filter.sortDirection}
onChangeSortBy={(s) =>
onChangeFilter(filter.setSortBy(s ?? undefined))
}
onChangeSortDirection={() =>
onChangeFilter(filter.toggleSortDirection())
}
onReshuffleRandomSort={() =>
onChangeFilter(filter.reshuffleRandomSort())
}
/>
<PageSizeSelector
pageSize={filter.itemsPerPage}
setPageSize={(s) => onChangeFilter(filter.setPageSize(s))}
/>
<ListViewOptions
displayMode={filter.displayMode}
zoomIndex={filter.zoomIndex}
displayModeOptions={filter.options.displayModeOptions}
onSetDisplayMode={(mode) =>
onChangeFilter(filter.setDisplayMode(mode))
}
onSetZoom={(zoom) => onChangeFilter(filter.setZoom(zoom))}
/>
</div>
</ButtonToolbar>
return (
<OperationDropdownItem
key={o.text}
onClick={o.onClick}
text={o.text}
className={o.className}
/>
);
})}
</OperationDropdown>
</ButtonGroup>
</div>
);
};
@@ -823,34 +690,46 @@ export const FilteredSceneList = (props: IFilteredScenes) => {
/>
</Sidebar>
<div>
<ButtonToolbar
className={cx("scene-list-toolbar", {
"has-selection": hasSelection,
})}
>
<ListToolbarContent
filter={filter}
onSetFilter={setFilter}
items={items}
selectedIds={selectedIds}
operations={otherOperations}
onToggleSidebar={() => setShowSidebar(!showSidebar)}
onEditCriterion={(c) => showEditFilter(c?.criterionOption.type)}
onRemoveCriterion={removeCriterion}
onRemoveAllCriterion={() => clearAllCriteria(true)}
onEditSearchTerm={() => {
setShowSidebar(true);
setSearchFocus(true);
}}
onRemoveSearchTerm={() => setFilter(filter.clearSearchTerm())}
onSelectAll={() => onSelectAll()}
onSelectNone={() => onSelectNone()}
onEdit={onEdit}
onDelete={onDelete}
onCreateNew={onCreateNew}
onPlay={onPlay}
/>
</ButtonToolbar>
<FilteredListToolbar2
className="scene-list-toolbar"
hasSelection={hasSelection}
filterSection={
<ToolbarFilterSection
filter={filter}
onSetFilter={setFilter}
onToggleSidebar={() => setShowSidebar(!showSidebar)}
onEditCriterion={(c) =>
showEditFilter(c?.criterionOption.type)
}
onRemoveCriterion={removeCriterion}
onRemoveAllCriterion={() => clearAllCriteria(true)}
onEditSearchTerm={() => {
setShowSidebar(true);
setSearchFocus(true);
}}
onRemoveSearchTerm={() => setFilter(filter.clearSearchTerm())}
/>
}
selectionSection={
<ToolbarSelectionSection
selected={selectedIds.size}
onToggleSidebar={() => setShowSidebar(!showSidebar)}
onSelectAll={() => onSelectAll()}
onSelectNone={() => onSelectNone()}
/>
}
operationSection={
<SceneListOperations
items={items.length}
hasSelection={hasSelection}
operations={otherOperations}
onEdit={onEdit}
onDelete={onDelete}
onPlay={onPlay}
onCreateNew={onCreateNew}
/>
}
/>
<ListResultsHeader
loading={cachedResult.loading}