Custom page size and saved zoom level (#1636)

* Allow custom page sizes
* Save zoom level in filters
This commit is contained in:
WithoutPants
2021-08-18 12:14:56 +10:00
committed by GitHub
parent fc6cafa15f
commit 680af72dcf
9 changed files with 144 additions and 60 deletions

View File

@@ -1,4 +1,6 @@
### ✨ New Features ### ✨ New Features
* Allow saving query page zoom level in saved and default filters. ([#1636](https://github.com/stashapp/stash/pull/1636))
* Support custom page sizes in the query page size dropdown. ([#1636](https://github.com/stashapp/stash/pull/1636))
* Added between/not between modifiers for number criteria. ([#1559](https://github.com/stashapp/stash/pull/1559)) * Added between/not between modifiers for number criteria. ([#1559](https://github.com/stashapp/stash/pull/1559))
* Support excluding tag patterns when scraping. ([#1617](https://github.com/stashapp/stash/pull/1617)) * Support excluding tag patterns when scraping. ([#1617](https://github.com/stashapp/stash/pull/1617))
* Support setting a custom directory for default performer images. ([#1489](https://github.com/stashapp/stash/pull/1489)) * Support setting a custom directory for default performer images. ([#1489](https://github.com/stashapp/stash/pull/1489))

View File

@@ -156,8 +156,7 @@ export const GalleryList: React.FC<IGalleryList> = ({
function renderGalleries( function renderGalleries(
result: FindGalleriesQueryResult, result: FindGalleriesQueryResult,
filter: ListFilterModel, filter: ListFilterModel,
selectedIds: Set<string>, selectedIds: Set<string>
zoomIndex: number
) { ) {
if (!result.data || !result.data.findGalleries) { if (!result.data || !result.data.findGalleries) {
return; return;
@@ -169,7 +168,7 @@ export const GalleryList: React.FC<IGalleryList> = ({
<GalleryCard <GalleryCard
key={gallery.id} key={gallery.id}
gallery={gallery} gallery={gallery}
zoomIndex={zoomIndex} zoomIndex={filter.zoomIndex}
selecting={selectedIds.size > 0} selecting={selectedIds.size > 0}
selected={selectedIds.has(gallery.id)} selected={selectedIds.has(gallery.id)}
onSelectedChanged={(selected: boolean, shiftKey: boolean) => onSelectedChanged={(selected: boolean, shiftKey: boolean) =>
@@ -235,13 +234,12 @@ export const GalleryList: React.FC<IGalleryList> = ({
function renderContent( function renderContent(
result: FindGalleriesQueryResult, result: FindGalleriesQueryResult,
filter: ListFilterModel, filter: ListFilterModel,
selectedIds: Set<string>, selectedIds: Set<string>
zoomIndex: number
) { ) {
return ( return (
<> <>
{maybeRenderGalleryExportDialog(selectedIds)} {maybeRenderGalleryExportDialog(selectedIds)}
{renderGalleries(result, filter, selectedIds, zoomIndex)} {renderGalleries(result, filter, selectedIds)}
</> </>
); );
} }

View File

@@ -262,7 +262,6 @@ export const ImageList: React.FC<IImageList> = ({
result: FindImagesQueryResult, result: FindImagesQueryResult,
filter: ListFilterModel, filter: ListFilterModel,
selectedIds: Set<string>, selectedIds: Set<string>,
zoomIndex: number,
onChangePage: (page: number) => void, onChangePage: (page: number) => void,
pageCount: number pageCount: number
) { ) {
@@ -273,7 +272,7 @@ export const ImageList: React.FC<IImageList> = ({
return ( return (
<div className="row justify-content-center"> <div className="row justify-content-center">
{result.data.findImages.images.map((image) => {result.data.findImages.images.map((image) =>
renderImageCard(image, selectedIds, zoomIndex) renderImageCard(image, selectedIds, filter.zoomIndex)
)} )}
</div> </div>
); );
@@ -294,21 +293,13 @@ export const ImageList: React.FC<IImageList> = ({
result: FindImagesQueryResult, result: FindImagesQueryResult,
filter: ListFilterModel, filter: ListFilterModel,
selectedIds: Set<string>, selectedIds: Set<string>,
zoomIndex: number,
onChangePage: (page: number) => void, onChangePage: (page: number) => void,
pageCount: number pageCount: number
) { ) {
return ( return (
<> <>
{maybeRenderImageExportDialog(selectedIds)} {maybeRenderImageExportDialog(selectedIds)}
{renderImages( {renderImages(result, filter, selectedIds, onChangePage, pageCount)}
result,
filter,
selectedIds,
zoomIndex,
onChangePage,
pageCount
)}
</> </>
); );
} }

View File

@@ -1,5 +1,5 @@
import _, { debounce } from "lodash"; import _, { debounce } from "lodash";
import React, { HTMLAttributes, useEffect } from "react"; import React, { HTMLAttributes, useEffect, useRef, useState } from "react";
import Mousetrap from "mousetrap"; import Mousetrap from "mousetrap";
import { SortDirectionEnum } from "src/core/generated-graphql"; import { SortDirectionEnum } from "src/core/generated-graphql";
import { import {
@@ -11,6 +11,8 @@ import {
Tooltip, Tooltip,
InputGroup, InputGroup,
FormControl, FormControl,
Popover,
Overlay,
} from "react-bootstrap"; } from "react-bootstrap";
import { Icon } from "src/components/Shared"; import { Icon } from "src/components/Shared";
@@ -40,7 +42,10 @@ export const ListFilter: React.FC<IListFilterProps> = ({
openFilterDialog, openFilterDialog,
persistState, persistState,
}) => { }) => {
const [customPageSizeShowing, setCustomPageSizeShowing] = useState(false);
const [queryRef, setQueryFocus] = useFocus(); const [queryRef, setQueryFocus] = useFocus();
const perPageSelect = useRef(null);
const [perPageInput, perPageFocus] = useFocus();
const searchCallback = debounce((value: string) => { const searchCallback = debounce((value: string) => {
const newFilter = _.cloneDeep(filter); const newFilter = _.cloneDeep(filter);
@@ -65,11 +70,29 @@ export const ListFilter: React.FC<IListFilterProps> = ({
}; };
}); });
function onChangePageSize(event: React.ChangeEvent<HTMLSelectElement>) { useEffect(() => {
const val = event.currentTarget.value; if (customPageSizeShowing) {
perPageFocus();
}
}, [customPageSizeShowing, perPageFocus]);
function onChangePageSize(val: string) {
if (val === "custom") {
// added timeout since Firefox seems to trigger the rootClose immediately
// without it
setTimeout(() => setCustomPageSizeShowing(true), 0);
return;
}
setCustomPageSizeShowing(false);
const pp = parseInt(val, 10);
if (Number.isNaN(pp) || pp <= 0) {
return;
}
const newFilter = _.cloneDeep(filter); const newFilter = _.cloneDeep(filter);
newFilter.itemsPerPage = parseInt(val, 10); newFilter.itemsPerPage = pp;
newFilter.currentPage = 1; newFilter.currentPage = 1;
onFilterUpdate(newFilter); onFilterUpdate(newFilter);
} }
@@ -144,6 +167,25 @@ export const ListFilter: React.FC<IListFilterProps> = ({
(o) => o.value === filter.sortBy (o) => o.value === filter.sortBy
); );
const pageSizeOptions = PAGE_SIZE_OPTIONS.map((o) => {
return {
label: o,
value: o,
};
});
const currentPerPage = filter.itemsPerPage.toString();
if (!pageSizeOptions.find((o) => o.value === currentPerPage)) {
pageSizeOptions.push({ label: currentPerPage, value: currentPerPage });
pageSizeOptions.sort(
(a, b) => parseInt(a.value, 10) - parseInt(b.value, 10)
);
}
pageSizeOptions.push({
label: `${intl.formatMessage({ id: "custom" })}...`,
value: "custom",
});
return ( return (
<> <>
<div className="d-flex mb-1"> <div className="d-flex mb-1">
@@ -240,18 +282,63 @@ export const ListFilter: React.FC<IListFilterProps> = ({
)} )}
</Dropdown> </Dropdown>
<Form.Control <div>
as="select" <Form.Control
onChange={onChangePageSize} as="select"
value={filter.itemsPerPage.toString()} ref={perPageSelect}
className="btn-secondary mx-1 mb-1" onChange={(e) => onChangePageSize(e.target.value)}
> value={filter.itemsPerPage.toString()}
{PAGE_SIZE_OPTIONS.map((s) => ( className="btn-secondary mx-1 mb-1"
<option value={s} key={s}> >
{s} {pageSizeOptions.map((s) => (
</option> <option value={s.value} key={s.value}>
))} {s.label}
</Form.Control> </option>
))}
</Form.Control>
<Overlay
target={perPageSelect.current}
show={customPageSizeShowing}
placement="bottom"
rootClose
onHide={() => setCustomPageSizeShowing(false)}
>
<Popover id="custom_pagesize_popover">
<Form inline>
<InputGroup>
<Form.Control
type="number"
min={1}
className="text-input"
ref={perPageInput}
onKeyPress={(e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === "Enter") {
onChangePageSize(
(perPageInput.current as HTMLInputElement)?.value ??
""
);
e.preventDefault();
}
}}
/>
<InputGroup.Append>
<Button
variant="primary"
onClick={() =>
onChangePageSize(
(perPageInput.current as HTMLInputElement)?.value ??
""
)
}
>
<Icon icon="check" />
</Button>
</InputGroup.Append>
</InputGroup>
</Form>
</Popover>
</Overlay>
</div>
</> </>
); );
} }

View File

@@ -194,8 +194,7 @@ export const SceneList: React.FC<ISceneList> = ({
function renderScenes( function renderScenes(
result: FindScenesQueryResult, result: FindScenesQueryResult,
filter: ListFilterModel, filter: ListFilterModel,
selectedIds: Set<string>, selectedIds: Set<string>
zoomIndex: number
) { ) {
if (!result.data || !result.data.findScenes) { if (!result.data || !result.data.findScenes) {
return; return;
@@ -208,7 +207,7 @@ export const SceneList: React.FC<ISceneList> = ({
<SceneCardsGrid <SceneCardsGrid
scenes={result.data.findScenes.scenes} scenes={result.data.findScenes.scenes}
queue={queue} queue={queue}
zoomIndex={zoomIndex} zoomIndex={filter.zoomIndex}
selectedIds={selectedIds} selectedIds={selectedIds}
onSelectChange={(id, selected, shiftKey) => onSelectChange={(id, selected, shiftKey) =>
listData.onSelectChange(id, selected, shiftKey) listData.onSelectChange(id, selected, shiftKey)
@@ -234,14 +233,13 @@ export const SceneList: React.FC<ISceneList> = ({
function renderContent( function renderContent(
result: FindScenesQueryResult, result: FindScenesQueryResult,
filter: ListFilterModel, filter: ListFilterModel,
selectedIds: Set<string>, selectedIds: Set<string>
zoomIndex: number
) { ) {
return ( return (
<> <>
{maybeRenderSceneGenerateDialog(selectedIds)} {maybeRenderSceneGenerateDialog(selectedIds)}
{maybeRenderSceneExportDialog(selectedIds)} {maybeRenderSceneExportDialog(selectedIds)}
{renderScenes(result, filter, selectedIds, zoomIndex)} {renderScenes(result, filter, selectedIds)}
</> </>
); );
} }

View File

@@ -195,8 +195,7 @@ export const TagList: React.FC<ITagList> = ({ filterHook }) => {
function renderTags( function renderTags(
result: FindTagsQueryResult, result: FindTagsQueryResult,
filter: ListFilterModel, filter: ListFilterModel,
selectedIds: Set<string>, selectedIds: Set<string>
zoomIndex: number
) { ) {
if (!result.data?.findTags) return; if (!result.data?.findTags) return;
@@ -207,7 +206,7 @@ export const TagList: React.FC<ITagList> = ({ filterHook }) => {
<TagCard <TagCard
key={tag.id} key={tag.id}
tag={tag} tag={tag}
zoomIndex={zoomIndex} zoomIndex={filter.zoomIndex}
selecting={selectedIds.size > 0} selecting={selectedIds.size > 0}
selected={selectedIds.has(tag.id)} selected={selectedIds.has(tag.id)}
onSelectedChanged={(selected: boolean, shiftKey: boolean) => onSelectedChanged={(selected: boolean, shiftKey: boolean) =>
@@ -310,13 +309,12 @@ export const TagList: React.FC<ITagList> = ({ filterHook }) => {
function renderContent( function renderContent(
result: FindTagsQueryResult, result: FindTagsQueryResult,
filter: ListFilterModel, filter: ListFilterModel,
selectedIds: Set<string>, selectedIds: Set<string>
zoomIndex: number
) { ) {
return ( return (
<> <>
{maybeRenderExportDialog(selectedIds)} {maybeRenderExportDialog(selectedIds)}
{renderTags(result, filter, selectedIds, zoomIndex)} {renderTags(result, filter, selectedIds)}
</> </>
); );
} }

View File

@@ -124,7 +124,6 @@ interface IListHookOptions<T, E> {
result: T, result: T,
filter: ListFilterModel, filter: ListFilterModel,
selectedIds: Set<string>, selectedIds: Set<string>,
zoomIndex: number,
onChangePage: (page: number) => void, onChangePage: (page: number) => void,
pageCount: number pageCount: number
) => React.ReactNode; ) => React.ReactNode;
@@ -170,7 +169,6 @@ const RenderList = <
QueryResult extends IQueryResult, QueryResult extends IQueryResult,
QueryData extends IDataItem QueryData extends IDataItem
>({ >({
defaultZoomIndex,
filter, filter,
filterOptions, filterOptions,
onChangePage, onChangePage,
@@ -194,7 +192,6 @@ const RenderList = <
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set()); const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
const [lastClickedId, setLastClickedId] = useState<string | undefined>(); const [lastClickedId, setLastClickedId] = useState<string | undefined>();
const [zoomIndex, setZoomIndex] = useState<number>(defaultZoomIndex ?? 1);
const [editingCriterion, setEditingCriterion] = useState< const [editingCriterion, setEditingCriterion] = useState<
Criterion<CriterionValue> | undefined Criterion<CriterionValue> | undefined
@@ -334,7 +331,9 @@ const RenderList = <
} }
function onChangeZoom(newZoomIndex: number) { function onChangeZoom(newZoomIndex: number) {
setZoomIndex(newZoomIndex); const newFilter = _.cloneDeep(filter);
newFilter.zoomIndex = newZoomIndex;
updateQueryParams(newFilter);
} }
async function onOperationClicked(o: IListHookOperation<QueryResult>) { async function onOperationClicked(o: IListHookOperation<QueryResult>) {
@@ -405,14 +404,7 @@ const RenderList = <
return ( return (
<> <>
{renderPagination()} {renderPagination()}
{renderContent( {renderContent(result, filter, selectedIds, onChangePage, pages)}
result,
filter,
selectedIds,
zoomIndex,
onChangePage,
pages
)}
<PaginationIndex <PaginationIndex
itemsPerPage={filter.itemsPerPage} itemsPerPage={filter.itemsPerPage}
currentPage={filter.currentPage} currentPage={filter.currentPage}
@@ -501,7 +493,7 @@ const RenderList = <
displayMode={filter.displayMode} displayMode={filter.displayMode}
displayModeOptions={filterOptions.displayModeOptions} displayModeOptions={filterOptions.displayModeOptions}
onSetDisplayMode={onChangeDisplayMode} onSetDisplayMode={onChangeDisplayMode}
zoomIndex={zoomable ? zoomIndex : undefined} zoomIndex={zoomable ? filter.zoomIndex : undefined}
onSetZoom={zoomable ? onChangeZoom : undefined} onSetZoom={zoomable ? onChangeZoom : undefined}
/> />
</ButtonToolbar> </ButtonToolbar>
@@ -566,7 +558,8 @@ const useList = <QueryResult extends IQueryResult, QueryData extends IDataItem>(
options.filterMode, options.filterMode,
queryString.parse(location.search), queryString.parse(location.search),
defaultSort, defaultSort,
defaultDisplayMode defaultDisplayMode,
options.defaultZoomIndex
) )
); );

View File

@@ -401,6 +401,7 @@
"between": "between", "between": "between",
"not_between": "not between" "not_between": "not between"
}, },
"custom": "Custom",
"date": "Date", "date": "Date",
"death_date": "Death Date", "death_date": "Death Date",
"death_year": "Death Year", "death_year": "Death Year",

View File

@@ -16,6 +16,7 @@ interface IQueryParameters {
q?: string; q?: string;
p?: string; p?: string;
c?: string[]; c?: string[];
z?: string;
} }
const DEFAULT_PARAMS = { const DEFAULT_PARAMS = {
@@ -34,19 +35,26 @@ export class ListFilterModel {
public sortDirection: SortDirectionEnum = SortDirectionEnum.Asc; public sortDirection: SortDirectionEnum = SortDirectionEnum.Asc;
public sortBy?: string; public sortBy?: string;
public displayMode: DisplayMode = DEFAULT_PARAMS.displayMode; public displayMode: DisplayMode = DEFAULT_PARAMS.displayMode;
public zoomIndex: number = 1;
public criteria: Array<Criterion<CriterionValue>> = []; public criteria: Array<Criterion<CriterionValue>> = [];
public randomSeed = -1; public randomSeed = -1;
private defaultZoomIndex: number = 1;
public constructor( public constructor(
mode: FilterMode, mode: FilterMode,
rawParms?: ParsedQuery<string>, rawParms?: ParsedQuery<string>,
defaultSort?: string, defaultSort?: string,
defaultDisplayMode?: DisplayMode defaultDisplayMode?: DisplayMode,
defaultZoomIndex?: number
) { ) {
this.mode = mode; this.mode = mode;
const params = rawParms as IQueryParameters; const params = rawParms as IQueryParameters;
this.sortBy = defaultSort; this.sortBy = defaultSort;
if (defaultDisplayMode !== undefined) this.displayMode = defaultDisplayMode; if (defaultDisplayMode !== undefined) this.displayMode = defaultDisplayMode;
if (defaultZoomIndex !== undefined) {
this.defaultZoomIndex = defaultZoomIndex;
this.zoomIndex = defaultZoomIndex;
}
if (params) this.configureFromQueryParameters(params); if (params) this.configureFromQueryParameters(params);
} }
@@ -85,6 +93,12 @@ export class ListFilterModel {
this.currentPage = Number.parseInt(params.p, 10); this.currentPage = Number.parseInt(params.p, 10);
} }
if (params.perPage) this.itemsPerPage = Number.parseInt(params.perPage, 10); if (params.perPage) this.itemsPerPage = Number.parseInt(params.perPage, 10);
if (params.z !== undefined) {
const zoomIndex = Number.parseInt(params.z, 10);
if (zoomIndex >= 0 && !Number.isNaN(zoomIndex)) {
this.zoomIndex = zoomIndex;
}
}
if (params.c !== undefined) { if (params.c !== undefined) {
this.criteria = []; this.criteria = [];
@@ -158,6 +172,7 @@ export class ListFilterModel {
this.currentPage !== DEFAULT_PARAMS.currentPage this.currentPage !== DEFAULT_PARAMS.currentPage
? this.currentPage ? this.currentPage
: undefined, : undefined,
z: this.zoomIndex !== this.defaultZoomIndex ? this.zoomIndex : undefined,
c: encodedCriteria, c: encodedCriteria,
}; };
@@ -176,6 +191,7 @@ export class ListFilterModel {
this.sortDirection === SortDirectionEnum.Desc ? "desc" : undefined, this.sortDirection === SortDirectionEnum.Desc ? "desc" : undefined,
disp: this.displayMode, disp: this.displayMode,
q: this.searchTerm, q: this.searchTerm,
z: this.zoomIndex,
c: encodedCriteria, c: encodedCriteria,
}; };