mirror of
https://github.com/stashapp/stash.git
synced 2025-12-09 09:53:40 +01:00
Improved wall view for images (#3511)
* Proper masonry wall view for images * allow user to configure margin and direction
This commit is contained in:
parent
9ede271c05
commit
d4fb6b2acf
9 changed files with 199 additions and 38 deletions
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
||||
|
|
|
|||
|
|
@ -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.",
|
||||
|
|
|
|||
23
ui/v2.5/src/utils/imageWall.ts
Normal file
23
ui/v2.5/src/utils/imageWall.ts
Normal 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,
|
||||
};
|
||||
|
|
@ -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==
|
||||
|
|
|
|||
Loading…
Reference in a new issue