mirror of
https://github.com/stashapp/stash.git
synced 2025-12-06 08:26:00 +01:00
829 lines
23 KiB
TypeScript
829 lines
23 KiB
TypeScript
import React, { useCallback, useEffect, useMemo } from "react";
|
|
import cloneDeep from "lodash-es/cloneDeep";
|
|
import { FormattedMessage, useIntl } from "react-intl";
|
|
import { useHistory } from "react-router-dom";
|
|
import Mousetrap from "mousetrap";
|
|
import * as GQL from "src/core/generated-graphql";
|
|
import { queryFindScenes, useFindScenes } from "src/core/StashService";
|
|
import { ListFilterModel } from "src/models/list-filter/filter";
|
|
import { DisplayMode } from "src/models/list-filter/types";
|
|
import { Tagger } from "../Tagger/scenes/SceneTagger";
|
|
import { IPlaySceneOptions, SceneQueue } from "src/models/sceneQueue";
|
|
import { SceneWallPanel } from "./SceneWallPanel";
|
|
import { SceneListTable } from "./SceneListTable";
|
|
import { EditScenesDialog } from "./EditScenesDialog";
|
|
import { DeleteScenesDialog } from "./DeleteScenesDialog";
|
|
import { GenerateDialog } from "../Dialogs/GenerateDialog";
|
|
import { ExportDialog } from "../Shared/ExportDialog";
|
|
import { SceneCardsGrid } from "./SceneCardsGrid";
|
|
import { TaggerContext } from "../Tagger/context";
|
|
import { IdentifyDialog } from "../Dialogs/IdentifyDialog/IdentifyDialog";
|
|
import { useConfigurationContext } from "src/hooks/Config";
|
|
import {
|
|
faPencil,
|
|
faPlay,
|
|
faPlus,
|
|
faTrash,
|
|
} from "@fortawesome/free-solid-svg-icons";
|
|
import { SceneMergeModal } from "./SceneMergeDialog";
|
|
import { objectTitle } from "src/core/files";
|
|
import TextUtils from "src/utils/text";
|
|
import { View } from "../List/views";
|
|
import { FileSize } from "../Shared/FileSize";
|
|
import { LoadedContent } from "../List/PagedList";
|
|
import { useCloseEditDelete, useFilterOperations } from "../List/util";
|
|
import {
|
|
OperationDropdown,
|
|
OperationDropdownItem,
|
|
} from "../List/ListOperationButtons";
|
|
import { useFilteredItemList } from "../List/ItemList";
|
|
import {
|
|
Sidebar,
|
|
SidebarPane,
|
|
SidebarPaneContent,
|
|
SidebarStateContext,
|
|
useSidebarState,
|
|
} from "../Shared/Sidebar";
|
|
import { SidebarPerformersFilter } from "../List/Filters/PerformersFilter";
|
|
import { SidebarStudiosFilter } from "../List/Filters/StudiosFilter";
|
|
import { PerformersCriterionOption } from "src/models/list-filter/criteria/performers";
|
|
import { StudiosCriterionOption } from "src/models/list-filter/criteria/studios";
|
|
import { TagsCriterionOption } from "src/models/list-filter/criteria/tags";
|
|
import { SidebarTagsFilter } from "../List/Filters/TagsFilter";
|
|
import cx from "classnames";
|
|
import { RatingCriterionOption } from "src/models/list-filter/criteria/rating";
|
|
import { SidebarRatingFilter } from "../List/Filters/RatingFilter";
|
|
import { OrganizedCriterionOption } from "src/models/list-filter/criteria/organized";
|
|
import { HasMarkersCriterionOption } from "src/models/list-filter/criteria/has-markers";
|
|
import { SidebarBooleanFilter } from "../List/Filters/BooleanFilter";
|
|
import {
|
|
DurationCriterionOption,
|
|
PerformerAgeCriterionOption,
|
|
} from "src/models/list-filter/scenes";
|
|
import { SidebarAgeFilter } from "../List/Filters/SidebarAgeFilter";
|
|
import { SidebarDurationFilter } from "../List/Filters/SidebarDurationFilter";
|
|
import {
|
|
FilteredSidebarHeader,
|
|
useFilteredSidebarKeybinds,
|
|
} from "../List/Filters/FilterSidebar";
|
|
import { PatchContainerComponent } from "src/patch";
|
|
import { Pagination } from "../List/Pagination";
|
|
import { Button, ButtonGroup } from "react-bootstrap";
|
|
import { Icon } from "../Shared/Icon";
|
|
import useFocus from "src/utils/focus";
|
|
import {
|
|
FilteredListToolbar2,
|
|
ToolbarFilterSection,
|
|
ToolbarSelectionSection,
|
|
} from "../List/ListToolbar";
|
|
import { ListResultsHeader } from "../List/ListResultsHeader";
|
|
import { useZoomKeybinds } from "../List/ZoomSlider";
|
|
|
|
function renderMetadataByline(result: GQL.FindScenesQueryResult) {
|
|
const duration = result?.data?.findScenes?.duration;
|
|
const size = result?.data?.findScenes?.filesize;
|
|
|
|
if (!duration && !size) {
|
|
return;
|
|
}
|
|
|
|
const separator = duration && size ? " - " : "";
|
|
|
|
return (
|
|
<span className="scenes-stats">
|
|
(
|
|
{duration ? (
|
|
<span className="scenes-duration">
|
|
{TextUtils.secondsAsTimeString(duration, 3)}
|
|
</span>
|
|
) : undefined}
|
|
{separator}
|
|
{size ? (
|
|
<span className="scenes-size">
|
|
<FileSize size={size} />
|
|
</span>
|
|
) : undefined}
|
|
)
|
|
</span>
|
|
);
|
|
}
|
|
|
|
function usePlayScene() {
|
|
const history = useHistory();
|
|
|
|
const { configuration: config } = useConfigurationContext();
|
|
const cont = config?.interface.continuePlaylistDefault ?? false;
|
|
const autoPlay = config?.interface.autostartVideoOnPlaySelected ?? false;
|
|
|
|
const playScene = useCallback(
|
|
(queue: SceneQueue, sceneID: string, options?: IPlaySceneOptions) => {
|
|
history.push(
|
|
queue.makeLink(sceneID, { autoPlay, continue: cont, ...options })
|
|
);
|
|
},
|
|
[history, cont, autoPlay]
|
|
);
|
|
|
|
return playScene;
|
|
}
|
|
|
|
function usePlaySelected(selectedIds: Set<string>) {
|
|
const playScene = usePlayScene();
|
|
|
|
const playSelected = useCallback(() => {
|
|
// populate queue and go to first scene
|
|
const sceneIDs = Array.from(selectedIds.values());
|
|
const queue = SceneQueue.fromSceneIDList(sceneIDs);
|
|
|
|
playScene(queue, sceneIDs[0]);
|
|
}, [selectedIds, playScene]);
|
|
|
|
return playSelected;
|
|
}
|
|
|
|
function usePlayFirst() {
|
|
const playScene = usePlayScene();
|
|
|
|
const playFirst = useCallback(
|
|
(queue: SceneQueue, sceneID: string, index: number) => {
|
|
// populate queue and go to first scene
|
|
playScene(queue, sceneID, { sceneIndex: index });
|
|
},
|
|
[playScene]
|
|
);
|
|
|
|
return playFirst;
|
|
}
|
|
|
|
function usePlayRandom(filter: ListFilterModel, count: number) {
|
|
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);
|
|
playScene(queue, scene.id, { sceneIndex: index });
|
|
}
|
|
}, [filter, count, 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 && filter.displayMode !== DisplayMode.Tagger) {
|
|
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}
|
|
zoomIndex={filter.zoomIndex}
|
|
/>
|
|
);
|
|
}
|
|
if (filter.displayMode === DisplayMode.Tagger) {
|
|
return <Tagger scenes={scenes} queue={queue} />;
|
|
}
|
|
|
|
return null;
|
|
};
|
|
|
|
const ScenesFilterSidebarSections = PatchContainerComponent(
|
|
"FilteredSceneList.SidebarSections"
|
|
);
|
|
|
|
const SidebarContent: React.FC<{
|
|
filter: ListFilterModel;
|
|
setFilter: (filter: ListFilterModel) => void;
|
|
filterHook?: (filter: ListFilterModel) => ListFilterModel;
|
|
view?: View;
|
|
sidebarOpen: boolean;
|
|
onClose?: () => void;
|
|
showEditFilter: (editingCriterion?: string) => void;
|
|
count?: number;
|
|
focus?: ReturnType<typeof useFocus>;
|
|
}> = ({
|
|
filter,
|
|
setFilter,
|
|
filterHook,
|
|
view,
|
|
showEditFilter,
|
|
sidebarOpen,
|
|
onClose,
|
|
count,
|
|
focus,
|
|
}) => {
|
|
const showResultsId =
|
|
count !== undefined ? "actions.show_count_results" : "actions.show_results";
|
|
|
|
const hideStudios = view === View.StudioScenes;
|
|
|
|
return (
|
|
<>
|
|
<FilteredSidebarHeader
|
|
sidebarOpen={sidebarOpen}
|
|
showEditFilter={showEditFilter}
|
|
filter={filter}
|
|
setFilter={setFilter}
|
|
view={view}
|
|
focus={focus}
|
|
/>
|
|
|
|
<ScenesFilterSidebarSections>
|
|
{!hideStudios && (
|
|
<SidebarStudiosFilter
|
|
title={<FormattedMessage id="studios" />}
|
|
data-type={StudiosCriterionOption.type}
|
|
option={StudiosCriterionOption}
|
|
filter={filter}
|
|
setFilter={setFilter}
|
|
filterHook={filterHook}
|
|
sectionID="studios"
|
|
/>
|
|
)}
|
|
<SidebarPerformersFilter
|
|
title={<FormattedMessage id="performers" />}
|
|
data-type={PerformersCriterionOption.type}
|
|
option={PerformersCriterionOption}
|
|
filter={filter}
|
|
setFilter={setFilter}
|
|
filterHook={filterHook}
|
|
sectionID="performers"
|
|
/>
|
|
<SidebarTagsFilter
|
|
title={<FormattedMessage id="tags" />}
|
|
data-type={TagsCriterionOption.type}
|
|
option={TagsCriterionOption}
|
|
filter={filter}
|
|
setFilter={setFilter}
|
|
filterHook={filterHook}
|
|
sectionID="tags"
|
|
/>
|
|
<SidebarRatingFilter
|
|
title={<FormattedMessage id="rating" />}
|
|
data-type={RatingCriterionOption.type}
|
|
option={RatingCriterionOption}
|
|
filter={filter}
|
|
setFilter={setFilter}
|
|
sectionID="rating"
|
|
/>
|
|
<SidebarDurationFilter
|
|
title={<FormattedMessage id="duration" />}
|
|
option={DurationCriterionOption}
|
|
filter={filter}
|
|
setFilter={setFilter}
|
|
sectionID="duration"
|
|
/>
|
|
<SidebarBooleanFilter
|
|
title={<FormattedMessage id="hasMarkers" />}
|
|
data-type={HasMarkersCriterionOption.type}
|
|
option={HasMarkersCriterionOption}
|
|
filter={filter}
|
|
setFilter={setFilter}
|
|
sectionID="hasMarkers"
|
|
/>
|
|
<SidebarBooleanFilter
|
|
title={<FormattedMessage id="organized" />}
|
|
data-type={OrganizedCriterionOption.type}
|
|
option={OrganizedCriterionOption}
|
|
filter={filter}
|
|
setFilter={setFilter}
|
|
sectionID="organized"
|
|
/>
|
|
<SidebarAgeFilter
|
|
title={<FormattedMessage id="performer_age" />}
|
|
option={PerformerAgeCriterionOption}
|
|
filter={filter}
|
|
setFilter={setFilter}
|
|
sectionID="performer_age"
|
|
/>
|
|
</ScenesFilterSidebarSections>
|
|
|
|
<div className="sidebar-footer">
|
|
<Button className="sidebar-close-button" onClick={onClose}>
|
|
<FormattedMessage id={showResultsId} values={{ count }} />
|
|
</Button>
|
|
</div>
|
|
</>
|
|
);
|
|
};
|
|
|
|
interface IOperations {
|
|
text: string;
|
|
onClick: () => void;
|
|
isDisplayed?: () => boolean;
|
|
className?: string;
|
|
}
|
|
|
|
const SceneListOperations: React.FC<{
|
|
items: number;
|
|
hasSelection: boolean;
|
|
operations: IOperations[];
|
|
onEdit: () => void;
|
|
onDelete: () => void;
|
|
onPlay: () => void;
|
|
onCreateNew: () => void;
|
|
}> = ({
|
|
items,
|
|
hasSelection,
|
|
operations,
|
|
onEdit,
|
|
onDelete,
|
|
onPlay,
|
|
onCreateNew,
|
|
}) => {
|
|
const intl = useIntl();
|
|
|
|
return (
|
|
<div className="scene-list-operations">
|
|
<ButtonGroup>
|
|
{!!items && (
|
|
<Button
|
|
className="play-button"
|
|
variant="secondary"
|
|
onClick={() => onPlay()}
|
|
title={intl.formatMessage({ id: "actions.play" })}
|
|
>
|
|
<Icon icon={faPlay} />
|
|
</Button>
|
|
)}
|
|
{!hasSelection && (
|
|
<Button
|
|
className="create-new-button"
|
|
variant="secondary"
|
|
onClick={() => onCreateNew()}
|
|
title={intl.formatMessage(
|
|
{ id: "actions.create_entity" },
|
|
{ entityType: intl.formatMessage({ id: "scene" }) }
|
|
)}
|
|
>
|
|
<Icon icon={faPlus} />
|
|
</Button>
|
|
)}
|
|
|
|
{hasSelection && (
|
|
<>
|
|
<Button variant="secondary" onClick={() => onEdit()}>
|
|
<Icon icon={faPencil} />
|
|
</Button>
|
|
<Button
|
|
variant="danger"
|
|
className="btn-danger-minimal"
|
|
onClick={() => onDelete()}
|
|
>
|
|
<Icon icon={faTrash} />
|
|
</Button>
|
|
</>
|
|
)}
|
|
|
|
<OperationDropdown
|
|
className="scene-list-operations"
|
|
menuPortalTarget={document.body}
|
|
>
|
|
{operations.map((o) => {
|
|
if (o.isDisplayed && !o.isDisplayed()) {
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<OperationDropdownItem
|
|
key={o.text}
|
|
onClick={o.onClick}
|
|
text={o.text}
|
|
className={o.className}
|
|
/>
|
|
);
|
|
})}
|
|
</OperationDropdown>
|
|
</ButtonGroup>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
interface IFilteredScenes {
|
|
filterHook?: (filter: ListFilterModel) => ListFilterModel;
|
|
defaultSort?: string;
|
|
view?: View;
|
|
alterQuery?: boolean;
|
|
fromGroupId?: string;
|
|
}
|
|
|
|
export const FilteredSceneList = (props: IFilteredScenes) => {
|
|
const intl = useIntl();
|
|
const history = useHistory();
|
|
|
|
const searchFocus = useFocus();
|
|
const [, setSearchFocus] = searchFocus;
|
|
|
|
const { filterHook, defaultSort, view, alterQuery, fromGroupId } = props;
|
|
|
|
// States
|
|
const {
|
|
showSidebar,
|
|
setShowSidebar,
|
|
loading: sidebarStateLoading,
|
|
sectionOpen,
|
|
setSectionOpen,
|
|
} = useSidebarState(view);
|
|
|
|
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 } = filterState;
|
|
|
|
const { effectiveFilter, result, cachedResult, items, totalCount } =
|
|
queryResult;
|
|
|
|
const {
|
|
selectedIds,
|
|
selectedItems,
|
|
onSelectChange,
|
|
onSelectAll,
|
|
onSelectNone,
|
|
hasSelection,
|
|
} = listSelect;
|
|
|
|
const { modal, showModal, closeModal } = modalState;
|
|
|
|
// Utility hooks
|
|
const { setPage, removeCriterion, clearAllCriteria } = useFilterOperations({
|
|
filter,
|
|
setFilter,
|
|
});
|
|
|
|
useAddKeybinds(filter, totalCount);
|
|
useFilteredSidebarKeybinds({
|
|
showSidebar,
|
|
setShowSidebar,
|
|
});
|
|
|
|
useEffect(() => {
|
|
Mousetrap.bind("e", () => {
|
|
if (hasSelection) {
|
|
onEdit?.();
|
|
}
|
|
});
|
|
|
|
Mousetrap.bind("d d", () => {
|
|
if (hasSelection) {
|
|
onDelete?.();
|
|
}
|
|
});
|
|
|
|
return () => {
|
|
Mousetrap.unbind("e");
|
|
Mousetrap.unbind("d d");
|
|
};
|
|
});
|
|
useZoomKeybinds({
|
|
zoomIndex: filter.zoomIndex,
|
|
onChangeZoom: (zoom) => setFilter(filter.setZoom(zoom)),
|
|
});
|
|
|
|
const onCloseEditDelete = useCloseEditDelete({
|
|
closeModal,
|
|
onSelectNone,
|
|
result,
|
|
});
|
|
|
|
const metadataByline = useMemo(() => {
|
|
if (cachedResult.loading) return null;
|
|
|
|
return renderMetadataByline(cachedResult) ?? null;
|
|
}, [cachedResult]);
|
|
|
|
const queue = useMemo(() => SceneQueue.fromListFilterModel(filter), [filter]);
|
|
|
|
const playRandom = usePlayRandom(effectiveFilter, totalCount);
|
|
const playSelected = usePlaySelected(selectedIds);
|
|
const playFirst = usePlayFirst();
|
|
|
|
function onCreateNew() {
|
|
history.push("/scenes/new");
|
|
}
|
|
|
|
function onPlay() {
|
|
if (items.length === 0) {
|
|
return;
|
|
}
|
|
|
|
// if there are selected items, play those
|
|
if (hasSelection) {
|
|
playSelected();
|
|
return;
|
|
}
|
|
|
|
// otherwise, play the first item in the list
|
|
const sceneID = items[0].id;
|
|
playFirst(queue, sceneID, 0);
|
|
}
|
|
|
|
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
|
|
/>
|
|
);
|
|
}
|
|
|
|
function onEdit() {
|
|
showModal(
|
|
<EditScenesDialog selected={selectedItems} onClose={onCloseEditDelete} />
|
|
);
|
|
}
|
|
|
|
function onDelete() {
|
|
showModal(
|
|
<DeleteScenesDialog
|
|
selected={selectedItems}
|
|
onClose={onCloseEditDelete}
|
|
/>
|
|
);
|
|
}
|
|
|
|
const otherOperations = [
|
|
{
|
|
text: intl.formatMessage({ id: "actions.play" }),
|
|
onClick: () => onPlay(),
|
|
isDisplayed: () => items.length > 0,
|
|
className: "play-item",
|
|
},
|
|
{
|
|
text: intl.formatMessage(
|
|
{ id: "actions.create_entity" },
|
|
{ entityType: intl.formatMessage({ id: "scene" }) }
|
|
),
|
|
onClick: () => onCreateNew(),
|
|
isDisplayed: () => !hasSelection,
|
|
className: "create-new-item",
|
|
},
|
|
{
|
|
text: intl.formatMessage({ id: "actions.select_all" }),
|
|
onClick: () => onSelectAll(),
|
|
isDisplayed: () => totalCount > 0,
|
|
},
|
|
{
|
|
text: intl.formatMessage({ id: "actions.select_none" }),
|
|
onClick: () => onSelectNone(),
|
|
isDisplayed: () => hasSelection,
|
|
},
|
|
{
|
|
text: intl.formatMessage({ id: "actions.play_random" }),
|
|
onClick: playRandom,
|
|
isDisplayed: () => totalCount > 1,
|
|
},
|
|
{
|
|
text: `${intl.formatMessage({ id: "actions.generate" })}…`,
|
|
onClick: () =>
|
|
showModal(
|
|
<GenerateDialog
|
|
type="scene"
|
|
selectedIds={Array.from(selectedIds.values())}
|
|
onClose={() => closeModal()}
|
|
/>
|
|
),
|
|
isDisplayed: () => hasSelection,
|
|
},
|
|
{
|
|
text: `${intl.formatMessage({ id: "actions.identify" })}…`,
|
|
onClick: () =>
|
|
showModal(
|
|
<IdentifyDialog
|
|
selectedIds={Array.from(selectedIds.values())}
|
|
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),
|
|
},
|
|
];
|
|
|
|
// render
|
|
if (sidebarStateLoading) return null;
|
|
|
|
const operations = (
|
|
<SceneListOperations
|
|
items={items.length}
|
|
hasSelection={hasSelection}
|
|
operations={otherOperations}
|
|
onEdit={onEdit}
|
|
onDelete={onDelete}
|
|
onPlay={onPlay}
|
|
onCreateNew={onCreateNew}
|
|
/>
|
|
);
|
|
|
|
return (
|
|
<TaggerContext>
|
|
<div
|
|
className={cx("item-list-container scene-list", {
|
|
"hide-sidebar": !showSidebar,
|
|
})}
|
|
>
|
|
{modal}
|
|
|
|
<SidebarStateContext.Provider value={{ sectionOpen, setSectionOpen }}>
|
|
<SidebarPane hideSidebar={!showSidebar}>
|
|
<Sidebar hide={!showSidebar} onHide={() => setShowSidebar(false)}>
|
|
<SidebarContent
|
|
filter={filter}
|
|
setFilter={setFilter}
|
|
filterHook={filterHook}
|
|
showEditFilter={showEditFilter}
|
|
view={view}
|
|
sidebarOpen={showSidebar}
|
|
onClose={() => setShowSidebar(false)}
|
|
count={cachedResult.loading ? undefined : totalCount}
|
|
focus={searchFocus}
|
|
/>
|
|
</Sidebar>
|
|
<SidebarPaneContent>
|
|
<FilteredListToolbar2
|
|
className="scene-list-toolbar"
|
|
hasSelection={hasSelection}
|
|
filterSection={
|
|
<ToolbarFilterSection
|
|
filter={filter}
|
|
onSetFilter={setFilter}
|
|
onToggleSidebar={() => setShowSidebar(!showSidebar)}
|
|
onEditCriterion={(c) =>
|
|
showEditFilter(c?.criterionOption.type)
|
|
}
|
|
onRemoveCriterion={removeCriterion}
|
|
onRemoveAllCriterion={() => clearAllCriteria(true)}
|
|
onEditSearchTerm={() => {
|
|
setShowSidebar(true);
|
|
setSearchFocus(true);
|
|
}}
|
|
onRemoveSearchTerm={() =>
|
|
setFilter(filter.clearSearchTerm())
|
|
}
|
|
view={view}
|
|
/>
|
|
}
|
|
selectionSection={
|
|
<ToolbarSelectionSection
|
|
selected={selectedIds.size}
|
|
onToggleSidebar={() => setShowSidebar(!showSidebar)}
|
|
onSelectAll={() => onSelectAll()}
|
|
onSelectNone={() => onSelectNone()}
|
|
operations={operations}
|
|
/>
|
|
}
|
|
operationSection={operations}
|
|
/>
|
|
|
|
<ListResultsHeader
|
|
loading={cachedResult.loading}
|
|
filter={filter}
|
|
totalCount={totalCount}
|
|
metadataByline={metadataByline}
|
|
onChangeFilter={(newFilter) => setFilter(newFilter)}
|
|
/>
|
|
|
|
<LoadedContent loading={result.loading} error={result.error}>
|
|
<SceneList
|
|
filter={effectiveFilter}
|
|
scenes={items}
|
|
selectedIds={selectedIds}
|
|
onSelectChange={onSelectChange}
|
|
fromGroupId={fromGroupId}
|
|
/>
|
|
</LoadedContent>
|
|
|
|
{totalCount > filter.itemsPerPage && (
|
|
<div className="pagination-footer">
|
|
<Pagination
|
|
itemsPerPage={filter.itemsPerPage}
|
|
currentPage={filter.currentPage}
|
|
totalItems={totalCount}
|
|
metadataByline={metadataByline}
|
|
onChangePage={setPage}
|
|
pagePopupPlacement="top"
|
|
/>
|
|
</div>
|
|
)}
|
|
</SidebarPaneContent>
|
|
</SidebarPane>
|
|
</SidebarStateContext.Provider>
|
|
</div>
|
|
</TaggerContext>
|
|
);
|
|
};
|
|
|
|
export default FilteredSceneList;
|