diff --git a/ui/v2.5/src/components/Changelog/versions/v090.md b/ui/v2.5/src/components/Changelog/versions/v090.md index 69c23e827..02cd38300 100644 --- a/ui/v2.5/src/components/Changelog/versions/v090.md +++ b/ui/v2.5/src/components/Changelog/versions/v090.md @@ -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)) diff --git a/ui/v2.5/src/components/Galleries/GalleryList.tsx b/ui/v2.5/src/components/Galleries/GalleryList.tsx index 91d1cedea..e56f728fa 100644 --- a/ui/v2.5/src/components/Galleries/GalleryList.tsx +++ b/ui/v2.5/src/components/Galleries/GalleryList.tsx @@ -156,8 +156,7 @@ export const GalleryList: React.FC = ({ function renderGalleries( result: FindGalleriesQueryResult, filter: ListFilterModel, - selectedIds: Set, - zoomIndex: number + selectedIds: Set ) { if (!result.data || !result.data.findGalleries) { return; @@ -169,7 +168,7 @@ export const GalleryList: React.FC = ({ 0} selected={selectedIds.has(gallery.id)} onSelectedChanged={(selected: boolean, shiftKey: boolean) => @@ -235,13 +234,12 @@ export const GalleryList: React.FC = ({ function renderContent( result: FindGalleriesQueryResult, filter: ListFilterModel, - selectedIds: Set, - zoomIndex: number + selectedIds: Set ) { return ( <> {maybeRenderGalleryExportDialog(selectedIds)} - {renderGalleries(result, filter, selectedIds, zoomIndex)} + {renderGalleries(result, filter, selectedIds)} ); } diff --git a/ui/v2.5/src/components/Images/ImageList.tsx b/ui/v2.5/src/components/Images/ImageList.tsx index 76fc329bd..7e7e2305d 100644 --- a/ui/v2.5/src/components/Images/ImageList.tsx +++ b/ui/v2.5/src/components/Images/ImageList.tsx @@ -262,7 +262,6 @@ export const ImageList: React.FC = ({ result: FindImagesQueryResult, filter: ListFilterModel, selectedIds: Set, - zoomIndex: number, onChangePage: (page: number) => void, pageCount: number ) { @@ -273,7 +272,7 @@ export const ImageList: React.FC = ({ return (
{result.data.findImages.images.map((image) => - renderImageCard(image, selectedIds, zoomIndex) + renderImageCard(image, selectedIds, filter.zoomIndex) )}
); @@ -294,21 +293,13 @@ export const ImageList: React.FC = ({ result: FindImagesQueryResult, filter: ListFilterModel, selectedIds: Set, - zoomIndex: number, onChangePage: (page: number) => void, pageCount: number ) { return ( <> {maybeRenderImageExportDialog(selectedIds)} - {renderImages( - result, - filter, - selectedIds, - zoomIndex, - onChangePage, - pageCount - )} + {renderImages(result, filter, selectedIds, onChangePage, pageCount)} ); } diff --git a/ui/v2.5/src/components/List/ListFilter.tsx b/ui/v2.5/src/components/List/ListFilter.tsx index b064c7b21..1adaa37fb 100644 --- a/ui/v2.5/src/components/List/ListFilter.tsx +++ b/ui/v2.5/src/components/List/ListFilter.tsx @@ -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 = ({ 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 = ({ }; }); - function onChangePageSize(event: React.ChangeEvent) { - 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 = ({ (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 ( <>
@@ -240,18 +282,63 @@ export const ListFilter: React.FC = ({ )} - - {PAGE_SIZE_OPTIONS.map((s) => ( - - ))} - +
+ onChangePageSize(e.target.value)} + value={filter.itemsPerPage.toString()} + className="btn-secondary mx-1 mb-1" + > + {pageSizeOptions.map((s) => ( + + ))} + + setCustomPageSizeShowing(false)} + > + +
+ + ) => { + if (e.key === "Enter") { + onChangePageSize( + (perPageInput.current as HTMLInputElement)?.value ?? + "" + ); + e.preventDefault(); + } + }} + /> + + + + +
+
+
+
); } diff --git a/ui/v2.5/src/components/Scenes/SceneList.tsx b/ui/v2.5/src/components/Scenes/SceneList.tsx index 792e089d2..347b0cbf1 100644 --- a/ui/v2.5/src/components/Scenes/SceneList.tsx +++ b/ui/v2.5/src/components/Scenes/SceneList.tsx @@ -194,8 +194,7 @@ export const SceneList: React.FC = ({ function renderScenes( result: FindScenesQueryResult, filter: ListFilterModel, - selectedIds: Set, - zoomIndex: number + selectedIds: Set ) { if (!result.data || !result.data.findScenes) { return; @@ -208,7 +207,7 @@ export const SceneList: React.FC = ({ listData.onSelectChange(id, selected, shiftKey) @@ -234,14 +233,13 @@ export const SceneList: React.FC = ({ function renderContent( result: FindScenesQueryResult, filter: ListFilterModel, - selectedIds: Set, - zoomIndex: number + selectedIds: Set ) { return ( <> {maybeRenderSceneGenerateDialog(selectedIds)} {maybeRenderSceneExportDialog(selectedIds)} - {renderScenes(result, filter, selectedIds, zoomIndex)} + {renderScenes(result, filter, selectedIds)} ); } diff --git a/ui/v2.5/src/components/Tags/TagList.tsx b/ui/v2.5/src/components/Tags/TagList.tsx index 8c0f0600b..bf29af891 100644 --- a/ui/v2.5/src/components/Tags/TagList.tsx +++ b/ui/v2.5/src/components/Tags/TagList.tsx @@ -195,8 +195,7 @@ export const TagList: React.FC = ({ filterHook }) => { function renderTags( result: FindTagsQueryResult, filter: ListFilterModel, - selectedIds: Set, - zoomIndex: number + selectedIds: Set ) { if (!result.data?.findTags) return; @@ -207,7 +206,7 @@ export const TagList: React.FC = ({ filterHook }) => { 0} selected={selectedIds.has(tag.id)} onSelectedChanged={(selected: boolean, shiftKey: boolean) => @@ -310,13 +309,12 @@ export const TagList: React.FC = ({ filterHook }) => { function renderContent( result: FindTagsQueryResult, filter: ListFilterModel, - selectedIds: Set, - zoomIndex: number + selectedIds: Set ) { return ( <> {maybeRenderExportDialog(selectedIds)} - {renderTags(result, filter, selectedIds, zoomIndex)} + {renderTags(result, filter, selectedIds)} ); } diff --git a/ui/v2.5/src/hooks/ListHook.tsx b/ui/v2.5/src/hooks/ListHook.tsx index 16370df1a..3a5ebb2a1 100644 --- a/ui/v2.5/src/hooks/ListHook.tsx +++ b/ui/v2.5/src/hooks/ListHook.tsx @@ -124,7 +124,6 @@ interface IListHookOptions { result: T, filter: ListFilterModel, selectedIds: Set, - 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>(new Set()); const [lastClickedId, setLastClickedId] = useState(); - const [zoomIndex, setZoomIndex] = useState(defaultZoomIndex ?? 1); const [editingCriterion, setEditingCriterion] = useState< Criterion | 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) { @@ -405,14 +404,7 @@ const RenderList = < return ( <> {renderPagination()} - {renderContent( - result, - filter, - selectedIds, - zoomIndex, - onChangePage, - pages - )} + {renderContent(result, filter, selectedIds, onChangePage, pages)} @@ -566,7 +558,8 @@ const useList = ( options.filterMode, queryString.parse(location.search), defaultSort, - defaultDisplayMode + defaultDisplayMode, + options.defaultZoomIndex ) ); diff --git a/ui/v2.5/src/locales/en-GB.json b/ui/v2.5/src/locales/en-GB.json index c38e54134..6a373b07c 100644 --- a/ui/v2.5/src/locales/en-GB.json +++ b/ui/v2.5/src/locales/en-GB.json @@ -401,6 +401,7 @@ "between": "between", "not_between": "not between" }, + "custom": "Custom", "date": "Date", "death_date": "Death Date", "death_year": "Death Year", diff --git a/ui/v2.5/src/models/list-filter/filter.ts b/ui/v2.5/src/models/list-filter/filter.ts index c08337258..809529422 100644 --- a/ui/v2.5/src/models/list-filter/filter.ts +++ b/ui/v2.5/src/models/list-filter/filter.ts @@ -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> = []; public randomSeed = -1; + private defaultZoomIndex: number = 1; public constructor( mode: FilterMode, rawParms?: ParsedQuery, 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, };