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
* 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))
* 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))

View File

@@ -156,8 +156,7 @@ export const GalleryList: React.FC<IGalleryList> = ({
function renderGalleries(
result: FindGalleriesQueryResult,
filter: ListFilterModel,
selectedIds: Set<string>,
zoomIndex: number
selectedIds: Set<string>
) {
if (!result.data || !result.data.findGalleries) {
return;
@@ -169,7 +168,7 @@ export const GalleryList: React.FC<IGalleryList> = ({
<GalleryCard
key={gallery.id}
gallery={gallery}
zoomIndex={zoomIndex}
zoomIndex={filter.zoomIndex}
selecting={selectedIds.size > 0}
selected={selectedIds.has(gallery.id)}
onSelectedChanged={(selected: boolean, shiftKey: boolean) =>
@@ -235,13 +234,12 @@ export const GalleryList: React.FC<IGalleryList> = ({
function renderContent(
result: FindGalleriesQueryResult,
filter: ListFilterModel,
selectedIds: Set<string>,
zoomIndex: number
selectedIds: Set<string>
) {
return (
<>
{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,
filter: ListFilterModel,
selectedIds: Set<string>,
zoomIndex: number,
onChangePage: (page: number) => void,
pageCount: number
) {
@@ -273,7 +272,7 @@ export const ImageList: React.FC<IImageList> = ({
return (
<div className="row justify-content-center">
{result.data.findImages.images.map((image) =>
renderImageCard(image, selectedIds, zoomIndex)
renderImageCard(image, selectedIds, filter.zoomIndex)
)}
</div>
);
@@ -294,21 +293,13 @@ export const ImageList: React.FC<IImageList> = ({
result: FindImagesQueryResult,
filter: ListFilterModel,
selectedIds: Set<string>,
zoomIndex: number,
onChangePage: (page: number) => void,
pageCount: number
) {
return (
<>
{maybeRenderImageExportDialog(selectedIds)}
{renderImages(
result,
filter,
selectedIds,
zoomIndex,
onChangePage,
pageCount
)}
{renderImages(result, filter, selectedIds, onChangePage, pageCount)}
</>
);
}

View File

@@ -1,5 +1,5 @@
import _, { debounce } from "lodash";
import React, { HTMLAttributes, useEffect } from "react";
import React, { HTMLAttributes, useEffect, useRef, useState } from "react";
import Mousetrap from "mousetrap";
import { SortDirectionEnum } from "src/core/generated-graphql";
import {
@@ -11,6 +11,8 @@ import {
Tooltip,
InputGroup,
FormControl,
Popover,
Overlay,
} from "react-bootstrap";
import { Icon } from "src/components/Shared";
@@ -40,7 +42,10 @@ export const ListFilter: React.FC<IListFilterProps> = ({
openFilterDialog,
persistState,
}) => {
const [customPageSizeShowing, setCustomPageSizeShowing] = useState(false);
const [queryRef, setQueryFocus] = useFocus();
const perPageSelect = useRef(null);
const [perPageInput, perPageFocus] = useFocus();
const searchCallback = debounce((value: string) => {
const newFilter = _.cloneDeep(filter);
@@ -65,11 +70,29 @@ export const ListFilter: React.FC<IListFilterProps> = ({
};
});
function onChangePageSize(event: React.ChangeEvent<HTMLSelectElement>) {
const val = event.currentTarget.value;
useEffect(() => {
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);
newFilter.itemsPerPage = parseInt(val, 10);
newFilter.itemsPerPage = pp;
newFilter.currentPage = 1;
onFilterUpdate(newFilter);
}
@@ -144,6 +167,25 @@ export const ListFilter: React.FC<IListFilterProps> = ({
(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 (
<>
<div className="d-flex mb-1">
@@ -240,18 +282,63 @@ export const ListFilter: React.FC<IListFilterProps> = ({
)}
</Dropdown>
<Form.Control
as="select"
onChange={onChangePageSize}
value={filter.itemsPerPage.toString()}
className="btn-secondary mx-1 mb-1"
>
{PAGE_SIZE_OPTIONS.map((s) => (
<option value={s} key={s}>
{s}
</option>
))}
</Form.Control>
<div>
<Form.Control
as="select"
ref={perPageSelect}
onChange={(e) => onChangePageSize(e.target.value)}
value={filter.itemsPerPage.toString()}
className="btn-secondary mx-1 mb-1"
>
{pageSizeOptions.map((s) => (
<option value={s.value} key={s.value}>
{s.label}
</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(
result: FindScenesQueryResult,
filter: ListFilterModel,
selectedIds: Set<string>,
zoomIndex: number
selectedIds: Set<string>
) {
if (!result.data || !result.data.findScenes) {
return;
@@ -208,7 +207,7 @@ export const SceneList: React.FC<ISceneList> = ({
<SceneCardsGrid
scenes={result.data.findScenes.scenes}
queue={queue}
zoomIndex={zoomIndex}
zoomIndex={filter.zoomIndex}
selectedIds={selectedIds}
onSelectChange={(id, selected, shiftKey) =>
listData.onSelectChange(id, selected, shiftKey)
@@ -234,14 +233,13 @@ export const SceneList: React.FC<ISceneList> = ({
function renderContent(
result: FindScenesQueryResult,
filter: ListFilterModel,
selectedIds: Set<string>,
zoomIndex: number
selectedIds: Set<string>
) {
return (
<>
{maybeRenderSceneGenerateDialog(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(
result: FindTagsQueryResult,
filter: ListFilterModel,
selectedIds: Set<string>,
zoomIndex: number
selectedIds: Set<string>
) {
if (!result.data?.findTags) return;
@@ -207,7 +206,7 @@ export const TagList: React.FC<ITagList> = ({ filterHook }) => {
<TagCard
key={tag.id}
tag={tag}
zoomIndex={zoomIndex}
zoomIndex={filter.zoomIndex}
selecting={selectedIds.size > 0}
selected={selectedIds.has(tag.id)}
onSelectedChanged={(selected: boolean, shiftKey: boolean) =>
@@ -310,13 +309,12 @@ export const TagList: React.FC<ITagList> = ({ filterHook }) => {
function renderContent(
result: FindTagsQueryResult,
filter: ListFilterModel,
selectedIds: Set<string>,
zoomIndex: number
selectedIds: Set<string>
) {
return (
<>
{maybeRenderExportDialog(selectedIds)}
{renderTags(result, filter, selectedIds, zoomIndex)}
{renderTags(result, filter, selectedIds)}
</>
);
}

View File

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

View File

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

View File

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