mirror of
https://github.com/stashapp/stash.git
synced 2025-12-06 16:34:02 +01:00
Add wall zoom functionality (#6011)
* Show zoom slider when wall view active * Add zoom functionality to scene wall * Add zoom functionality to image wall * Add zoom functionality to gallery wall * Add zoom functionality for marker wall
This commit is contained in:
parent
b1883f3df5
commit
cc97e96eaa
8 changed files with 148 additions and 37 deletions
|
|
@ -149,7 +149,7 @@ export const GalleryList: React.FC<IGalleryList> = ({
|
||||||
if (filter.displayMode === DisplayMode.Wall) {
|
if (filter.displayMode === DisplayMode.Wall) {
|
||||||
return (
|
return (
|
||||||
<div className="row">
|
<div className="row">
|
||||||
<div className="GalleryWall">
|
<div className={`GalleryWall zoom-${filter.zoomIndex}`}>
|
||||||
{result.data.findGalleries.galleries.map((gallery) => (
|
{result.data.findGalleries.galleries.map((gallery) => (
|
||||||
<GalleryWallCard key={gallery.id} gallery={gallery} />
|
<GalleryWallCard key={gallery.id} gallery={gallery} />
|
||||||
))}
|
))}
|
||||||
|
|
|
||||||
|
|
@ -206,7 +206,7 @@ $galleryTabWidth: 450px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.GalleryWall {
|
div.GalleryWall {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
|
|
@ -249,28 +249,6 @@ $galleryTabWidth: 450px;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@mixin galleryWidth($width) {
|
|
||||||
height: math.div($width, 3) * 2;
|
|
||||||
|
|
||||||
&-landscape {
|
|
||||||
width: $width;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-portrait {
|
|
||||||
width: math.div($width, 2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 576px) {
|
|
||||||
@include galleryWidth(96vw);
|
|
||||||
}
|
|
||||||
@media (min-width: 768px) {
|
|
||||||
@include galleryWidth(48vw);
|
|
||||||
}
|
|
||||||
@media (min-width: 1200px) {
|
|
||||||
@include galleryWidth(32vw);
|
|
||||||
}
|
|
||||||
|
|
||||||
&-img {
|
&-img {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
|
|
@ -355,6 +333,62 @@ $galleryTabWidth: 450px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
div.GalleryWall {
|
||||||
|
@mixin galleryWidth($width) {
|
||||||
|
height: math.div($width, 3) * 2;
|
||||||
|
|
||||||
|
&-landscape {
|
||||||
|
width: $width;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-portrait {
|
||||||
|
width: math.div($width, 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.GalleryWallCard {
|
||||||
|
@media (min-width: 576px) {
|
||||||
|
@include galleryWidth(96vw);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.zoom-0 .GalleryWallCard {
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
@include galleryWidth(16vw);
|
||||||
|
}
|
||||||
|
@media (min-width: 1200px) {
|
||||||
|
@include galleryWidth(10vw);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.zoom-1 .GalleryWallCard {
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
@include galleryWidth(24vw);
|
||||||
|
}
|
||||||
|
@media (min-width: 1200px) {
|
||||||
|
@include galleryWidth(16vw);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.zoom-2 .GalleryWallCard {
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
@include galleryWidth(32vw);
|
||||||
|
}
|
||||||
|
@media (min-width: 1200px) {
|
||||||
|
@include galleryWidth(24vw);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.zoom-3 .GalleryWallCard {
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
@include galleryWidth(48vw);
|
||||||
|
}
|
||||||
|
@media (min-width: 1200px) {
|
||||||
|
@include galleryWidth(32vw);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.gallery-file-card.card {
|
.gallery-file-card.card {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
|
|
||||||
|
|
@ -35,9 +35,22 @@ interface IImageWallProps {
|
||||||
currentPage: number;
|
currentPage: number;
|
||||||
pageCount: number;
|
pageCount: number;
|
||||||
handleImageOpen: (index: number) => void;
|
handleImageOpen: (index: number) => void;
|
||||||
|
zoomIndex: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ImageWall: React.FC<IImageWallProps> = ({ images, handleImageOpen }) => {
|
const zoomWidths = [280, 340, 480, 640];
|
||||||
|
const breakpointZoomHeights = [
|
||||||
|
{ minWidth: 576, heights: [100, 120, 240, 360] },
|
||||||
|
{ minWidth: 768, heights: [120, 160, 240, 480] },
|
||||||
|
{ minWidth: 1200, heights: [120, 160, 240, 300] },
|
||||||
|
{ minWidth: 1400, heights: [160, 240, 300, 480] },
|
||||||
|
];
|
||||||
|
|
||||||
|
const ImageWall: React.FC<IImageWallProps> = ({
|
||||||
|
images,
|
||||||
|
zoomIndex,
|
||||||
|
handleImageOpen,
|
||||||
|
}) => {
|
||||||
const { configuration } = useContext(ConfigurationContext);
|
const { configuration } = useContext(ConfigurationContext);
|
||||||
const uiConfig = configuration?.ui;
|
const uiConfig = configuration?.ui;
|
||||||
|
|
||||||
|
|
@ -76,11 +89,21 @@ const ImageWall: React.FC<IImageWallProps> = ({ images, handleImageOpen }) => {
|
||||||
);
|
);
|
||||||
|
|
||||||
function columns(containerWidth: number) {
|
function columns(containerWidth: number) {
|
||||||
let preferredSize = 300;
|
let preferredSize = zoomWidths[zoomIndex];
|
||||||
let columnCount = containerWidth / preferredSize;
|
let columnCount = containerWidth / preferredSize;
|
||||||
return Math.round(columnCount);
|
return Math.round(columnCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function targetRowHeight(containerWidth: number) {
|
||||||
|
let zoomHeight = 280;
|
||||||
|
breakpointZoomHeights.forEach((e) => {
|
||||||
|
if (containerWidth >= e.minWidth) {
|
||||||
|
zoomHeight = e.heights[zoomIndex];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return zoomHeight;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="gallery">
|
<div className="gallery">
|
||||||
{photos.length ? (
|
{photos.length ? (
|
||||||
|
|
@ -91,6 +114,7 @@ const ImageWall: React.FC<IImageWallProps> = ({ images, handleImageOpen }) => {
|
||||||
margin={uiConfig?.imageWallOptions?.margin!}
|
margin={uiConfig?.imageWallOptions?.margin!}
|
||||||
direction={uiConfig?.imageWallOptions?.direction!}
|
direction={uiConfig?.imageWallOptions?.direction!}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
|
targetRowHeight={targetRowHeight}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -211,6 +235,7 @@ const ImageListImages: React.FC<IImageListImages> = ({
|
||||||
currentPage={filter.currentPage}
|
currentPage={filter.currentPage}
|
||||||
pageCount={pageCount}
|
pageCount={pageCount}
|
||||||
handleImageOpen={handleImageOpen}
|
handleImageOpen={handleImageOpen}
|
||||||
|
zoomIndex={filter.zoomIndex}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -130,7 +130,8 @@ export const ListViewOptions: React.FC<IListViewOptionsProps> = ({
|
||||||
<div className="display-mode-menu">
|
<div className="display-mode-menu">
|
||||||
{onSetZoom &&
|
{onSetZoom &&
|
||||||
zoomIndex !== undefined &&
|
zoomIndex !== undefined &&
|
||||||
displayMode === DisplayMode.Grid ? (
|
(displayMode === DisplayMode.Grid ||
|
||||||
|
displayMode === DisplayMode.Wall) ? (
|
||||||
<div className="zoom-slider-container">
|
<div className="zoom-slider-container">
|
||||||
<ZoomSelect
|
<ZoomSelect
|
||||||
minZoom={minZoom}
|
minZoom={minZoom}
|
||||||
|
|
|
||||||
|
|
@ -219,7 +219,13 @@ const SceneList: React.FC<{
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (filter.displayMode === DisplayMode.Wall) {
|
if (filter.displayMode === DisplayMode.Wall) {
|
||||||
return <SceneWallPanel scenes={scenes} sceneQueue={queue} />;
|
return (
|
||||||
|
<SceneWallPanel
|
||||||
|
scenes={scenes}
|
||||||
|
sceneQueue={queue}
|
||||||
|
zoomIndex={filter.zoomIndex}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (filter.displayMode === DisplayMode.Tagger) {
|
if (filter.displayMode === DisplayMode.Tagger) {
|
||||||
return <Tagger scenes={scenes} queue={queue} />;
|
return <Tagger scenes={scenes} queue={queue} />;
|
||||||
|
|
|
||||||
|
|
@ -95,7 +95,10 @@ export const SceneMarkerList: React.FC<ISceneMarkerList> = ({
|
||||||
|
|
||||||
if (filter.displayMode === DisplayMode.Wall) {
|
if (filter.displayMode === DisplayMode.Wall) {
|
||||||
return (
|
return (
|
||||||
<MarkerWallPanel markers={result.data.findSceneMarkers.scene_markers} />
|
<MarkerWallPanel
|
||||||
|
markers={result.data.findSceneMarkers.scene_markers}
|
||||||
|
zoomIndex={filter.zoomIndex}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -120,6 +120,7 @@ export const MarkerWallItem: React.FC<RenderImageProps<IMarkerPhoto>> = (
|
||||||
|
|
||||||
interface IMarkerWallProps {
|
interface IMarkerWallProps {
|
||||||
markers: GQL.SceneMarkerDataFragment[];
|
markers: GQL.SceneMarkerDataFragment[];
|
||||||
|
zoomIndex: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
// HACK: typescript doesn't allow Gallery to accept a parameter for some reason
|
// HACK: typescript doesn't allow Gallery to accept a parameter for some reason
|
||||||
|
|
@ -152,9 +153,14 @@ function getDimensions(file?: IFile) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultTargetRowHeight = 250;
|
const breakpointZoomHeights = [
|
||||||
|
{ minWidth: 576, heights: [100, 120, 240, 360] },
|
||||||
|
{ minWidth: 768, heights: [120, 160, 240, 480] },
|
||||||
|
{ minWidth: 1200, heights: [120, 160, 240, 300] },
|
||||||
|
{ minWidth: 1400, heights: [160, 240, 300, 480] },
|
||||||
|
];
|
||||||
|
|
||||||
const MarkerWall: React.FC<IMarkerWallProps> = ({ markers }) => {
|
const MarkerWall: React.FC<IMarkerWallProps> = ({ markers, zoomIndex }) => {
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
|
|
||||||
const margin = 3;
|
const margin = 3;
|
||||||
|
|
@ -202,6 +208,16 @@ const MarkerWall: React.FC<IMarkerWallProps> = ({ markers }) => {
|
||||||
return Math.round(columnCount);
|
return Math.round(columnCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function targetRowHeight(containerWidth: number) {
|
||||||
|
let zoomHeight = 280;
|
||||||
|
breakpointZoomHeights.forEach((e) => {
|
||||||
|
if (containerWidth >= e.minWidth) {
|
||||||
|
zoomHeight = e.heights[zoomIndex];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return zoomHeight;
|
||||||
|
}
|
||||||
|
|
||||||
const renderImage = useCallback((props: RenderImageProps<IMarkerPhoto>) => {
|
const renderImage = useCallback((props: RenderImageProps<IMarkerPhoto>) => {
|
||||||
return <MarkerWallItem {...props} />;
|
return <MarkerWallItem {...props} />;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
@ -216,7 +232,7 @@ const MarkerWall: React.FC<IMarkerWallProps> = ({ markers }) => {
|
||||||
margin={margin}
|
margin={margin}
|
||||||
direction={direction}
|
direction={direction}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
targetRowHeight={defaultTargetRowHeight}
|
targetRowHeight={targetRowHeight}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -225,10 +241,12 @@ const MarkerWall: React.FC<IMarkerWallProps> = ({ markers }) => {
|
||||||
|
|
||||||
interface IMarkerWallPanelProps {
|
interface IMarkerWallPanelProps {
|
||||||
markers: GQL.SceneMarkerDataFragment[];
|
markers: GQL.SceneMarkerDataFragment[];
|
||||||
|
zoomIndex: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MarkerWallPanel: React.FC<IMarkerWallPanelProps> = ({
|
export const MarkerWallPanel: React.FC<IMarkerWallPanelProps> = ({
|
||||||
markers,
|
markers,
|
||||||
|
zoomIndex,
|
||||||
}) => {
|
}) => {
|
||||||
return <MarkerWall markers={markers} />;
|
return <MarkerWall markers={markers} zoomIndex={zoomIndex} />;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -126,14 +126,24 @@ function getDimensions(s: GQL.SlimSceneDataFragment) {
|
||||||
interface ISceneWallProps {
|
interface ISceneWallProps {
|
||||||
scenes: GQL.SlimSceneDataFragment[];
|
scenes: GQL.SlimSceneDataFragment[];
|
||||||
sceneQueue?: SceneQueue;
|
sceneQueue?: SceneQueue;
|
||||||
|
zoomIndex: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
// HACK: typescript doesn't allow Gallery to accept a parameter for some reason
|
// HACK: typescript doesn't allow Gallery to accept a parameter for some reason
|
||||||
const SceneGallery = Gallery as unknown as GalleryI<IScenePhoto>;
|
const SceneGallery = Gallery as unknown as GalleryI<IScenePhoto>;
|
||||||
|
|
||||||
const defaultTargetRowHeight = 250;
|
const breakpointZoomHeights = [
|
||||||
|
{ minWidth: 576, heights: [100, 120, 240, 360] },
|
||||||
|
{ minWidth: 768, heights: [120, 160, 240, 480] },
|
||||||
|
{ minWidth: 1200, heights: [120, 160, 240, 300] },
|
||||||
|
{ minWidth: 1400, heights: [160, 240, 300, 480] },
|
||||||
|
];
|
||||||
|
|
||||||
const SceneWall: React.FC<ISceneWallProps> = ({ scenes, sceneQueue }) => {
|
const SceneWall: React.FC<ISceneWallProps> = ({
|
||||||
|
scenes,
|
||||||
|
sceneQueue,
|
||||||
|
zoomIndex,
|
||||||
|
}) => {
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
|
|
||||||
const margin = 3;
|
const margin = 3;
|
||||||
|
|
@ -186,6 +196,16 @@ const SceneWall: React.FC<ISceneWallProps> = ({ scenes, sceneQueue }) => {
|
||||||
return Math.round(columnCount);
|
return Math.round(columnCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function targetRowHeight(containerWidth: number) {
|
||||||
|
let zoomHeight = 280;
|
||||||
|
breakpointZoomHeights.forEach((e) => {
|
||||||
|
if (containerWidth >= e.minWidth) {
|
||||||
|
zoomHeight = e.heights[zoomIndex];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return zoomHeight;
|
||||||
|
}
|
||||||
|
|
||||||
const renderImage = useCallback((props: RenderImageProps<IScenePhoto>) => {
|
const renderImage = useCallback((props: RenderImageProps<IScenePhoto>) => {
|
||||||
return <SceneWallItem {...props} />;
|
return <SceneWallItem {...props} />;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
@ -200,7 +220,7 @@ const SceneWall: React.FC<ISceneWallProps> = ({ scenes, sceneQueue }) => {
|
||||||
margin={margin}
|
margin={margin}
|
||||||
direction={direction}
|
direction={direction}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
targetRowHeight={defaultTargetRowHeight}
|
targetRowHeight={targetRowHeight}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -210,11 +230,15 @@ const SceneWall: React.FC<ISceneWallProps> = ({ scenes, sceneQueue }) => {
|
||||||
interface ISceneWallPanelProps {
|
interface ISceneWallPanelProps {
|
||||||
scenes: GQL.SlimSceneDataFragment[];
|
scenes: GQL.SlimSceneDataFragment[];
|
||||||
sceneQueue?: SceneQueue;
|
sceneQueue?: SceneQueue;
|
||||||
|
zoomIndex: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SceneWallPanel: React.FC<ISceneWallPanelProps> = ({
|
export const SceneWallPanel: React.FC<ISceneWallPanelProps> = ({
|
||||||
scenes,
|
scenes,
|
||||||
sceneQueue,
|
sceneQueue,
|
||||||
|
zoomIndex,
|
||||||
}) => {
|
}) => {
|
||||||
return <SceneWall scenes={scenes} sceneQueue={sceneQueue} />;
|
return (
|
||||||
|
<SceneWall scenes={scenes} sceneQueue={sceneQueue} zoomIndex={zoomIndex} />
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue