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
9 changed files with 199 additions and 38 deletions

View File

@@ -52,6 +52,7 @@
"react-dom": "^17.0.2", "react-dom": "^17.0.2",
"react-helmet": "^6.1.0", "react-helmet": "^6.1.0",
"react-intl": "^6.2.8", "react-intl": "^6.2.8",
"react-photo-gallery": "^8.0.0",
"react-remark": "^2.1.0", "react-remark": "^2.1.0",
"react-router-bootstrap": "^0.25.0", "react-router-bootstrap": "^0.25.0",
"react-router-dom": "^5.3.4", "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 { useLightbox } from "src/hooks/Lightbox/hooks";
import { LoadingIndicator } from "src/components/Shared/LoadingIndicator"; import { LoadingIndicator } from "src/components/Shared/LoadingIndicator";
import Gallery from "react-photo-gallery";
import "flexbin/flexbin.css"; import "flexbin/flexbin.css";
import { import {
CriterionModifier, CriterionModifier,
@@ -44,29 +45,42 @@ export const GalleryViewer: React.FC<IProps> = ({ galleryId }) => {
}, [images]); }, [images]);
const showLightbox = useLightbox(lightboxState); const showLightbox = useLightbox(lightboxState);
const showLightboxOnClick = useCallback(
(event, { index }) => {
showLightbox(index);
},
[showLightbox]
);
if (loading) return <LoadingIndicator />; if (loading) return <LoadingIndicator />;
const thumbs = images.map((file, index) => ( let photos: {
<div src: string;
role="link" srcSet?: string | string[] | undefined;
tabIndex={index} sizes?: string | string[] | undefined;
key={file.id ?? index} width: number;
onClick={() => showLightbox(index)} height: number;
onKeyPress={() => showLightbox(index)} alt?: string | undefined;
> key?: string | undefined;
<img }[] = [];
src={file.paths.thumbnail ?? ""}
loading="lazy" images.forEach((image, index) => {
className="gallery-image" let imageData = {
alt={file.title ?? index.toString()} src: image.paths.thumbnail!,
/> width: image.files[0].width,
</div> 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 ( return (
<div className="gallery"> <div className="gallery">
<div className="flexbin">{thumbs}</div> <Gallery photos={photos} onClick={showLightboxOnClick} margin={2.5} />
</div> </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 { FormattedNumber, useIntl } from "react-intl";
import cloneDeep from "lodash-es/cloneDeep"; import cloneDeep from "lodash-es/cloneDeep";
import { useHistory } from "react-router-dom"; import { useHistory } from "react-router-dom";
@@ -19,9 +25,12 @@ import { ImageCard } from "./ImageCard";
import { EditImagesDialog } from "./EditImagesDialog"; import { EditImagesDialog } from "./EditImagesDialog";
import { DeleteImagesDialog } from "./DeleteImagesDialog"; import { DeleteImagesDialog } from "./DeleteImagesDialog";
import "flexbin/flexbin.css"; import "flexbin/flexbin.css";
import Gallery from "react-photo-gallery";
import { ExportDialog } from "../Shared/ExportDialog"; import { ExportDialog } from "../Shared/ExportDialog";
import { objectTitle } from "src/core/files"; import { objectTitle } from "src/core/files";
import TextUtils from "src/utils/text"; import TextUtils from "src/utils/text";
import { ConfigurationContext } from "src/hooks/Config";
import { IUIConfig } from "src/core/config";
interface IImageWallProps { interface IImageWallProps {
images: GQL.SlimImageDataFragment[]; images: GQL.SlimImageDataFragment[];
@@ -32,26 +41,55 @@ interface IImageWallProps {
} }
const ImageWall: React.FC<IImageWallProps> = ({ images, handleImageOpen }) => { const ImageWall: React.FC<IImageWallProps> = ({ images, handleImageOpen }) => {
const thumbs = images.map((image, index) => ( const { configuration } = useContext(ConfigurationContext);
<div const uiConfig = configuration?.ui as IUIConfig | undefined;
role="link"
tabIndex={index} let photos: {
key={image.id} src: string;
onClick={() => handleImageOpen(index)} srcSet?: string | string[] | undefined;
onKeyPress={() => handleImageOpen(index)} sizes?: string | string[] | undefined;
> width: number;
<img height: number;
src={image.paths.thumbnail ?? ""} alt?: string | undefined;
loading="lazy" key?: string | undefined;
className="gallery-image" }[] = [];
alt={objectTitle(image)}
/> images.forEach((image, index) => {
</div> 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 ( return (
<div className="gallery"> <div className="gallery">
<div className="flexbin">{thumbs}</div> <Gallery
photos={photos}
onClick={showLightboxOnClick}
margin={uiConfig?.imageWallOptions?.margin!}
direction={uiConfig?.imageWallOptions?.direction!}
columns={columns}
/>
</div> </div>
); );
}; };

View File

@@ -35,6 +35,13 @@ import {
ratingSystemIntlMap, ratingSystemIntlMap,
RatingSystemType, RatingSystemType,
} from "src/utils/rating"; } from "src/utils/rating";
import {
imageWallDirectionIntlMap,
ImageWallDirection,
defaultImageWallOptions,
defaultImageWallDirection,
defaultImageWallMargin,
} from "src/utils/imageWall";
import { defaultMaxOptionsShown } from "src/core/config"; import { defaultMaxOptionsShown } from "src/core/config";
const allMenuItems = [ 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) { function saveRatingSystemType(t: RatingSystemType) {
saveUI({ saveUI({
ratingSystemOptions: { ratingSystemOptions: {
@@ -353,6 +378,31 @@ export const SettingsInterfacePanel: React.FC = () => {
/> />
</SettingSection> </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"> <SettingSection headingID="config.ui.image_lightbox.heading">
<NumberSetting <NumberSetting
headingID="config.ui.slideshow_delay.heading" headingID="config.ui.slideshow_delay.heading"

View File

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

View File

@@ -11,6 +11,7 @@
* Added toggleable favorite button to Performer cards. ([#3369](https://github.com/stashapp/stash/pull/3369)) * Added toggleable favorite button to Performer cards. ([#3369](https://github.com/stashapp/stash/pull/3369))
### 🎨 Improvements ### 🎨 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)) * 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)) * Overhauled and improved HLS streaming. ([#3274](https://github.com/stashapp/stash/pull/3274))

View File

@@ -547,6 +547,12 @@
"image_lightbox": { "image_lightbox": {
"heading": "Image Lightbox" "heading": "Image Lightbox"
}, },
"image_wall": {
"direction": "Direction",
"heading": "Image Wall",
"margin": "Margin (pixels)"
},
"images": { "images": {
"heading": "Images", "heading": "Images",
"options": { "options": {
@@ -735,6 +741,14 @@
"zoom": "Zoom" "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": { "merge": {
"destination": "Destination", "destination": "Destination",
"empty_results": "Destination field values will be unchanged.", "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" object-assign "^4.1.1"
react-is "^16.13.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: property-expr@^2.0.5:
version "2.0.5" version "2.0.5"
resolved "https://registry.yarnpkg.com/property-expr/-/property-expr-2.0.5.tgz#278bdb15308ae16af3e3b9640024524f4dc02cb4" 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" intl-messageformat "10.3.0"
tslib "^2.4.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" version "16.13.1"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
@@ -6509,6 +6518,14 @@ react-overlays@^5.1.2:
uncontrollable "^7.2.1" uncontrollable "^7.2.1"
warning "^4.0.3" 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: react-refresh@^0.14.0:
version "0.14.0" version "0.14.0"
resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.14.0.tgz#4e02825378a5f227079554d4284889354e5f553e" 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" resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b"
integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg== 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" version "1.5.1"
resolved "https://registry.yarnpkg.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz#0e9020dd3d21024458d4ebd27e23e40269810464" resolved "https://registry.yarnpkg.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz#0e9020dd3d21024458d4ebd27e23e40269810464"
integrity sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg== integrity sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==