WallPanel refactor (#3686)

This commit is contained in:
DingDongSoLong4
2023-05-03 09:05:30 +02:00
committed by GitHub
parent 899d1b9395
commit 79bc5c914f
5 changed files with 209 additions and 156 deletions

View File

@@ -3,7 +3,7 @@ import { Button } from "react-bootstrap";
import { FormattedMessage } from "react-intl"; import { FormattedMessage } from "react-intl";
import Mousetrap from "mousetrap"; import Mousetrap from "mousetrap";
import * as GQL from "src/core/generated-graphql"; import * as GQL from "src/core/generated-graphql";
import { WallPanel } from "src/components/Wall/WallPanel"; import { MarkerWallPanel } from "src/components/Wall/WallPanel";
import { PrimaryTags } from "./PrimaryTags"; import { PrimaryTags } from "./PrimaryTags";
import { SceneMarkerForm } from "./SceneMarkerForm"; import { SceneMarkerForm } from "./SceneMarkerForm";
@@ -77,11 +77,12 @@ export const SceneMarkersPanel: React.FC<ISceneMarkersPanelProps> = (
onEdit={onOpenEditor} onEdit={onOpenEditor}
/> />
</div> </div>
<WallPanel <MarkerWallPanel
sceneMarkers={sceneMarkers} markers={sceneMarkers}
clickHandler={(marker) => { clickHandler={(e, marker) => {
e.preventDefault();
window.scrollTo(0, 0); window.scrollTo(0, 0);
onClickMarker(marker as GQL.SceneMarkerDataFragment); onClickMarker(marker);
}} }}
/> />
</div> </div>

View File

@@ -14,7 +14,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 { WallPanel } from "../Wall/WallPanel"; import { SceneWallPanel } from "../Wall/WallPanel";
import { SceneListTable } from "./SceneListTable"; import { SceneListTable } from "./SceneListTable";
import { EditScenesDialog } from "./EditScenesDialog"; import { EditScenesDialog } from "./EditScenesDialog";
import { DeleteScenesDialog } from "./DeleteScenesDialog"; import { DeleteScenesDialog } from "./DeleteScenesDialog";
@@ -314,7 +314,7 @@ export const SceneList: React.FC<ISceneList> = ({
} }
if (filter.displayMode === DisplayMode.Wall) { if (filter.displayMode === DisplayMode.Wall) {
return ( return (
<WallPanel <SceneWallPanel
scenes={result.data.findScenes.scenes} scenes={result.data.findScenes.scenes}
sceneQueue={queue} sceneQueue={queue}
/> />

View File

@@ -12,7 +12,7 @@ import NavUtils from "src/utils/navigation";
import { makeItemList, PersistanceLevel } from "../List/ItemList"; import { makeItemList, PersistanceLevel } 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 { WallPanel } from "../Wall/WallPanel"; import { MarkerWallPanel } from "../Wall/WallPanel";
const SceneMarkerItemList = makeItemList({ const SceneMarkerItemList = makeItemList({
filterMode: GQL.FilterMode.SceneMarkers, filterMode: GQL.FilterMode.SceneMarkers,
@@ -88,7 +88,7 @@ export const SceneMarkerList: React.FC<ISceneMarkerList> = ({
if (filter.displayMode === DisplayMode.Wall) { if (filter.displayMode === DisplayMode.Wall) {
return ( return (
<WallPanel sceneMarkers={result.data.findSceneMarkers.scene_markers} /> <MarkerWallPanel markers={result.data.findSceneMarkers.scene_markers} />
); );
} }
} }

View File

@@ -1,4 +1,11 @@
import React, { useRef, useState, useEffect, useMemo } from "react"; import React, {
useRef,
useState,
useEffect,
useCallback,
MouseEvent,
useMemo,
} from "react";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import * as GQL from "src/core/generated-graphql"; import * as GQL from "src/core/generated-graphql";
import TextUtils from "src/utils/text"; import TextUtils from "src/utils/text";
@@ -9,18 +16,20 @@ import { ConfigurationContext } from "src/hooks/Config";
import { markerTitle } from "src/core/markers"; import { markerTitle } from "src/core/markers";
import { objectTitle } from "src/core/files"; import { objectTitle } from "src/core/files";
interface IWallItemProps { export type WallItemType = keyof WallItemData;
export type WallItemData = {
scene: GQL.SlimSceneDataFragment;
sceneMarker: GQL.SceneMarkerDataFragment;
image: GQL.SlimImageDataFragment;
};
interface IWallItemProps<T extends WallItemType> {
type: T;
index?: number; index?: number;
scene?: GQL.SlimSceneDataFragment; data: WallItemData[T];
sceneQueue?: SceneQueue; sceneQueue?: SceneQueue;
sceneMarker?: GQL.SceneMarkerDataFragment; clickHandler?: (e: MouseEvent, item: WallItemData[T]) => void;
image?: GQL.SlimImageDataFragment;
clickHandler?: (
item:
| GQL.SlimSceneDataFragment
| GQL.SceneMarkerDataFragment
| GQL.SlimImageDataFragment
) => void;
className: string; className: string;
} }
@@ -31,26 +40,29 @@ interface IPreviews {
} }
const Preview: React.FC<{ const Preview: React.FC<{
previews?: IPreviews; previews: IPreviews;
config?: GQL.ConfigDataFragment; config?: GQL.ConfigDataFragment;
active: boolean; active: boolean;
}> = ({ previews, config, active }) => { }> = ({ previews, config, active }) => {
const videoElement = useRef() as React.MutableRefObject<HTMLVideoElement>; const videoEl = useRef<HTMLVideoElement>(null);
const [isMissing, setIsMissing] = useState(false); const [isMissing, setIsMissing] = useState(false);
const previewType = config?.interface?.wallPlayback; const previewType = config?.interface?.wallPlayback;
const soundOnPreview = config?.interface?.soundOnPreview ?? false; const soundOnPreview = config?.interface?.soundOnPreview ?? false;
useEffect(() => { useEffect(() => {
if (!videoElement.current) return; const video = videoEl.current;
videoElement.current.muted = !(soundOnPreview && active); if (!video) return;
if (previewType !== "video") {
if (active) videoElement.current.play();
else videoElement.current.pause();
}
}, [videoElement, previewType, soundOnPreview, active]);
if (!previews) return <div />; video.muted = !(soundOnPreview && active);
if (previewType !== "video") {
if (active) {
video.play();
} else {
video.pause();
}
}
}, [previewType, soundOnPreview, active]);
const image = ( const image = (
<img <img
@@ -77,7 +89,7 @@ const Preview: React.FC<{
// Error code 4 indicates media not found or unsupported // Error code 4 indicates media not found or unsupported
setIsMissing(error.currentTarget.error?.code === 4); setIsMissing(error.currentTarget.error?.code === 4);
}} }}
ref={videoElement} ref={videoEl}
/> />
); );
@@ -105,108 +117,123 @@ const Preview: React.FC<{
); );
}; };
export const WallItem: React.FC<IWallItemProps> = (props: IWallItemProps) => { export const WallItem = <T extends WallItemType>({
type,
index,
data,
sceneQueue,
clickHandler,
className,
}: IWallItemProps<T>) => {
const [active, setActive] = useState(false); const [active, setActive] = useState(false);
const wallItem = useRef() as React.MutableRefObject<HTMLDivElement>; const itemEl = useRef<HTMLDivElement>(null);
const { configuration: config } = React.useContext(ConfigurationContext); const { configuration: config } = React.useContext(ConfigurationContext);
const showTextContainer = config?.interface.wallShowTitle ?? true; const showTextContainer = config?.interface.wallShowTitle ?? true;
const previews = props.sceneMarker const previews = useMemo(() => {
? { switch (type) {
video: props.sceneMarker.stream, case "scene":
animation: props.sceneMarker.preview, const scene = data as GQL.SlimSceneDataFragment;
image: props.sceneMarker.screenshot, return {
} video: scene.paths.preview ?? undefined,
: props.scene animation: scene.paths.webp ?? undefined,
? { image: scene.paths.screenshot ?? undefined,
video: props.scene?.paths.preview ?? undefined, };
animation: props.scene?.paths.webp ?? undefined, case "sceneMarker":
image: props.scene?.paths.screenshot ?? undefined, const sceneMarker = data as GQL.SceneMarkerDataFragment;
} return {
: props.image video: sceneMarker.stream,
? { animation: sceneMarker.preview,
image: props.image?.paths.thumbnail ?? undefined, image: sceneMarker.screenshot,
} };
: undefined; case "image":
const image = data as GQL.SlimImageDataFragment;
return {
image: image.paths.thumbnail ?? undefined,
};
default:
// this is unreachable, inference fails for some reason
return type as never;
}
}, [type, data]);
const linkSrc = useMemo(() => {
switch (type) {
case "scene":
const scene = data as GQL.SlimSceneDataFragment;
return sceneQueue
? sceneQueue.makeLink(scene.id, { sceneIndex: index })
: `/scenes/${scene.id}`;
case "sceneMarker":
const sceneMarker = data as GQL.SceneMarkerDataFragment;
return NavUtils.makeSceneMarkerUrl(sceneMarker);
case "image":
const image = data as GQL.SlimImageDataFragment;
return `/images/${image.id}`;
default:
return type;
}
}, [type, data, sceneQueue, index]);
const title = useMemo(() => {
switch (type) {
case "scene":
const scene = data as GQL.SlimSceneDataFragment;
return objectTitle(scene);
case "sceneMarker":
const sceneMarker = data as GQL.SceneMarkerDataFragment;
const newTitle = markerTitle(sceneMarker);
const seconds = TextUtils.secondsToTimestamp(sceneMarker.seconds);
if (newTitle) {
return `${newTitle} - ${seconds}`;
} else {
return seconds;
}
case "image":
return "";
default:
return type;
}
}, [type, data]);
const tags = useMemo(() => {
if (type === "sceneMarker") {
const sceneMarker = data as GQL.SceneMarkerDataFragment;
return [sceneMarker.primary_tag, ...sceneMarker.tags];
}
}, [type, data]);
const setInactive = () => setActive(false); const setInactive = () => setActive(false);
const toggleActive = (e: TransitionEvent) => { const toggleActive = useCallback((e: TransitionEvent) => {
if (e.propertyName === "transform" && e.elapsedTime === 0) { if (e.propertyName === "transform" && e.elapsedTime === 0) {
// Get the current scale of the wall-item. If it's smaller than 1.1 the item is being scaled up, otherwise down. // Get the current scale of the wall-item. If it's smaller than 1.1 the item is being scaled up, otherwise down.
const matrixScale = getComputedStyle(wallItem.current).transform.match( const matrixScale = getComputedStyle(itemEl.current!).transform.match(
/-?\d+\.?\d+|\d+/g /-?\d+\.?\d+|\d+/g
)?.[0]; )?.[0];
const scale = Number.parseFloat(matrixScale ?? "2") || 2; const scale = Number.parseFloat(matrixScale ?? "2") || 2;
setActive(scale <= 1.1 && !active); setActive((value) => scale <= 1.1 && !value);
} }
}; }, []);
useEffect(() => { useEffect(() => {
const { current } = wallItem; const item = itemEl.current!;
current?.addEventListener("transitioncancel", setInactive); item.addEventListener("transitioncancel", setInactive);
current?.addEventListener("transitionstart", toggleActive); item.addEventListener("transitionstart", toggleActive);
return () => { return () => {
current?.removeEventListener("transitioncancel", setInactive); item.removeEventListener("transitioncancel", setInactive);
current?.removeEventListener("transitionstart", toggleActive); item.removeEventListener("transitionstart", toggleActive);
}; };
}); }, [toggleActive]);
const clickHandler = () => { const onClick = (e: MouseEvent) => {
if (props.scene) { clickHandler?.(e, data);
props?.clickHandler?.(props.scene);
}
if (props.sceneMarker) {
props?.clickHandler?.(props.sceneMarker);
}
if (props.image) {
props?.clickHandler?.(props.image);
}
}; };
const cont = config?.interface.continuePlaylistDefault ?? false;
let linkSrc: string = "#";
if (!props.clickHandler) {
if (props.scene) {
linkSrc = props.sceneQueue
? props.sceneQueue.makeLink(props.scene.id, {
sceneIndex: props.index,
continue: cont,
})
: `/scenes/${props.scene.id}`;
} else if (props.sceneMarker) {
linkSrc = NavUtils.makeSceneMarkerUrl(props.sceneMarker);
} else if (props.image) {
linkSrc = `/images/${props.image.id}`;
}
}
const title = useMemo(() => {
if (props.sceneMarker) {
return `${markerTitle(
props.sceneMarker
)} - ${TextUtils.secondsToTimestamp(props.sceneMarker.seconds)}`;
}
if (props.scene) {
return objectTitle(props.scene);
}
return "";
}, [props.sceneMarker, props.scene]);
const renderText = () => { const renderText = () => {
if (!showTextContainer) return; if (!showTextContainer) return;
const tags = props.sceneMarker
? [props.sceneMarker.primary_tag, ...props.sceneMarker.tags]
: [];
return ( return (
<div className="wall-item-text"> <div className="wall-item-text">
<div>{title}</div> <div>{title}</div>
{tags.map((tag) => ( {tags?.map((tag) => (
<span key={tag.id} className="wall-tag"> <span key={tag.id} className="wall-tag">
{tag.name} {tag.name}
</span> </span>
@@ -217,8 +244,8 @@ export const WallItem: React.FC<IWallItemProps> = (props: IWallItemProps) => {
return ( return (
<div className="wall-item"> <div className="wall-item">
<div className={`wall-item-container ${props.className}`} ref={wallItem}> <div className={`wall-item-container ${className}`} ref={itemEl}>
<Link onClick={clickHandler} to={linkSrc} className="wall-item-anchor"> <Link onClick={onClick} to={linkSrc} className="wall-item-anchor">
<Preview previews={previews} config={config} active={active} /> <Preview previews={previews} config={config} active={active} />
{renderText()} {renderText()}
</Link> </Link>

View File

@@ -1,19 +1,13 @@
import React from "react"; import React, { MouseEvent } from "react";
import * as GQL from "src/core/generated-graphql"; import * as GQL from "src/core/generated-graphql";
import { SceneQueue } from "src/models/sceneQueue"; import { SceneQueue } from "src/models/sceneQueue";
import { WallItem } from "./WallItem"; import { WallItem, WallItemData, WallItemType } from "./WallItem";
interface IWallPanelProps { interface IWallPanelProps<T extends WallItemType> {
scenes?: GQL.SlimSceneDataFragment[]; type: T;
data: WallItemData[T][];
sceneQueue?: SceneQueue; sceneQueue?: SceneQueue;
sceneMarkers?: GQL.SceneMarkerDataFragment[]; clickHandler?: (e: MouseEvent, item: WallItemData[T]) => void;
images?: GQL.SlimImageDataFragment[];
clickHandler?: (
item:
| GQL.SlimSceneDataFragment
| GQL.SceneMarkerDataFragment
| GQL.SlimImageDataFragment
) => void;
} }
const calculateClass = (index: number, count: number) => { const calculateClass = (index: number, count: number) => {
@@ -33,53 +27,84 @@ const calculateClass = (index: number, count: number) => {
if (index % 5 === 4) return "transform-origin-right"; if (index % 5 === 4) return "transform-origin-right";
// Multiple of five // Multiple of five
if (index % 5 === 0) return "transform-origin-left"; if (index % 5 === 0) return "transform-origin-left";
// Position is equal or larger than first postion in last row // Position is equal or larger than first position in last row
if (count - (count % 5 || 5) <= index + 1) return "transform-origin-bottom"; if (count - (count % 5 || 5) <= index + 1) return "transform-origin-bottom";
// Default // Default
return "transform-origin-center"; return "transform-origin-center";
}; };
export const WallPanel: React.FC<IWallPanelProps> = ( const WallPanel = <T extends WallItemType>({
props: IWallPanelProps type,
) => { data,
const scenes = (props.scenes ?? []).map((scene, index, sceneArray) => ( sceneQueue,
<WallItem clickHandler,
key={scene.id} }: IWallPanelProps<T>) => {
index={index} function renderItems() {
scene={scene} return data.map((item, index, arr) => (
sceneQueue={props.sceneQueue}
clickHandler={props.clickHandler}
className={calculateClass(index, sceneArray.length)}
/>
));
const sceneMarkers = (props.sceneMarkers ?? []).map(
(marker, index, markerArray) => (
<WallItem <WallItem
key={marker.id} type={type}
sceneMarker={marker} key={item.id}
clickHandler={props.clickHandler} index={index}
className={calculateClass(index, markerArray.length)} data={item}
sceneQueue={sceneQueue}
clickHandler={clickHandler}
className={calculateClass(index, arr.length)}
/> />
) ));
); }
const images = (props.images ?? []).map((image, index, imageArray) => (
<WallItem
key={image.id}
image={image}
clickHandler={props.clickHandler}
className={calculateClass(index, imageArray.length)}
/>
));
return ( return (
<div className="row"> <div className="row">
<div className="wall w-100 row justify-content-center"> <div className="wall w-100 row justify-content-center">
{scenes} {renderItems()}
{sceneMarkers}
{images}
</div> </div>
</div> </div>
); );
}; };
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 {
markers: GQL.SceneMarkerDataFragment[];
clickHandler?: (e: MouseEvent, item: GQL.SceneMarkerDataFragment) => void;
}
export const MarkerWallPanel: React.FC<IMarkerWallPanelProps> = ({
markers,
clickHandler,
}) => {
return (
<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}
/>
);
};