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
* Remember gallery images view mode.
* Add option to skip checking of insecure SSL certificates when scraping.
* Auto-play video previews on mobile devices.
* Replace hover menu with dropdown menu for O-Counter.

View File

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

View File

@@ -6,6 +6,7 @@ import { ImageList } from "src/components/Images/ImageList";
import { showWhenSelected } from "src/hooks/ListHook";
import { mutateAddGalleryImages } from "src/core/StashService";
import { useToast } from "src/hooks";
import { TextUtils } from "src/utils";
interface IGalleryAddProps {
gallery: Partial<GQL.GalleryDataFragment>;
@@ -17,7 +18,7 @@ export const GalleryAddPanel: React.FC<IGalleryAddProps> = ({ gallery }) => {
function filterHook(filter: ListFilterModel) {
const galleryValue = {
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
let galleryCriterion = filter.criteria.find((c) => {
@@ -77,10 +78,6 @@ export const GalleryAddPanel: React.FC<IGalleryAddProps> = ({ gallery }) => {
];
return (
<ImageList
filterHook={filterHook}
extraOperations={otherOperations}
persistState={false}
/>
<ImageList filterHook={filterHook} extraOperations={otherOperations} />
);
};

View File

@@ -4,8 +4,9 @@ import { GalleriesCriterion } from "src/models/list-filter/criteria/galleries";
import { ListFilterModel } from "src/models/list-filter/filter";
import { ImageList } from "src/components/Images/ImageList";
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 { TextUtils } from "src/utils";
interface IGalleryDetailsProps {
gallery: GQL.GalleryDataFragment;
@@ -19,7 +20,7 @@ export const GalleryImagesPanel: React.FC<IGalleryDetailsProps> = ({
function filterHook(filter: ListFilterModel) {
const galleryValue = {
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
let galleryCriterion = filter.criteria.find((c) => {
@@ -82,7 +83,8 @@ export const GalleryImagesPanel: React.FC<IGalleryDetailsProps> = ({
<ImageList
filterHook={filterHook}
extraOperations={otherOperations}
persistState={false}
persistState={PersistanceLevel.VIEW}
persistanceKey="galleryimages"
/>
);
};

View File

@@ -8,7 +8,8 @@ import {
GallerySlimDataFragment,
} from "src/core/generated-graphql";
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 { DisplayMode } from "src/models/list-filter/types";
import { queryFindGalleries } from "src/core/StashService";
@@ -20,7 +21,7 @@ import { ExportDialog } from "../Shared/ExportDialog";
interface IGalleryList {
filterHook?: (filter: ListFilterModel) => ListFilterModel;
persistState?: boolean;
persistState?: PersistanceLevel;
}
export const GalleryList: React.FC<IGalleryList> = ({
@@ -202,7 +203,9 @@ export const GalleryList: React.FC<IGalleryList> = ({
</td>
<td className="d-none d-sm-block">
<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"})
</Link>
</td>

View File

@@ -24,7 +24,7 @@ const GalleryWallCard: React.FC<IProps> = ({ gallery }) => {
? "landscape"
: "portrait";
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 performers =
performerNames.length >= 2

View File

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

View File

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

View File

@@ -9,7 +9,11 @@ import {
import { ListFilterModel } from "src/models/list-filter/filter";
import { DisplayMode } from "src/models/list-filter/types";
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 { MovieCard } from "./MovieCard";
@@ -65,7 +69,7 @@ export const MovieList: React.FC = () => {
addKeybinds,
otherOperations,
selectable: true,
persistState: true,
persistState: PersistanceLevel.ALL,
renderDeleteDialog,
});

View File

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

View File

@@ -10,7 +10,7 @@ import { queryFindScenes } from "src/core/StashService";
import { useScenesList } from "src/hooks";
import { ListFilterModel } from "src/models/list-filter/filter";
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 { WallPanel } from "../Wall/WallPanel";
import { SceneCard } from "./SceneCard";
@@ -22,7 +22,7 @@ import { ExportDialog } from "../Shared/ExportDialog";
interface ISceneList {
filterHook?: (filter: ListFilterModel) => ListFilterModel;
persistState?: boolean;
persistState?: PersistanceLevel.ALL;
}
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 { NavUtils } from "src/utils";
import { useSceneMarkersList } from "src/hooks";
import { PersistanceLevel } from "src/hooks/ListHook";
import { ListFilterModel } from "src/models/list-filter/filter";
import { DisplayMode } from "src/models/list-filter/types";
import { WallPanel } from "../Wall/WallPanel";
@@ -41,7 +42,7 @@ export const SceneMarkerList: React.FC<ISceneMarkerList> = ({ filterHook }) => {
renderContent,
filterHook,
addKeybinds,
persistState: true,
persistState: PersistanceLevel.ALL,
});
async function playRandom(

View File

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

View File

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

View File

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

View File

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