mirror of
https://github.com/stashapp/stash.git
synced 2025-12-12 03:12:24 +01:00
431 lines
13 KiB
TypeScript
431 lines
13 KiB
TypeScript
import _ from "lodash";
|
|
import queryString from "query-string";
|
|
import React, { useState, useEffect, useRef } from "react";
|
|
import { ApolloError } from "apollo-client";
|
|
import { useHistory, useLocation } from "react-router-dom";
|
|
import {
|
|
SortDirectionEnum,
|
|
SlimSceneDataFragment,
|
|
SceneMarkerDataFragment,
|
|
GalleryDataFragment,
|
|
StudioDataFragment,
|
|
PerformerDataFragment,
|
|
FindScenesQueryResult,
|
|
FindSceneMarkersQueryResult,
|
|
FindGalleriesQueryResult,
|
|
FindStudiosQueryResult,
|
|
FindPerformersQueryResult
|
|
} from "src/core/generated-graphql";
|
|
import { useInterfaceLocalForage, IInterfaceConfig } from "src/hooks/LocalForage";
|
|
import { LoadingIndicator } from "src/components/Shared";
|
|
import { ListFilter } from "src/components/List/ListFilter";
|
|
import { Pagination } from "src/components/List/Pagination";
|
|
import { StashService } from "src/core/StashService";
|
|
import { Criterion } from "src/models/list-filter/criteria/criterion";
|
|
import { ListFilterModel } from "src/models/list-filter/filter";
|
|
import { DisplayMode, FilterMode } from "src/models/list-filter/types";
|
|
|
|
interface IListHookData {
|
|
filter: ListFilterModel;
|
|
template: JSX.Element;
|
|
onSelectChange: (id: string, selected: boolean, shiftKey: boolean) => void;
|
|
}
|
|
|
|
interface IListHookOperation<T> {
|
|
text: string;
|
|
onClick: (
|
|
result: T,
|
|
filter: ListFilterModel,
|
|
selectedIds: Set<string>
|
|
) => void;
|
|
}
|
|
|
|
interface IListHookOptions<T> {
|
|
subComponent?: boolean;
|
|
filterHook?: (filter: ListFilterModel) => ListFilterModel;
|
|
zoomable?: boolean;
|
|
otherOperations?: IListHookOperation<T>[];
|
|
renderContent: (
|
|
result: T,
|
|
filter: ListFilterModel,
|
|
selectedIds: Set<string>,
|
|
zoomIndex: number
|
|
) => JSX.Element | undefined;
|
|
renderSelectedOptions?: (
|
|
result: T,
|
|
selectedIds: Set<string>
|
|
) => JSX.Element | undefined;
|
|
}
|
|
|
|
interface IDataItem {
|
|
id: string;
|
|
}
|
|
interface IQueryResult {
|
|
error?: ApolloError;
|
|
loading: boolean;
|
|
}
|
|
|
|
interface IQuery<T extends IQueryResult, T2 extends IDataItem> {
|
|
filterMode: FilterMode;
|
|
useData: (filter: ListFilterModel) => T;
|
|
getData: (data: T) => T2[];
|
|
getCount: (data: T) => number;
|
|
}
|
|
|
|
const useList = <QueryResult extends IQueryResult, QueryData extends IDataItem>(
|
|
options: IListHookOptions<QueryResult> & IQuery<QueryResult, QueryData>
|
|
): IListHookData => {
|
|
const [interfaceForage, setInterfaceForage] = useInterfaceLocalForage();
|
|
const forageInitialised = useRef(false);
|
|
const history = useHistory();
|
|
const location = useLocation();
|
|
const [filter, setFilter] = useState<ListFilterModel>(
|
|
new ListFilterModel(
|
|
options.filterMode,
|
|
options.subComponent ? "" : queryString.parse(location.search)
|
|
)
|
|
);
|
|
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
|
|
const [lastClickedId, setLastClickedId] = useState<string | undefined>();
|
|
const [zoomIndex, setZoomIndex] = useState<number>(1);
|
|
|
|
const result = options.useData(getFilter());
|
|
const totalCount = options.getCount(result);
|
|
const items = options.getData(result);
|
|
|
|
useEffect(() => {
|
|
if (!forageInitialised.current && !interfaceForage.loading) {
|
|
forageInitialised.current = true;
|
|
|
|
// Don't use query parameters for sub-components
|
|
if (options.subComponent) return;
|
|
// Don't read localForage if page already had query parameters
|
|
if (history.location.search) return;
|
|
|
|
const queryData = interfaceForage.data?.queries[options.filterMode];
|
|
if (!queryData) return;
|
|
|
|
const newFilter = new ListFilterModel(
|
|
options.filterMode,
|
|
queryString.parse(queryData.filter)
|
|
);
|
|
newFilter.currentPage = queryData.currentPage;
|
|
newFilter.itemsPerPage = queryData.itemsPerPage;
|
|
|
|
const newLocation = { ...history.location };
|
|
newLocation.search = queryData.filter;
|
|
history.replace(newLocation);
|
|
}
|
|
}, [
|
|
interfaceForage.data,
|
|
interfaceForage.loading,
|
|
history,
|
|
options.subComponent,
|
|
options.filterMode
|
|
]);
|
|
|
|
useEffect(() => {
|
|
if (options.subComponent) return;
|
|
|
|
const newFilter = new ListFilterModel(
|
|
options.filterMode,
|
|
options.subComponent ? "" : queryString.parse(location.search)
|
|
);
|
|
setFilter(newFilter);
|
|
|
|
if (forageInitialised.current) {
|
|
setInterfaceForage(config => {
|
|
const data = { ...config } as IInterfaceConfig;
|
|
data.queries = {
|
|
...config?.queries,
|
|
[options.filterMode]: {
|
|
filter: location.search,
|
|
itemsPerPage: newFilter.itemsPerPage,
|
|
currentPage: newFilter.currentPage
|
|
}
|
|
}
|
|
return data;
|
|
});
|
|
}
|
|
}, [location, options.filterMode, options.subComponent, setInterfaceForage]);
|
|
|
|
function getFilter() {
|
|
if (!options.filterHook) {
|
|
return filter;
|
|
}
|
|
|
|
// make a copy of the filter and call the hook
|
|
const newFilter = _.cloneDeep(filter);
|
|
return options.filterHook(newFilter);
|
|
}
|
|
|
|
function updateQueryParams(listfilter: ListFilterModel) {
|
|
const newLocation = { ...history.location };
|
|
newLocation.search = listfilter.makeQueryParameters();
|
|
history.replace(newLocation);
|
|
}
|
|
|
|
function onChangePageSize(pageSize: number) {
|
|
const newFilter = _.cloneDeep(filter);
|
|
newFilter.itemsPerPage = pageSize;
|
|
newFilter.currentPage = 1;
|
|
updateQueryParams(newFilter);
|
|
}
|
|
|
|
function onChangeQuery(query: string) {
|
|
const newFilter = _.cloneDeep(filter);
|
|
newFilter.searchTerm = query;
|
|
newFilter.currentPage = 1;
|
|
updateQueryParams(newFilter);
|
|
}
|
|
|
|
function onChangeSortDirection(sortDirection: SortDirectionEnum) {
|
|
const newFilter = _.cloneDeep(filter);
|
|
newFilter.sortDirection = sortDirection;
|
|
updateQueryParams(newFilter);
|
|
}
|
|
|
|
function onChangeSortBy(sortBy: string) {
|
|
const newFilter = _.cloneDeep(filter);
|
|
newFilter.sortBy = sortBy;
|
|
newFilter.currentPage = 1;
|
|
updateQueryParams(newFilter);
|
|
}
|
|
|
|
function onChangeDisplayMode(displayMode: DisplayMode) {
|
|
const newFilter = _.cloneDeep(filter);
|
|
newFilter.displayMode = displayMode;
|
|
updateQueryParams(newFilter);
|
|
}
|
|
|
|
function onAddCriterion(criterion: Criterion, oldId?: string) {
|
|
const newFilter = _.cloneDeep(filter);
|
|
|
|
// Find if we are editing an existing criteria, then modify that. Or create a new one.
|
|
const existingIndex = newFilter.criteria.findIndex(c => {
|
|
// If we modified an existing criterion, then look for the old id.
|
|
const id = oldId || criterion.getId();
|
|
return c.getId() === id;
|
|
});
|
|
if (existingIndex === -1) {
|
|
newFilter.criteria.push(criterion);
|
|
} else {
|
|
newFilter.criteria[existingIndex] = criterion;
|
|
}
|
|
|
|
// Remove duplicate modifiers
|
|
newFilter.criteria = newFilter.criteria.filter((obj, pos, arr) => {
|
|
return (
|
|
arr.map((mapObj: any) => mapObj.getId()).indexOf(obj.getId()) === pos
|
|
);
|
|
});
|
|
|
|
newFilter.currentPage = 1;
|
|
updateQueryParams(newFilter);
|
|
}
|
|
|
|
function onRemoveCriterion(removedCriterion: Criterion) {
|
|
const newFilter = _.cloneDeep(filter);
|
|
newFilter.criteria = newFilter.criteria.filter(
|
|
criterion => criterion.getId() !== removedCriterion.getId()
|
|
);
|
|
newFilter.currentPage = 1;
|
|
updateQueryParams(newFilter);
|
|
}
|
|
|
|
function onChangePage(page: number) {
|
|
const newFilter = _.cloneDeep(filter);
|
|
newFilter.currentPage = page;
|
|
updateQueryParams(newFilter);
|
|
}
|
|
|
|
function singleSelect(id: string, selected: boolean) {
|
|
setLastClickedId(id);
|
|
|
|
const newSelectedIds = _.clone(selectedIds);
|
|
if (selected) {
|
|
newSelectedIds.add(id);
|
|
} else {
|
|
newSelectedIds.delete(id);
|
|
}
|
|
|
|
setSelectedIds(newSelectedIds);
|
|
}
|
|
|
|
function selectRange(startIndex: number, endIndex: number) {
|
|
let start = startIndex;
|
|
let end = endIndex;
|
|
if (start > end) {
|
|
const tmp = start;
|
|
start = end;
|
|
end = tmp;
|
|
}
|
|
|
|
const subset = items.slice(start, end + 1);
|
|
const newSelectedIds: Set<string> = new Set();
|
|
|
|
subset.forEach(item => {
|
|
newSelectedIds.add(item.id);
|
|
});
|
|
|
|
setSelectedIds(newSelectedIds);
|
|
}
|
|
|
|
function multiSelect(id: string) {
|
|
let startIndex = 0;
|
|
let thisIndex = -1;
|
|
|
|
if (lastClickedId) {
|
|
startIndex = items.findIndex(item => {
|
|
return item.id === lastClickedId;
|
|
});
|
|
}
|
|
|
|
thisIndex = items.findIndex(item => {
|
|
return item.id === id;
|
|
});
|
|
|
|
selectRange(startIndex, thisIndex);
|
|
}
|
|
|
|
function onSelectChange(id: string, selected: boolean, shiftKey: boolean) {
|
|
if (shiftKey) {
|
|
multiSelect(id);
|
|
} else {
|
|
singleSelect(id, selected);
|
|
}
|
|
}
|
|
|
|
function onSelectAll() {
|
|
const newSelectedIds: Set<string> = new Set();
|
|
items.forEach(item => {
|
|
newSelectedIds.add(item.id);
|
|
});
|
|
|
|
setSelectedIds(newSelectedIds);
|
|
setLastClickedId(undefined);
|
|
}
|
|
|
|
function onSelectNone() {
|
|
const newSelectedIds: Set<string> = new Set();
|
|
setSelectedIds(newSelectedIds);
|
|
setLastClickedId(undefined);
|
|
}
|
|
|
|
function onChangeZoom(newZoomIndex: number) {
|
|
setZoomIndex(newZoomIndex);
|
|
}
|
|
|
|
const otherOperations = options.otherOperations
|
|
? options.otherOperations.map(o => {
|
|
return {
|
|
text: o.text,
|
|
onClick: () => {
|
|
o.onClick(result, filter, selectedIds);
|
|
}
|
|
};
|
|
})
|
|
: undefined;
|
|
|
|
let template;
|
|
if (result.loading || !forageInitialised.current) {
|
|
template = <LoadingIndicator />;
|
|
} else if (result.error) {
|
|
template = <h1>{result.error.message}</h1>;
|
|
} else {
|
|
template = (
|
|
<div>
|
|
<ListFilter
|
|
onChangePageSize={onChangePageSize}
|
|
onChangeQuery={onChangeQuery}
|
|
onChangeSortDirection={onChangeSortDirection}
|
|
onChangeSortBy={onChangeSortBy}
|
|
onChangeDisplayMode={onChangeDisplayMode}
|
|
onAddCriterion={onAddCriterion}
|
|
onRemoveCriterion={onRemoveCriterion}
|
|
onSelectAll={onSelectAll}
|
|
onSelectNone={onSelectNone}
|
|
zoomIndex={options.zoomable ? zoomIndex : undefined}
|
|
onChangeZoom={options.zoomable ? onChangeZoom : undefined}
|
|
otherOperations={otherOperations}
|
|
filter={filter}
|
|
/>
|
|
{options.renderSelectedOptions && selectedIds.size > 0
|
|
? options.renderSelectedOptions(result, selectedIds)
|
|
: undefined}
|
|
{options.renderContent(result, filter, selectedIds, zoomIndex)}
|
|
<Pagination
|
|
itemsPerPage={filter.itemsPerPage}
|
|
currentPage={filter.currentPage}
|
|
totalItems={totalCount}
|
|
onChangePage={onChangePage}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return { filter, template, onSelectChange };
|
|
};
|
|
|
|
export const useScenesList = (props: IListHookOptions<FindScenesQueryResult>) =>
|
|
useList<FindScenesQueryResult, SlimSceneDataFragment>({
|
|
...props,
|
|
filterMode: FilterMode.Scenes,
|
|
useData: StashService.useFindScenes,
|
|
getData: (result: FindScenesQueryResult) =>
|
|
result?.data?.findScenes?.scenes ?? [],
|
|
getCount: (result: FindScenesQueryResult) =>
|
|
result?.data?.findScenes?.count ?? 0
|
|
});
|
|
|
|
export const useSceneMarkersList = (
|
|
props: IListHookOptions<FindSceneMarkersQueryResult>
|
|
) =>
|
|
useList<FindSceneMarkersQueryResult, SceneMarkerDataFragment>({
|
|
...props,
|
|
filterMode: FilterMode.SceneMarkers,
|
|
useData: StashService.useFindSceneMarkers,
|
|
getData: (result: FindSceneMarkersQueryResult) =>
|
|
result?.data?.findSceneMarkers?.scene_markers ?? [],
|
|
getCount: (result: FindSceneMarkersQueryResult) =>
|
|
result?.data?.findSceneMarkers?.count ?? 0
|
|
});
|
|
|
|
export const useGalleriesList = (
|
|
props: IListHookOptions<FindGalleriesQueryResult>
|
|
) =>
|
|
useList<FindGalleriesQueryResult, GalleryDataFragment>({
|
|
...props,
|
|
filterMode: FilterMode.Galleries,
|
|
useData: StashService.useFindGalleries,
|
|
getData: (result: FindGalleriesQueryResult) =>
|
|
result?.data?.findGalleries?.galleries ?? [],
|
|
getCount: (result: FindGalleriesQueryResult) =>
|
|
result?.data?.findGalleries?.count ?? 0
|
|
});
|
|
|
|
export const useStudiosList = (
|
|
props: IListHookOptions<FindStudiosQueryResult>
|
|
) =>
|
|
useList<FindStudiosQueryResult, StudioDataFragment>({
|
|
...props,
|
|
filterMode: FilterMode.Studios,
|
|
useData: StashService.useFindStudios,
|
|
getData: (result: FindStudiosQueryResult) =>
|
|
result?.data?.findStudios?.studios ?? [],
|
|
getCount: (result: FindStudiosQueryResult) =>
|
|
result?.data?.findStudios?.count ?? 0
|
|
});
|
|
|
|
export const usePerformersList = (
|
|
props: IListHookOptions<FindPerformersQueryResult>
|
|
) =>
|
|
useList<FindPerformersQueryResult, PerformerDataFragment>({
|
|
...props,
|
|
filterMode: FilterMode.Performers,
|
|
useData: StashService.useFindPerformers,
|
|
getData: (result: FindPerformersQueryResult) =>
|
|
result?.data?.findPerformers?.performers ?? [],
|
|
getCount: (result: FindPerformersQueryResult) =>
|
|
result?.data?.findPerformers?.count ?? 0
|
|
});
|