mirror of
https://github.com/stashapp/stash.git
synced 2025-12-17 12:24:38 +03:00
Gallery view persistence (#1105)
* Persist gallery image view separately from primary image view
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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} />
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
|
||||
@@ -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> = ({
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user