Refactor filtered list toolbar (#6317)

* Refactor list operation buttons into a single button group
* Refactor ListFilter into FilteredListToolbar and restyle
* Move zoom keybinds out of zoom control
* Use button group for display mode select
* Hide zoom slider on xs devices

(cherry picked from commit d6a2953371)
This commit is contained in:
WithoutPants 2025-11-25 17:36:13 +11:00
parent b8263535ea
commit b3d84187a3
16 changed files with 210 additions and 242 deletions

View file

@ -195,7 +195,6 @@ export const GalleryList: React.FC<IGalleryList> = ({
selectable selectable
> >
<ItemList <ItemList
zoomable
view={view} view={view}
otherOperations={otherOperations} otherOperations={otherOperations}
addKeybinds={addKeybinds} addKeybinds={addKeybinds}

View file

@ -226,7 +226,6 @@ export const GroupList: React.FC<IGroupList> = ({
selectable={selectable} selectable={selectable}
> >
<ItemList <ItemList
zoomable
view={view} view={view}
otherOperations={otherOperations} otherOperations={otherOperations}
addKeybinds={addKeybinds} addKeybinds={addKeybinds}

View file

@ -464,7 +464,6 @@ export const ImageList: React.FC<IImageList> = ({
selectable selectable
> >
<ItemList <ItemList
zoomable
view={view} view={view}
otherOperations={otherOperations} otherOperations={otherOperations}
addKeybinds={addKeybinds} addKeybinds={addKeybinds}

View file

@ -2,8 +2,8 @@ import React from "react";
import { QueryResult } from "@apollo/client"; import { QueryResult } from "@apollo/client";
import { ListFilterModel } from "src/models/list-filter/filter"; import { ListFilterModel } from "src/models/list-filter/filter";
import { IconDefinition } from "@fortawesome/fontawesome-svg-core"; import { IconDefinition } from "@fortawesome/fontawesome-svg-core";
import { ListFilter } from "./ListFilter"; import { PageSizeSelector, SearchTermInput, SortBySelect } from "./ListFilter";
import { ListViewOptions } from "./ListViewOptions"; import { ListViewButtonGroup } from "./ListViewOptions";
import { import {
IListFilterOperation, IListFilterOperation,
ListOperationButtons, ListOperationButtons,
@ -11,6 +11,8 @@ import {
import { ButtonGroup, ButtonToolbar } from "react-bootstrap"; import { ButtonGroup, ButtonToolbar } from "react-bootstrap";
import { View } from "./views"; import { View } from "./views";
import { IListSelect, useFilterOperations } from "./util"; import { IListSelect, useFilterOperations } from "./util";
import { SavedFilterDropdown } from "./SavedFilterList";
import { FilterButton } from "./Filters/FilterButton";
export interface IItemListOperation<T extends QueryResult> { export interface IItemListOperation<T extends QueryResult> {
text: string; text: string;
@ -63,15 +65,31 @@ export const FilteredListToolbar: React.FC<IFilteredListToolbar> = ({
return ( return (
<ButtonToolbar className="filtered-list-toolbar"> <ButtonToolbar className="filtered-list-toolbar">
<SearchTermInput filter={filter} onFilterUpdate={setFilter} />
<ButtonGroup> <ButtonGroup>
{showEditFilter && ( <SavedFilterDropdown
<ListFilter
onFilterUpdate={setFilter}
filter={filter} filter={filter}
openFilterDialog={() => showEditFilter()} onSetFilter={setFilter}
view={view} view={view}
/> />
)} <FilterButton onClick={() => showEditFilter()} count={filter.count()} />
</ButtonGroup>
<SortBySelect
sortBy={filter.sortBy}
sortDirection={filter.sortDirection}
options={filterOptions.sortByOptions}
onChangeSortBy={(e) => setFilter(filter.setSortBy(e ?? undefined))}
onChangeSortDirection={() => setFilter(filter.toggleSortDirection())}
onReshuffleRandomSort={() => setFilter(filter.reshuffleRandomSort())}
/>
<PageSizeSelector
pageSize={filter.itemsPerPage}
setPageSize={(size) => setFilter(filter.setPageSize(size))}
/>
<ListOperationButtons <ListOperationButtons
onSelectAll={onSelectAll} onSelectAll={onSelectAll}
onSelectNone={onSelectNone} onSelectNone={onSelectNone}
@ -80,17 +98,14 @@ export const FilteredListToolbar: React.FC<IFilteredListToolbar> = ({
onEdit={onEdit} onEdit={onEdit}
onDelete={onDelete} onDelete={onDelete}
/> />
<ButtonGroup>
<ListViewOptions <ListViewButtonGroup
displayMode={filter.displayMode} displayMode={filter.displayMode}
displayModeOptions={filterOptions.displayModeOptions} displayModeOptions={filterOptions.displayModeOptions}
onSetDisplayMode={setDisplayMode} onSetDisplayMode={setDisplayMode}
zoomIndex={zoomable ? filter.zoomIndex : undefined} zoomIndex={zoomable ? filter.zoomIndex : undefined}
onSetZoom={zoomable ? setZoom : undefined} onSetZoom={zoomable ? setZoom : undefined}
/> />
</ButtonGroup>
</ButtonGroup>
<ButtonGroup></ButtonGroup>
</ButtonToolbar> </ButtonToolbar>
); );
}; };

View file

@ -44,6 +44,8 @@ import {
} from "./FilteredListToolbar"; } from "./FilteredListToolbar";
import { PagedList } from "./PagedList"; import { PagedList } from "./PagedList";
import { ConfigurationContext } from "src/hooks/Config"; import { ConfigurationContext } from "src/hooks/Config";
import { useZoomKeybinds } from "./ZoomSlider";
import { DisplayMode } from "src/models/list-filter/types";
interface IFilteredItemList<T extends QueryResult, E extends IHasID = IHasID> { interface IFilteredItemList<T extends QueryResult, E extends IHasID = IHasID> {
filterStateProps: IFilterStateHook; filterStateProps: IFilterStateHook;
@ -113,7 +115,6 @@ export function useFilteredItemList<
interface IItemListProps<T extends QueryResult, E extends IHasID> { interface IItemListProps<T extends QueryResult, E extends IHasID> {
view?: View; view?: View;
zoomable?: boolean;
otherOperations?: IItemListOperation<T>[]; otherOperations?: IItemListOperation<T>[];
renderContent: ( renderContent: (
result: T, result: T,
@ -145,7 +146,6 @@ export const ItemList = <T extends QueryResult, E extends IHasID>(
) => { ) => {
const { const {
view, view,
zoomable,
otherOperations, otherOperations,
renderContent, renderContent,
renderEditDialog, renderEditDialog,
@ -217,6 +217,15 @@ export const ItemList = <T extends QueryResult, E extends IHasID>(
showEditFilter, showEditFilter,
}); });
const zoomable =
filter.displayMode === DisplayMode.Grid ||
filter.displayMode === DisplayMode.Wall;
useZoomKeybinds({
zoomIndex: zoomable ? filter.zoomIndex : undefined,
onChangeZoom: (zoom) => updateFilter(filter.setZoom(zoom)),
});
useEffect(() => { useEffect(() => {
if (addKeybinds) { if (addKeybinds) {
const unbindExtras = addKeybinds(result, effectiveFilter, selectedIds); const unbindExtras = addKeybinds(result, effectiveFilter, selectedIds);

View file

@ -1,4 +1,3 @@
import cloneDeep from "lodash-es/cloneDeep";
import React, { import React, {
useCallback, useCallback,
useEffect, useEffect,
@ -23,17 +22,14 @@ import {
import { Icon } from "../Shared/Icon"; import { Icon } from "../Shared/Icon";
import { ListFilterModel } from "src/models/list-filter/filter"; import { ListFilterModel } from "src/models/list-filter/filter";
import useFocus from "src/utils/focus"; import useFocus from "src/utils/focus";
import { FormattedMessage, useIntl } from "react-intl"; import { useIntl } from "react-intl";
import { SavedFilterDropdown } from "./SavedFilterList";
import { import {
faCaretDown, faCaretDown,
faCaretUp, faCaretUp,
faCheck, faCheck,
faRandom, faRandom,
} from "@fortawesome/free-solid-svg-icons"; } from "@fortawesome/free-solid-svg-icons";
import { FilterButton } from "./Filters/FilterButton";
import { useDebounce } from "src/hooks/debounce"; import { useDebounce } from "src/hooks/debounce";
import { View } from "./views";
import { ClearableInput } from "../Shared/ClearableInput"; import { ClearableInput } from "../Shared/ClearableInput";
import { useStopWheelScroll } from "src/utils/form"; import { useStopWheelScroll } from "src/utils/form";
import { ISortByOption } from "src/models/list-filter/filter-options"; import { ISortByOption } from "src/models/list-filter/filter-options";
@ -318,109 +314,3 @@ export const SortBySelect: React.FC<{
</Dropdown> </Dropdown>
); );
}; };
interface IListFilterProps {
onFilterUpdate: (newFilter: ListFilterModel) => void;
filter: ListFilterModel;
view?: View;
openFilterDialog: () => void;
}
export const ListFilter: React.FC<IListFilterProps> = ({
onFilterUpdate,
filter,
openFilterDialog,
view,
}) => {
const filterOptions = filter.options;
useEffect(() => {
Mousetrap.bind("r", () => onReshuffleRandomSort());
return () => {
Mousetrap.unbind("r");
};
});
function onChangePageSize(pp: number) {
const newFilter = cloneDeep(filter);
newFilter.itemsPerPage = pp;
newFilter.currentPage = 1;
onFilterUpdate(newFilter);
}
function onChangeSortDirection() {
const newFilter = cloneDeep(filter);
if (filter.sortDirection === SortDirectionEnum.Asc) {
newFilter.sortDirection = SortDirectionEnum.Desc;
} else {
newFilter.sortDirection = SortDirectionEnum.Asc;
}
onFilterUpdate(newFilter);
}
function onChangeSortBy(eventKey: string | null) {
const newFilter = cloneDeep(filter);
newFilter.sortBy = eventKey ?? undefined;
newFilter.currentPage = 1;
onFilterUpdate(newFilter);
}
function onReshuffleRandomSort() {
const newFilter = cloneDeep(filter);
newFilter.currentPage = 1;
newFilter.randomSeed = -1;
onFilterUpdate(newFilter);
}
function render() {
return (
<>
<div className="d-flex">
<SearchTermInput filter={filter} onFilterUpdate={onFilterUpdate} />
</div>
<ButtonGroup className="mr-2">
<SavedFilterDropdown
filter={filter}
onSetFilter={(f) => {
onFilterUpdate(f);
}}
view={view}
/>
<OverlayTrigger
placement="top"
overlay={
<Tooltip id="filter-tooltip">
<FormattedMessage id="search_filter.name" />
</Tooltip>
}
>
<FilterButton
onClick={() => openFilterDialog()}
count={filter.count()}
/>
</OverlayTrigger>
</ButtonGroup>
<SortBySelect
className="mr-2"
sortBy={filter.sortBy}
sortDirection={filter.sortDirection}
options={filterOptions.sortByOptions}
onChangeSortBy={onChangeSortBy}
onChangeSortDirection={onChangeSortDirection}
onReshuffleRandomSort={onReshuffleRandomSort}
/>
<PageSizeSelector
pageSize={filter.itemsPerPage}
setPageSize={onChangePageSize}
/>
</>
);
}
return render();
};

View file

@ -1,11 +1,5 @@
import React, { PropsWithChildren, useEffect } from "react"; import React, { PropsWithChildren, useEffect, useMemo } from "react";
import { import { Button, ButtonGroup, Dropdown } from "react-bootstrap";
Button,
ButtonGroup,
Dropdown,
OverlayTrigger,
Tooltip,
} from "react-bootstrap";
import Mousetrap from "mousetrap"; import Mousetrap from "mousetrap";
import { FormattedMessage, useIntl } from "react-intl"; import { FormattedMessage, useIntl } from "react-intl";
import { IconDefinition } from "@fortawesome/fontawesome-svg-core"; import { IconDefinition } from "@fortawesome/fontawesome-svg-core";
@ -108,8 +102,8 @@ export const ListOperationButtons: React.FC<IListOperationButtonsProps> = ({
}; };
}); });
function maybeRenderButtons() { const buttons = useMemo(() => {
const buttons = (otherOperations ?? []).filter((o) => { const ret = (otherOperations ?? []).filter((o) => {
if (!o.icon) { if (!o.icon) {
return false; return false;
} }
@ -120,16 +114,17 @@ export const ListOperationButtons: React.FC<IListOperationButtonsProps> = ({
return o.isDisplayed(); return o.isDisplayed();
}); });
if (itemsSelected) { if (itemsSelected) {
if (onEdit) { if (onEdit) {
buttons.push({ ret.push({
icon: faPencilAlt, icon: faPencilAlt,
text: intl.formatMessage({ id: "actions.edit" }), text: intl.formatMessage({ id: "actions.edit" }),
onClick: onEdit, onClick: onEdit,
}); });
} }
if (onDelete) { if (onDelete) {
buttons.push({ ret.push({
icon: faTrash, icon: faTrash,
text: intl.formatMessage({ id: "actions.delete" }), text: intl.formatMessage({ id: "actions.delete" }),
onClick: onDelete, onClick: onDelete,
@ -138,29 +133,29 @@ export const ListOperationButtons: React.FC<IListOperationButtonsProps> = ({
} }
} }
if (buttons.length > 0) { return ret;
}, [otherOperations, itemsSelected, onEdit, onDelete, intl]);
const operationButtons = useMemo(() => {
return ( return (
<ButtonGroup className="ml-2"> <>
{buttons.map((button) => { {buttons.map((button) => {
return ( return (
<OverlayTrigger
overlay={<Tooltip id="edit">{button.text}</Tooltip>}
key={button.text}
>
<Button <Button
key={button.text}
variant={button.buttonVariant ?? "secondary"} variant={button.buttonVariant ?? "secondary"}
onClick={button.onClick} onClick={button.onClick}
title={button.text}
> >
{button.icon ? <Icon icon={button.icon} /> : undefined} <Icon icon={button.icon!} />
</Button> </Button>
</OverlayTrigger>
); );
})} })}
</ButtonGroup> </>
); );
} }, [buttons]);
}
const moreDropdown = useMemo(() => {
function renderSelectAll() { function renderSelectAll() {
if (onSelectAll) { if (onSelectAll) {
return ( return (
@ -189,7 +184,6 @@ export const ListOperationButtons: React.FC<IListOperationButtonsProps> = ({
} }
} }
function renderMore() {
const options = [renderSelectAll(), renderSelectNone()].filter((o) => o); const options = [renderSelectAll(), renderSelectNone()].filter((o) => o);
if (otherOperations) { if (otherOperations) {
@ -224,13 +218,19 @@ export const ListOperationButtons: React.FC<IListOperationButtonsProps> = ({
{options.length > 0 ? options : undefined} {options.length > 0 ? options : undefined}
</OperationDropdown> </OperationDropdown>
); );
}, [otherOperations, onSelectAll, onSelectNone]);
// don't render anything if there are no buttons or operations
if (buttons.length === 0 && !moreDropdown) {
return null;
} }
return ( return (
<> <>
{maybeRenderButtons()} <ButtonGroup>
{operationButtons}
<ButtonGroup className="ml-2">{renderMore()}</ButtonGroup> {moreDropdown}
</ButtonGroup>
</> </>
); );
}; };

View file

@ -1,8 +1,16 @@
import React, { useEffect, useRef, useState } from "react"; import React, { useEffect, useRef, useState } from "react";
import Mousetrap from "mousetrap"; import Mousetrap from "mousetrap";
import { Button, Dropdown, Overlay, Popover } from "react-bootstrap"; import {
Button,
ButtonGroup,
Dropdown,
Overlay,
OverlayTrigger,
Popover,
Tooltip,
} from "react-bootstrap";
import { DisplayMode } from "src/models/list-filter/types"; import { DisplayMode } from "src/models/list-filter/types";
import { useIntl } from "react-intl"; import { IntlShape, useIntl } from "react-intl";
import { Icon } from "../Shared/Icon"; import { Icon } from "../Shared/Icon";
import { import {
faChevronDown, faChevronDown,
@ -53,6 +61,10 @@ function getLabelId(option: DisplayMode) {
return `display_mode.${displayModeId}`; return `display_mode.${displayModeId}`;
} }
function getLabel(intl: IntlShape, option: DisplayMode) {
return intl.formatMessage({ id: getLabelId(option) });
}
export const ListViewOptions: React.FC<IListViewOptionsProps> = ({ export const ListViewOptions: React.FC<IListViewOptionsProps> = ({
zoomIndex, zoomIndex,
onSetZoom, onSetZoom,
@ -60,9 +72,6 @@ export const ListViewOptions: React.FC<IListViewOptionsProps> = ({
onSetDisplayMode, onSetDisplayMode,
displayModeOptions, displayModeOptions,
}) => { }) => {
const minZoom = 0;
const maxZoom = 3;
const intl = useIntl(); const intl = useIntl();
const overlayTarget = useRef(null); const overlayTarget = useRef(null);
@ -98,10 +107,6 @@ export const ListViewOptions: React.FC<IListViewOptionsProps> = ({
}; };
}); });
function getLabel(option: DisplayMode) {
return intl.formatMessage({ id: getLabelId(option) });
}
function onChangeZoom(v: number) { function onChangeZoom(v: number) {
if (onSetZoom) { if (onSetZoom) {
onSetZoom(v); onSetZoom(v);
@ -116,7 +121,7 @@ export const ListViewOptions: React.FC<IListViewOptionsProps> = ({
variant="secondary" variant="secondary"
title={intl.formatMessage( title={intl.formatMessage(
{ id: "display_mode.label_current" }, { id: "display_mode.label_current" },
{ current: getLabel(displayMode) } { current: getLabel(intl, displayMode) }
)} )}
onClick={() => setShowOptions(!showOptions)} onClick={() => setShowOptions(!showOptions)}
> >
@ -140,8 +145,6 @@ export const ListViewOptions: React.FC<IListViewOptionsProps> = ({
displayMode === DisplayMode.Wall) ? ( displayMode === DisplayMode.Wall) ? (
<div className="zoom-slider-container"> <div className="zoom-slider-container">
<ZoomSelect <ZoomSelect
minZoom={minZoom}
maxZoom={maxZoom}
zoomIndex={zoomIndex} zoomIndex={zoomIndex}
onChangeZoom={onChangeZoom} onChangeZoom={onChangeZoom}
/> />
@ -156,7 +159,7 @@ export const ListViewOptions: React.FC<IListViewOptionsProps> = ({
onSetDisplayMode(option); onSetDisplayMode(option);
}} }}
> >
<Icon icon={getIcon(option)} /> {getLabel(option)} <Icon icon={getIcon(option)} /> {getLabel(intl, option)}
</Dropdown.Item> </Dropdown.Item>
))} ))}
</div> </div>
@ -167,3 +170,48 @@ export const ListViewOptions: React.FC<IListViewOptionsProps> = ({
</> </>
); );
}; };
export const ListViewButtonGroup: React.FC<IListViewOptionsProps> = ({
zoomIndex,
onSetZoom,
displayMode,
onSetDisplayMode,
displayModeOptions,
}) => {
const intl = useIntl();
return (
<>
{displayModeOptions.length > 1 && (
<ButtonGroup>
{displayModeOptions.map((option) => (
<OverlayTrigger
key={option}
overlay={
<Tooltip id="display-mode-tooltip">
{getLabel(intl, option)}
</Tooltip>
}
>
<Button
variant="secondary"
active={displayMode === option}
onClick={() => onSetDisplayMode(option)}
>
<Icon icon={getIcon(option)} />
</Button>
</OverlayTrigger>
))}
</ButtonGroup>
)}
<div className="zoom-slider-container">
{onSetZoom &&
zoomIndex !== undefined &&
(displayMode === DisplayMode.Grid ||
displayMode === DisplayMode.Wall) ? (
<ZoomSelect zoomIndex={zoomIndex} onChangeZoom={onSetZoom} />
) : null}
</div>
</>
);
};

View file

@ -2,19 +2,14 @@ import React, { useEffect } from "react";
import Mousetrap from "mousetrap"; import Mousetrap from "mousetrap";
import { Form } from "react-bootstrap"; import { Form } from "react-bootstrap";
export interface IZoomSelectProps { const minZoom = 0;
minZoom: number; const maxZoom = 3;
maxZoom: number;
zoomIndex: number;
onChangeZoom: (v: number) => void;
}
export const ZoomSelect: React.FC<IZoomSelectProps> = ({ export function useZoomKeybinds(props: {
minZoom, zoomIndex: number | undefined;
maxZoom, onChangeZoom: (v: number) => void;
zoomIndex, }) {
onChangeZoom, const { zoomIndex, onChangeZoom } = props;
}) => {
useEffect(() => { useEffect(() => {
Mousetrap.bind("+", () => { Mousetrap.bind("+", () => {
if (zoomIndex !== undefined && zoomIndex < maxZoom) { if (zoomIndex !== undefined && zoomIndex < maxZoom) {
@ -32,7 +27,17 @@ export const ZoomSelect: React.FC<IZoomSelectProps> = ({
Mousetrap.unbind("-"); Mousetrap.unbind("-");
}; };
}); });
}
export interface IZoomSelectProps {
zoomIndex: number;
onChangeZoom: (v: number) => void;
}
export const ZoomSelect: React.FC<IZoomSelectProps> = ({
zoomIndex,
onChangeZoom,
}) => {
return ( return (
<Form.Control <Form.Control
className="zoom-slider" className="zoom-slider"

View file

@ -93,7 +93,8 @@
// hide zoom slider in xs viewport // hide zoom slider in xs viewport
@include media-breakpoint-down(xs) { @include media-breakpoint-down(xs) {
.display-mode-menu .zoom-slider-container { .display-mode-menu .zoom-slider-container,
.zoom-slider-container {
display: none; display: none;
} }
} }
@ -916,6 +917,8 @@ input[type="range"].zoom-slider {
} }
.filtered-list-toolbar { .filtered-list-toolbar {
align-items: center;
gap: 0.5rem;
justify-content: center; justify-content: center;
margin-bottom: 0.5rem; margin-bottom: 0.5rem;
@ -933,8 +936,10 @@ input[type="range"].zoom-slider {
} }
} }
.btn.display-mode-select { // set the width of the zoom-slider-container to prevent buttons moving when
margin-left: 0.5rem; // the slider appears/disappears
.zoom-slider-container {
min-width: 60px;
} }
} }
@ -946,10 +951,6 @@ input[type="range"].zoom-slider {
} }
} }
.search-term-input {
margin-right: 0.5rem;
}
.custom-field-filter { .custom-field-filter {
align-items: center; align-items: center;
display: flex; display: flex;

View file

@ -329,7 +329,6 @@ export const PerformerList: React.FC<IPerformerList> = ({
selectable selectable
> >
<ItemList <ItemList
zoomable
view={view} view={view}
otherOperations={otherOperations} otherOperations={otherOperations}
addKeybinds={addKeybinds} addKeybinds={addKeybinds}

View file

@ -70,6 +70,7 @@ import {
ToolbarSelectionSection, ToolbarSelectionSection,
} from "../List/ListToolbar"; } from "../List/ListToolbar";
import { ListResultsHeader } from "../List/ListResultsHeader"; import { ListResultsHeader } from "../List/ListResultsHeader";
import { useZoomKeybinds } from "../List/ZoomSlider";
function renderMetadataByline(result: GQL.FindScenesQueryResult) { function renderMetadataByline(result: GQL.FindScenesQueryResult) {
const duration = result?.data?.findScenes?.duration; const duration = result?.data?.findScenes?.duration;
@ -519,6 +520,10 @@ export const FilteredSceneList = (props: IFilteredScenes) => {
Mousetrap.unbind("d d"); Mousetrap.unbind("d d");
}; };
}); });
useZoomKeybinds({
zoomIndex: filter.zoomIndex,
onChangeZoom: (zoom) => setFilter(filter.setZoom(zoom)),
});
const onCloseEditDelete = useCloseEditDelete({ const onCloseEditDelete = useCloseEditDelete({
closeModal, closeModal,

View file

@ -138,7 +138,6 @@ export const SceneMarkerList: React.FC<ISceneMarkerList> = ({
selectable selectable
> >
<ItemList <ItemList
zoomable
view={view} view={view}
otherOperations={otherOperations} otherOperations={otherOperations}
addKeybinds={addKeybinds} addKeybinds={addKeybinds}

View file

@ -188,7 +188,6 @@ export const StudioList: React.FC<IStudioList> = ({
selectable selectable
> >
<ItemList <ItemList
zoomable
view={view} view={view}
otherOperations={otherOperations} otherOperations={otherOperations}
addKeybinds={addKeybinds} addKeybinds={addKeybinds}

View file

@ -367,7 +367,6 @@ export const TagList: React.FC<ITagList> = ({ filterHook, alterQuery }) => {
> >
<ItemList <ItemList
view={view} view={view}
zoomable
otherOperations={otherOperations} otherOperations={otherOperations}
addKeybinds={addKeybinds} addKeybinds={addKeybinds}
renderContent={renderContent} renderContent={renderContent}

View file

@ -5,6 +5,7 @@
### 🎨 Improvements ### 🎨 Improvements
* **[0.29.4]** Restored display mode button group to non-scene list pages. ([#6317](https://github.com/stashapp/stash/pull/6317))
* **[0.29.4]** Added keyboard shortcut for tagger view (`v t`). ([#6261](https://github.com/stashapp/stash/pull/6261)) * **[0.29.4]** Added keyboard shortcut for tagger view (`v t`). ([#6261](https://github.com/stashapp/stash/pull/6261))
* **[0.29.2]** Returned saved filters button to the top toolbar in the Scene list. ([#6215](https://github.com/stashapp/stash/pull/6215)) * **[0.29.2]** Returned saved filters button to the top toolbar in the Scene list. ([#6215](https://github.com/stashapp/stash/pull/6215))
* **[0.29.2]** Top pagination can now be optionally shown in the scene list with [custom css](https://github.com/stashapp/stash/pull/6234#issue-3593190476). ([#6234](https://github.com/stashapp/stash/pull/6234)) * **[0.29.2]** Top pagination can now be optionally shown in the scene list with [custom css](https://github.com/stashapp/stash/pull/6234#issue-3593190476). ([#6234](https://github.com/stashapp/stash/pull/6234))
@ -34,6 +35,7 @@
* Include IP address in login errors in log. ([#5760](https://github.com/stashapp/stash/pull/5760)) * Include IP address in login errors in log. ([#5760](https://github.com/stashapp/stash/pull/5760))
### 🐛 Bug fixes ### 🐛 Bug fixes
* **[0.29.4]** Fixed zoom keyboard shortcuts not working. ([#6317](https://github.com/stashapp/stash/pull/6317))
* **[0.29.4]** Fixed existing match stash ID sometimes not being displayed in the performer scrape dialog. ([#6257](https://github.com/stashapp/stash/pull/6257)) * **[0.29.4]** Fixed existing match stash ID sometimes not being displayed in the performer scrape dialog. ([#6257](https://github.com/stashapp/stash/pull/6257))
* **[0.29.4]** Fixed inline videos showing as full-screen on iPhone devices. ([#6259](https://github.com/stashapp/stash/pull/6259)) * **[0.29.4]** Fixed inline videos showing as full-screen on iPhone devices. ([#6259](https://github.com/stashapp/stash/pull/6259))
* **[0.29.4]** Fixed download backup function not working when generated directory is on a different filesystem. ([#6244](https://github.com/stashapp/stash/pull/6244)) * **[0.29.4]** Fixed download backup function not working when generated directory is on a different filesystem. ([#6244](https://github.com/stashapp/stash/pull/6244))