openreplay/spot/entrypoints/content/SavingControls.tsx
Delirium 1326bb2eae
feat spot: init commit for extension (#2452)
* feat spot: init commit for extension

* nvmrc

* fix login flow

* Spots Gridview Updates (#2422)

* feat ui: login flow for spot extension

* spot list, store and service created

* some fixing for header

* start work on single spot

* spot player start

* header for player, comments, icons, etc

* split stuff into compoennts, create player state manager

* player controls, activity panel etc etc

* comments, empty page, rename and stuff

* interval buttons etc

* access modal

* pubkey support

* fix tooltip

* limit 10 -> 9

* hls lib instead of videojs

* some warnings

* fix date display for exp

* change public links

* display more client data

* fix cleaning, init comment

* map network to replay player network ev

* stream support, console panel, close panels on X

* fixing streaming, destroy on leave

* fix autoplay

* show notification on spot login

* fix spot login

* backup player added, fix audio issue

* show thumbnail when no video, add spot roles

* add poster thumbnail

* some fixes to video check

* fix events jump

* fix play btn

* try catch over pubkey

* feat ui: login flow for spot extension

* spot list, store and service created

* some fixing for header

* start work on single spot

* spot player start

* header for player, comments, icons, etc

* split stuff into compoennts, create player state manager

* player controls, activity panel etc etc

* comments, empty page, rename and stuff

* interval buttons etc

* access modal

* pubkey support

* fix tooltip

* limit 10 -> 9

* hls lib instead of videojs

* some warnings

* fix date display for exp

* change public links

* display more client data

* fix cleaning, init comment

* map network to replay player network ev

* stream support, console panel, close panels on X

* fixing streaming, destroy on leave

* fix autoplay

* show notification on spot login

* fix spot login

* backup player added, fix audio issue

* show thumbnail when no video, add spot roles

* add poster thumbnail

* some fixes to video check

* fix events jump

* fix play btn

* try catch over pubkey

* icons

* Various updates

* Update SVG.tsx

* Update SideMenu.tsx

* SpotList & Menu updates

* feat ui: login flow for spot extension

* spot list, store and service created

* some fixing for header

* start work on single spot

* spot player start

* header for player, comments, icons, etc

* split stuff into compoennts, create player state manager

* player controls, activity panel etc etc

* comments, empty page, rename and stuff

* interval buttons etc

* access modal

* pubkey support

* fix tooltip

* limit 10 -> 9

* hls lib instead of videojs

* some warnings

* fix date display for exp

* change public links

* display more client data

* fix cleaning, init comment

* map network to replay player network ev

* stream support, console panel, close panels on X

* fixing streaming, destroy on leave

* fix autoplay

* show notification on spot login

* fix spot login

* backup player added, fix audio issue

* show thumbnail when no video, add spot roles

* add poster thumbnail

* some fixes to video check

* fix events jump

* fix play btn

* try catch over pubkey

* icons

* spot login pinging

* Spot List & Player Updates

* move spot login flow to login comp, use separate spot login path for unique jwt

* invalidate spot jwt on logout

* add visual data on page load event

* typo fix

* Spot Listing improvements post review.

* Update SpotListItem.tsx

* Improved Spot List and Item Details

* Minor improvements

* More improvements

* Public player header improvements

* Moved formatExpirationTime to utils

* fixes after merge

---------

Co-authored-by: nick-delirium <nikita@openreplay.com>

* set sso link to <a>?

* some small perf fixes

* login duck reformat...

* Update frontend.yaml

* add observer to spot list header

* split list header

* update spotjwt param in router

* fix toast in router

* fix async fetch, move ctx

* capture space btn ev

* fix header link

* public sharing error msg

* fix err msg for unsuccessful rec start

* fix list alignment

* Caching assets. Finally!!!

* fix typing in comment field

* add pubkey to comments, fix console jump btn

* no content comp

* change refresh token logic

* move thumbnail ts

* move thumbnail ts

* fix tab change

* switch up toggler

* early exit if no jwt present

* regenerate icons

* fix location str

* fix ctx

* change thumnail res, return autoplay for video player

* parse links in console rows, fix injected method parse?

* remove ts from js

* fix console parsing order?

* fixes for autoplay

* xray for spot player

* move to spot list after login;
esc to cancel;
fix signup link;
move ux commit

* kb sc for skipping; xray for spot ext

* track aborted requests

* tooltip for readability

* fixing empty state

* New blank state + various minor improvements (#2471)

* New blank state + various minor improvements

* apres merge

---------

Co-authored-by: nick-delirium <nikita@openreplay.com>

* rm temp v

* init or card

* empty state debug

* empty state debug

* empty state debug

* fix initor img

* spotonly scope support

* Improved Spot dead-end pages (#2475)

* Improved Spot dead-end pages

* Initiate OpenReplay Setup and some more

* get scope changes

* fix crash

* scope upgrade/downgrade

* scope setup flow

* ping for backend

* upgrade wxt deps

* cancel ping int on expiration

* check rec status

* fix ping

* check video processing state

* check video processing state

* fix xray close, network highlight, fcp rounding

* update wxt, move open spot stuff to settings

* fix some history issues

* fix spot login flow

* fix spot login again

* fix spot login again

* don't send two requests

* limit messages for logged users

* limit messages for logged users

* fix public ignore

* microphone stuff

* microphone stuff

* Various improvements (#2509)

* Various improvements

- Updated icons in mic settings
- Included prefix in Spot title
- Save recording notification has been updated
- Other minor UI improvements

* Inline declaration of spot name field, and settings UI

* str f

---------

Co-authored-by: nick-delirium <nikita@openreplay.com>

* UI changes in player header, spot list (#2510)

* Added UI elements in player page

- Badge with counts for comments
- Download and Delete dropdown in player
- Spot selection -- UI improvement

* Minor copy updates

* completing changes

---------

Co-authored-by: nick-delirium <nikita@openreplay.com>

* rm cmt

* fix cellmeasurer

* thumbnail dur

* fix download

* Minor fixes (#2512)

- Spot delete confirmation
- Spot comments UI update
- Minor copy updates

* limit number of notif messages

* add spot title to doc title, add cache groups for webpack

* drop mic controls from recording popup view

* fix for webpack compress

* fix for auto mic pickup

* change status banners

* move svgs around, remove undefined check

* refactor svgs

* fix timetable scaling

* fix error popup

* self contain css

* pre-select spot on spot onboarding

---------

Co-authored-by: Sudheer Salavadi <connect.uxmaster@gmail.com>
Co-authored-by: Rajesh Rajendran <rjshrjndrn@users.noreply.github.com>
2024-08-29 13:35:58 +02:00

504 lines
16 KiB
TypeScript

// noinspection SpellCheckingInspection
import { createSignal, onCleanup, createEffect } from "solid-js";
import { formatMsToTime } from "@/entrypoints/content/utils";
import "./style.css";
import "./dragControls.css";
interface ISavingControls {
onClose: (
save: boolean,
obj?: {
blob?: Blob;
name?: string;
comment?: string;
useHook?: boolean;
thumbnail?: string;
crop?: [number, number];
},
) => void;
getVideoData: () => Promise<any>;
getErrorEvents: () => Promise<{title:string,time:number}>
}
const base64ToBlob = (base64: string) => {
const splitStr = base64.split(",");
const len = splitStr.length;
const byteString = atob(splitStr[len - 1]);
const ab = new ArrayBuffer(byteString.length);
const ia = new Uint8Array(ab);
for (let i = 0; i < byteString.length; i++) {
ia[i] = byteString.charCodeAt(i);
}
return new Blob([ab], { type: "video/webm" });
};
function SavingControls({ onClose, getVideoData, getErrorEvents }: ISavingControls) {
const [name, setName] = createSignal(`Issues in — ${document.title}`);
const [description, setDescription] = createSignal("");
const [currentTime, setCurrentTime] = createSignal(0);
const [duration, setDuration] = createSignal(0);
const [playing, setPlaying] = createSignal(false);
const [trimBounds, setTrimBounds] = createSignal([0, 0]);
const [videoData, setVideoData] = createSignal<string | undefined>(undefined);
const [videoBlob, setVideoBlob] = createSignal<Blob | undefined>(undefined);
const [processing, setProcessing] = createSignal(false);
const [startPos, setStartPos] = createSignal(0);
const [endPos, setEndPos] = createSignal(100);
const [dragging, setDragging] = createSignal<string | null>(null);
const [isTyping, setIsTyping] = createSignal(false);
const [errorEvents, setErrorEvents] = createSignal([])
createEffect(() => {
setTrimBounds([0, 0]);
getErrorEvents().then(r => {
setErrorEvents(r)
})
});
const spacePressed = (e: KeyboardEvent) => {
if (
e.target instanceof HTMLInputElement ||
e.target instanceof HTMLTextAreaElement ||
isTyping()
) {
return;
}
e.preventDefault()
e.stopPropagation()
if (e.key === " ") {
if (playing()) {
pause();
} else {
resume();
}
}
};
createEffect(() => {
window.addEventListener("keydown", spacePressed);
onCleanup(() => window.removeEventListener("keydown", spacePressed));
});
const convertToPercentage = (clientX: number, element: HTMLElement) => {
const rect = element.getBoundingClientRect();
const x = clientX - rect.left;
return (x / rect.width) * 100;
};
const startDrag =
(marker: "start" | "end" | "body") => (event: MouseEvent) => {
event.stopPropagation();
setDragging(marker);
};
const onDrag = (event: MouseEvent) => {
if (dragging() && event.clientX !== 0) {
const newPos = convertToPercentage(
event.clientX,
event.currentTarget as HTMLElement,
);
if (dragging() === "start") {
if (endPos() - newPos <= 1 || newPos < 0 || newPos > 100) {
return;
}
setStartPos(newPos);
} else if (dragging() === "end") {
if (newPos - startPos() <= 1 || newPos < 0 || newPos > 100) {
return;
}
setEndPos(newPos);
}
onTrimChange(
(startPos() / 100) * duration(),
(endPos() / 100) * duration(),
);
}
};
const endDrag = () => {
setDragging(null);
};
onCleanup(() => {
setDragging(null);
});
if (videoData() === undefined) {
getVideoData().then(async (data: Record<string, any>) => {
const fullData = data.base64data.join("");
const blob = base64ToBlob(fullData);
const blobUrl = URL.createObjectURL(blob);
setVideoBlob(blob);
setVideoData(blobUrl);
});
}
let videoRef: HTMLVideoElement;
const onSave = async () => {
setProcessing(true);
const thumbnail = await generateThumbnail();
setProcessing(false);
const bounds = trimBounds();
const trim =
bounds[0] + bounds[1] === 0
? null
: (bounds.map((i: number) => Math.round(i * 1000)) as [number, number]);
const dataObj = {
blob: videoBlob(),
name: name(),
comment: description(),
useHook: false,
thumbnail,
crop: trim,
};
onClose(true, dataObj);
};
const onCancel = () => {
onClose(false);
};
const pause = () => {
videoRef.pause();
setPlaying(false);
};
const resume = () => {
void videoRef.play();
setPlaying(true);
};
const updateCurrentTime = () => {
setCurrentTime(videoRef.currentTime);
};
const generateThumbnail = async (): Promise<string> => {
const canvas = document.createElement("canvas");
const context = canvas.getContext("2d");
if (!context) return "";
let thumbnailRes = "";
const aspectRatio = videoRef.videoWidth / videoRef.videoHeight;
const width = 1080;
const height = width / aspectRatio;
canvas.width = width;
canvas.height = height;
videoRef.currentTime = duration() ? duration() : 3;
context.drawImage(videoRef, 0, 0, canvas.width, canvas.height);
thumbnailRes = canvas.toDataURL("image/jpeg", 0.7);
return new Promise((res) => {
const interval = setInterval(() => {
if (thumbnailRes) {
clearInterval(interval);
res(thumbnailRes);
}
}, 100);
});
};
const getDuration = async () => {
videoRef.currentTime = 1e101;
await new Promise((resolve) => {
videoRef.ontimeupdate = () => {
videoRef.ontimeupdate = null;
resolve(videoRef.duration);
};
});
setTimeout(() => {
videoRef.currentTime = 0;
}, 25);
return videoRef.duration;
};
const onMetaLoad = async () => {
let videoDuration = videoRef.duration;
if (videoDuration === Infinity || Number.isNaN(videoDuration)) {
videoDuration = await getDuration();
}
setDuration(videoDuration);
void generateThumbnail();
};
const onVideoEnd = () => {
setPlaying(false);
};
const setVideoRef = (el: HTMLVideoElement) => {
videoRef = el;
videoRef.addEventListener("loadedmetadata", onMetaLoad);
videoRef.addEventListener("ended", onVideoEnd);
};
const round = (num: number) => {
return Math.round((num + Number.EPSILON) * 100) / 100;
};
const onTrimChange = (a: number, b: number) => {
const start = round(a);
const end = round(b);
setTrimBounds([start, end]);
};
const getTrimDuration = () => {
const [trimStart, trimEnd] = trimBounds();
return trimEnd - trimStart;
};
const pageUrl = document.location.href;
let dialogRef: HTMLDialogElement;
createEffect(() => {
if (dialogRef) {
dialogRef.showModal();
}
});
const safeUrl = pageUrl.length > 60 ? pageUrl.slice(0, 60) + "..." : pageUrl;
const int = setInterval(() => {
updateCurrentTime();
}, 100);
onCleanup(() => {
clearInterval(int);
videoRef.removeEventListener("loadedmetadata", onMetaLoad);
videoRef.removeEventListener("ended", onVideoEnd);
});
return (
<dialog
ref={(el) => (dialogRef = el)}
id="editRecording"
class="modal save-controls"
>
<div class="modal-box bg-slate-50 p-0 max-w-[85%]">
<div class={"savingcontainer flex xl:flex-row flex-col"}>
{processing() ? (
<div class={"processingloader"}>
<div class="flex flex-col gap-2 justify-center items-center">
<span class="loading loading-spinner text-primary text-center justify-center items-center"></span>
Saving...
</div>
</div>
) : null}
<div class={"replayarea flex-1 p-4 join join-vertical"}>
<div
class={
"card join-item border-t border-r border-l border-slate-100 "
}
>
<div
class={
"urlcontainer text-sm p-2 text-neutral/70 flex gap-1 items-center overflow-hidden"
}
>
<span>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="lucide lucide-link-2"
>
<path d="M9 17H7A5 5 0 0 1 7 7h2" />
<path d="M15 7h2a5 5 0 1 1 0 10h-2" />
<line x1="8" x2="16" y1="12" y2="12" />
</svg>
</span>
{safeUrl}
</div>
<video
ref={setVideoRef}
class={"videocontainer"}
src={videoData()}
/>
</div>
<div class={"card p-1"}>
{errorEvents().length ? (
<div class={'relative w-full h-4'}>
{errorEvents().map(e => (
<div
class={'w-3 h-3 rounded-full bg-red-600 absolute tooltip'}
style={{ top: '2px', left: `${(e.time/duration()) * 100}%` }}
data-tip={e.title}
/>
))}
</div>
) : null}
<div class={"flex items-center gap-2"}>
<div
class={`${playing() ? "" : "bg-indigo-100"} cursor-pointer btn btn-ghost btn-circle btn-sm hover:bg-indigo-50 border border-slate-100`}
>
{playing() ? (
<div
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
pause();
}}
class={"pause-icon w-5"}
/>
) : (
<div
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
resume();
}}
class={"play-icon ml-0.5 w-5"}
/>
)}
</div>
<div class="flex flex-1 items-center gap-4">
<div class="w-11 text-sm font-medium">
{formatMsToTime(currentTime() * 1000)}
</div>
<div
style={{
position: "relative",
width: "100%",
height: "21px",
}}
onMouseMove={onDrag}
onMouseUp={endDrag}
>
<div
class="marker start"
onMouseDown={startDrag("start")}
style={{ left: `${startPos()}%` }}
>
<div class="handle"></div>
<div class="handle"></div>
</div>
<div
class="slider-body"
// onMouseDown={startDrag("body")}
style={{
left: `calc(${startPos()}% + 3px)`,
width: `calc(${endPos() - startPos()}% - 0px)`,
}}
/>
<div
class="marker end"
onMouseDown={startDrag("end")}
style={{ left: `${endPos()}%` }}
>
<div class="handle"></div>
<div class="handle"></div>
</div>
<input
type="range"
min="0"
step={0.01}
max={duration() - 0.1}
value={currentTime()}
onInput={(e) => {
const time = parseFloat(e.currentTarget.value);
videoRef.currentTime = time;
setCurrentTime(time);
}}
/>
</div>
<div class="w-11 text-sm font-medium">
{formatMsToTime(duration() * 1000)}
</div>
</div>
</div>
{getTrimDuration() > 0 ? (
<p class="text-xs block text-center py-2">
<span class="font-meidum me-1">
{formatMsToTime(getTrimDuration() * 1000)}
</span>
The selected portion of the recording will be saved.
</p>
) : null}
</div>
</div>
<div class={"commentarea flex-none p-4 xl:ps-0 gap-2 "}>
<div class="flex flex-col ">
<div>
<div class="flex justify-between items-center">
<h4 class="text-lg font-medium mb-4">Save Spot</h4>
<form method="dialog">
<button class="btn btn-sm btn-circle btn-ghost absolute right-3 top-3.5">
</button>
</form>
</div>
<div class="mb-4">
<label class={"text-base font-medium mb-2"}>Title</label>
<input
type="text"
placeholder="Name this Spot"
maxlength={64}
value={name()}
onFocus={() => setIsTyping(true)}
onBlur={() => setIsTyping(false)}
onInput={(e) => setName(e.currentTarget.value)}
class="input input-bordered w-full input-sm text-base mt-1"
/>
</div>
<div>
<label class={"text-base font-medium"}>Comments</label>
<textarea
placeholder="Add more details..."
value={description()}
maxLength={256}
onFocus={() => setIsTyping(true)}
onBlur={() => setIsTyping(false)}
onInput={(e) => setDescription(e.currentTarget.value)}
class="textarea textarea-bordered w-full textarea-sm text-base leading-normal mt-1"
rows={4}
/>
</div>
</div>
<div class={"flex flex-col gap-3 justify-end mt-4"}>
<div class="flex items-center gap-3">
<div
onClick={onSave}
class={"btn btn-primary btn-sm text-white text-base"}
>
Save Spot
</div>
<div
onClick={onCancel}
class={
"btn btn-outline btn-primary btn-sm text-base hover:bg-white"
}
>
Cancel
</div>
</div>
<p class="text-xs">
Spots are saved to your{" "}
<a
href="https://foss.openreplay.com/spots"
class="text-primary no-underline"
target="blank"
>
OpenReplay account.
</a>
</p>
</div>
</div>
</div>
</div>
</div>
</dialog>
);
}
export default SavingControls;