mirror of
https://github.com/stashapp/stash.git
synced 2025-12-18 04:44:37 +03:00
Custom page size and saved zoom level (#1636)
* Allow custom page sizes * Save zoom level in filters
This commit is contained in:
@@ -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))
|
||||
|
||||
@@ -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)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
<div>
|
||||
<Form.Control
|
||||
as="select"
|
||||
onChange={onChangePageSize}
|
||||
ref={perPageSelect}
|
||||
onChange={(e) => onChangePageSize(e.target.value)}
|
||||
value={filter.itemsPerPage.toString()}
|
||||
className="btn-secondary mx-1 mb-1"
|
||||
>
|
||||
{PAGE_SIZE_OPTIONS.map((s) => (
|
||||
<option value={s} key={s}>
|
||||
{s}
|
||||
{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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
);
|
||||
|
||||
|
||||
@@ -401,6 +401,7 @@
|
||||
"between": "between",
|
||||
"not_between": "not between"
|
||||
},
|
||||
"custom": "Custom",
|
||||
"date": "Date",
|
||||
"death_date": "Death Date",
|
||||
"death_year": "Death Year",
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user