mirror of
https://github.com/stashapp/stash.git
synced 2025-12-06 08:26:00 +01:00
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:
parent
b8263535ea
commit
b3d84187a3
16 changed files with 210 additions and 242 deletions
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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();
|
|
||||||
};
|
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -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))
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue