Gallery view persistence (#1105)

* Persist gallery image view separately from primary image view
This commit is contained in:
InfiniteTF
2021-03-01 02:10:05 +01:00
committed by GitHub
parent 7e0db2aad4
commit 01da28010d
16 changed files with 101 additions and 46 deletions

View File

@@ -1,4 +1,5 @@
### 🎨 Improvements ### 🎨 Improvements
* Remember gallery images view mode.
* Add option to skip checking of insecure SSL certificates when scraping. * Add option to skip checking of insecure SSL certificates when scraping.
* Auto-play video previews on mobile devices. * Auto-play video previews on mobile devices.
* Replace hover menu with dropdown menu for O-Counter. * Replace hover menu with dropdown menu for O-Counter.

View File

@@ -1,5 +1,6 @@
import React from "react"; import React from "react";
import { Route, Switch } from "react-router-dom"; import { Route, Switch } from "react-router-dom";
import { PersistanceLevel } from "src/hooks/ListHook";
import { Gallery } from "./GalleryDetails/Gallery"; import { Gallery } from "./GalleryDetails/Gallery";
import { GalleryList } from "./GalleryList"; import { GalleryList } from "./GalleryList";
@@ -8,7 +9,9 @@ const Galleries = () => (
<Route <Route
exact exact
path="/galleries" path="/galleries"
render={(props) => <GalleryList {...props} persistState />} render={(props) => (
<GalleryList {...props} persistState={PersistanceLevel.ALL} />
)}
/> />
<Route path="/galleries/:id/:tab?" component={Gallery} /> <Route path="/galleries/:id/:tab?" component={Gallery} />
</Switch> </Switch>

View File

@@ -6,6 +6,7 @@ import { ImageList } from "src/components/Images/ImageList";
import { showWhenSelected } from "src/hooks/ListHook"; import { showWhenSelected } from "src/hooks/ListHook";
import { mutateAddGalleryImages } from "src/core/StashService"; import { mutateAddGalleryImages } from "src/core/StashService";
import { useToast } from "src/hooks"; import { useToast } from "src/hooks";
import { TextUtils } from "src/utils";
interface IGalleryAddProps { interface IGalleryAddProps {
gallery: Partial<GQL.GalleryDataFragment>; gallery: Partial<GQL.GalleryDataFragment>;
@@ -17,7 +18,7 @@ export const GalleryAddPanel: React.FC<IGalleryAddProps> = ({ gallery }) => {
function filterHook(filter: ListFilterModel) { function filterHook(filter: ListFilterModel) {
const galleryValue = { const galleryValue = {
id: gallery.id!, id: gallery.id!,
label: gallery.title ?? gallery.path ?? "", label: gallery.title ?? TextUtils.fileNameFromPath(gallery.path ?? ""),
}; };
// if galleries is already present, then we modify it, otherwise add // if galleries is already present, then we modify it, otherwise add
let galleryCriterion = filter.criteria.find((c) => { let galleryCriterion = filter.criteria.find((c) => {
@@ -77,10 +78,6 @@ export const GalleryAddPanel: React.FC<IGalleryAddProps> = ({ gallery }) => {
]; ];
return ( return (
<ImageList <ImageList filterHook={filterHook} extraOperations={otherOperations} />
filterHook={filterHook}
extraOperations={otherOperations}
persistState={false}
/>
); );
}; };

View File

@@ -4,8 +4,9 @@ import { GalleriesCriterion } from "src/models/list-filter/criteria/galleries";
import { ListFilterModel } from "src/models/list-filter/filter"; import { ListFilterModel } from "src/models/list-filter/filter";
import { ImageList } from "src/components/Images/ImageList"; import { ImageList } from "src/components/Images/ImageList";
import { mutateRemoveGalleryImages } from "src/core/StashService"; import { mutateRemoveGalleryImages } from "src/core/StashService";
import { showWhenSelected } from "src/hooks/ListHook"; import { showWhenSelected, PersistanceLevel } from "src/hooks/ListHook";
import { useToast } from "src/hooks"; import { useToast } from "src/hooks";
import { TextUtils } from "src/utils";
interface IGalleryDetailsProps { interface IGalleryDetailsProps {
gallery: GQL.GalleryDataFragment; gallery: GQL.GalleryDataFragment;
@@ -19,7 +20,7 @@ export const GalleryImagesPanel: React.FC<IGalleryDetailsProps> = ({
function filterHook(filter: ListFilterModel) { function filterHook(filter: ListFilterModel) {
const galleryValue = { const galleryValue = {
id: gallery.id!, id: gallery.id!,
label: gallery.title ?? gallery.path ?? "", label: gallery.title ?? TextUtils.fileNameFromPath(gallery.path ?? ""),
}; };
// if galleries is already present, then we modify it, otherwise add // if galleries is already present, then we modify it, otherwise add
let galleryCriterion = filter.criteria.find((c) => { let galleryCriterion = filter.criteria.find((c) => {
@@ -82,7 +83,8 @@ export const GalleryImagesPanel: React.FC<IGalleryDetailsProps> = ({
<ImageList <ImageList
filterHook={filterHook} filterHook={filterHook}
extraOperations={otherOperations} extraOperations={otherOperations}
persistState={false} persistState={PersistanceLevel.VIEW}
persistanceKey="galleryimages"
/> />
); );
}; };

View File

@@ -8,7 +8,8 @@ import {
GallerySlimDataFragment, GallerySlimDataFragment,
} from "src/core/generated-graphql"; } from "src/core/generated-graphql";
import { useGalleriesList } from "src/hooks"; import { useGalleriesList } from "src/hooks";
import { showWhenSelected } from "src/hooks/ListHook"; import { TextUtils } from "src/utils";
import { showWhenSelected, PersistanceLevel } from "src/hooks/ListHook";
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 { queryFindGalleries } from "src/core/StashService"; import { queryFindGalleries } from "src/core/StashService";
@@ -20,7 +21,7 @@ import { ExportDialog } from "../Shared/ExportDialog";
interface IGalleryList { interface IGalleryList {
filterHook?: (filter: ListFilterModel) => ListFilterModel; filterHook?: (filter: ListFilterModel) => ListFilterModel;
persistState?: boolean; persistState?: PersistanceLevel;
} }
export const GalleryList: React.FC<IGalleryList> = ({ export const GalleryList: React.FC<IGalleryList> = ({
@@ -202,7 +203,9 @@ export const GalleryList: React.FC<IGalleryList> = ({
</td> </td>
<td className="d-none d-sm-block"> <td className="d-none d-sm-block">
<Link to={`/galleries/${gallery.id}`}> <Link to={`/galleries/${gallery.id}`}>
{gallery.title ?? gallery.path} ({gallery.image_count}{" "} {gallery.title ??
TextUtils.fileNameFromPath(gallery.path ?? "")}{" "}
({gallery.image_count}{" "}
{gallery.image_count === 1 ? "image" : "images"}) {gallery.image_count === 1 ? "image" : "images"})
</Link> </Link>
</td> </td>

View File

@@ -24,7 +24,7 @@ const GalleryWallCard: React.FC<IProps> = ({ gallery }) => {
? "landscape" ? "landscape"
: "portrait"; : "portrait";
const cover = gallery?.cover?.paths.thumbnail ?? ""; const cover = gallery?.cover?.paths.thumbnail ?? "";
const title = gallery.title ?? gallery.path; const title = gallery.title ?? TextUtils.fileNameFromPath(gallery.path ?? "");
const performerNames = gallery.performers.map((p) => p.name); const performerNames = gallery.performers.map((p) => p.name);
const performers = const performers =
performerNames.length >= 2 performerNames.length >= 2

View File

@@ -12,7 +12,11 @@ import { useImagesList, useLightbox } from "src/hooks";
import { TextUtils } from "src/utils"; import { TextUtils } from "src/utils";
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 { IListHookOperation, showWhenSelected } from "src/hooks/ListHook"; import {
IListHookOperation,
showWhenSelected,
PersistanceLevel,
} from "src/hooks/ListHook";
import { ImageCard } from "./ImageCard"; import { ImageCard } from "./ImageCard";
import { EditImagesDialog } from "./EditImagesDialog"; import { EditImagesDialog } from "./EditImagesDialog";
import { DeleteImagesDialog } from "./DeleteImagesDialog"; import { DeleteImagesDialog } from "./DeleteImagesDialog";
@@ -79,13 +83,15 @@ const ImageWall: React.FC<IImageWallProps> = ({
interface IImageList { interface IImageList {
filterHook?: (filter: ListFilterModel) => ListFilterModel; filterHook?: (filter: ListFilterModel) => ListFilterModel;
persistState?: boolean; persistState?: PersistanceLevel;
persistanceKey?: string;
extraOperations?: IListHookOperation<FindImagesQueryResult>[]; extraOperations?: IListHookOperation<FindImagesQueryResult>[];
} }
export const ImageList: React.FC<IImageList> = ({ export const ImageList: React.FC<IImageList> = ({
filterHook, filterHook,
persistState, persistState,
persistanceKey,
extraOperations, extraOperations,
}) => { }) => {
const history = useHistory(); const history = useHistory();
@@ -131,6 +137,7 @@ export const ImageList: React.FC<IImageList> = ({
filterHook, filterHook,
addKeybinds, addKeybinds,
persistState, persistState,
persistanceKey,
}); });
async function viewRandom( async function viewRandom(

View File

@@ -1,5 +1,6 @@
import React from "react"; import React from "react";
import { Route, Switch } from "react-router-dom"; import { Route, Switch } from "react-router-dom";
import { PersistanceLevel } from "src/hooks/ListHook";
import { Image } from "./ImageDetails/Image"; import { Image } from "./ImageDetails/Image";
import { ImageList } from "./ImageList"; import { ImageList } from "./ImageList";
@@ -8,7 +9,9 @@ const Images = () => (
<Route <Route
exact exact
path="/images" path="/images"
render={(props) => <ImageList persistState {...props} />} render={(props) => (
<ImageList persistState={PersistanceLevel.ALL} {...props} />
)}
/> />
<Route path="/images/:id" component={Image} /> <Route path="/images/:id" component={Image} />
</Switch> </Switch>

View File

@@ -9,7 +9,11 @@ import {
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 { queryFindMovies, useMoviesDestroy } from "src/core/StashService"; import { queryFindMovies, useMoviesDestroy } from "src/core/StashService";
import { showWhenSelected, useMoviesList } from "src/hooks/ListHook"; import {
showWhenSelected,
useMoviesList,
PersistanceLevel,
} from "src/hooks/ListHook";
import { ExportDialog, DeleteEntityDialog } from "src/components/Shared"; import { ExportDialog, DeleteEntityDialog } from "src/components/Shared";
import { MovieCard } from "./MovieCard"; import { MovieCard } from "./MovieCard";
@@ -65,7 +69,7 @@ export const MovieList: React.FC = () => {
addKeybinds, addKeybinds,
otherOperations, otherOperations,
selectable: true, selectable: true,
persistState: true, persistState: PersistanceLevel.ALL,
renderDeleteDialog, renderDeleteDialog,
}); });

View File

@@ -11,7 +11,7 @@ import {
usePerformersDestroy, usePerformersDestroy,
} from "src/core/StashService"; } from "src/core/StashService";
import { usePerformersList } from "src/hooks"; import { usePerformersList } from "src/hooks";
import { showWhenSelected } from "src/hooks/ListHook"; import { showWhenSelected, PersistanceLevel } from "src/hooks/ListHook";
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 { ExportDialog, DeleteEntityDialog } from "src/components/Shared"; import { ExportDialog, DeleteEntityDialog } from "src/components/Shared";
@@ -100,7 +100,7 @@ export const PerformerList: React.FC = () => {
renderContent, renderContent,
addKeybinds, addKeybinds,
selectable: true, selectable: true,
persistState: true, persistState: PersistanceLevel.ALL,
renderDeleteDialog, renderDeleteDialog,
}); });

View File

@@ -10,7 +10,7 @@ import { queryFindScenes } from "src/core/StashService";
import { useScenesList } from "src/hooks"; import { useScenesList } from "src/hooks";
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 { showWhenSelected } from "src/hooks/ListHook"; import { showWhenSelected, PersistanceLevel } from "src/hooks/ListHook";
import Tagger from "src/components/Tagger"; import Tagger from "src/components/Tagger";
import { WallPanel } from "../Wall/WallPanel"; import { WallPanel } from "../Wall/WallPanel";
import { SceneCard } from "./SceneCard"; import { SceneCard } from "./SceneCard";
@@ -22,7 +22,7 @@ import { ExportDialog } from "../Shared/ExportDialog";
interface ISceneList { interface ISceneList {
filterHook?: (filter: ListFilterModel) => ListFilterModel; filterHook?: (filter: ListFilterModel) => ListFilterModel;
persistState?: boolean; persistState?: PersistanceLevel.ALL;
} }
export const SceneList: React.FC<ISceneList> = ({ export const SceneList: React.FC<ISceneList> = ({

View File

@@ -6,6 +6,7 @@ import { FindSceneMarkersQueryResult } from "src/core/generated-graphql";
import { queryFindSceneMarkers } from "src/core/StashService"; import { queryFindSceneMarkers } from "src/core/StashService";
import { NavUtils } from "src/utils"; import { NavUtils } from "src/utils";
import { useSceneMarkersList } from "src/hooks"; import { useSceneMarkersList } from "src/hooks";
import { PersistanceLevel } from "src/hooks/ListHook";
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 { WallPanel } from "../Wall/WallPanel"; import { WallPanel } from "../Wall/WallPanel";
@@ -41,7 +42,7 @@ export const SceneMarkerList: React.FC<ISceneMarkerList> = ({ filterHook }) => {
renderContent, renderContent,
filterHook, filterHook,
addKeybinds, addKeybinds,
persistState: true, persistState: PersistanceLevel.ALL,
}); });
async function playRandom( async function playRandom(

View File

@@ -1,5 +1,6 @@
import React from "react"; import React from "react";
import { Route, Switch } from "react-router-dom"; import { Route, Switch } from "react-router-dom";
import { PersistanceLevel } from "src/hooks/ListHook";
import { Scene } from "./SceneDetails/Scene"; import { Scene } from "./SceneDetails/Scene";
import { SceneList } from "./SceneList"; import { SceneList } from "./SceneList";
import { SceneMarkerList } from "./SceneMarkerList"; import { SceneMarkerList } from "./SceneMarkerList";
@@ -9,7 +10,9 @@ const Scenes = () => (
<Route <Route
exact exact
path="/scenes" path="/scenes"
render={(props) => <SceneList persistState {...props} />} render={(props) => (
<SceneList persistState={PersistanceLevel.ALL} {...props} />
)}
/> />
<Route exact path="/scenes/markers" component={SceneMarkerList} /> <Route exact path="/scenes/markers" component={SceneMarkerList} />
<Route path="/scenes/:id" component={Scene} /> <Route path="/scenes/:id" component={Scene} />

View File

@@ -7,7 +7,7 @@ import {
SlimStudioDataFragment, SlimStudioDataFragment,
} from "src/core/generated-graphql"; } from "src/core/generated-graphql";
import { useStudiosList } from "src/hooks"; import { useStudiosList } from "src/hooks";
import { showWhenSelected } from "src/hooks/ListHook"; import { showWhenSelected, PersistanceLevel } from "src/hooks/ListHook";
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 { queryFindStudios, useStudiosDestroy } from "src/core/StashService"; import { queryFindStudios, useStudiosDestroy } from "src/core/StashService";
@@ -131,7 +131,7 @@ export const StudioList: React.FC<IStudioList> = ({
addKeybinds, addKeybinds,
otherOperations, otherOperations,
selectable: true, selectable: true,
persistState: !fromParent, persistState: !fromParent ? PersistanceLevel.ALL : PersistanceLevel.NONE,
renderDeleteDialog, renderDeleteDialog,
}); });

View File

@@ -4,7 +4,11 @@ import Mousetrap from "mousetrap";
import { FindTagsQueryResult } from "src/core/generated-graphql"; import { FindTagsQueryResult } from "src/core/generated-graphql";
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 { showWhenSelected, useTagsList } from "src/hooks/ListHook"; import {
showWhenSelected,
useTagsList,
PersistanceLevel,
} from "src/hooks/ListHook";
import { Button } from "react-bootstrap"; import { Button } from "react-bootstrap";
import { Link, useHistory } from "react-router-dom"; import { Link, useHistory } from "react-router-dom";
import * as GQL from "src/core/generated-graphql"; import * as GQL from "src/core/generated-graphql";
@@ -144,7 +148,7 @@ export const TagList: React.FC<ITagList> = ({ filterHook }) => {
selectable: true, selectable: true,
zoomable: true, zoomable: true,
defaultZoomIndex: 0, defaultZoomIndex: 0,
persistState: true, persistState: PersistanceLevel.ALL,
renderDeleteDialog, renderDeleteDialog,
}); });

View File

@@ -79,8 +79,15 @@ export interface IListHookOperation<T> {
postRefetch?: boolean; postRefetch?: boolean;
} }
export enum PersistanceLevel {
NONE,
ALL,
VIEW,
}
interface IListHookOptions<T, E> { interface IListHookOptions<T, E> {
persistState?: boolean; persistState?: PersistanceLevel;
persistanceKey?: string;
filterHook?: (filter: ListFilterModel) => ListFilterModel; filterHook?: (filter: ListFilterModel) => ListFilterModel;
zoomable?: boolean; zoomable?: boolean;
selectable?: boolean; selectable?: boolean;
@@ -421,22 +428,36 @@ const useList = <QueryResult extends IQueryResult, QueryData extends IDataItem>(
); );
// Store initial pathname to prevent hooks from operating outside this page // Store initial pathname to prevent hooks from operating outside this page
const originalPathName = useRef(location.pathname); const originalPathName = useRef(location.pathname);
const persistanceKey = options.persistanceKey ?? options.filterMode;
const [filter, setFilter] = useState<ListFilterModel>( const [filter, setFilter] = useState<ListFilterModel>(
new ListFilterModel(options.filterMode, queryString.parse(location.search)) new ListFilterModel(options.filterMode, queryString.parse(location.search))
); );
const updateInterfaceConfig = useCallback( const updateInterfaceConfig = useCallback(
(updatedFilter: ListFilterModel) => { (updatedFilter: ListFilterModel, level: PersistanceLevel) => {
setInterfaceState({ setInterfaceState((prevState) => {
[options.filterMode]: { if (level === PersistanceLevel.VIEW) {
return {
[persistanceKey]: {
...prevState[persistanceKey],
filter: queryString.stringify({
...queryString.parse(prevState[persistanceKey]?.filter ?? ""),
disp: updatedFilter.displayMode,
}),
},
};
}
return {
[persistanceKey]: {
filter: updatedFilter.makeQueryParameters(), filter: updatedFilter.makeQueryParameters(),
itemsPerPage: updatedFilter.itemsPerPage, itemsPerPage: updatedFilter.itemsPerPage,
currentPage: updatedFilter.currentPage, currentPage: updatedFilter.currentPage,
}, },
};
}); });
}, },
[options.filterMode, setInterfaceState] [persistanceKey, setInterfaceState]
); );
useEffect(() => { useEffect(() => {
@@ -451,20 +472,25 @@ const useList = <QueryResult extends IQueryResult, QueryData extends IDataItem>(
if (!options.persistState) return; if (!options.persistState) return;
const storedQuery = interfaceState.data?.[options.filterMode]; const storedQuery = interfaceState.data?.[persistanceKey];
if (!storedQuery) return; if (!storedQuery) return;
const queryFilter = queryString.parse(history.location.search); const queryFilter = queryString.parse(history.location.search);
const storedFilter = queryString.parse(storedQuery.filter); const storedFilter = queryString.parse(storedQuery.filter);
const activeFilter =
options.persistState === PersistanceLevel.ALL
? storedFilter
: { disp: storedFilter.disp };
const query = history.location.search const query = history.location.search
? { ? {
sortby: storedFilter.sortby, sortby: activeFilter.sortby,
sortdir: storedFilter.sortdir, sortdir: activeFilter.sortdir,
disp: storedFilter.disp, disp: activeFilter.disp,
perPage: storedFilter.perPage, perPage: activeFilter.perPage,
...queryFilter, ...queryFilter,
} }
: storedFilter; : activeFilter;
const newFilter = new ListFilterModel(options.filterMode, query); const newFilter = new ListFilterModel(options.filterMode, query);
@@ -474,7 +500,7 @@ const useList = <QueryResult extends IQueryResult, QueryData extends IDataItem>(
newLocation.search = newFilter.makeQueryParameters(); newLocation.search = newFilter.makeQueryParameters();
if (newLocation.search !== filter.makeQueryParameters()) { if (newLocation.search !== filter.makeQueryParameters()) {
setFilter(newFilter); setFilter(newFilter);
updateInterfaceConfig(newFilter); updateInterfaceConfig(newFilter, options.persistState);
} }
// If constructed search is different from current, update it as well // If constructed search is different from current, update it as well
if (newLocation.search !== location.search) { if (newLocation.search !== location.search) {
@@ -488,6 +514,7 @@ const useList = <QueryResult extends IQueryResult, QueryData extends IDataItem>(
history, history,
location.search, location.search,
options.filterMode, options.filterMode,
persistanceKey,
forageInitialised, forageInitialised,
updateInterfaceConfig, updateInterfaceConfig,
options.persistState, options.persistState,
@@ -499,7 +526,7 @@ const useList = <QueryResult extends IQueryResult, QueryData extends IDataItem>(
newLocation.search = listFilter.makeQueryParameters(); newLocation.search = listFilter.makeQueryParameters();
history.replace(newLocation); history.replace(newLocation);
if (options.persistState) { if (options.persistState) {
updateInterfaceConfig(listFilter); updateInterfaceConfig(listFilter, options.persistState);
} }
} }