diff --git a/ui/v2.5/package.json b/ui/v2.5/package.json index 6b69c1a54..45e07e342 100644 --- a/ui/v2.5/package.json +++ b/ui/v2.5/package.json @@ -60,6 +60,7 @@ "@types/query-string": "6.3.0", "@types/react": "16.9.15", "@types/react-dom": "16.9.4", + "@types/react-images": "^0.5.1", "@types/react-router-bootstrap": "^0.24.5", "@types/react-router-dom": "5.1.3", "@types/react-select": "^3.0.8", diff --git a/ui/v2.5/src/components/Galleries/GalleryList.tsx b/ui/v2.5/src/components/Galleries/GalleryList.tsx index 49f306307..bbae430e3 100644 --- a/ui/v2.5/src/components/Galleries/GalleryList.tsx +++ b/ui/v2.5/src/components/Galleries/GalleryList.tsx @@ -3,17 +3,12 @@ import { Table } from 'react-bootstrap'; import { QueryHookResult } from "react-apollo-hooks"; import { Link } from "react-router-dom"; import { FindGalleriesQuery, FindGalleriesVariables } from "src/core/generated-graphql"; -import { ListHook } from "src/hooks"; -import { IBaseProps } from "src/models/base-props"; +import { useGalleriesList } from "src/hooks"; import { ListFilterModel } from "src/models/list-filter/filter"; -import { DisplayMode, FilterMode } from "src/models/list-filter/types"; +import { DisplayMode } from "src/models/list-filter/types"; -interface IProps extends IBaseProps {} - -export const GalleryList: React.FC = (props: IProps) => { - const listData = ListHook.useList({ - filterMode: FilterMode.Galleries, - props, +export const GalleryList: React.FC = () => { + const listData = useGalleriesList({ renderContent, }); diff --git a/ui/v2.5/src/components/MainNavbar.tsx b/ui/v2.5/src/components/MainNavbar.tsx index 802b78671..22e1f0eb9 100644 --- a/ui/v2.5/src/components/MainNavbar.tsx +++ b/ui/v2.5/src/components/MainNavbar.tsx @@ -1,10 +1,10 @@ -import { Nav, Navbar, Button } from "react-bootstrap"; -import { LinkContainer } from 'react-router-bootstrap'; import React from "react"; +import { Nav, Navbar, Button } from "react-bootstrap"; +import { IconName } from '@fortawesome/fontawesome-svg-core'; +import { LinkContainer } from 'react-router-bootstrap'; import { Link, useLocation } from "react-router-dom"; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' -import { IconName } from '@fortawesome/fontawesome-svg-core'; +import { Icon } from 'src/components/Shared' interface IMenuItem { text: string; @@ -74,7 +74,7 @@ export const MainNavbar: React.FC = () => { key={i.href} > @@ -86,7 +86,7 @@ export const MainNavbar: React.FC = () => { exact={true} to="/settings"> diff --git a/ui/v2.5/src/components/Shared/DurationInput.tsx b/ui/v2.5/src/components/Shared/DurationInput.tsx index 9f3c6a885..152af3df7 100644 --- a/ui/v2.5/src/components/Shared/DurationInput.tsx +++ b/ui/v2.5/src/components/Shared/DurationInput.tsx @@ -1,6 +1,6 @@ import React, { useState, useEffect } from "react"; import { Button, ButtonGroup, InputGroup, Form } from 'react-bootstrap'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { Icon } from 'src/components/Shared' import { TextUtils } from "src/utils"; interface IProps { @@ -83,13 +83,13 @@ export const DurationInput: React.FC = (props: IProps) => { disabled={props.disabled} onClick={() => increment()} > - + ) @@ -107,7 +107,7 @@ export const DurationInput: React.FC = (props: IProps) => { ) } diff --git a/ui/v2.5/src/components/Studios/StudioList.tsx b/ui/v2.5/src/components/Studios/StudioList.tsx index 77d6f5b48..745577e91 100644 --- a/ui/v2.5/src/components/Studios/StudioList.tsx +++ b/ui/v2.5/src/components/Studios/StudioList.tsx @@ -1,18 +1,13 @@ -import React, { FunctionComponent } from "react"; +import React from "react"; import { QueryHookResult } from "react-apollo-hooks"; import { FindStudiosQuery, FindStudiosVariables } from "src/core/generated-graphql"; -import { ListHook } from "src/hooks"; -import { IBaseProps } from "src/models/base-props"; +import { useStudiosList } from "src/hooks"; import { ListFilterModel } from "src/models/list-filter/filter"; -import { DisplayMode, FilterMode } from "src/models/list-filter/types"; +import { DisplayMode } from "src/models/list-filter/types"; import { StudioCard } from "./StudioCard"; -interface IProps extends IBaseProps {} - -export const StudioList: FunctionComponent = (props: IProps) => { - const listData = ListHook.useList({ - filterMode: FilterMode.Studios, - props, +export const StudioList: React.FC = () => { + const listData = useStudiosList({ renderContent, }); diff --git a/ui/v2.5/src/components/list/Pagination.tsx b/ui/v2.5/src/components/list/Pagination.tsx index dd86d7898..ff832bcb1 100644 --- a/ui/v2.5/src/components/list/Pagination.tsx +++ b/ui/v2.5/src/components/list/Pagination.tsx @@ -6,6 +6,7 @@ interface IPaginationProps { currentPage: number; totalItems: number; onChangePage: (page: number) => void; + loading?: boolean; } interface IPaginationState { @@ -27,6 +28,8 @@ export class Pagination extends React.Component = (props: IPerformerListProps) => { +export const PerformerList: React.FC = () => { + const history = useHistory(); const otherOperations = [ { text: "Open Random", @@ -20,9 +19,7 @@ export const PerformerList: React.FC = (props: IPerformerLi } ]; - const listData = ListHook.useList({ - filterMode: FilterMode.Performers, - props, + const listData = usePerformersList({ otherOperations: otherOperations, renderContent, }); @@ -37,7 +34,7 @@ export const PerformerList: React.FC = (props: IPerformerLi const singleResult = await StashService.queryFindPerformers(filterCopy); if (singleResult && singleResult.data && singleResult.data.findPerformers && singleResult.data.findPerformers.performers.length === 1) { let id = singleResult!.data!.findPerformers!.performers[0]!.id; - props.history.push("/performers/" + id); + history.push("/performers/" + id); } } } diff --git a/ui/v2.5/src/components/scenes/SceneList.tsx b/ui/v2.5/src/components/scenes/SceneList.tsx index 1b762c16f..a72ae0495 100644 --- a/ui/v2.5/src/components/scenes/SceneList.tsx +++ b/ui/v2.5/src/components/scenes/SceneList.tsx @@ -1,20 +1,19 @@ import React from "react"; import _ from "lodash"; import { QueryHookResult } from "react-apollo-hooks"; +import { useHistory } from 'react-router-dom'; import { FindScenesQuery, FindScenesVariables, SlimSceneDataFragment } from "src/core/generated-graphql"; import { StashService } from "src/core/StashService"; -import { ListHook } from "src/hooks"; -import { IBaseProps } from "src/models/base-props"; +import { useScenesList } from "src/hooks"; import { ListFilterModel } from "src/models/list-filter/filter"; -import { DisplayMode, FilterMode } from "src/models/list-filter/types"; +import { DisplayMode } from "src/models/list-filter/types"; import { WallPanel } from "../Wall/WallPanel"; import { SceneCard } from "./SceneCard"; import { SceneListTable } from "./SceneListTable"; import { SceneSelectedOptions } from "./SceneSelectedOptions"; -interface ISceneListProps extends IBaseProps {} - -export const SceneList: React.FC = (props: ISceneListProps) => { +export const SceneList: React.FC = () => { + const history = useHistory(); const otherOperations = [ { text: "Play Random", @@ -22,9 +21,7 @@ export const SceneList: React.FC = (props: ISceneListProps) => } ]; - const listData = ListHook.useList({ - filterMode: FilterMode.Scenes, - props, + const listData = useScenesList({ zoomable: true, otherOperations: otherOperations, renderContent, @@ -44,7 +41,7 @@ export const SceneList: React.FC = (props: ISceneListProps) => if (singleResult && singleResult.data && singleResult.data.findScenes && singleResult.data.findScenes.scenes.length === 1) { let id = singleResult!.data!.findScenes!.scenes[0].id; // navigate to the scene player page - props.history.push("/scenes/" + id + "?autoplay=true"); + history.push("/scenes/" + id + "?autoplay=true"); } } } diff --git a/ui/v2.5/src/components/scenes/SceneMarkerList.tsx b/ui/v2.5/src/components/scenes/SceneMarkerList.tsx index 5e0b33517..23541da34 100644 --- a/ui/v2.5/src/components/scenes/SceneMarkerList.tsx +++ b/ui/v2.5/src/components/scenes/SceneMarkerList.tsx @@ -1,27 +1,24 @@ import _ from "lodash"; import React from "react"; import { QueryHookResult } from "react-apollo-hooks"; +import { useHistory } from 'react-router-dom'; import { FindSceneMarkersQuery, FindSceneMarkersVariables } from "src/core/generated-graphql"; import { StashService } from "src/core/StashService"; import { NavUtils } from "src/utils"; -import { ListHook } from "src/hooks"; -import { IBaseProps } from "src/models/base-props"; +import { useSceneMarkersList } from "src/hooks"; import { ListFilterModel } from "src/models/list-filter/filter"; -import { DisplayMode, FilterMode } from "src/models/list-filter/types"; +import { DisplayMode } from "src/models/list-filter/types"; import { WallPanel } from "../Wall/WallPanel"; -interface IProps extends IBaseProps {} - -export const SceneMarkerList: React.FC = (props: IProps) => { +export const SceneMarkerList: React.FC = () => { + const history = useHistory(); const otherOperations = [{ text: "Play Random", onClick: playRandom }]; - const listData = ListHook.useList({ - filterMode: FilterMode.SceneMarkers, + const listData = useSceneMarkersList({ otherOperations: otherOperations, - props, renderContent, }); @@ -38,7 +35,7 @@ export const SceneMarkerList: React.FC = (props: IProps) => { if (singleResult && singleResult.data && singleResult.data.findSceneMarkers && singleResult.data.findSceneMarkers.scene_markers.length === 1) { // navigate to the scene player page let url = NavUtils.makeSceneMarkerUrl(singleResult.data.findSceneMarkers.scene_markers[0]) - props.history.push(url); + history.push(url); } } } diff --git a/ui/v2.5/src/hooks/ListHook.tsx b/ui/v2.5/src/hooks/ListHook.tsx index 534755e67..5e5695e3d 100644 --- a/ui/v2.5/src/hooks/ListHook.tsx +++ b/ui/v2.5/src/hooks/ListHook.tsx @@ -1,310 +1,333 @@ import _ from "lodash"; import queryString from "query-string"; -import React, { useEffect, useState } from "react"; +import React, { useState } from "react"; import { Spinner } from 'react-bootstrap'; import { QueryHookResult } from "react-apollo-hooks"; +import { ApolloError } from 'apollo-client'; +import { useHistory } from 'react-router-dom'; +import { + FindScenesQuery, + FindScenesVariables, + SlimSceneDataFragment, + FindSceneMarkersQuery, + FindSceneMarkersVariables, + FindSceneMarkersSceneMarkers, + FindGalleriesQuery, + FindGalleriesVariables, + GalleryDataFragment, + FindStudiosQuery, + FindStudiosVariables, + StudioDataFragment, + FindPerformersQuery, + FindPerformersVariables, + PerformerDataFragment +} from 'src/core/generated-graphql'; import { ListFilter } from "../components/list/ListFilter"; import { Pagination } from "../components/list/Pagination"; import { StashService } from "../core/StashService"; -import { IBaseProps } from "../models"; import { Criterion } from "../models/list-filter/criteria/criterion"; import { ListFilterModel } from "../models/list-filter/filter"; import { DisplayMode, FilterMode } from "../models/list-filter/types"; -export interface IListHookData { +interface IListHookData { filter: ListFilterModel; template: JSX.Element; - options: IListHookOptions; onSelectChange: (id: string, selected : boolean, shiftKey: boolean) => void; } -interface IListHookOperation { +interface IListHookOperation { text: string; - onClick: (result: QueryHookResult, filter: ListFilterModel, selectedIds: Set) => void; + onClick: (result: T, filter: ListFilterModel, selectedIds: Set) => void; } -export interface IListHookOptions { - filterMode: FilterMode; - props: IBaseProps; +interface IListHookOptions { zoomable?: boolean; - otherOperations?: IListHookOperation[]; - renderContent: (result: QueryHookResult, filter: ListFilterModel, selectedIds: Set, zoomIndex: number) => JSX.Element | undefined; - renderSelectedOptions?: (result: QueryHookResult, selectedIds: Set) => JSX.Element | undefined; + otherOperations?: IListHookOperation[]; + renderContent: (result: T, filter: ListFilterModel, selectedIds: Set, zoomIndex: number) => JSX.Element | undefined; + renderSelectedOptions?: (result: T, selectedIds: Set) => JSX.Element | undefined; } -export class ListHook { - public static useList(options: IListHookOptions): IListHookData { - const [filter, setFilter] = useState(new ListFilterModel(options.filterMode)); - const [selectedIds, setSelectedIds] = useState>(new Set()); - const [lastClickedId, setLastClickedId] = useState(undefined); - const [totalCount, setTotalCount] = useState(0); - const [zoomIndex, setZoomIndex] = useState(1); +interface IDataItem { + id: string; +} +interface IQueryResult { + error?: ApolloError; + loading: boolean; +} - // Update the filter when the query parameters change - useEffect(() => { - const queryParams = queryString.parse(options.props.location.search); - const newFilter = _.cloneDeep(filter); - newFilter.configureFromQueryParameters(queryParams); - setFilter(newFilter); +interface IQuery { + filterMode: FilterMode; + useData: (filter: ListFilterModel) => T; + getData: (data: T) => T2[]; + getCount: (data: T) => number; +} - // TODO: Need this side effect to update the query params properly - filter.configureFromQueryParameters(queryParams); - }, [options.props.location.search]); +type ScenesQuery = QueryHookResult; +export const useScenesList = (props:IListHookOptions) => ( + useList({ + ...props, + filterMode: FilterMode.Scenes, + useData: StashService.useFindScenes, + getData: (result:ScenesQuery) => (result?.data?.findScenes?.scenes ?? []), + getCount: (result:ScenesQuery) => (result?.data?.findScenes?.count ?? 0) + }) +) - let result: QueryHookResult; +type SceneMarkersQuery = QueryHookResult; +export const useSceneMarkersList = (props:IListHookOptions) => ( + useList({ + ...props, + filterMode: FilterMode.SceneMarkers, + useData: StashService.useFindSceneMarkers, + getData: (result:SceneMarkersQuery) => (result?.data?.findSceneMarkers?.scene_markers?? []), + getCount: (result:SceneMarkersQuery) => (result?.data?.findSceneMarkers?.count ?? 0) + }) +) - let getData: (filter : ListFilterModel) => QueryHookResult; - let getItems: () => any[]; - let getCount: () => number; +type GalleriesQuery = QueryHookResult; +export const useGalleriesList = (props:IListHookOptions) => ( + useList({ + ...props, + filterMode: FilterMode.Galleries, + useData: StashService.useFindGalleries, + getData: (result:GalleriesQuery) => (result?.data?.findGalleries?.galleries ?? []), + getCount: (result:GalleriesQuery) => (result?.data?.findGalleries?.count ?? 0) + }) +) - switch (options.filterMode) { - case FilterMode.Scenes: { - getData = (filter : ListFilterModel) => { return StashService.useFindScenes(filter); } - getItems = () => { return !!result.data && !!result.data.findScenes ? result.data.findScenes.scenes : []; } - getCount = () => { return !!result.data && !!result.data.findScenes ? result.data.findScenes.count : 0; } - break; - } - case FilterMode.SceneMarkers: { - getData = (filter : ListFilterModel) => { return StashService.useFindSceneMarkers(filter); } - getItems = () => { return !!result.data && !!result.data.findSceneMarkers ? result.data.findSceneMarkers.scene_markers : []; } - getCount = () => { return !!result.data && !!result.data.findSceneMarkers ? result.data.findSceneMarkers.count : 0; } - break; - } - case FilterMode.Galleries: { - getData = (filter : ListFilterModel) => { return StashService.useFindGalleries(filter); } - getItems = () => { return !!result.data && !!result.data.findGalleries ? result.data.findGalleries.galleries : []; } - getCount = () => { return !!result.data && !!result.data.findGalleries ? result.data.findGalleries.count : 0; } - break; - } - case FilterMode.Studios: { - getData = (filter : ListFilterModel) => { return StashService.useFindStudios(filter); } - getItems = () => { return !!result.data && !!result.data.findStudios ? result.data.findStudios.studios : []; } - getCount = () => { return !!result.data && !!result.data.findStudios ? result.data.findStudios.count : 0; } - break; - } - case FilterMode.Performers: { - getData = (filter : ListFilterModel) => { return StashService.useFindPerformers(filter); } - getItems = () => { return !!result.data && !!result.data.findPerformers ? result.data.findPerformers.performers : []; } - getCount = () => { return !!result.data && !!result.data.findPerformers ? result.data.findPerformers.count : 0; } - break; - } - default: { - console.error("REMOVE DEFAULT IN LIST HOOK"); - getData = (filter : ListFilterModel) => { return StashService.useFindScenes(filter); } - getItems = () => { return !!result.data && !!result.data.findScenes ? result.data.findScenes.scenes : []; } - getCount = () => { return !!result.data && !!result.data.findScenes ? result.data.findScenes.count : 0; } - break; - } - } +type StudiosQuery = QueryHookResult; +export const useStudiosList = (props:IListHookOptions) => ( + useList({ + ...props, + filterMode: FilterMode.Studios, + useData: StashService.useFindStudios, + getData: (result:StudiosQuery) => (result?.data?.findStudios?.studios ?? []), + getCount: (result:StudiosQuery) => (result?.data?.findStudios?.count ?? 0) + }) +) - result = getData(filter); +type PerformersQuery = QueryHookResult; +export const usePerformersList = (props:IListHookOptions) => ( + useList({ + ...props, + filterMode: FilterMode.Performers, + useData: StashService.useFindPerformers, + getData: (result:PerformersQuery) => (result?.data?.findPerformers?.performers ?? []), + getCount: (result:PerformersQuery) => (result?.data?.findPerformers?.count ?? 0) + }) +) - useEffect(() => { - setTotalCount(getCount()); +const useList = ( + options: (IListHookOptions & IQuery) +): IListHookData => { + const history = useHistory(); + const [filter, setFilter] = useState(new ListFilterModel(options.filterMode, queryString.parse(history.location.search))); + const [selectedIds, setSelectedIds] = useState>(new Set()); + const [lastClickedId, setLastClickedId] = useState(); + const [zoomIndex, setZoomIndex] = useState(1); - // select none when data changes - onSelectNone(); - setLastClickedId(undefined); - }, [result.data]) + const result = options.useData(filter); + const totalCount = options.getCount(result); + const items = options.getData(result); - // Update the query parameters when the data changes - useEffect(() => { - const location = Object.assign({}, options.props.history.location); - location.search = filter.makeQueryParameters(); - options.props.history.replace(location); - }, [result.data, filter.displayMode]); - - // Update the total count - useEffect(() => { - const newFilter = _.cloneDeep(filter); - newFilter.totalCount = totalCount; - setFilter(newFilter); - }, [totalCount]); - - function onChangePageSize(pageSize: number) { - const newFilter = _.cloneDeep(filter); - newFilter.itemsPerPage = pageSize; - newFilter.currentPage = 1; - setFilter(newFilter); - } - - function onChangeQuery(query: string) { - const newFilter = _.cloneDeep(filter); - newFilter.searchTerm = query; - newFilter.currentPage = 1; - setFilter(newFilter); - } - - function onChangeSortDirection(sortDirection: "asc" | "desc") { - const newFilter = _.cloneDeep(filter); - newFilter.sortDirection = sortDirection; - setFilter(newFilter); - } - - function onChangeSortBy(sortBy: string) { - const newFilter = _.cloneDeep(filter); - newFilter.sortBy = sortBy; - newFilter.currentPage = 1; - setFilter(newFilter); - } - - function onChangeDisplayMode(displayMode: DisplayMode) { - const newFilter = _.cloneDeep(filter); - newFilter.displayMode = displayMode; - setFilter(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 ? 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; - setFilter(newFilter); - } - - function onRemoveCriterion(removedCriterion: Criterion) { - const newFilter = _.cloneDeep(filter); - newFilter.criteria = newFilter.criteria.filter((criterion) => criterion.getId() !== removedCriterion.getId()); - newFilter.currentPage = 1; - setFilter(newFilter); - } - - function onChangePage(page: number) { - const newFilter = _.cloneDeep(filter); - newFilter.currentPage = page; - setFilter(newFilter); - } - - function onSelectChange(id: string, selected : boolean, shiftKey: boolean) { - if (shiftKey) { - multiSelect(id, selected); - } else { - singleSelect(id, selected); - } - } - - function singleSelect(id: string, selected: boolean) { - setLastClickedId(id); - - const newSelectedIds = _.clone(selectedIds); - if (selected) { - newSelectedIds.add(id); - } else { - newSelectedIds.delete(id); - } - - setSelectedIds(newSelectedIds); - } - - function multiSelect(id: string, selected : boolean) { - let startIndex = 0; - let thisIndex = -1; - - if (!!lastClickedId) { - startIndex = getItems().findIndex((item) => { - return item.id === lastClickedId; - }); - } - - thisIndex = getItems().findIndex((item) => { - return item.id === id; - }); - - selectRange(startIndex, thisIndex); - } - - function selectRange(startIndex : number, endIndex : number) { - if (startIndex > endIndex) { - let tmp = startIndex; - startIndex = endIndex; - endIndex = tmp; - } - - const subset = getItems().slice(startIndex, endIndex + 1); - const newSelectedIds : Set = new Set(); - - subset.forEach((item) => { - newSelectedIds.add(item.id); - }); - - setSelectedIds(newSelectedIds); - } - - function onSelectAll() { - const newSelectedIds : Set = new Set(); - getItems().forEach((item) => { - newSelectedIds.add(item.id); - }); - - setSelectedIds(newSelectedIds); - setLastClickedId(undefined); - } - - function onSelectNone() { - const newSelectedIds : Set = 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; - - const template = ( -
- - {options.renderSelectedOptions && selectedIds.size > 0 ? options.renderSelectedOptions(result, selectedIds) : undefined} - {result.loading ? : undefined} - {result.error ?

{result.error.message}

: undefined} - {options.renderContent(result, filter, selectedIds, zoomIndex)} - -
- ); - - return { filter, template, options, onSelectChange }; + function updateQueryParams(filter:ListFilterModel) { + const newLocation = Object.assign({}, history.location); + newLocation.search = filter.makeQueryParameters(); + history.replace(newLocation); } + + function onChangePageSize(pageSize: number) { + const newFilter = _.cloneDeep(filter); + newFilter.itemsPerPage = pageSize; + newFilter.currentPage = 1; + setFilter(newFilter); + updateQueryParams(newFilter); + } + + function onChangeQuery(query: string) { + const newFilter = _.cloneDeep(filter); + newFilter.searchTerm = query; + newFilter.currentPage = 1; + setFilter(newFilter); + updateQueryParams(newFilter); + } + + function onChangeSortDirection(sortDirection: "asc" | "desc") { + const newFilter = _.cloneDeep(filter); + newFilter.sortDirection = sortDirection; + setFilter(newFilter); + updateQueryParams(newFilter); + } + + function onChangeSortBy(sortBy: string) { + const newFilter = _.cloneDeep(filter); + newFilter.sortBy = sortBy; + newFilter.currentPage = 1; + setFilter(newFilter); + updateQueryParams(newFilter); + } + + function onChangeDisplayMode(displayMode: DisplayMode) { + const newFilter = _.cloneDeep(filter); + newFilter.displayMode = displayMode; + setFilter(newFilter); + 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 ? 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; + setFilter(newFilter); + updateQueryParams(newFilter); + } + + function onRemoveCriterion(removedCriterion: Criterion) { + const newFilter = _.cloneDeep(filter); + newFilter.criteria = newFilter.criteria.filter((criterion) => criterion.getId() !== removedCriterion.getId()); + newFilter.currentPage = 1; + setFilter(newFilter); + updateQueryParams(newFilter); + } + + function onChangePage(page: number) { + const newFilter = _.cloneDeep(filter); + newFilter.currentPage = page; + setFilter(newFilter); + updateQueryParams(newFilter); + } + + function onSelectChange(id: string, selected : boolean, shiftKey: boolean) { + if (shiftKey) { + multiSelect(id); + } else { + singleSelect(id, selected); + } + } + + function singleSelect(id: string, selected: boolean) { + setLastClickedId(id); + + const newSelectedIds = _.clone(selectedIds); + if (selected) { + newSelectedIds.add(id); + } else { + newSelectedIds.delete(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 selectRange(startIndex : number, endIndex : number) { + if (startIndex > endIndex) { + let tmp = startIndex; + startIndex = endIndex; + endIndex = tmp; + } + + const subset = items.slice(startIndex, endIndex + 1); + const newSelectedIds : Set = new Set(); + + subset.forEach((item) => { + newSelectedIds.add(item.id); + }); + + setSelectedIds(newSelectedIds); + } + + function onSelectAll() { + const newSelectedIds : Set = new Set(); + items.forEach((item) => { + newSelectedIds.add(item.id); + }); + + setSelectedIds(newSelectedIds); + setLastClickedId(undefined); + } + + function onSelectNone() { + const newSelectedIds : Set = 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; + + const template = ( +
+ + {options.renderSelectedOptions && selectedIds.size > 0 ? options.renderSelectedOptions(result, selectedIds) : undefined} + {result.loading ? : undefined} + {result.error ?

{result.error.message}

: undefined} + {options.renderContent(result, filter, selectedIds, zoomIndex)} + +
+ ); + + return { filter, template, onSelectChange }; } diff --git a/ui/v2.5/src/hooks/index.ts b/ui/v2.5/src/hooks/index.ts index 8696e3455..f422e3cf9 100644 --- a/ui/v2.5/src/hooks/index.ts +++ b/ui/v2.5/src/hooks/index.ts @@ -1,4 +1,4 @@ export { default as useToast } from './Toast'; export { useInterfaceLocalForage } from './LocalForage'; export { VideoHoverHook } from './VideoHover'; -export { ListHook } from './ListHook'; +export { useScenesList, useSceneMarkersList, useGalleriesList, useStudiosList, usePerformersList } from './ListHook'; diff --git a/ui/v2.5/src/models/index.ts b/ui/v2.5/src/models/index.ts index 1f967f1aa..0e1646573 100644 --- a/ui/v2.5/src/models/index.ts +++ b/ui/v2.5/src/models/index.ts @@ -1,2 +1 @@ export * from "./base-props"; -export * from "./types"; diff --git a/ui/v2.5/src/models/list-filter/filter.ts b/ui/v2.5/src/models/list-filter/filter.ts index dc0421caf..560d3e771 100644 --- a/ui/v2.5/src/models/list-filter/filter.ts +++ b/ui/v2.5/src/models/list-filter/filter.ts @@ -45,9 +45,8 @@ export class ListFilterModel { public displayModeOptions: DisplayMode[] = []; public criterionOptions: ICriterionOption[] = []; public criteria: Array> = []; - public totalCount: number = 0; - public constructor(filterMode: FilterMode) { + public constructor(filterMode: FilterMode, rawParms?: any) { switch (filterMode) { case FilterMode.Scenes: if (!!this.sortBy === false) { this.sortBy = "date"; } @@ -142,6 +141,8 @@ export class ListFilterModel { } if (!!this.displayMode === false) { this.displayMode = this.displayModeOptions[0]; } this.sortByOptions = [...this.sortByOptions, "created_at", "updated_at"]; + if(rawParms) + this.configureFromQueryParameters(rawParms); } public configureFromQueryParameters(rawParms: any) { diff --git a/ui/v2.5/src/models/react-images.d.ts b/ui/v2.5/src/models/react-images.d.ts deleted file mode 100644 index 420834950..000000000 --- a/ui/v2.5/src/models/react-images.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -declare module "react-images" { - // typing module default export as `any` will allow you to access its members without compiler warning - var Lightbox: any; - export default Lightbox; -} diff --git a/ui/v2.5/src/models/types.ts b/ui/v2.5/src/models/types.ts deleted file mode 100644 index b2e881da1..000000000 --- a/ui/v2.5/src/models/types.ts +++ /dev/null @@ -1 +0,0 @@ -export type HTMLInputProps = React.InputHTMLAttributes; diff --git a/ui/v2.5/yarn.lock b/ui/v2.5/yarn.lock index d786f70b5..1fb3fe3eb 100644 --- a/ui/v2.5/yarn.lock +++ b/ui/v2.5/yarn.lock @@ -1559,6 +1559,13 @@ dependencies: "@types/react" "*" +"@types/react-images@^0.5.1": + version "0.5.1" + resolved "https://registry.yarnpkg.com/@types/react-images/-/react-images-0.5.1.tgz#e00ccce7221a03ed7b43072cefdc0e5a66fc5a08" + integrity sha512-n2guyR+kblfNEAr1TA3GnrpEdt0/2dHxMOFaFcCgI62NRQaQClaNdgidsim3JRVWLlUiWilDjzZnJUSR0kMncQ== + dependencies: + "@types/react" "*" + "@types/react-router-bootstrap@^0.24.5": version "0.24.5" resolved "https://registry.yarnpkg.com/@types/react-router-bootstrap/-/react-router-bootstrap-0.24.5.tgz#9257ba3dfb01cda201aac9fa05cde3eb09ea5b27"