mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-05 17:58:55 +00:00
added cropping to EntityInput, and ZoomSlider
This commit is contained in:
1 parent
f09ed11c6e
commit
1e983d7b91
14 files changed
+562
-93
No files matched your search
@@ -36,6 +36,7 @@
|
||||
"framer-motion": "6.5.1",
|
||||
"local-storage": "^2.0.0",
|
||||
"moment": "^2.29.4",
|
||||
"react-easy-crop": "^5.0.7",
|
||||
"universal-cookie": "^6.1.1",
|
||||
"yarn": "^1.22.19"
|
||||
},
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
import MakeRequest from "../MakeRequest";
|
||||
|
||||
export interface EnqueueActingFaceRequest {
|
||||
creator_set_visibility: "public" | "private";
|
||||
face_driver_media_file_token: string;
|
||||
remove_watermark: boolean;
|
||||
source_media_file_token: string;
|
||||
uuid_idempotency_token: string;
|
||||
}
|
||||
|
||||
export interface EnqueueActingFaceResponse {
|
||||
inference_job_token?: string;
|
||||
success: boolean;
|
||||
}
|
||||
|
||||
export const EnqueueActingFace = MakeRequest<
|
||||
string,
|
||||
EnqueueActingFaceRequest,
|
||||
EnqueueActingFaceResponse,
|
||||
{}
|
||||
>({
|
||||
method: "POST",
|
||||
routingFunction: () => `/v1/workflows/enqueue_acting_face`,
|
||||
});
|
||||
@@ -0,0 +1,33 @@
|
||||
import MakeRequest from "../MakeRequest";
|
||||
|
||||
// this type is eqivelent to react-easy-crops Area type
|
||||
|
||||
export interface MediaFileCropArea {
|
||||
height: number;
|
||||
width: number;
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
export interface EnqueueFaceMirrorRequest {
|
||||
creator_set_visibility: "public" | "private";
|
||||
face_driver_media_file_token: string;
|
||||
maybe_crop: MediaFileCropArea;
|
||||
remove_watermark: boolean;
|
||||
source_media_file_token: string;
|
||||
uuid_idempotency_token: string;
|
||||
}
|
||||
|
||||
export interface EnqueueFaceMirrorResponse {
|
||||
inference_job_token?: string;
|
||||
success: boolean;
|
||||
}
|
||||
|
||||
export const EnqueueFaceMirror = MakeRequest<
|
||||
string,
|
||||
EnqueueFaceMirrorRequest,
|
||||
EnqueueFaceMirrorResponse,
|
||||
{}
|
||||
>({
|
||||
method: "POST",
|
||||
routingFunction: () => `/v1/workflows/enqueue_face_mirror`,
|
||||
});
|
||||
@@ -0,0 +1,71 @@
|
||||
import React from "react";
|
||||
import { Direction, Range } from "react-range";
|
||||
import { FontAwesomeIcon as Icon } from "@fortawesome/react-fontawesome";
|
||||
import {
|
||||
faMagnifyingGlassPlus,
|
||||
faMagnifyingGlassMinus,
|
||||
} from "@fortawesome/pro-solid-svg-icons";
|
||||
|
||||
export interface ZoomSliderOnChangeEvent {
|
||||
target: { name?: string; type: "zoom"; value: number };
|
||||
}
|
||||
|
||||
interface Props {
|
||||
name?: string;
|
||||
onChange: (event: ZoomSliderOnChangeEvent) => void;
|
||||
value: number;
|
||||
}
|
||||
|
||||
const renderTrack = ({ props: { style, ...props }, children }: any) => (
|
||||
<div {...{ ...props, className: "fy-zoom-slider-track", style }}>
|
||||
{children}
|
||||
<div {...{ className: "fy-zoom-slider-track-bar" }}></div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const thumb =
|
||||
(thumbTip = "") =>
|
||||
({ props: { style, ...props } }: any) => {
|
||||
return (
|
||||
<div {...{ ...props, className: "fy-zoom-slider-thumb", style }}>
|
||||
{thumbTip}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default function ZoomSlider({
|
||||
name = "",
|
||||
onChange: inChange,
|
||||
value,
|
||||
}: Props) {
|
||||
const onChange = (newVal: number[]) =>
|
||||
inChange({
|
||||
target: { ...(name && { name }), type: "zoom", value: newVal[0] },
|
||||
});
|
||||
|
||||
return (
|
||||
<div {...{ className: "fy-zoom-slider" }}>
|
||||
<Icon {...{ icon: faMagnifyingGlassPlus }} />
|
||||
<div {...{ className: "fy-zoom-slider-container" }}>
|
||||
<Range
|
||||
{...{
|
||||
direction: Direction.Up,
|
||||
min: 1,
|
||||
max: 3,
|
||||
onChange,
|
||||
step: 0.01,
|
||||
renderThumb: thumb(
|
||||
Math.round(((value - 1) / 2) * 100).toString() + "%"
|
||||
),
|
||||
renderTrack,
|
||||
// renderTrack,
|
||||
// step: step,
|
||||
// thumbTip,
|
||||
values: [value],
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<Icon {...{ icon: faMagnifyingGlassMinus }} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export { default } from "./ZoomSlider";
|
||||
export type { ZoomSliderOnChangeEvent } from "./ZoomSlider";
|
||||
@@ -59,6 +59,7 @@ import VideoBasic from "./VideoBasic";
|
||||
import VideoFakeyou from "./VideoFakeyou";
|
||||
import VideoInput from "./VideoInput";
|
||||
import WeightCoverImage from "./WeightCoverImage";
|
||||
import ZoomSlider, { ZoomSliderOnChangeEvent } from "./ZoomSlider";
|
||||
|
||||
export {
|
||||
Accordion,
|
||||
@@ -127,6 +128,13 @@ export {
|
||||
VideoFakeyou,
|
||||
VideoInput,
|
||||
WeightCoverImage,
|
||||
ZoomSlider,
|
||||
};
|
||||
|
||||
export type { ActionButtonProps, ModalUtilities, SelectModalData, Socials };
|
||||
export type {
|
||||
ActionButtonProps,
|
||||
ModalUtilities,
|
||||
SelectModalData,
|
||||
Socials,
|
||||
ZoomSliderOnChangeEvent,
|
||||
};
|
||||
@@ -13,6 +13,47 @@
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
// background-color: $panel;
|
||||
|
||||
.fy-entity-input-crop-container-hidden {
|
||||
cursor: default;
|
||||
background-color: rgba($secondary-very-dark, 0.75);
|
||||
|
||||
.fy-entity-input-crop-area {
|
||||
color: rgba(#f00, 0);
|
||||
}
|
||||
}
|
||||
|
||||
.fy-entity-input-crop-area {
|
||||
color: rgba($secondary-very-dark, 0.75);
|
||||
}
|
||||
|
||||
.fy-entity-input-crop-media {
|
||||
box-shadow:
|
||||
// 0 0 0 1px rgba(#000, 0.16),
|
||||
0 0 15px rgb(#000, 0.5);
|
||||
}
|
||||
|
||||
.fy-entity-input-play-button {
|
||||
border: none;
|
||||
background-color: rgba($secondary-very-dark, 0.5);
|
||||
backdrop-filter: blur(5px);
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 1rem;
|
||||
margin: auto;
|
||||
width: fit-content;
|
||||
// background-color: red;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.fy-entity-input-crop-tip {
|
||||
display: none;
|
||||
position: absolute;
|
||||
background-color: rgba($secondary-very-dark, 0.5);
|
||||
padding: 0.5rem;
|
||||
border-radius: 0.5rem;
|
||||
color: rgba(#fff, 0.5);
|
||||
}
|
||||
}
|
||||
|
||||
.fy-entity-input-square {
|
||||
@@ -31,7 +72,8 @@
|
||||
|
||||
.fy-entity-input-empty,
|
||||
.fy-entity-input-full {
|
||||
// position: relative;
|
||||
// background-color: red;
|
||||
position: relative;
|
||||
// overflow: hidden;
|
||||
justify-content: center;
|
||||
|
||||
@@ -49,6 +91,9 @@
|
||||
}
|
||||
|
||||
.fy-entity-input-file-details {
|
||||
padding: 0 0.5rem;
|
||||
// background-color: red;
|
||||
// display: inline-flex;
|
||||
div {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
@@ -72,33 +117,67 @@
|
||||
}
|
||||
|
||||
.fy-entity-input-full {
|
||||
display: flex;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr fit-content(50%);
|
||||
// grid-template-rows: 1fr 1fr;
|
||||
// display: flex;
|
||||
|
||||
video {
|
||||
width: 100%;
|
||||
}
|
||||
// video {
|
||||
// width: 100%;
|
||||
// }
|
||||
}
|
||||
|
||||
.fy-entity-input-full-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
position: absolute;
|
||||
.fy-entity-input-media-preview {
|
||||
// display: inline-block;
|
||||
position: relative;
|
||||
// width: 66%;
|
||||
// height: 100%;
|
||||
// width: 100%;
|
||||
// aspect-ratio: 1;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.fy-entity-input-preview-controls {
|
||||
display: grid;
|
||||
// background-color: red;
|
||||
background-color: rgba(#000, 0.5);
|
||||
background: linear-gradient(
|
||||
0deg,
|
||||
rgba(0, 0, 0, 0) 0%,
|
||||
rgba(0, 0, 0, 0.1) 6.25%,
|
||||
rgba(0, 0, 0, 0.25) 12.5%,
|
||||
rgba(0, 0, 0, 0.4) 18.75%,
|
||||
rgba(0, 0, 0, 0.5) 25%
|
||||
);
|
||||
grid-template-rows: 1fr auto;
|
||||
gap: 0.5rem;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.fy-entity-input-preview-tools {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
// grid-template-rows: 1fr auto;
|
||||
gap: 0.5rem;
|
||||
// display: inline-block;
|
||||
// width: 33%;
|
||||
// height: 100%;
|
||||
background-color: $secondary-dark;
|
||||
|
||||
// display: flex;
|
||||
// align-items: center;
|
||||
// justify-content: space-between;
|
||||
// position: absolute;
|
||||
// background-color: red;
|
||||
// background-color: rgba(#000, 0.5);
|
||||
// background: linear-gradient(
|
||||
// 0deg,
|
||||
// rgba(0, 0, 0, 0) 0%,
|
||||
// rgba(0, 0, 0, 0.1) 6.25%,
|
||||
// rgba(0, 0, 0, 0.25) 12.5%,
|
||||
// rgba(0, 0, 0, 0.4) 18.75%,
|
||||
// rgba(0, 0, 0, 0.5) 25%
|
||||
// );
|
||||
// padding: 1rem;
|
||||
// backdrop-filter: blur(2px);
|
||||
overflow: hidden;
|
||||
border-radius: 0.5rem 0.5rem 0 0;
|
||||
width: 100%;
|
||||
// width: 100%;
|
||||
|
||||
.fy-zoom-slider {
|
||||
padding: 0.5rem 0;
|
||||
}
|
||||
}
|
||||
|
||||
.fy-entity-input-empty-controls {
|
||||
@@ -146,3 +225,69 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.fy-zoom-slider {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
// padding: 1rem 0;
|
||||
|
||||
svg {
|
||||
color: rgba(#fff, 0.25);
|
||||
}
|
||||
}
|
||||
|
||||
.fy-zoom-slider-container {
|
||||
height: 100%;
|
||||
// background-color: red;
|
||||
margin: 1.5rem 0;
|
||||
// width: 3rem;
|
||||
}
|
||||
|
||||
.fy-zoom-slider-track {
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
width: 2.25rem;
|
||||
// background-color: green;
|
||||
|
||||
.fy-zoom-slider-track-bar {
|
||||
background-color: $secondary-darker;
|
||||
border-radius: 0.25rem;
|
||||
// margin: 1rem 0;
|
||||
// flex-shrink: 1;
|
||||
box-shadow:
|
||||
// inset 0 0 1px 1px rgba(#000, 0.16),
|
||||
inset 0 5px 10px rgb(#000, 0.25);
|
||||
border-bottom: 1px solid rgba(#fff, 0.07);
|
||||
// border-left: none;
|
||||
// border-right: none;
|
||||
width: 0.5rem;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.fy-zoom-slider-thumb {
|
||||
color: rgba(#fff, 0.6);
|
||||
border: 1px solid rgba(#fff, 0.04);
|
||||
border-left: none;
|
||||
border-right: none;
|
||||
box-shadow:
|
||||
// 0 0 0 1px rgba(#000, 0.16),
|
||||
0 5px 10px rgb(#000, 0.25);
|
||||
// @include panel;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-size: 0.75rem;
|
||||
background-color: $secondary;
|
||||
border-radius: 0.75rem;
|
||||
height: 2.25rem;
|
||||
width: 2.25rem;
|
||||
|
||||
&:focus-visible {
|
||||
// color: $secondary;
|
||||
background-color: $secondary-light;
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,8 @@ import { useLocation } from "react-router-dom";
|
||||
import { a, useTransition } from "@react-spring/web";
|
||||
import { MediaFile } from "@storyteller/components/src/api/media_files/GetMedia";
|
||||
import Iframe from "react-iframe";
|
||||
import { Button, Label, Spinner } from "components/common";
|
||||
import { Area, Point } from "react-easy-crop";
|
||||
import { Label, Spinner, ZoomSliderOnChangeEvent } from "components/common";
|
||||
import {
|
||||
AcceptTypes,
|
||||
EntityModeProp,
|
||||
@@ -15,12 +16,25 @@ import { useMedia, useMediaUploader } from "hooks";
|
||||
import { BucketConfig } from "@storyteller/components/src/api/BucketConfig";
|
||||
import { Prompt } from "@storyteller/components/src/api/prompts/GetPrompts";
|
||||
import EntityInputEmpty from "./EntityInputEmpty";
|
||||
import EntityInputSidePanel from "./EntityInputSidePanel";
|
||||
import EntityInputImageVideoPreview from "./EntityInputImageVideoPreview";
|
||||
import "./EntityInput.scss";
|
||||
|
||||
export type OnCropComplete = (
|
||||
croppedArea: Area,
|
||||
croppedAreaPixels: Area
|
||||
) => void;
|
||||
|
||||
export interface CropProps {
|
||||
aspect: number;
|
||||
onCropComplete?: OnCropComplete;
|
||||
}
|
||||
|
||||
interface EntityInputProps {
|
||||
accept?: AcceptTypes | AcceptTypes[];
|
||||
aspectRatio?: "square" | "landscape" | "portrait";
|
||||
className?: string;
|
||||
cropProps?: CropProps;
|
||||
debug?: string;
|
||||
label?: string;
|
||||
name?: string;
|
||||
@@ -31,14 +45,16 @@ interface EntityInputProps {
|
||||
}
|
||||
|
||||
export interface SlideProps {
|
||||
media?: MediaFile;
|
||||
clear: () => void;
|
||||
cropProps?: CropProps;
|
||||
media?: MediaFile;
|
||||
}
|
||||
|
||||
interface AniProps {
|
||||
animating: boolean;
|
||||
className: string;
|
||||
isLeaving: boolean;
|
||||
onCropComplete?: OnCropComplete;
|
||||
render: any;
|
||||
style: any;
|
||||
}
|
||||
@@ -47,53 +63,68 @@ const MediaBusy = () => {
|
||||
return <Spinner />;
|
||||
};
|
||||
|
||||
const EntityInputFull = ({ media, clear }: SlideProps) => {
|
||||
const EntityInputFull = ({ clear, cropProps, media }: SlideProps) => {
|
||||
const bucketConfig = new BucketConfig();
|
||||
const mediaUrl = media?.public_bucket_path
|
||||
? bucketConfig.getGcsUrl(media.public_bucket_path)
|
||||
: "";
|
||||
const mediaType = mediaCategoryfromString(media?.media_type || "");
|
||||
const uploader = `Uploaded by ${
|
||||
media?.maybe_creator_user?.display_name || "User"
|
||||
}`;
|
||||
|
||||
const [crop, cropSet] = useState<Point>({ x: 0, y: 0 });
|
||||
const [zoom, zoomSet] = useState(1);
|
||||
|
||||
const zoomSliderChange = ({ target }: ZoomSliderOnChangeEvent) =>
|
||||
zoomSet(target.value);
|
||||
|
||||
switch (mediaType) {
|
||||
case MediaFilters.image:
|
||||
return (
|
||||
<>
|
||||
<img {...{ src: mediaUrl, alt: "Selected media file" }} />
|
||||
<div {...{ className: "fy-entity-input-full-controls" }}>
|
||||
<div {...{ className: "fy-entity-input-file-details" }}>
|
||||
{media?.maybe_title || "Untitled image"}
|
||||
<div>{uploader}</div>
|
||||
</div>
|
||||
<Button
|
||||
{...{
|
||||
label: "Clear",
|
||||
variant: "secondary",
|
||||
onClick: () => clear(),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<EntityInputImageVideoPreview
|
||||
{...{
|
||||
crop,
|
||||
cropProps,
|
||||
cropSet,
|
||||
image: mediaUrl,
|
||||
zoom,
|
||||
zoomSet,
|
||||
}}
|
||||
/>
|
||||
<EntityInputSidePanel
|
||||
{...{
|
||||
clear,
|
||||
entityType: "image",
|
||||
media,
|
||||
showCrop: !!cropProps,
|
||||
zoomSliderChange,
|
||||
zoom,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
case MediaFilters.video:
|
||||
return (
|
||||
<>
|
||||
<video controls {...{ src: mediaUrl }} />
|
||||
<div {...{ className: "fy-entity-input-full-controls" }}>
|
||||
<div {...{ className: "fy-entity-input-file-details" }}>
|
||||
{media?.maybe_title || "Untitled video"}
|
||||
<div>{uploader}</div>
|
||||
</div>
|
||||
<Button
|
||||
{...{
|
||||
label: "Clear",
|
||||
variant: "secondary",
|
||||
onClick: () => clear(),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<EntityInputImageVideoPreview
|
||||
{...{
|
||||
crop,
|
||||
cropProps,
|
||||
cropSet,
|
||||
video: mediaUrl,
|
||||
zoom,
|
||||
zoomSet,
|
||||
}}
|
||||
/>
|
||||
<EntityInputSidePanel
|
||||
{...{
|
||||
clear,
|
||||
entityType: "video",
|
||||
media,
|
||||
showCrop: !!cropProps,
|
||||
zoomSliderChange,
|
||||
zoom,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
case MediaFilters.engine_asset:
|
||||
@@ -105,7 +136,7 @@ const EntityInputFull = ({ media, clear }: SlideProps) => {
|
||||
className: "fy-entity-input-mocap-preview",
|
||||
}}
|
||||
/>
|
||||
<div {...{ className: "fy-entity-input-full-controls" }}>
|
||||
<div {...{ className: ".fy-entity-input-preview-info" }}>
|
||||
Your file
|
||||
</div>
|
||||
</>
|
||||
@@ -137,6 +168,7 @@ export default function EntityInput({
|
||||
accept,
|
||||
aspectRatio = "square",
|
||||
className,
|
||||
cropProps,
|
||||
debug,
|
||||
label,
|
||||
name = "",
|
||||
@@ -250,6 +282,7 @@ export default function EntityInput({
|
||||
{...{
|
||||
className: "fy-entity-input-full",
|
||||
clear,
|
||||
cropProps,
|
||||
media,
|
||||
render: EntityInputFull,
|
||||
...sharedProps,
|
||||
|
||||
+101
@@ -0,0 +1,101 @@
|
||||
import React, { useState } from "react";
|
||||
import Cropper, { Point } from "react-easy-crop";
|
||||
import { Button } from "components/common";
|
||||
import { useVideo } from "hooks";
|
||||
import { faPlay, faPause } from "@fortawesome/free-solid-svg-icons";
|
||||
import { CropProps } from "./EntityInput";
|
||||
|
||||
interface EntityInputImageVideoPreviewProps {
|
||||
crop: Point;
|
||||
cropProps?: CropProps;
|
||||
cropSet: (crop: Point) => void;
|
||||
image?: string;
|
||||
video?: string;
|
||||
zoom: number;
|
||||
zoomSet: (zoom: number) => void;
|
||||
}
|
||||
|
||||
export default function EntityInputImageVideoPreview({
|
||||
crop,
|
||||
cropProps,
|
||||
cropSet,
|
||||
image,
|
||||
video,
|
||||
zoom,
|
||||
zoomSet,
|
||||
}: EntityInputImageVideoPreviewProps) {
|
||||
const [videoRef, videoRefSet] = useState<
|
||||
React.RefObject<HTMLVideoElement> | undefined
|
||||
>();
|
||||
const [isPlaying, isPlayingSet] = useState(false);
|
||||
const [{ playCtrl }, { onEnded }] = useVideo({
|
||||
videoRef,
|
||||
onEnded: (playPause: boolean) => isPlayingSet(!playPause),
|
||||
});
|
||||
|
||||
return (
|
||||
<div {...{ className: "fy-entity-input-media-preview" }}>
|
||||
<Cropper
|
||||
{...{
|
||||
aspect: cropProps?.aspect || 1,
|
||||
...(image ? { image } : {}),
|
||||
classes: {
|
||||
containerClassName: `fy-entity-input-crop-container${
|
||||
cropProps ? "" : "-hidden"
|
||||
}`,
|
||||
cropAreaClassName: "fy-entity-input-crop-area",
|
||||
mediaClassName: "fy-entity-input-crop-media",
|
||||
},
|
||||
crop,
|
||||
mediaProps: {
|
||||
autoPlay: false,
|
||||
loop: false,
|
||||
onEnded,
|
||||
},
|
||||
onCropChange: (cropLocation: Point) => {
|
||||
if (cropProps) {
|
||||
cropSet(cropLocation);
|
||||
}
|
||||
},
|
||||
...(cropProps?.onCropComplete
|
||||
? { onCropComplete: cropProps.onCropComplete }
|
||||
: {}),
|
||||
onZoomChange: (zoomValue: number) => {
|
||||
if (cropProps) {
|
||||
zoomSet(zoomValue);
|
||||
}
|
||||
},
|
||||
setVideoRef: (ref: React.RefObject<HTMLVideoElement>) => {
|
||||
videoRefSet(ref);
|
||||
},
|
||||
showGrid: !!cropProps,
|
||||
style: {
|
||||
cropAreaStyle: {
|
||||
border: "none",
|
||||
// color: "rgba(#f00,0.0)",
|
||||
},
|
||||
},
|
||||
...(video ? { video } : {}),
|
||||
zoom,
|
||||
}}
|
||||
/>
|
||||
{video ? (
|
||||
<Button
|
||||
{...{
|
||||
className: "fy-entity-input-play-button",
|
||||
icon: isPlaying ? faPause : faPlay,
|
||||
onClick: () => {
|
||||
playCtrl!();
|
||||
isPlayingSet(!isPlaying);
|
||||
},
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
{cropProps ? (
|
||||
<div {...{ className: "fy-entity-input-crop-tip" }}>
|
||||
Drag and scroll to crop
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import React from "react";
|
||||
import { MediaFile } from "@storyteller/components/src/api/media_files/GetMedia";
|
||||
import { Button, ZoomSlider, ZoomSliderOnChangeEvent } from "components/common";
|
||||
|
||||
interface Props {
|
||||
clear: () => void;
|
||||
entityType: "image" | "video";
|
||||
media?: MediaFile;
|
||||
showCrop: boolean;
|
||||
zoom: number;
|
||||
zoomSliderChange: (zoom: ZoomSliderOnChangeEvent) => void;
|
||||
}
|
||||
|
||||
export default function EntityInputSidePanel({
|
||||
clear,
|
||||
entityType,
|
||||
media,
|
||||
showCrop,
|
||||
zoomSliderChange,
|
||||
zoom,
|
||||
}: Props) {
|
||||
const uploader = `Uploaded by ${
|
||||
media?.maybe_creator_user?.display_name || "User"
|
||||
}`;
|
||||
return (
|
||||
<div {...{ className: "fy-entity-input-preview-controls" }}>
|
||||
<div {...{ className: "fy-entity-input-preview-tools" }}>
|
||||
{showCrop && (
|
||||
<ZoomSlider {...{ onChange: zoomSliderChange, value: zoom }} />
|
||||
)}
|
||||
|
||||
<div {...{ className: "fy-entity-input-file-details" }}>
|
||||
{media?.maybe_title || `Untitled ${entityType}`}
|
||||
<div>{uploader}</div>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
{...{
|
||||
label: `Choose another ${entityType}`,
|
||||
variant: "secondary",
|
||||
onClick: () => clear(),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,14 +1,21 @@
|
||||
import { useRef } from 'react';
|
||||
import { useRef } from "react";
|
||||
|
||||
interface Props {
|
||||
onEnded?: any;
|
||||
videoRef?: React.RefObject<HTMLVideoElement>;
|
||||
}
|
||||
|
||||
export default function useVideo({ onEnded }: Props) {
|
||||
const ref = useRef<HTMLVideoElement>(null);
|
||||
const playCtrl = (toDo = (b:any) => {}) => {
|
||||
if (ref.current) {
|
||||
let isPlaying = !!(ref.current.currentTime > 0 && !ref.current.paused && !ref.current.ended && ref.current.readyState > 2);
|
||||
export default function useVideo({ onEnded, videoRef }: Props) {
|
||||
const internalRef = useRef<HTMLVideoElement>(null);
|
||||
const ref = videoRef || internalRef;
|
||||
const playCtrl = (toDo = (b: any) => {}) => {
|
||||
if (ref.current) {
|
||||
let isPlaying = !!(
|
||||
ref.current.currentTime > 0 &&
|
||||
!ref.current.paused &&
|
||||
!ref.current.ended &&
|
||||
ref.current.readyState > 2
|
||||
);
|
||||
if (!isPlaying) {
|
||||
toDo(false);
|
||||
ref.current.play();
|
||||
@@ -19,5 +26,5 @@ export default function useVideo({ onEnded }: Props) {
|
||||
}
|
||||
};
|
||||
|
||||
return [{ playCtrl },{ onEnded, ref }];
|
||||
};
|
||||
return [{ playCtrl }, { onEnded, ref }];
|
||||
}
|
||||
@@ -45,6 +45,9 @@ $primary-light: desaturate(lighten($primary, 20%), 15%);
|
||||
$primary-dark: saturate(darken($primary, 15%), 10%);
|
||||
$primary-very-light: desaturate(lighten($primary, 45%), 35%);
|
||||
|
||||
$secondary-dark: saturate(darken($secondary, 6%), 2%);
|
||||
$secondary-darker: saturate(darken($secondary, 9%), 2%);
|
||||
$secondary-very-dark: saturate(darken($secondary, 12%), 2%);
|
||||
$secondary-light: lighten($secondary, 5%);
|
||||
$secondary-lighter: lighten($secondary, 12%);
|
||||
$secondary-very-light: lighten($secondary, 24%);
|
||||
|
||||
@@ -14,9 +14,10 @@ import {
|
||||
import { SessionSubscriptionsWrapper } from "@storyteller/components/src/session/SessionSubscriptionsWrapper";
|
||||
import { FrontendInferenceJobType } from "@storyteller/components/src/jobs/InferenceJob";
|
||||
import {
|
||||
EnqueueActingFace,
|
||||
EnqueueActingFaceResponse,
|
||||
} from "@storyteller/components/src/api/workflows/EnqueueActingFace";
|
||||
MediaFileCropArea,
|
||||
EnqueueFaceMirror,
|
||||
EnqueueFaceMirrorResponse,
|
||||
} from "@storyteller/components/src/api/workflows/EnqueueFaceMirror";
|
||||
import { EntityInput } from "components/entities";
|
||||
import {
|
||||
useInferenceJobs,
|
||||
@@ -37,6 +38,13 @@ export default function AIFaceMirror({ sessionSubscriptionsWrapper }: Props) {
|
||||
"m_41caq6n7nw15y9e68009bgkn23m3yf"
|
||||
);
|
||||
const [visibility, visibilitySet] = useState<"private" | "public">("public");
|
||||
const [cropArea, cropAreaSet] = useState<MediaFileCropArea>({
|
||||
height: 0,
|
||||
width: 0,
|
||||
x: 0,
|
||||
y: 0,
|
||||
});
|
||||
|
||||
const { enqueue } = useInferenceJobs();
|
||||
// const { canBanUsers } = useSession();
|
||||
|
||||
@@ -51,13 +59,14 @@ export default function AIFaceMirror({ sessionSubscriptionsWrapper }: Props) {
|
||||
|
||||
const enqueueClick = () => {
|
||||
if (hasTokens) {
|
||||
EnqueueActingFace("", {
|
||||
EnqueueFaceMirror("", {
|
||||
creator_set_visibility: visibility,
|
||||
face_driver_media_file_token: faceDriverToken,
|
||||
maybe_crop: cropArea,
|
||||
remove_watermark: removeWatermark,
|
||||
source_media_file_token: sourceMediaToken,
|
||||
uuid_idempotency_token: uuidv4(),
|
||||
}).then((res: EnqueueActingFaceResponse) => {
|
||||
}).then((res: EnqueueFaceMirrorResponse) => {
|
||||
if (res.success && res.inference_job_token) {
|
||||
enqueue(
|
||||
res.inference_job_token,
|
||||
@@ -121,6 +130,12 @@ export default function AIFaceMirror({ sessionSubscriptionsWrapper }: Props) {
|
||||
<EntityInput
|
||||
{...{
|
||||
accept: ["video"],
|
||||
cropProps: {
|
||||
aspect: 1,
|
||||
onCropComplete: (croppedArea, croppedAreaPixels) => {
|
||||
cropAreaSet(croppedAreaPixels);
|
||||
},
|
||||
},
|
||||
// aspectRatio: "landscape",
|
||||
// debug: "AFM driver input",
|
||||
name: "faceDriverToken",
|
||||
|
||||
@@ -4486,6 +4486,7 @@ __metadata:
|
||||
moment: ^2.29.4
|
||||
prettier: ^3.1.0
|
||||
pretty-quick: ^3.1.3
|
||||
react-easy-crop: ^5.0.7
|
||||
universal-cookie: ^6.1.1
|
||||
yarn: ^1.22.19
|
||||
languageName: unknown
|
||||
@@ -15425,6 +15426,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"normalize-wheel@npm:^1.0.1":
|
||||
version: 1.0.1
|
||||
resolution: "normalize-wheel@npm:1.0.1"
|
||||
checksum: 00b32efa040bad9438e732385a4ca27f8532fa2c8c06b54be43b9f75b2da6642bf41a0b4f81e542639dc8d682cde8b059b0a02ae0723fb8ebc3c8f036c8e51d8
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"npm-bundled@npm:^1.1.1":
|
||||
version: 1.1.2
|
||||
resolution: "npm-bundled@npm:1.1.2"
|
||||
@@ -17800,6 +17808,19 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"react-easy-crop@npm:^5.0.7":
|
||||
version: 5.0.7
|
||||
resolution: "react-easy-crop@npm:5.0.7"
|
||||
dependencies:
|
||||
normalize-wheel: ^1.0.1
|
||||
tslib: ^2.0.1
|
||||
peerDependencies:
|
||||
react: ">=16.4.0"
|
||||
react-dom: ">=16.4.0"
|
||||
checksum: 562ea94b8c938e842965cee05b9ab99d829ff89cbdf155d03a834800a930b5429be4ee62981dca111533c063bc630a6bb1d649a00024481f80426d7a1325ff79
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"react-error-overlay@npm:^6.0.10":
|
||||
version: 6.0.10
|
||||
resolution: "react-error-overlay@npm:6.0.10"
|
||||
@@ -20751,6 +20772,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"tslib@npm:^2.0.1":
|
||||
version: 2.6.3
|
||||
resolution: "tslib@npm:2.6.3"
|
||||
checksum: 74fce0e100f1ebd95b8995fbbd0e6c91bdd8f4c35c00d4da62e285a3363aaa534de40a80db30ecfd388ed7c313c42d930ee0eaf108e8114214b180eec3dbe6f5
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"tslib@npm:^2.0.3":
|
||||
version: 2.3.1
|
||||
resolution: "tslib@npm:2.3.1"
|
||||
|
||||
Reference in new issue
Block a user