Improved wall view for images (#3511)

* Proper masonry wall view for images
* allow user to configure margin and direction
This commit is contained in:
CJ 2023-03-07 19:36:47 -06:00 committed by GitHub
parent 9ede271c05
commit d4fb6b2acf
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 199 additions and 38 deletions

View file

@ -52,6 +52,7 @@
"react-dom": "^17.0.2",
"react-helmet": "^6.1.0",
"react-intl": "^6.2.8",
"react-photo-gallery": "^8.0.0",
"react-remark": "^2.1.0",
"react-router-bootstrap": "^0.25.0",
"react-router-dom": "^5.3.4",

View file

@ -1,6 +1,7 @@
import React, { useMemo } from "react";
import React, { useCallback, useMemo } from "react";
import { useLightbox } from "src/hooks/Lightbox/hooks";
import { LoadingIndicator } from "src/components/Shared/LoadingIndicator";
import Gallery from "react-photo-gallery";
import "flexbin/flexbin.css";
import {
CriterionModifier,
@ -44,29 +45,42 @@ export const GalleryViewer: React.FC<IProps> = ({ galleryId }) => {
}, [images]);
const showLightbox = useLightbox(lightboxState);
const showLightboxOnClick = useCallback(
(event, { index }) => {
showLightbox(index);
},
[showLightbox]
);
if (loading) return <LoadingIndicator />;
const thumbs = images.map((file, index) => (
<div
role="link"
tabIndex={index}
key={file.id ?? index}
onClick={() => showLightbox(index)}
onKeyPress={() => showLightbox(index)}
>
<img
src={file.paths.thumbnail ?? ""}
loading="lazy"
className="gallery-image"
alt={file.title ?? index.toString()}
/>
</div>
));
let photos: {
src: string;
srcSet?: string | string[] | undefined;
sizes?: string | string[] | undefined;
width: number;
height: number;
alt?: string | undefined;
key?: string | undefined;
}[] = [];
images.forEach((image, index) => {
let imageData = {
src: image.paths.thumbnail!,
width: image.files[0].width,
height: image.files[0].height,
tabIndex: index,
key: image.id ?? index,
loading: "lazy",
className: "gallery-image",
alt: image.title ?? index.toString(),
};
photos.push(imageData);
});
return (
<div className="gallery">
<div className="flexbin">{thumbs}</div>
<Gallery photos={photos} onClick={showLightboxOnClick} margin={2.5} />
</div>
);
};

View file

@ -1,4 +1,10 @@
import React, { useCallback, useState, useMemo, MouseEvent } from "react";
import React, {
useCallback,
useState,
useMemo,
MouseEvent,
useContext,
} from "react";
import { FormattedNumber, useIntl } from "react-intl";
import cloneDeep from "lodash-es/cloneDeep";
import { useHistory } from "react-router-dom";
@ -19,9 +25,12 @@ import { ImageCard } from "./ImageCard";
import { EditImagesDialog } from "./EditImagesDialog";
import { DeleteImagesDialog } from "./DeleteImagesDialog";
import "flexbin/flexbin.css";
import Gallery from "react-photo-gallery";
import { ExportDialog } from "../Shared/ExportDialog";
import { objectTitle } from "src/core/files";
import TextUtils from "src/utils/text";
import { ConfigurationContext } from "src/hooks/Config";
import { IUIConfig } from "src/core/config";
interface IImageWallProps {
images: GQL.SlimImageDataFragment[];
@ -32,26 +41,55 @@ interface IImageWallProps {
}
const ImageWall: React.FC<IImageWallProps> = ({ images, handleImageOpen }) => {
const thumbs = images.map((image, index) => (
<div
role="link"
tabIndex={index}
key={image.id}
onClick={() => handleImageOpen(index)}
onKeyPress={() => handleImageOpen(index)}
>
<img
src={image.paths.thumbnail ?? ""}
loading="lazy"
className="gallery-image"
alt={objectTitle(image)}
/>
</div>
));
const { configuration } = useContext(ConfigurationContext);
const uiConfig = configuration?.ui as IUIConfig | undefined;
let photos: {
src: string;
srcSet?: string | string[] | undefined;
sizes?: string | string[] | undefined;
width: number;
height: number;
alt?: string | undefined;
key?: string | undefined;
}[] = [];
images.forEach((image, index) => {
let imageData = {
src: image.paths.thumbnail!,
width: image.files[0].width,
height: image.files[0].height,
tabIndex: index,
key: image.id,
loading: "lazy",
className: "gallery-image",
alt: objectTitle(image),
};
photos.push(imageData);
});
const showLightboxOnClick = useCallback(
(event, { index }) => {
handleImageOpen(index);
},
[handleImageOpen]
);
function columns(containerWidth: number) {
let preferredSize = 250;
let columnCount = containerWidth / preferredSize;
return Math.floor(columnCount);
}
return (
<div className="gallery">
<div className="flexbin">{thumbs}</div>
<Gallery
photos={photos}
onClick={showLightboxOnClick}
margin={uiConfig?.imageWallOptions?.margin!}
direction={uiConfig?.imageWallOptions?.direction!}
columns={columns}
/>
</div>
);
};

View file

@ -35,6 +35,13 @@ import {
ratingSystemIntlMap,
RatingSystemType,
} from "src/utils/rating";
import {
imageWallDirectionIntlMap,
ImageWallDirection,
defaultImageWallOptions,
defaultImageWallDirection,
defaultImageWallMargin,
} from "src/utils/imageWall";
import { defaultMaxOptionsShown } from "src/core/config";
const allMenuItems = [
@ -92,6 +99,24 @@ export const SettingsInterfacePanel: React.FC = () => {
});
}
function saveImageWallMargin(m: number) {
saveUI({
imageWallOptions: {
...(ui.imageWallOptions ?? defaultImageWallOptions),
margin: m,
},
});
}
function saveImageWallDirection(d: ImageWallDirection) {
saveUI({
imageWallOptions: {
...(ui.imageWallOptions ?? defaultImageWallOptions),
direction: d,
},
});
}
function saveRatingSystemType(t: RatingSystemType) {
saveUI({
ratingSystemOptions: {
@ -353,6 +378,31 @@ export const SettingsInterfacePanel: React.FC = () => {
/>
</SettingSection>
<SettingSection headingID="config.ui.image_wall.heading">
<NumberSetting
headingID="config.ui.image_wall.margin"
subHeadingID="dialogs.imagewall.margin_desc"
value={ui.imageWallOptions?.margin ?? defaultImageWallMargin}
onChange={(v) => saveImageWallMargin(v)}
/>
<SelectSetting
id="image_wall_direction"
headingID="config.ui.image_wall.direction"
subHeadingID="dialogs.imagewall.direction.description"
value={ui.imageWallOptions?.direction ?? defaultImageWallDirection}
onChange={(v) => saveImageWallDirection(v as ImageWallDirection)}
>
{Array.from(imageWallDirectionIntlMap.entries()).map((v) => (
<option key={v[0]} value={v[0]}>
{intl.formatMessage({
id: v[1],
})}
</option>
))}
</SelectSetting>
</SettingSection>
<SettingSection headingID="config.ui.image_lightbox.heading">
<NumberSetting
headingID="config.ui.slideshow_delay.heading"

View file

@ -1,5 +1,6 @@
import { IntlShape } from "react-intl";
import { ITypename } from "src/utils/data";
import { ImageWallOptions } from "src/utils/imageWall";
import { RatingSystemOptions } from "src/utils/rating";
import { FilterMode, SortDirectionEnum } from "./generated-graphql";
@ -51,6 +52,8 @@ export interface IUIConfig {
// upper limit of 1000
maxOptionsShown?: number;
imageWallOptions?: ImageWallOptions;
lastNoteSeen?: number;
}

View file

@ -11,6 +11,7 @@
* Added toggleable favorite button to Performer cards. ([#3369](https://github.com/stashapp/stash/pull/3369))
### 🎨 Improvements
* Improved Images wall view layout and added Interface settings to adjust the layout. ([#3511](https://github.com/stashapp/stash/pull/3511))
* Added collapsible divider to Gallery page. ([#3508](https://github.com/stashapp/stash/pull/3508))
* Overhauled and improved HLS streaming. ([#3274](https://github.com/stashapp/stash/pull/3274))

View file

@ -547,6 +547,12 @@
"image_lightbox": {
"heading": "Image Lightbox"
},
"image_wall": {
"direction": "Direction",
"heading": "Image Wall",
"margin": "Margin (pixels)"
},
"images": {
"heading": "Images",
"options": {
@ -735,6 +741,14 @@
"zoom": "Zoom"
}
},
"imagewall": {
"margin_desc": "Number of margin pixels around each entire image.",
"direction": {
"description": "Column or row based layout.",
"column": "Column",
"row": "Row"
}
},
"merge": {
"destination": "Destination",
"empty_results": "Destination field values will be unchanged.",

View file

@ -0,0 +1,23 @@
export enum ImageWallDirection {
Column = "column",
Row = "row",
}
export type ImageWallOptions = {
margin: number;
direction: ImageWallDirection;
};
export const defaultImageWallDirection: ImageWallDirection =
ImageWallDirection.Row;
export const defaultImageWallMargin = 3;
export const imageWallDirectionIntlMap = new Map<ImageWallDirection, string>([
[ImageWallDirection.Column, "dialogs.imagewall.direction.column"],
[ImageWallDirection.Row, "dialogs.imagewall.direction.row"],
]);
export const defaultImageWallOptions = {
margin: defaultImageWallMargin,
direction: defaultImageWallDirection,
};

View file

@ -6368,6 +6368,15 @@ prop-types@^15.5.10, prop-types@^15.6.0, prop-types@^15.6.2, prop-types@^15.7.2,
object-assign "^4.1.1"
react-is "^16.13.1"
prop-types@~15.7.2:
version "15.7.2"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5"
integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==
dependencies:
loose-envify "^1.4.0"
object-assign "^4.1.1"
react-is "^16.8.1"
property-expr@^2.0.5:
version "2.0.5"
resolved "https://registry.yarnpkg.com/property-expr/-/property-expr-2.0.5.tgz#278bdb15308ae16af3e3b9640024524f4dc02cb4"
@ -6485,7 +6494,7 @@ react-intl@^6.2.8:
intl-messageformat "10.3.0"
tslib "^2.4.0"
react-is@^16.13.1, react-is@^16.3.2, react-is@^16.6.0, react-is@^16.7.0:
react-is@^16.13.1, react-is@^16.3.2, react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.1:
version "16.13.1"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
@ -6509,6 +6518,14 @@ react-overlays@^5.1.2:
uncontrollable "^7.2.1"
warning "^4.0.3"
react-photo-gallery@^8.0.0:
version "8.0.0"
resolved "https://registry.yarnpkg.com/react-photo-gallery/-/react-photo-gallery-8.0.0.tgz#04ff9f902a2342660e63e6817b4f010488db02b8"
integrity sha512-Y9458yygEB9cIZAWlBWuenlR+ghin1RopmmU3Vice8BeJl0Se7hzfxGDq8W1armB/ic/kphGg+G1jq5fOEd0sw==
dependencies:
prop-types "~15.7.2"
resize-observer-polyfill "^1.5.0"
react-refresh@^0.14.0:
version "0.14.0"
resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.14.0.tgz#4e02825378a5f227079554d4284889354e5f553e"
@ -6786,7 +6803,7 @@ require-main-filename@^2.0.0:
resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b"
integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==
resize-observer-polyfill@^1.5.1:
resize-observer-polyfill@^1.5.0, resize-observer-polyfill@^1.5.1:
version "1.5.1"
resolved "https://registry.yarnpkg.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz#0e9020dd3d21024458d4ebd27e23e40269810464"
integrity sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==