mirror of
https://github.com/stashapp/stash.git
synced 2025-12-17 12:24:38 +03:00
Revamp scene and marker wall views (#5816)
* Use gallery for scene wall * Move into separate file * Remove unnecessary class names * Apply configuration * Reuse styling * Add Scene Marker wall panel * Adjust target row height
This commit is contained in:
@@ -9,7 +9,7 @@ import { ListFilterModel } from "src/models/list-filter/filter";
|
|||||||
import { DisplayMode } from "src/models/list-filter/types";
|
import { DisplayMode } from "src/models/list-filter/types";
|
||||||
import { Tagger } from "../Tagger/scenes/SceneTagger";
|
import { Tagger } from "../Tagger/scenes/SceneTagger";
|
||||||
import { IPlaySceneOptions, SceneQueue } from "src/models/sceneQueue";
|
import { IPlaySceneOptions, SceneQueue } from "src/models/sceneQueue";
|
||||||
import { SceneWallPanel } from "../Wall/WallPanel";
|
import { SceneWallPanel } from "./SceneWallPanel";
|
||||||
import { SceneListTable } from "./SceneListTable";
|
import { SceneListTable } from "./SceneListTable";
|
||||||
import { EditScenesDialog } from "./EditScenesDialog";
|
import { EditScenesDialog } from "./EditScenesDialog";
|
||||||
import { DeleteScenesDialog } from "./DeleteScenesDialog";
|
import { DeleteScenesDialog } from "./DeleteScenesDialog";
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import NavUtils from "src/utils/navigation";
|
|||||||
import { ItemList, ItemListContext } from "../List/ItemList";
|
import { ItemList, ItemListContext } from "../List/ItemList";
|
||||||
import { ListFilterModel } from "src/models/list-filter/filter";
|
import { ListFilterModel } from "src/models/list-filter/filter";
|
||||||
import { DisplayMode } from "src/models/list-filter/types";
|
import { DisplayMode } from "src/models/list-filter/types";
|
||||||
import { MarkerWallPanel } from "../Wall/WallPanel";
|
import { MarkerWallPanel } from "./SceneMarkerWallPanel";
|
||||||
import { View } from "../List/views";
|
import { View } from "../List/views";
|
||||||
import { SceneMarkerCardsGrid } from "./SceneMarkerCardsGrid";
|
import { SceneMarkerCardsGrid } from "./SceneMarkerCardsGrid";
|
||||||
import { DeleteSceneMarkersDialog } from "./DeleteSceneMarkersDialog";
|
import { DeleteSceneMarkersDialog } from "./DeleteSceneMarkersDialog";
|
||||||
|
|||||||
234
ui/v2.5/src/components/Scenes/SceneMarkerWallPanel.tsx
Normal file
234
ui/v2.5/src/components/Scenes/SceneMarkerWallPanel.tsx
Normal file
@@ -0,0 +1,234 @@
|
|||||||
|
import React, {
|
||||||
|
useCallback,
|
||||||
|
useContext,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useState,
|
||||||
|
} from "react";
|
||||||
|
import * as GQL from "src/core/generated-graphql";
|
||||||
|
import Gallery, {
|
||||||
|
GalleryI,
|
||||||
|
PhotoProps,
|
||||||
|
RenderImageProps,
|
||||||
|
} from "react-photo-gallery";
|
||||||
|
import { ConfigurationContext } from "src/hooks/Config";
|
||||||
|
import { objectTitle } from "src/core/files";
|
||||||
|
import { Link, useHistory } from "react-router-dom";
|
||||||
|
import { TruncatedText } from "../Shared/TruncatedText";
|
||||||
|
import TextUtils from "src/utils/text";
|
||||||
|
import cx from "classnames";
|
||||||
|
import NavUtils from "src/utils/navigation";
|
||||||
|
import { markerTitle } from "src/core/markers";
|
||||||
|
|
||||||
|
function wallItemTitle(sceneMarker: GQL.SceneMarkerDataFragment) {
|
||||||
|
const newTitle = markerTitle(sceneMarker);
|
||||||
|
const seconds = TextUtils.formatTimestampRange(
|
||||||
|
sceneMarker.seconds,
|
||||||
|
sceneMarker.end_seconds ?? undefined
|
||||||
|
);
|
||||||
|
if (newTitle) {
|
||||||
|
return `${newTitle} - ${seconds}`;
|
||||||
|
} else {
|
||||||
|
return seconds;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IMarkerPhoto {
|
||||||
|
marker: GQL.SceneMarkerDataFragment;
|
||||||
|
link: string;
|
||||||
|
onError?: (photo: PhotoProps<IMarkerPhoto>) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MarkerWallItem: React.FC<RenderImageProps<IMarkerPhoto>> = (
|
||||||
|
props: RenderImageProps<IMarkerPhoto>
|
||||||
|
) => {
|
||||||
|
const { configuration } = useContext(ConfigurationContext);
|
||||||
|
const playSound = configuration?.interface.soundOnPreview ?? false;
|
||||||
|
const showTitle = configuration?.interface.wallShowTitle ?? false;
|
||||||
|
|
||||||
|
const [active, setActive] = useState(false);
|
||||||
|
|
||||||
|
type style = Record<string, string | number | undefined>;
|
||||||
|
var divStyle: style = {
|
||||||
|
margin: props.margin,
|
||||||
|
display: "block",
|
||||||
|
};
|
||||||
|
|
||||||
|
if (props.direction === "column") {
|
||||||
|
divStyle.position = "absolute";
|
||||||
|
divStyle.left = props.left;
|
||||||
|
divStyle.top = props.top;
|
||||||
|
}
|
||||||
|
|
||||||
|
var handleClick = function handleClick(event: React.MouseEvent) {
|
||||||
|
if (props.onClick) {
|
||||||
|
props.onClick(event, { index: props.index });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const video = props.photo.src.includes("stream");
|
||||||
|
const ImagePreview = video ? "video" : "img";
|
||||||
|
|
||||||
|
const { marker } = props.photo;
|
||||||
|
const title = wallItemTitle(marker);
|
||||||
|
const tagNames = marker.tags.map((p) => p.name);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cx("wall-item", { "show-title": showTitle })}
|
||||||
|
role="button"
|
||||||
|
style={{
|
||||||
|
...divStyle,
|
||||||
|
width: props.photo.width,
|
||||||
|
height: props.photo.height,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ImagePreview
|
||||||
|
loading="lazy"
|
||||||
|
loop={video}
|
||||||
|
muted={!video || !playSound || !active}
|
||||||
|
autoPlay={video}
|
||||||
|
key={props.photo.key}
|
||||||
|
src={props.photo.src}
|
||||||
|
width={props.photo.width}
|
||||||
|
height={props.photo.height}
|
||||||
|
alt={props.photo.alt}
|
||||||
|
onMouseEnter={() => setActive(true)}
|
||||||
|
onMouseLeave={() => setActive(false)}
|
||||||
|
onClick={handleClick}
|
||||||
|
onError={() => {
|
||||||
|
props.photo.onError?.(props.photo);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className="lineargradient">
|
||||||
|
<footer className="wall-item-footer">
|
||||||
|
<Link to={props.photo.link} onClick={(e) => e.stopPropagation()}>
|
||||||
|
{title && (
|
||||||
|
<TruncatedText
|
||||||
|
text={title}
|
||||||
|
lineCount={1}
|
||||||
|
className="wall-item-title"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<TruncatedText text={tagNames.join(", ")} />
|
||||||
|
</Link>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface IMarkerWallProps {
|
||||||
|
markers: GQL.SceneMarkerDataFragment[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// HACK: typescript doesn't allow Gallery to accept a parameter for some reason
|
||||||
|
const MarkerGallery = Gallery as unknown as GalleryI<IMarkerPhoto>;
|
||||||
|
|
||||||
|
function getFirstValidSrc(srcSet: string[], invalidSrcSet: string[]) {
|
||||||
|
if (!srcSet.length) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
srcSet.find((src) => !invalidSrcSet.includes(src)) ??
|
||||||
|
([...srcSet].pop() as string)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IFile {
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDimensions(file?: IFile) {
|
||||||
|
const defaults = { width: 1280, height: 720 };
|
||||||
|
|
||||||
|
if (!file) return defaults;
|
||||||
|
|
||||||
|
return {
|
||||||
|
width: file.width || defaults.width,
|
||||||
|
height: file.height || defaults.height,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultTargetRowHeight = 250;
|
||||||
|
|
||||||
|
const MarkerWall: React.FC<IMarkerWallProps> = ({ markers }) => {
|
||||||
|
const history = useHistory();
|
||||||
|
|
||||||
|
const margin = 3;
|
||||||
|
const direction = "row";
|
||||||
|
|
||||||
|
const [erroredImgs, setErroredImgs] = useState<string[]>([]);
|
||||||
|
|
||||||
|
const handleError = useCallback((photo: PhotoProps<IMarkerPhoto>) => {
|
||||||
|
setErroredImgs((prev) => [...prev, photo.src]);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setErroredImgs([]);
|
||||||
|
}, [markers]);
|
||||||
|
|
||||||
|
const photos: PhotoProps<IMarkerPhoto>[] = useMemo(() => {
|
||||||
|
return markers.map((m, index) => {
|
||||||
|
const { width = 1280, height = 720 } = getDimensions(m.scene.files[0]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
marker: m,
|
||||||
|
src: getFirstValidSrc([m.stream, m.preview, m.screenshot], erroredImgs),
|
||||||
|
link: NavUtils.makeSceneMarkerUrl(m),
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
tabIndex: index,
|
||||||
|
key: m.id,
|
||||||
|
loading: "lazy",
|
||||||
|
alt: objectTitle(m),
|
||||||
|
onError: handleError,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}, [markers, erroredImgs, handleError]);
|
||||||
|
|
||||||
|
const onClick = useCallback(
|
||||||
|
(event, { index }) => {
|
||||||
|
history.push(photos[index].link);
|
||||||
|
},
|
||||||
|
[history, photos]
|
||||||
|
);
|
||||||
|
|
||||||
|
function columns(containerWidth: number) {
|
||||||
|
let preferredSize = 300;
|
||||||
|
let columnCount = containerWidth / preferredSize;
|
||||||
|
return Math.round(columnCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderImage = useCallback((props: RenderImageProps<IMarkerPhoto>) => {
|
||||||
|
return <MarkerWallItem {...props} />;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="marker-wall">
|
||||||
|
{photos.length ? (
|
||||||
|
<MarkerGallery
|
||||||
|
photos={photos}
|
||||||
|
renderImage={renderImage}
|
||||||
|
onClick={onClick}
|
||||||
|
margin={margin}
|
||||||
|
direction={direction}
|
||||||
|
columns={columns}
|
||||||
|
targetRowHeight={defaultTargetRowHeight}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface IMarkerWallPanelProps {
|
||||||
|
markers: GQL.SceneMarkerDataFragment[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MarkerWallPanel: React.FC<IMarkerWallPanelProps> = ({
|
||||||
|
markers,
|
||||||
|
}) => {
|
||||||
|
return <MarkerWall markers={markers} />;
|
||||||
|
};
|
||||||
220
ui/v2.5/src/components/Scenes/SceneWallPanel.tsx
Normal file
220
ui/v2.5/src/components/Scenes/SceneWallPanel.tsx
Normal file
@@ -0,0 +1,220 @@
|
|||||||
|
import React, {
|
||||||
|
useCallback,
|
||||||
|
useContext,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useState,
|
||||||
|
} from "react";
|
||||||
|
import * as GQL from "src/core/generated-graphql";
|
||||||
|
import { SceneQueue } from "src/models/sceneQueue";
|
||||||
|
import Gallery, {
|
||||||
|
GalleryI,
|
||||||
|
PhotoProps,
|
||||||
|
RenderImageProps,
|
||||||
|
} from "react-photo-gallery";
|
||||||
|
import { ConfigurationContext } from "src/hooks/Config";
|
||||||
|
import { objectTitle } from "src/core/files";
|
||||||
|
import { Link, useHistory } from "react-router-dom";
|
||||||
|
import { TruncatedText } from "../Shared/TruncatedText";
|
||||||
|
import TextUtils from "src/utils/text";
|
||||||
|
import { useIntl } from "react-intl";
|
||||||
|
import cx from "classnames";
|
||||||
|
|
||||||
|
interface IScenePhoto {
|
||||||
|
scene: GQL.SlimSceneDataFragment;
|
||||||
|
link: string;
|
||||||
|
onError?: (photo: PhotoProps<IScenePhoto>) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SceneWallItem: React.FC<RenderImageProps<IScenePhoto>> = (
|
||||||
|
props: RenderImageProps<IScenePhoto>
|
||||||
|
) => {
|
||||||
|
const intl = useIntl();
|
||||||
|
|
||||||
|
const { configuration } = useContext(ConfigurationContext);
|
||||||
|
const playSound = configuration?.interface.soundOnPreview ?? false;
|
||||||
|
const showTitle = configuration?.interface.wallShowTitle ?? false;
|
||||||
|
|
||||||
|
const [active, setActive] = useState(false);
|
||||||
|
|
||||||
|
type style = Record<string, string | number | undefined>;
|
||||||
|
var divStyle: style = {
|
||||||
|
margin: props.margin,
|
||||||
|
display: "block",
|
||||||
|
};
|
||||||
|
|
||||||
|
if (props.direction === "column") {
|
||||||
|
divStyle.position = "absolute";
|
||||||
|
divStyle.left = props.left;
|
||||||
|
divStyle.top = props.top;
|
||||||
|
}
|
||||||
|
|
||||||
|
var handleClick = function handleClick(event: React.MouseEvent) {
|
||||||
|
if (props.onClick) {
|
||||||
|
props.onClick(event, { index: props.index });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const video = props.photo.src.includes("preview");
|
||||||
|
const ImagePreview = video ? "video" : "img";
|
||||||
|
|
||||||
|
const { scene } = props.photo;
|
||||||
|
const title = objectTitle(scene);
|
||||||
|
const performerNames = scene.performers.map((p) => p.name);
|
||||||
|
const performers =
|
||||||
|
performerNames.length >= 2
|
||||||
|
? [...performerNames.slice(0, -2), performerNames.slice(-2).join(" & ")]
|
||||||
|
: performerNames;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cx("wall-item", { "show-title": showTitle })}
|
||||||
|
role="button"
|
||||||
|
style={{
|
||||||
|
...divStyle,
|
||||||
|
width: props.photo.width,
|
||||||
|
height: props.photo.height,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ImagePreview
|
||||||
|
loading="lazy"
|
||||||
|
loop={video}
|
||||||
|
muted={!video || !playSound || !active}
|
||||||
|
autoPlay={video}
|
||||||
|
key={props.photo.key}
|
||||||
|
src={props.photo.src}
|
||||||
|
width={props.photo.width}
|
||||||
|
height={props.photo.height}
|
||||||
|
alt={props.photo.alt}
|
||||||
|
onMouseEnter={() => setActive(true)}
|
||||||
|
onMouseLeave={() => setActive(false)}
|
||||||
|
onClick={handleClick}
|
||||||
|
onError={() => {
|
||||||
|
props.photo.onError?.(props.photo);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className="lineargradient">
|
||||||
|
<footer className="wall-item-footer">
|
||||||
|
<Link to={props.photo.link} onClick={(e) => e.stopPropagation()}>
|
||||||
|
{title && (
|
||||||
|
<TruncatedText
|
||||||
|
text={title}
|
||||||
|
lineCount={1}
|
||||||
|
className="wall-item-title"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<TruncatedText text={performers.join(", ")} />
|
||||||
|
<div>{scene.date && TextUtils.formatDate(intl, scene.date)}</div>
|
||||||
|
</Link>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
function getDimensions(s: GQL.SlimSceneDataFragment) {
|
||||||
|
const defaults = { width: 1280, height: 720 };
|
||||||
|
|
||||||
|
if (!s.files.length) return defaults;
|
||||||
|
|
||||||
|
return {
|
||||||
|
width: s.files[0].width || defaults.width,
|
||||||
|
height: s.files[0].height || defaults.height,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ISceneWallProps {
|
||||||
|
scenes: GQL.SlimSceneDataFragment[];
|
||||||
|
sceneQueue?: SceneQueue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// HACK: typescript doesn't allow Gallery to accept a parameter for some reason
|
||||||
|
const SceneGallery = Gallery as unknown as GalleryI<IScenePhoto>;
|
||||||
|
|
||||||
|
const defaultTargetRowHeight = 250;
|
||||||
|
|
||||||
|
const SceneWall: React.FC<ISceneWallProps> = ({ scenes, sceneQueue }) => {
|
||||||
|
const history = useHistory();
|
||||||
|
|
||||||
|
const margin = 3;
|
||||||
|
const direction = "row";
|
||||||
|
|
||||||
|
const [erroredImgs, setErroredImgs] = useState<string[]>([]);
|
||||||
|
|
||||||
|
const handleError = useCallback((photo: PhotoProps<IScenePhoto>) => {
|
||||||
|
setErroredImgs((prev) => [...prev, photo.src]);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setErroredImgs([]);
|
||||||
|
}, [scenes]);
|
||||||
|
|
||||||
|
const photos: PhotoProps<IScenePhoto>[] = useMemo(() => {
|
||||||
|
return scenes.map((s, index) => {
|
||||||
|
const { width, height } = getDimensions(s);
|
||||||
|
|
||||||
|
return {
|
||||||
|
scene: s,
|
||||||
|
src:
|
||||||
|
s.paths.preview && !erroredImgs.includes(s.paths.preview)
|
||||||
|
? s.paths.preview!
|
||||||
|
: s.paths.screenshot!,
|
||||||
|
link: sceneQueue
|
||||||
|
? sceneQueue.makeLink(s.id, { sceneIndex: index })
|
||||||
|
: `/scenes/${s.id}`,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
tabIndex: index,
|
||||||
|
key: s.id,
|
||||||
|
loading: "lazy",
|
||||||
|
alt: objectTitle(s),
|
||||||
|
onError: handleError,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}, [scenes, sceneQueue, erroredImgs, handleError]);
|
||||||
|
|
||||||
|
const onClick = useCallback(
|
||||||
|
(event, { index }) => {
|
||||||
|
history.push(photos[index].link);
|
||||||
|
},
|
||||||
|
[history, photos]
|
||||||
|
);
|
||||||
|
|
||||||
|
function columns(containerWidth: number) {
|
||||||
|
let preferredSize = 300;
|
||||||
|
let columnCount = containerWidth / preferredSize;
|
||||||
|
return Math.round(columnCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderImage = useCallback((props: RenderImageProps<IScenePhoto>) => {
|
||||||
|
return <SceneWallItem {...props} />;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="scene-wall">
|
||||||
|
{photos.length ? (
|
||||||
|
<SceneGallery
|
||||||
|
photos={photos}
|
||||||
|
renderImage={renderImage}
|
||||||
|
onClick={onClick}
|
||||||
|
margin={margin}
|
||||||
|
direction={direction}
|
||||||
|
columns={columns}
|
||||||
|
targetRowHeight={defaultTargetRowHeight}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface ISceneWallPanelProps {
|
||||||
|
scenes: GQL.SlimSceneDataFragment[];
|
||||||
|
sceneQueue?: SceneQueue;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SceneWallPanel: React.FC<ISceneWallPanelProps> = ({
|
||||||
|
scenes,
|
||||||
|
sceneQueue,
|
||||||
|
}) => {
|
||||||
|
return <SceneWall scenes={scenes} sceneQueue={sceneQueue} />;
|
||||||
|
};
|
||||||
@@ -561,7 +561,7 @@ input[type="range"].blue-slider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.scene-markers-panel {
|
.scene-markers-panel {
|
||||||
.wall-item {
|
.wall .wall-item {
|
||||||
height: inherit;
|
height: inherit;
|
||||||
min-height: 14rem;
|
min-height: 14rem;
|
||||||
width: calc(100% - 2rem);
|
width: calc(100% - 2rem);
|
||||||
@@ -901,3 +901,60 @@ input[type="range"].blue-slider {
|
|||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.scene-wall,
|
||||||
|
.marker-wall {
|
||||||
|
.wall-item {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.lineargradient {
|
||||||
|
background-image: linear-gradient(rgba(0, 0, 0, 0), rgba(0, 0, 0, 0.3));
|
||||||
|
bottom: 0;
|
||||||
|
height: 100px;
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-title {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-footer {
|
||||||
|
bottom: 20px;
|
||||||
|
padding: 0 1rem;
|
||||||
|
position: absolute;
|
||||||
|
text-shadow: 1px 1px 3px black;
|
||||||
|
transition: 0s opacity;
|
||||||
|
width: 100%;
|
||||||
|
z-index: 2;
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
.wall-item-title {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover .wall-item-footer {
|
||||||
|
opacity: 1;
|
||||||
|
transition: 1s opacity;
|
||||||
|
transition-delay: 500ms;
|
||||||
|
|
||||||
|
a {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.show-title .wall-item-footer {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -62,18 +62,6 @@ const WallPanel = <T extends WallItemType>({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
interface IImageWallPanelProps {
|
|
||||||
images: GQL.SlimImageDataFragment[];
|
|
||||||
clickHandler?: (e: MouseEvent, item: GQL.SlimImageDataFragment) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ImageWallPanel: React.FC<IImageWallPanelProps> = ({
|
|
||||||
images,
|
|
||||||
clickHandler,
|
|
||||||
}) => {
|
|
||||||
return <WallPanel type="image" data={images} clickHandler={clickHandler} />;
|
|
||||||
};
|
|
||||||
|
|
||||||
interface IMarkerWallPanelProps {
|
interface IMarkerWallPanelProps {
|
||||||
markers: GQL.SceneMarkerDataFragment[];
|
markers: GQL.SceneMarkerDataFragment[];
|
||||||
clickHandler?: (e: MouseEvent, item: GQL.SceneMarkerDataFragment) => void;
|
clickHandler?: (e: MouseEvent, item: GQL.SceneMarkerDataFragment) => void;
|
||||||
@@ -87,24 +75,3 @@ export const MarkerWallPanel: React.FC<IMarkerWallPanelProps> = ({
|
|||||||
<WallPanel type="sceneMarker" data={markers} clickHandler={clickHandler} />
|
<WallPanel type="sceneMarker" data={markers} clickHandler={clickHandler} />
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
interface ISceneWallPanelProps {
|
|
||||||
scenes: GQL.SlimSceneDataFragment[];
|
|
||||||
sceneQueue?: SceneQueue;
|
|
||||||
clickHandler?: (e: MouseEvent, item: GQL.SlimSceneDataFragment) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const SceneWallPanel: React.FC<ISceneWallPanelProps> = ({
|
|
||||||
scenes,
|
|
||||||
sceneQueue,
|
|
||||||
clickHandler,
|
|
||||||
}) => {
|
|
||||||
return (
|
|
||||||
<WallPanel
|
|
||||||
type="scene"
|
|
||||||
data={scenes}
|
|
||||||
sceneQueue={sceneQueue}
|
|
||||||
clickHandler={clickHandler}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
max-width: 2250px;
|
max-width: 2250px;
|
||||||
|
|
||||||
&-item {
|
.wall-item {
|
||||||
height: 11.25vw;
|
height: 11.25vw;
|
||||||
line-height: 0;
|
line-height: 0;
|
||||||
max-height: 253px;
|
max-height: 253px;
|
||||||
|
|||||||
Reference in New Issue
Block a user