mirror of
https://github.com/stashapp/stash.git
synced 2025-12-17 20:34:37 +03:00
Refactor scene list to not use ItemList component (#5767)
* Add fields to useListSelect * Add more utility hooks * Remove context from FilteredListToolbar * Refactor SceneList to not use ItemList * Move common logic into useFilteredListHook
This commit is contained in:
@@ -5,7 +5,7 @@ import {
|
|||||||
GroupsCriterionOption,
|
GroupsCriterionOption,
|
||||||
} from "src/models/list-filter/criteria/groups";
|
} from "src/models/list-filter/criteria/groups";
|
||||||
import { ListFilterModel } from "src/models/list-filter/filter";
|
import { ListFilterModel } from "src/models/list-filter/filter";
|
||||||
import { SceneList } from "src/components/Scenes/SceneList";
|
import { FilteredSceneList } from "src/components/Scenes/SceneList";
|
||||||
import { View } from "src/components/List/views";
|
import { View } from "src/components/List/views";
|
||||||
|
|
||||||
interface IGroupScenesPanel {
|
interface IGroupScenesPanel {
|
||||||
@@ -64,7 +64,7 @@ export const GroupScenesPanel: React.FC<IGroupScenesPanel> = ({
|
|||||||
|
|
||||||
if (group && group.id) {
|
if (group && group.id) {
|
||||||
return (
|
return (
|
||||||
<SceneList
|
<FilteredSceneList
|
||||||
filterHook={filterHook}
|
filterHook={filterHook}
|
||||||
defaultSort="group_scene_number"
|
defaultSort="group_scene_number"
|
||||||
alterQuery={active}
|
alterQuery={active}
|
||||||
|
|||||||
@@ -488,3 +488,33 @@ export const EditFilterDialog: React.FC<IEditFilterProps> = ({
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export function useShowEditFilter(props: {
|
||||||
|
filter: ListFilterModel;
|
||||||
|
setFilter: (f: ListFilterModel) => void;
|
||||||
|
showModal: (content: React.ReactNode) => void;
|
||||||
|
closeModal: () => void;
|
||||||
|
}) {
|
||||||
|
const { filter, setFilter, showModal, closeModal } = props;
|
||||||
|
|
||||||
|
const showEditFilter = useCallback(
|
||||||
|
(editingCriterion?: string) => {
|
||||||
|
function onApplyEditFilter(f: ListFilterModel) {
|
||||||
|
closeModal();
|
||||||
|
setFilter(f);
|
||||||
|
}
|
||||||
|
|
||||||
|
showModal(
|
||||||
|
<EditFilterDialog
|
||||||
|
filter={filter}
|
||||||
|
onApply={onApplyEditFilter}
|
||||||
|
onCancel={() => closeModal()}
|
||||||
|
editingCriterion={editingCriterion}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[filter, setFilter, showModal, closeModal]
|
||||||
|
);
|
||||||
|
|
||||||
|
return showEditFilter;
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,11 +8,9 @@ import {
|
|||||||
IListFilterOperation,
|
IListFilterOperation,
|
||||||
ListOperationButtons,
|
ListOperationButtons,
|
||||||
} from "./ListOperationButtons";
|
} from "./ListOperationButtons";
|
||||||
import { DisplayMode } from "src/models/list-filter/types";
|
|
||||||
import { ButtonToolbar } from "react-bootstrap";
|
import { ButtonToolbar } from "react-bootstrap";
|
||||||
import { View } from "./views";
|
import { View } from "./views";
|
||||||
import { useListContext } from "./ListProvider";
|
import { IListSelect, useFilterOperations } from "./util";
|
||||||
import { useFilter } from "./FilterProvider";
|
|
||||||
|
|
||||||
export interface IItemListOperation<T extends QueryResult> {
|
export interface IItemListOperation<T extends QueryResult> {
|
||||||
text: string;
|
text: string;
|
||||||
@@ -32,8 +30,13 @@ export interface IItemListOperation<T extends QueryResult> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface IFilteredListToolbar {
|
export interface IFilteredListToolbar {
|
||||||
showEditFilter?: (editingCriterion?: string) => void;
|
filter: ListFilterModel;
|
||||||
|
setFilter: (
|
||||||
|
value: ListFilterModel | ((prevState: ListFilterModel) => ListFilterModel)
|
||||||
|
) => void;
|
||||||
|
showEditFilter: () => void;
|
||||||
view?: View;
|
view?: View;
|
||||||
|
listSelect: IListSelect;
|
||||||
onEdit?: () => void;
|
onEdit?: () => void;
|
||||||
onDelete?: () => void;
|
onDelete?: () => void;
|
||||||
operations?: IListFilterOperation[];
|
operations?: IListFilterOperation[];
|
||||||
@@ -41,25 +44,22 @@ export interface IFilteredListToolbar {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const FilteredListToolbar: React.FC<IFilteredListToolbar> = ({
|
export const FilteredListToolbar: React.FC<IFilteredListToolbar> = ({
|
||||||
|
filter,
|
||||||
|
setFilter,
|
||||||
showEditFilter,
|
showEditFilter,
|
||||||
view,
|
view,
|
||||||
|
listSelect,
|
||||||
onEdit,
|
onEdit,
|
||||||
onDelete,
|
onDelete,
|
||||||
operations,
|
operations,
|
||||||
zoomable = false,
|
zoomable = false,
|
||||||
}) => {
|
}) => {
|
||||||
const { getSelected, onSelectAll, onSelectNone } = useListContext();
|
|
||||||
const { filter, setFilter } = useFilter();
|
|
||||||
|
|
||||||
const filterOptions = filter.options;
|
const filterOptions = filter.options;
|
||||||
|
const { setDisplayMode, setZoom } = useFilterOperations({
|
||||||
function onChangeDisplayMode(displayMode: DisplayMode) {
|
filter,
|
||||||
setFilter(filter.setDisplayMode(displayMode));
|
setFilter,
|
||||||
}
|
});
|
||||||
|
const { selectedIds, onSelectAll, onSelectNone } = listSelect;
|
||||||
function onChangeZoom(newZoomIndex: number) {
|
|
||||||
setFilter(filter.setZoom(newZoomIndex));
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ButtonToolbar className="filtered-list-toolbar">
|
<ButtonToolbar className="filtered-list-toolbar">
|
||||||
@@ -75,16 +75,16 @@ export const FilteredListToolbar: React.FC<IFilteredListToolbar> = ({
|
|||||||
onSelectAll={onSelectAll}
|
onSelectAll={onSelectAll}
|
||||||
onSelectNone={onSelectNone}
|
onSelectNone={onSelectNone}
|
||||||
otherOperations={operations}
|
otherOperations={operations}
|
||||||
itemsSelected={getSelected().length > 0}
|
itemsSelected={selectedIds.size > 0}
|
||||||
onEdit={onEdit}
|
onEdit={onEdit}
|
||||||
onDelete={onDelete}
|
onDelete={onDelete}
|
||||||
/>
|
/>
|
||||||
<ListViewOptions
|
<ListViewOptions
|
||||||
displayMode={filter.displayMode}
|
displayMode={filter.displayMode}
|
||||||
displayModeOptions={filterOptions.displayModeOptions}
|
displayModeOptions={filterOptions.displayModeOptions}
|
||||||
onSetDisplayMode={onChangeDisplayMode}
|
onSetDisplayMode={setDisplayMode}
|
||||||
zoomIndex={zoomable ? filter.zoomIndex : undefined}
|
zoomIndex={zoomable ? filter.zoomIndex : undefined}
|
||||||
onSetZoom={zoomable ? onChangeZoom : undefined}
|
onSetZoom={zoomable ? setZoom : undefined}
|
||||||
/>
|
/>
|
||||||
</ButtonToolbar>
|
</ButtonToolbar>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -10,7 +10,10 @@ import * as GQL from "src/core/generated-graphql";
|
|||||||
import { QueryResult } from "@apollo/client";
|
import { QueryResult } from "@apollo/client";
|
||||||
import { Criterion } from "src/models/list-filter/criteria/criterion";
|
import { Criterion } from "src/models/list-filter/criteria/criterion";
|
||||||
import { ListFilterModel } from "src/models/list-filter/filter";
|
import { ListFilterModel } from "src/models/list-filter/filter";
|
||||||
import { EditFilterDialog } from "src/components/List/EditFilterDialog";
|
import {
|
||||||
|
EditFilterDialog,
|
||||||
|
useShowEditFilter,
|
||||||
|
} from "src/components/List/EditFilterDialog";
|
||||||
import { FilterTags } from "./FilterTags";
|
import { FilterTags } from "./FilterTags";
|
||||||
import { View } from "./views";
|
import { View } from "./views";
|
||||||
import { IHasID } from "src/utils/data";
|
import { IHasID } from "src/utils/data";
|
||||||
@@ -23,9 +26,15 @@ import {
|
|||||||
import { FilterContext, SetFilterURL, useFilter } from "./FilterProvider";
|
import { FilterContext, SetFilterURL, useFilter } from "./FilterProvider";
|
||||||
import { useModal } from "src/hooks/modal";
|
import { useModal } from "src/hooks/modal";
|
||||||
import {
|
import {
|
||||||
|
IFilterStateHook,
|
||||||
|
IQueryResultHook,
|
||||||
useDefaultFilter,
|
useDefaultFilter,
|
||||||
useEnsureValidPage,
|
useEnsureValidPage,
|
||||||
|
useFilterOperations,
|
||||||
|
useFilterState,
|
||||||
useListKeyboardShortcuts,
|
useListKeyboardShortcuts,
|
||||||
|
useListSelect,
|
||||||
|
useQueryResult,
|
||||||
useScrollToTopOnPageChange,
|
useScrollToTopOnPageChange,
|
||||||
} from "./util";
|
} from "./util";
|
||||||
import {
|
import {
|
||||||
@@ -36,6 +45,72 @@ import {
|
|||||||
import { PagedList } from "./PagedList";
|
import { PagedList } from "./PagedList";
|
||||||
import { ConfigurationContext } from "src/hooks/Config";
|
import { ConfigurationContext } from "src/hooks/Config";
|
||||||
|
|
||||||
|
interface IFilteredItemList<T extends QueryResult, E extends IHasID = IHasID> {
|
||||||
|
filterStateProps: IFilterStateHook;
|
||||||
|
queryResultProps: IQueryResultHook<T, E>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Provides the common state and behaviour for filtered item list components
|
||||||
|
export function useFilteredItemList<
|
||||||
|
T extends QueryResult,
|
||||||
|
E extends IHasID = IHasID
|
||||||
|
>(props: IFilteredItemList<T, E>) {
|
||||||
|
const { configuration: config } = useContext(ConfigurationContext);
|
||||||
|
|
||||||
|
// States
|
||||||
|
const filterState = useFilterState({
|
||||||
|
config,
|
||||||
|
...props.filterStateProps,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { filter, setFilter } = filterState;
|
||||||
|
|
||||||
|
const queryResult = useQueryResult({
|
||||||
|
filter,
|
||||||
|
...props.queryResultProps,
|
||||||
|
});
|
||||||
|
const { result, items, totalCount, pages } = queryResult;
|
||||||
|
|
||||||
|
const listSelect = useListSelect(items);
|
||||||
|
const { onSelectAll, onSelectNone } = listSelect;
|
||||||
|
|
||||||
|
const modalState = useModal();
|
||||||
|
const { showModal, closeModal } = modalState;
|
||||||
|
|
||||||
|
// Utility hooks
|
||||||
|
const { setPage } = useFilterOperations({ filter, setFilter });
|
||||||
|
|
||||||
|
// scroll to the top of the page when the page changes
|
||||||
|
useScrollToTopOnPageChange(filter.currentPage, result.loading);
|
||||||
|
|
||||||
|
// ensure that the current page is valid
|
||||||
|
useEnsureValidPage(filter, totalCount, setFilter);
|
||||||
|
|
||||||
|
const showEditFilter = useShowEditFilter({
|
||||||
|
showModal,
|
||||||
|
closeModal,
|
||||||
|
filter,
|
||||||
|
setFilter,
|
||||||
|
});
|
||||||
|
|
||||||
|
useListKeyboardShortcuts({
|
||||||
|
currentPage: filter.currentPage,
|
||||||
|
onChangePage: setPage,
|
||||||
|
onSelectAll,
|
||||||
|
onSelectNone,
|
||||||
|
pages,
|
||||||
|
showEditFilter,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
filterState,
|
||||||
|
queryResult,
|
||||||
|
listSelect,
|
||||||
|
modalState,
|
||||||
|
showEditFilter,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
interface IItemListProps<T extends QueryResult, E extends IHasID> {
|
interface IItemListProps<T extends QueryResult, E extends IHasID> {
|
||||||
view?: View;
|
view?: View;
|
||||||
zoomable?: boolean;
|
zoomable?: boolean;
|
||||||
@@ -83,13 +158,14 @@ export const ItemList = <T extends QueryResult, E extends IHasID>(
|
|||||||
const { filter, setFilter: updateFilter } = useFilter();
|
const { filter, setFilter: updateFilter } = useFilter();
|
||||||
const { effectiveFilter, result, cachedResult, totalCount } =
|
const { effectiveFilter, result, cachedResult, totalCount } =
|
||||||
useQueryResultContext<T, E>();
|
useQueryResultContext<T, E>();
|
||||||
|
const listSelect = useListContext<E>();
|
||||||
const {
|
const {
|
||||||
selectedIds,
|
selectedIds,
|
||||||
getSelected,
|
getSelected,
|
||||||
onSelectChange,
|
onSelectChange,
|
||||||
onSelectAll,
|
onSelectAll,
|
||||||
onSelectNone,
|
onSelectNone,
|
||||||
} = useListContext<E>();
|
} = listSelect;
|
||||||
|
|
||||||
// scroll to the top of the page when the page changes
|
// scroll to the top of the page when the page changes
|
||||||
useScrollToTopOnPageChange(filter.currentPage, result.loading);
|
useScrollToTopOnPageChange(filter.currentPage, result.loading);
|
||||||
@@ -236,7 +312,10 @@ export const ItemList = <T extends QueryResult, E extends IHasID>(
|
|||||||
updateFilter(filter.clearCriteria());
|
updateFilter(filter.clearCriteria());
|
||||||
}
|
}
|
||||||
|
|
||||||
const filterListToolbarProps = {
|
const filterListToolbarProps: IFilteredListToolbar = {
|
||||||
|
filter,
|
||||||
|
setFilter: updateFilter,
|
||||||
|
listSelect,
|
||||||
showEditFilter,
|
showEditFilter,
|
||||||
view: view,
|
view: view,
|
||||||
operations: operations,
|
operations: operations,
|
||||||
|
|||||||
@@ -29,22 +29,12 @@ export const ListContext = <T extends IHasID = IHasID>(
|
|||||||
) => {
|
) => {
|
||||||
const { selectable = false, items, children } = props;
|
const { selectable = false, items, children } = props;
|
||||||
|
|
||||||
const {
|
const listSelect = useListSelect(items);
|
||||||
selectedIds,
|
|
||||||
getSelected,
|
|
||||||
onSelectChange,
|
|
||||||
onSelectAll,
|
|
||||||
onSelectNone,
|
|
||||||
} = useListSelect(items);
|
|
||||||
|
|
||||||
const state: IListContextState<T> = {
|
const state: IListContextState<T> = {
|
||||||
selectable,
|
selectable,
|
||||||
selectedIds,
|
|
||||||
getSelected,
|
|
||||||
onSelectChange,
|
|
||||||
onSelectAll,
|
|
||||||
onSelectNone,
|
|
||||||
items,
|
items,
|
||||||
|
...listSelect,
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -74,6 +64,8 @@ const emptyState: IListContextState = {
|
|||||||
onSelectAll: () => {},
|
onSelectAll: () => {},
|
||||||
onSelectNone: () => {},
|
onSelectNone: () => {},
|
||||||
items: [],
|
items: [],
|
||||||
|
hasSelection: false,
|
||||||
|
selectedItems: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
export function useListContextOptional<T extends IHasID = IHasID>() {
|
export function useListContextOptional<T extends IHasID = IHasID>() {
|
||||||
|
|||||||
@@ -8,6 +8,9 @@ import { IHasID } from "src/utils/data";
|
|||||||
import { ConfigurationContext } from "src/hooks/Config";
|
import { ConfigurationContext } from "src/hooks/Config";
|
||||||
import { View } from "./views";
|
import { View } from "./views";
|
||||||
import { usePrevious } from "src/hooks/state";
|
import { usePrevious } from "src/hooks/state";
|
||||||
|
import * as GQL from "src/core/generated-graphql";
|
||||||
|
import { DisplayMode } from "src/models/list-filter/types";
|
||||||
|
import { Criterion } from "src/models/list-filter/criteria/criterion";
|
||||||
|
|
||||||
export function useFilterURL(
|
export function useFilterURL(
|
||||||
filter: ListFilterModel,
|
filter: ListFilterModel,
|
||||||
@@ -106,6 +109,106 @@ export function useDefaultFilter(emptyFilter: ListFilterModel, view?: View) {
|
|||||||
return { defaultFilter: retFilter, loading };
|
return { defaultFilter: retFilter, loading };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function useEmptyFilter(props: {
|
||||||
|
filterMode: GQL.FilterMode;
|
||||||
|
defaultSort?: string;
|
||||||
|
config?: GQL.ConfigDataFragment;
|
||||||
|
}) {
|
||||||
|
const { filterMode, defaultSort, config } = props;
|
||||||
|
|
||||||
|
const emptyFilter = useMemo(
|
||||||
|
() =>
|
||||||
|
new ListFilterModel(filterMode, config, {
|
||||||
|
defaultSortBy: defaultSort,
|
||||||
|
}),
|
||||||
|
[config, filterMode, defaultSort]
|
||||||
|
);
|
||||||
|
|
||||||
|
return emptyFilter;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IFilterStateHook {
|
||||||
|
filterMode: GQL.FilterMode;
|
||||||
|
defaultSort?: string;
|
||||||
|
view?: View;
|
||||||
|
useURL?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useFilterState(
|
||||||
|
props: IFilterStateHook & {
|
||||||
|
config?: GQL.ConfigDataFragment;
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
const { filterMode, defaultSort, config, view, useURL } = props;
|
||||||
|
|
||||||
|
const [filter, setFilterState] = useState<ListFilterModel>(
|
||||||
|
() =>
|
||||||
|
new ListFilterModel(filterMode, config, { defaultSortBy: defaultSort })
|
||||||
|
);
|
||||||
|
|
||||||
|
const emptyFilter = useEmptyFilter({ filterMode, defaultSort, config });
|
||||||
|
|
||||||
|
const { defaultFilter, loading } = useDefaultFilter(emptyFilter, view);
|
||||||
|
|
||||||
|
const { setFilter } = useFilterURL(filter, setFilterState, {
|
||||||
|
defaultFilter,
|
||||||
|
active: useURL,
|
||||||
|
});
|
||||||
|
|
||||||
|
return { loading, filter, setFilter };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useFilterOperations(props: {
|
||||||
|
filter: ListFilterModel;
|
||||||
|
setFilter: (
|
||||||
|
value: ListFilterModel | ((prevState: ListFilterModel) => ListFilterModel)
|
||||||
|
) => void;
|
||||||
|
}) {
|
||||||
|
const { setFilter } = props;
|
||||||
|
|
||||||
|
const setPage = useCallback(
|
||||||
|
(p: number) => {
|
||||||
|
setFilter((cv) => cv.changePage(p));
|
||||||
|
},
|
||||||
|
[setFilter]
|
||||||
|
);
|
||||||
|
|
||||||
|
const setDisplayMode = useCallback(
|
||||||
|
(displayMode: DisplayMode) => {
|
||||||
|
setFilter((cv) => cv.setDisplayMode(displayMode));
|
||||||
|
},
|
||||||
|
[setFilter]
|
||||||
|
);
|
||||||
|
|
||||||
|
const setZoom = useCallback(
|
||||||
|
(newZoomIndex: number) => {
|
||||||
|
setFilter((cv) => cv.setZoom(newZoomIndex));
|
||||||
|
},
|
||||||
|
[setFilter]
|
||||||
|
);
|
||||||
|
|
||||||
|
const removeCriterion = useCallback(
|
||||||
|
(removedCriterion: Criterion) => {
|
||||||
|
setFilter((cv) =>
|
||||||
|
cv.removeCriterion(removedCriterion.criterionOption.type)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[setFilter]
|
||||||
|
);
|
||||||
|
|
||||||
|
const clearAllCriteria = useCallback(() => {
|
||||||
|
setFilter((cv) => cv.clearCriteria());
|
||||||
|
}, [setFilter]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
setPage,
|
||||||
|
setDisplayMode,
|
||||||
|
setZoom,
|
||||||
|
removeCriterion,
|
||||||
|
clearAllCriteria,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function useListKeyboardShortcuts(props: {
|
export function useListKeyboardShortcuts(props: {
|
||||||
currentPage?: number;
|
currentPage?: number;
|
||||||
onChangePage?: (page: number) => void;
|
onChangePage?: (page: number) => void;
|
||||||
@@ -190,10 +293,11 @@ export function useListKeyboardShortcuts(props: {
|
|||||||
}, [onSelectAll, onSelectNone]);
|
}, [onSelectAll, onSelectNone]);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useListSelect<T extends { id: string }>(items: T[]) {
|
export function useListSelect<T extends IHasID = IHasID>(items: T[]) {
|
||||||
const [itemsSelected, setItemsSelected] = useState<T[]>([]);
|
const [itemsSelected, setItemsSelected] = useState<T[]>([]);
|
||||||
const [lastClickedId, setLastClickedId] = useState<string>();
|
const [lastClickedId, setLastClickedId] = useState<string>();
|
||||||
|
|
||||||
|
// TODO - this doesn't get updated when items changes
|
||||||
const selectedIds = useMemo(() => {
|
const selectedIds = useMemo(() => {
|
||||||
const newSelectedIds = new Set<string>();
|
const newSelectedIds = new Set<string>();
|
||||||
itemsSelected.forEach((item) => {
|
itemsSelected.forEach((item) => {
|
||||||
@@ -303,18 +407,26 @@ export function useListSelect<T extends { id: string }>(items: T[]) {
|
|||||||
setLastClickedId(undefined);
|
setLastClickedId(undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO - this is for backwards compatibility
|
||||||
const getSelected = useCallback(() => itemsSelected, [itemsSelected]);
|
const getSelected = useCallback(() => itemsSelected, [itemsSelected]);
|
||||||
|
|
||||||
|
// convenience state
|
||||||
|
const hasSelection = itemsSelected.length > 0;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
selectedItems: itemsSelected,
|
||||||
selectedIds,
|
selectedIds,
|
||||||
getSelected,
|
getSelected,
|
||||||
onSelectChange,
|
onSelectChange,
|
||||||
onSelectAll,
|
onSelectAll,
|
||||||
onSelectNone,
|
onSelectNone,
|
||||||
|
hasSelection,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export type IListSelect<T extends IHasID> = ReturnType<typeof useListSelect<T>>;
|
export type IListSelect<T extends IHasID = IHasID> = ReturnType<
|
||||||
|
typeof useListSelect<T>
|
||||||
|
>;
|
||||||
|
|
||||||
// returns true if the filter has changed in a way that impacts the total count
|
// returns true if the filter has changed in a way that impacts the total count
|
||||||
function totalCountImpacted(
|
function totalCountImpacted(
|
||||||
@@ -358,6 +470,81 @@ export function useCachedQueryResult<T extends QueryResult>(
|
|||||||
return cachedResult;
|
return cachedResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IQueryResultHook<
|
||||||
|
T extends QueryResult,
|
||||||
|
E extends IHasID = IHasID
|
||||||
|
> {
|
||||||
|
filterHook?: (filter: ListFilterModel) => ListFilterModel;
|
||||||
|
useResult: (filter: ListFilterModel) => T;
|
||||||
|
getCount: (data: T) => number;
|
||||||
|
getItems: (data: T) => E[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useQueryResult<
|
||||||
|
T extends QueryResult,
|
||||||
|
E extends IHasID = IHasID
|
||||||
|
>(
|
||||||
|
props: IQueryResultHook<T, E> & {
|
||||||
|
filter: ListFilterModel;
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
const { filter, filterHook, useResult, getItems, getCount } = props;
|
||||||
|
|
||||||
|
const effectiveFilter = useMemo(() => {
|
||||||
|
if (filterHook) {
|
||||||
|
return filterHook(filter.clone());
|
||||||
|
}
|
||||||
|
return filter;
|
||||||
|
}, [filter, filterHook]);
|
||||||
|
|
||||||
|
const result = useResult(effectiveFilter);
|
||||||
|
|
||||||
|
// use cached query result for pagination and metadata rendering
|
||||||
|
const cachedResult = useCachedQueryResult(effectiveFilter, result);
|
||||||
|
|
||||||
|
const items = useMemo(() => getItems(result), [getItems, result]);
|
||||||
|
const totalCount = useMemo(
|
||||||
|
() => getCount(cachedResult),
|
||||||
|
[getCount, cachedResult]
|
||||||
|
);
|
||||||
|
|
||||||
|
const pages = Math.ceil(totalCount / filter.itemsPerPage);
|
||||||
|
|
||||||
|
return {
|
||||||
|
effectiveFilter,
|
||||||
|
result,
|
||||||
|
cachedResult,
|
||||||
|
items,
|
||||||
|
totalCount,
|
||||||
|
pages,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// this hook collects the common logic when closing the edit/delete dialog
|
||||||
|
// if applied is true, then the list should be refetched and selection cleared
|
||||||
|
export function useCloseEditDelete(props: {
|
||||||
|
onSelectNone: () => void;
|
||||||
|
closeModal: () => void;
|
||||||
|
result: QueryResult;
|
||||||
|
}) {
|
||||||
|
const { onSelectNone, closeModal, result } = props;
|
||||||
|
|
||||||
|
const onCloseEditDelete = useCallback(
|
||||||
|
(applied?: boolean) => {
|
||||||
|
closeModal();
|
||||||
|
if (applied) {
|
||||||
|
onSelectNone();
|
||||||
|
|
||||||
|
// refetch
|
||||||
|
result.refetch();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[onSelectNone, closeModal, result]
|
||||||
|
);
|
||||||
|
|
||||||
|
return onCloseEditDelete;
|
||||||
|
}
|
||||||
|
|
||||||
export function useScrollToTopOnPageChange(
|
export function useScrollToTopOnPageChange(
|
||||||
currentPage: number,
|
currentPage: number,
|
||||||
loading: boolean
|
loading: boolean
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import * as GQL from "src/core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import { SceneList } from "src/components/Scenes/SceneList";
|
import { FilteredSceneList } from "src/components/Scenes/SceneList";
|
||||||
import { usePerformerFilterHook } from "src/core/performers";
|
import { usePerformerFilterHook } from "src/core/performers";
|
||||||
import { View } from "src/components/List/views";
|
import { View } from "src/components/List/views";
|
||||||
import { PatchComponent } from "src/patch";
|
import { PatchComponent } from "src/patch";
|
||||||
@@ -14,7 +14,7 @@ export const PerformerScenesPanel: React.FC<IPerformerDetailsProps> =
|
|||||||
PatchComponent("PerformerScenesPanel", ({ active, performer }) => {
|
PatchComponent("PerformerScenesPanel", ({ active, performer }) => {
|
||||||
const filterHook = usePerformerFilterHook(performer);
|
const filterHook = usePerformerFilterHook(performer);
|
||||||
return (
|
return (
|
||||||
<SceneList
|
<FilteredSceneList
|
||||||
filterHook={filterHook}
|
filterHook={filterHook}
|
||||||
alterQuery={active}
|
alterQuery={active}
|
||||||
view={View.PerformerScenes}
|
view={View.PerformerScenes}
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useCallback, useContext, useEffect, useMemo } from "react";
|
||||||
import cloneDeep from "lodash-es/cloneDeep";
|
import cloneDeep from "lodash-es/cloneDeep";
|
||||||
import { useIntl } from "react-intl";
|
import { useIntl } from "react-intl";
|
||||||
import { useHistory } from "react-router-dom";
|
import { useHistory } from "react-router-dom";
|
||||||
import Mousetrap from "mousetrap";
|
import Mousetrap from "mousetrap";
|
||||||
import * as GQL from "src/core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import { queryFindScenes, useFindScenes } from "src/core/StashService";
|
import { queryFindScenes, useFindScenes } from "src/core/StashService";
|
||||||
import { ItemList, ItemListContext, showWhenSelected } from "../List/ItemList";
|
|
||||||
import { ListFilterModel } from "src/models/list-filter/filter";
|
import { ListFilterModel } from "src/models/list-filter/filter";
|
||||||
import { DisplayMode } from "src/models/list-filter/types";
|
import { DisplayMode } from "src/models/list-filter/types";
|
||||||
import { Tagger } from "../Tagger/scenes/SceneTagger";
|
import { Tagger } from "../Tagger/scenes/SceneTagger";
|
||||||
@@ -26,14 +25,12 @@ import { objectTitle } from "src/core/files";
|
|||||||
import TextUtils from "src/utils/text";
|
import TextUtils from "src/utils/text";
|
||||||
import { View } from "../List/views";
|
import { View } from "../List/views";
|
||||||
import { FileSize } from "../Shared/FileSize";
|
import { FileSize } from "../Shared/FileSize";
|
||||||
|
import { PagedList } from "../List/PagedList";
|
||||||
function getItems(result: GQL.FindScenesQueryResult) {
|
import { useCloseEditDelete, useFilterOperations } from "../List/util";
|
||||||
return result?.data?.findScenes?.scenes ?? [];
|
import { IListFilterOperation } from "../List/ListOperationButtons";
|
||||||
}
|
import { FilteredListToolbar } from "../List/FilteredListToolbar";
|
||||||
|
import { useFilteredItemList } from "../List/ItemList";
|
||||||
function getCount(result: GQL.FindScenesQueryResult) {
|
import { FilterTags } from "../List/FilterTags";
|
||||||
return result?.data?.findScenes?.count ?? 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderMetadataByline(result: GQL.FindScenesQueryResult) {
|
function renderMetadataByline(result: GQL.FindScenesQueryResult) {
|
||||||
const duration = result?.data?.findScenes?.duration;
|
const duration = result?.data?.findScenes?.duration;
|
||||||
@@ -64,7 +61,130 @@ function renderMetadataByline(result: GQL.FindScenesQueryResult) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ISceneList {
|
function usePlayScene() {
|
||||||
|
const history = useHistory();
|
||||||
|
|
||||||
|
const playScene = useCallback(
|
||||||
|
(queue: SceneQueue, sceneID: string, options: IPlaySceneOptions) => {
|
||||||
|
history.push(queue.makeLink(sceneID, options));
|
||||||
|
},
|
||||||
|
[history]
|
||||||
|
);
|
||||||
|
|
||||||
|
return playScene;
|
||||||
|
}
|
||||||
|
|
||||||
|
function usePlaySelected(selectedIds: Set<string>) {
|
||||||
|
const { configuration: config } = useContext(ConfigurationContext);
|
||||||
|
const playScene = usePlayScene();
|
||||||
|
|
||||||
|
const playSelected = useCallback(() => {
|
||||||
|
// populate queue and go to first scene
|
||||||
|
const sceneIDs = Array.from(selectedIds.values());
|
||||||
|
const queue = SceneQueue.fromSceneIDList(sceneIDs);
|
||||||
|
const autoPlay = config?.interface.autostartVideoOnPlaySelected ?? false;
|
||||||
|
playScene(queue, sceneIDs[0], { autoPlay });
|
||||||
|
}, [selectedIds, config?.interface.autostartVideoOnPlaySelected, playScene]);
|
||||||
|
|
||||||
|
return playSelected;
|
||||||
|
}
|
||||||
|
|
||||||
|
function usePlayRandom(filter: ListFilterModel, count: number) {
|
||||||
|
const { configuration: config } = useContext(ConfigurationContext);
|
||||||
|
const playScene = usePlayScene();
|
||||||
|
|
||||||
|
const playRandom = useCallback(async () => {
|
||||||
|
// query for a random scene
|
||||||
|
if (count === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const pages = Math.ceil(count / filter.itemsPerPage);
|
||||||
|
const page = Math.floor(Math.random() * pages) + 1;
|
||||||
|
|
||||||
|
const indexMax = Math.min(filter.itemsPerPage, count);
|
||||||
|
const index = Math.floor(Math.random() * indexMax);
|
||||||
|
const filterCopy = cloneDeep(filter);
|
||||||
|
filterCopy.currentPage = page;
|
||||||
|
filterCopy.sortBy = "random";
|
||||||
|
const queryResults = await queryFindScenes(filterCopy);
|
||||||
|
const scene = queryResults.data.findScenes.scenes[index];
|
||||||
|
if (scene) {
|
||||||
|
// navigate to the image player page
|
||||||
|
const queue = SceneQueue.fromListFilterModel(filterCopy);
|
||||||
|
const autoPlay = config?.interface.autostartVideoOnPlaySelected ?? false;
|
||||||
|
playScene(queue, scene.id, { sceneIndex: index, autoPlay });
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
filter,
|
||||||
|
count,
|
||||||
|
config?.interface.autostartVideoOnPlaySelected,
|
||||||
|
playScene,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return playRandom;
|
||||||
|
}
|
||||||
|
|
||||||
|
function useAddKeybinds(filter: ListFilterModel, count: number) {
|
||||||
|
const playRandom = usePlayRandom(filter, count);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
Mousetrap.bind("p r", () => {
|
||||||
|
playRandom();
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
Mousetrap.unbind("p r");
|
||||||
|
};
|
||||||
|
}, [playRandom]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const SceneList: React.FC<{
|
||||||
|
scenes: GQL.SlimSceneDataFragment[];
|
||||||
|
filter: ListFilterModel;
|
||||||
|
selectedIds: Set<string>;
|
||||||
|
onSelectChange: (id: string, selected: boolean, shiftKey: boolean) => void;
|
||||||
|
fromGroupId?: string;
|
||||||
|
}> = ({ scenes, filter, selectedIds, onSelectChange, fromGroupId }) => {
|
||||||
|
const queue = useMemo(() => SceneQueue.fromListFilterModel(filter), [filter]);
|
||||||
|
|
||||||
|
if (scenes.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filter.displayMode === DisplayMode.Grid) {
|
||||||
|
return (
|
||||||
|
<SceneCardsGrid
|
||||||
|
scenes={scenes}
|
||||||
|
queue={queue}
|
||||||
|
zoomIndex={filter.zoomIndex}
|
||||||
|
selectedIds={selectedIds}
|
||||||
|
onSelectChange={onSelectChange}
|
||||||
|
fromGroupId={fromGroupId}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (filter.displayMode === DisplayMode.List) {
|
||||||
|
return (
|
||||||
|
<SceneListTable
|
||||||
|
scenes={scenes}
|
||||||
|
queue={queue}
|
||||||
|
selectedIds={selectedIds}
|
||||||
|
onSelectChange={onSelectChange}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (filter.displayMode === DisplayMode.Wall) {
|
||||||
|
return <SceneWallPanel scenes={scenes} sceneQueue={queue} />;
|
||||||
|
}
|
||||||
|
if (filter.displayMode === DisplayMode.Tagger) {
|
||||||
|
return <Tagger scenes={scenes} queue={queue} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface IFilteredScenes {
|
||||||
filterHook?: (filter: ListFilterModel) => ListFilterModel;
|
filterHook?: (filter: ListFilterModel) => ListFilterModel;
|
||||||
defaultSort?: string;
|
defaultSort?: string;
|
||||||
view?: View;
|
view?: View;
|
||||||
@@ -72,30 +192,108 @@ interface ISceneList {
|
|||||||
fromGroupId?: string;
|
fromGroupId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SceneList: React.FC<ISceneList> = ({
|
export const FilteredSceneList = (props: IFilteredScenes) => {
|
||||||
filterHook,
|
|
||||||
defaultSort,
|
|
||||||
view,
|
|
||||||
alterQuery,
|
|
||||||
fromGroupId,
|
|
||||||
}) => {
|
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const config = React.useContext(ConfigurationContext);
|
|
||||||
const [isGenerateDialogOpen, setIsGenerateDialogOpen] = useState(false);
|
|
||||||
const [mergeScenes, setMergeScenes] =
|
|
||||||
useState<{ id: string; title: string }[]>();
|
|
||||||
const [isIdentifyDialogOpen, setIsIdentifyDialogOpen] = useState(false);
|
|
||||||
const [isExportDialogOpen, setIsExportDialogOpen] = useState(false);
|
|
||||||
const [isExportAll, setIsExportAll] = useState(false);
|
|
||||||
|
|
||||||
const filterMode = GQL.FilterMode.Scenes;
|
const { filterHook, defaultSort, view, alterQuery, fromGroupId } = props;
|
||||||
|
|
||||||
const otherOperations = [
|
// States
|
||||||
|
const { filterState, queryResult, modalState, listSelect, showEditFilter } =
|
||||||
|
useFilteredItemList({
|
||||||
|
filterStateProps: {
|
||||||
|
filterMode: GQL.FilterMode.Scenes,
|
||||||
|
defaultSort,
|
||||||
|
view,
|
||||||
|
useURL: alterQuery,
|
||||||
|
},
|
||||||
|
queryResultProps: {
|
||||||
|
useResult: useFindScenes,
|
||||||
|
getCount: (r) => r.data?.findScenes.count ?? 0,
|
||||||
|
getItems: (r) => r.data?.findScenes.scenes ?? [],
|
||||||
|
filterHook,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { filter, setFilter, loading: filterLoading } = filterState;
|
||||||
|
|
||||||
|
const { effectiveFilter, result, cachedResult, items, totalCount } =
|
||||||
|
queryResult;
|
||||||
|
|
||||||
|
const {
|
||||||
|
selectedIds,
|
||||||
|
selectedItems,
|
||||||
|
onSelectChange,
|
||||||
|
onSelectNone,
|
||||||
|
hasSelection,
|
||||||
|
} = listSelect;
|
||||||
|
|
||||||
|
const { modal, showModal, closeModal } = modalState;
|
||||||
|
|
||||||
|
// Utility hooks
|
||||||
|
const { setPage, removeCriterion, clearAllCriteria } = useFilterOperations({
|
||||||
|
filter,
|
||||||
|
setFilter,
|
||||||
|
});
|
||||||
|
|
||||||
|
useAddKeybinds(filter, totalCount);
|
||||||
|
|
||||||
|
const onCloseEditDelete = useCloseEditDelete({
|
||||||
|
closeModal,
|
||||||
|
onSelectNone,
|
||||||
|
result,
|
||||||
|
});
|
||||||
|
|
||||||
|
const metadataByline = useMemo(() => {
|
||||||
|
if (cachedResult.loading) return "";
|
||||||
|
|
||||||
|
return renderMetadataByline(cachedResult) ?? "";
|
||||||
|
}, [cachedResult]);
|
||||||
|
|
||||||
|
const playSelected = usePlaySelected(selectedIds);
|
||||||
|
const playRandom = usePlayRandom(filter, totalCount);
|
||||||
|
|
||||||
|
function onExport(all: boolean) {
|
||||||
|
showModal(
|
||||||
|
<ExportDialog
|
||||||
|
exportInput={{
|
||||||
|
scenes: {
|
||||||
|
ids: Array.from(selectedIds.values()),
|
||||||
|
all: all,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
onClose={() => closeModal()}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onMerge() {
|
||||||
|
const selected =
|
||||||
|
selectedItems.map((s) => {
|
||||||
|
return {
|
||||||
|
id: s.id,
|
||||||
|
title: objectTitle(s),
|
||||||
|
};
|
||||||
|
}) ?? [];
|
||||||
|
showModal(
|
||||||
|
<SceneMergeModal
|
||||||
|
scenes={selected}
|
||||||
|
onClose={(mergedID?: string) => {
|
||||||
|
closeModal();
|
||||||
|
if (mergedID) {
|
||||||
|
history.push(`/scenes/${mergedID}`);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
show
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const otherOperations: IListFilterOperation[] = [
|
||||||
{
|
{
|
||||||
text: intl.formatMessage({ id: "actions.play_selected" }),
|
text: intl.formatMessage({ id: "actions.play_selected" }),
|
||||||
onClick: playSelected,
|
onClick: playSelected,
|
||||||
isDisplayed: showWhenSelected,
|
isDisplayed: () => hasSelection,
|
||||||
icon: faPlay,
|
icon: faPlay,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -104,273 +302,103 @@ export const SceneList: React.FC<ISceneList> = ({
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: `${intl.formatMessage({ id: "actions.generate" })}…`,
|
text: `${intl.formatMessage({ id: "actions.generate" })}…`,
|
||||||
onClick: async () => setIsGenerateDialogOpen(true),
|
onClick: () =>
|
||||||
isDisplayed: showWhenSelected,
|
showModal(
|
||||||
},
|
|
||||||
{
|
|
||||||
text: `${intl.formatMessage({ id: "actions.identify" })}…`,
|
|
||||||
onClick: async () => setIsIdentifyDialogOpen(true),
|
|
||||||
isDisplayed: showWhenSelected,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: `${intl.formatMessage({ id: "actions.merge" })}…`,
|
|
||||||
onClick: onMerge,
|
|
||||||
isDisplayed: showWhenSelected,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: intl.formatMessage({ id: "actions.export" }),
|
|
||||||
onClick: onExport,
|
|
||||||
isDisplayed: showWhenSelected,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: intl.formatMessage({ id: "actions.export_all" }),
|
|
||||||
onClick: onExportAll,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
function addKeybinds(
|
|
||||||
result: GQL.FindScenesQueryResult,
|
|
||||||
filter: ListFilterModel
|
|
||||||
) {
|
|
||||||
Mousetrap.bind("p r", () => {
|
|
||||||
playRandom(result, filter);
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
Mousetrap.unbind("p r");
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function playScene(
|
|
||||||
queue: SceneQueue,
|
|
||||||
sceneID: string,
|
|
||||||
options: IPlaySceneOptions
|
|
||||||
) {
|
|
||||||
history.push(queue.makeLink(sceneID, options));
|
|
||||||
}
|
|
||||||
|
|
||||||
async function playSelected(
|
|
||||||
result: GQL.FindScenesQueryResult,
|
|
||||||
filter: ListFilterModel,
|
|
||||||
selectedIds: Set<string>
|
|
||||||
) {
|
|
||||||
// populate queue and go to first scene
|
|
||||||
const sceneIDs = Array.from(selectedIds.values());
|
|
||||||
const queue = SceneQueue.fromSceneIDList(sceneIDs);
|
|
||||||
const autoPlay =
|
|
||||||
config.configuration?.interface.autostartVideoOnPlaySelected ?? false;
|
|
||||||
playScene(queue, sceneIDs[0], { autoPlay });
|
|
||||||
}
|
|
||||||
|
|
||||||
async function playRandom(
|
|
||||||
result: GQL.FindScenesQueryResult,
|
|
||||||
filter: ListFilterModel
|
|
||||||
) {
|
|
||||||
// query for a random scene
|
|
||||||
if (result.data?.findScenes) {
|
|
||||||
const { count } = result.data.findScenes;
|
|
||||||
|
|
||||||
const pages = Math.ceil(count / filter.itemsPerPage);
|
|
||||||
const page = Math.floor(Math.random() * pages) + 1;
|
|
||||||
|
|
||||||
const indexMax = Math.min(filter.itemsPerPage, count);
|
|
||||||
const index = Math.floor(Math.random() * indexMax);
|
|
||||||
const filterCopy = cloneDeep(filter);
|
|
||||||
filterCopy.currentPage = page;
|
|
||||||
filterCopy.sortBy = "random";
|
|
||||||
const queryResults = await queryFindScenes(filterCopy);
|
|
||||||
const scene = queryResults.data.findScenes.scenes[index];
|
|
||||||
if (scene) {
|
|
||||||
// navigate to the image player page
|
|
||||||
const queue = SceneQueue.fromListFilterModel(filterCopy);
|
|
||||||
const autoPlay =
|
|
||||||
config.configuration?.interface.autostartVideoOnPlaySelected ?? false;
|
|
||||||
playScene(queue, scene.id, { sceneIndex: index, autoPlay });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function onMerge(
|
|
||||||
result: GQL.FindScenesQueryResult,
|
|
||||||
filter: ListFilterModel,
|
|
||||||
selectedIds: Set<string>
|
|
||||||
) {
|
|
||||||
const selected =
|
|
||||||
result.data?.findScenes.scenes
|
|
||||||
.filter((s) => selectedIds.has(s.id))
|
|
||||||
.map((s) => {
|
|
||||||
return {
|
|
||||||
id: s.id,
|
|
||||||
title: objectTitle(s),
|
|
||||||
};
|
|
||||||
}) ?? [];
|
|
||||||
|
|
||||||
setMergeScenes(selected);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function onExport() {
|
|
||||||
setIsExportAll(false);
|
|
||||||
setIsExportDialogOpen(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function onExportAll() {
|
|
||||||
setIsExportAll(true);
|
|
||||||
setIsExportDialogOpen(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderContent(
|
|
||||||
result: GQL.FindScenesQueryResult,
|
|
||||||
filter: ListFilterModel,
|
|
||||||
selectedIds: Set<string>,
|
|
||||||
onSelectChange: (id: string, selected: boolean, shiftKey: boolean) => void
|
|
||||||
) {
|
|
||||||
function maybeRenderSceneGenerateDialog() {
|
|
||||||
if (isGenerateDialogOpen) {
|
|
||||||
return (
|
|
||||||
<GenerateDialog
|
<GenerateDialog
|
||||||
type="scene"
|
type="scene"
|
||||||
selectedIds={Array.from(selectedIds.values())}
|
selectedIds={Array.from(selectedIds.values())}
|
||||||
onClose={() => setIsGenerateDialogOpen(false)}
|
onClose={() => closeModal()}
|
||||||
/>
|
/>
|
||||||
);
|
),
|
||||||
}
|
isDisplayed: () => hasSelection,
|
||||||
}
|
},
|
||||||
|
{
|
||||||
function maybeRenderSceneIdentifyDialog() {
|
text: `${intl.formatMessage({ id: "actions.identify" })}…`,
|
||||||
if (isIdentifyDialogOpen) {
|
onClick: () =>
|
||||||
return (
|
showModal(
|
||||||
<IdentifyDialog
|
<IdentifyDialog
|
||||||
selectedIds={Array.from(selectedIds.values())}
|
selectedIds={Array.from(selectedIds.values())}
|
||||||
onClose={() => setIsIdentifyDialogOpen(false)}
|
onClose={() => closeModal()}
|
||||||
/>
|
/>
|
||||||
);
|
),
|
||||||
}
|
isDisplayed: () => hasSelection,
|
||||||
}
|
},
|
||||||
|
{
|
||||||
|
text: `${intl.formatMessage({ id: "actions.merge" })}…`,
|
||||||
|
onClick: () => onMerge(),
|
||||||
|
isDisplayed: () => hasSelection,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: intl.formatMessage({ id: "actions.export" }),
|
||||||
|
onClick: () => onExport(false),
|
||||||
|
isDisplayed: () => hasSelection,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: intl.formatMessage({ id: "actions.export_all" }),
|
||||||
|
onClick: () => onExport(true),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
function maybeRenderSceneExportDialog() {
|
// render
|
||||||
if (isExportDialogOpen) {
|
if (filterLoading) return null;
|
||||||
return (
|
|
||||||
<ExportDialog
|
|
||||||
exportInput={{
|
|
||||||
scenes: {
|
|
||||||
ids: Array.from(selectedIds.values()),
|
|
||||||
all: isExportAll,
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
onClose={() => setIsExportDialogOpen(false)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderMergeDialog() {
|
return (
|
||||||
if (mergeScenes) {
|
<TaggerContext>
|
||||||
return (
|
<div className="item-list-container">
|
||||||
<SceneMergeModal
|
{modal}
|
||||||
scenes={mergeScenes}
|
|
||||||
onClose={(mergedID?: string) => {
|
|
||||||
setMergeScenes(undefined);
|
|
||||||
if (mergedID) {
|
|
||||||
history.push(`/scenes/${mergedID}`);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
show
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderScenes() {
|
<FilteredListToolbar
|
||||||
if (!result.data?.findScenes) return;
|
filter={filter}
|
||||||
|
setFilter={setFilter}
|
||||||
|
showEditFilter={showEditFilter}
|
||||||
|
view={view}
|
||||||
|
listSelect={listSelect}
|
||||||
|
onEdit={() =>
|
||||||
|
showModal(
|
||||||
|
<EditScenesDialog
|
||||||
|
selected={selectedItems}
|
||||||
|
onClose={onCloseEditDelete}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
onDelete={() => {
|
||||||
|
showModal(
|
||||||
|
<DeleteScenesDialog
|
||||||
|
selected={selectedItems}
|
||||||
|
onClose={onCloseEditDelete}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
operations={otherOperations}
|
||||||
|
zoomable
|
||||||
|
/>
|
||||||
|
|
||||||
const queue = SceneQueue.fromListFilterModel(filter);
|
<FilterTags
|
||||||
|
criteria={filter.criteria}
|
||||||
|
onEditCriterion={(c) => showEditFilter(c.criterionOption.type)}
|
||||||
|
onRemoveCriterion={removeCriterion}
|
||||||
|
onRemoveAll={() => clearAllCriteria()}
|
||||||
|
/>
|
||||||
|
|
||||||
if (filter.displayMode === DisplayMode.Grid) {
|
<PagedList
|
||||||
return (
|
result={result}
|
||||||
<SceneCardsGrid
|
cachedResult={cachedResult}
|
||||||
scenes={result.data.findScenes.scenes}
|
filter={filter}
|
||||||
queue={queue}
|
totalCount={totalCount}
|
||||||
zoomIndex={filter.zoomIndex}
|
onChangePage={setPage}
|
||||||
|
metadataByline={metadataByline}
|
||||||
|
>
|
||||||
|
<SceneList
|
||||||
|
filter={effectiveFilter}
|
||||||
|
scenes={items}
|
||||||
selectedIds={selectedIds}
|
selectedIds={selectedIds}
|
||||||
onSelectChange={onSelectChange}
|
onSelectChange={onSelectChange}
|
||||||
fromGroupId={fromGroupId}
|
fromGroupId={fromGroupId}
|
||||||
/>
|
/>
|
||||||
);
|
</PagedList>
|
||||||
}
|
</div>
|
||||||
if (filter.displayMode === DisplayMode.List) {
|
|
||||||
return (
|
|
||||||
<SceneListTable
|
|
||||||
scenes={result.data.findScenes.scenes}
|
|
||||||
queue={queue}
|
|
||||||
selectedIds={selectedIds}
|
|
||||||
onSelectChange={onSelectChange}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (filter.displayMode === DisplayMode.Wall) {
|
|
||||||
return (
|
|
||||||
<SceneWallPanel
|
|
||||||
scenes={result.data.findScenes.scenes}
|
|
||||||
sceneQueue={queue}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (filter.displayMode === DisplayMode.Tagger) {
|
|
||||||
return <Tagger scenes={result.data.findScenes.scenes} queue={queue} />;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{maybeRenderSceneGenerateDialog()}
|
|
||||||
{maybeRenderSceneIdentifyDialog()}
|
|
||||||
{maybeRenderSceneExportDialog()}
|
|
||||||
{renderMergeDialog()}
|
|
||||||
{renderScenes()}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderEditDialog(
|
|
||||||
selectedScenes: GQL.SlimSceneDataFragment[],
|
|
||||||
onClose: (applied: boolean) => void
|
|
||||||
) {
|
|
||||||
return <EditScenesDialog selected={selectedScenes} onClose={onClose} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderDeleteDialog(
|
|
||||||
selectedScenes: GQL.SlimSceneDataFragment[],
|
|
||||||
onClose: (confirmed: boolean) => void
|
|
||||||
) {
|
|
||||||
return <DeleteScenesDialog selected={selectedScenes} onClose={onClose} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TaggerContext>
|
|
||||||
<ItemListContext
|
|
||||||
filterMode={filterMode}
|
|
||||||
defaultSort={defaultSort}
|
|
||||||
useResult={useFindScenes}
|
|
||||||
getItems={getItems}
|
|
||||||
getCount={getCount}
|
|
||||||
alterQuery={alterQuery}
|
|
||||||
filterHook={filterHook}
|
|
||||||
view={view}
|
|
||||||
selectable
|
|
||||||
>
|
|
||||||
<ItemList
|
|
||||||
zoomable
|
|
||||||
view={view}
|
|
||||||
otherOperations={otherOperations}
|
|
||||||
addKeybinds={addKeybinds}
|
|
||||||
renderContent={renderContent}
|
|
||||||
renderEditDialog={renderEditDialog}
|
|
||||||
renderDeleteDialog={renderDeleteDialog}
|
|
||||||
renderMetadataByline={renderMetadataByline}
|
|
||||||
/>
|
|
||||||
</ItemListContext>
|
|
||||||
</TaggerContext>
|
</TaggerContext>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default SceneList;
|
export default FilteredSceneList;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import * as GQL from "src/core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import { SceneList } from "src/components/Scenes/SceneList";
|
import { FilteredSceneList } from "src/components/Scenes/SceneList";
|
||||||
import { useStudioFilterHook } from "src/core/studios";
|
import { useStudioFilterHook } from "src/core/studios";
|
||||||
import { View } from "src/components/List/views";
|
import { View } from "src/components/List/views";
|
||||||
|
|
||||||
@@ -17,7 +17,7 @@ export const StudioScenesPanel: React.FC<IStudioScenesPanel> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const filterHook = useStudioFilterHook(studio, showChildStudioContent);
|
const filterHook = useStudioFilterHook(studio, showChildStudioContent);
|
||||||
return (
|
return (
|
||||||
<SceneList
|
<FilteredSceneList
|
||||||
filterHook={filterHook}
|
filterHook={filterHook}
|
||||||
alterQuery={active}
|
alterQuery={active}
|
||||||
view={View.StudioScenes}
|
view={View.StudioScenes}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import * as GQL from "src/core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import { SceneList } from "src/components/Scenes/SceneList";
|
import { FilteredSceneList } from "src/components/Scenes/SceneList";
|
||||||
import { useTagFilterHook } from "src/core/tags";
|
import { useTagFilterHook } from "src/core/tags";
|
||||||
import { View } from "src/components/List/views";
|
import { View } from "src/components/List/views";
|
||||||
|
|
||||||
@@ -17,7 +17,7 @@ export const TagScenesPanel: React.FC<ITagScenesPanel> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const filterHook = useTagFilterHook(tag, showSubTagContent);
|
const filterHook = useTagFilterHook(tag, showSubTagContent);
|
||||||
return (
|
return (
|
||||||
<SceneList
|
<FilteredSceneList
|
||||||
filterHook={filterHook}
|
filterHook={filterHook}
|
||||||
alterQuery={active}
|
alterQuery={active}
|
||||||
view={View.TagScenes}
|
view={View.TagScenes}
|
||||||
|
|||||||
Reference in New Issue
Block a user