Files
wehub-resource-sync adf0d17497
publish / version_or_publish (push) Has been cancelled
storybook-build / changes (push) Has been cancelled
storybook-build / :storybook-build (push) Has been cancelled
Sync Gradio Skills to Hugging Face / sync-skills (push) Has been cancelled
functional / changes (push) Has been cancelled
functional / build-frontend (push) Has been cancelled
functional / functional-test-SSR=false (push) Has been cancelled
functional / functional-reload (push) Has been cancelled
js / changes (push) Has been cancelled
js / js-test (push) Has been cancelled
docs-build / changes (push) Has been cancelled
docs-build / docs-build (push) Has been cancelled
docs-build / website-build (push) Has been cancelled
functional / functional-test-SSR=true (push) Has been cancelled
hygiene / hygiene-test (push) Has been cancelled
python / changes (push) Has been cancelled
python / build (push) Has been cancelled
python / test-ubuntu-latest-flaky (push) Has been cancelled
python / test-ubuntu-latest-not-flaky (push) Has been cancelled
python / test-windows-latest-flaky (push) Has been cancelled
python / test-windows-latest-not-flaky (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:17:32 +08:00

294 lines
7.0 KiB
Svelte

<script lang="ts">
import { onMount, onDestroy } from "svelte";
interface Props {
videoElement: HTMLVideoElement;
trimmedDuration?: number | null;
dragStart?: number;
dragEnd?: number;
loadingTimeline?: boolean;
}
let {
videoElement,
trimmedDuration = $bindable(null),
dragStart = $bindable(0),
dragEnd = $bindable(0),
loadingTimeline = $bindable(false)
}: Props = $props();
let thumbnails = $state<string[]>([]);
let numberOfThumbnails = 10;
let intervalId: ReturnType<typeof setInterval> | undefined;
let videoDuration: number;
let leftHandlePosition = $state(0);
let rightHandlePosition = $state(100);
let dragging = $state<string | null>(null);
const startDragging = (side: string | null): void => {
dragging = side;
};
let loadingTimelineValue = $derived(thumbnails.length !== numberOfThumbnails);
$effect(() => {
loadingTimeline = loadingTimelineValue;
});
const stopDragging = (): void => {
dragging = null;
};
const drag = (event: { clientX: number }, distance?: number): void => {
if (dragging) {
const timeline = document.getElementById("timeline");
if (!timeline) return;
const rect = timeline.getBoundingClientRect();
let newPercentage = ((event.clientX - rect.left) / rect.width) * 100;
if (distance) {
// Move handle based on arrow key press
newPercentage =
dragging === "left"
? leftHandlePosition + distance
: rightHandlePosition + distance;
} else {
// Move handle based on mouse drag
newPercentage = ((event.clientX - rect.left) / rect.width) * 100;
}
newPercentage = Math.max(0, Math.min(newPercentage, 100)); // Keep within 0 and 100
if (dragging === "left") {
leftHandlePosition = Math.min(newPercentage, rightHandlePosition);
// Calculate the new time and set it for the videoElement
const newTimeLeft = (leftHandlePosition / 100) * videoDuration;
videoElement.currentTime = newTimeLeft;
dragStart = newTimeLeft;
} else if (dragging === "right") {
rightHandlePosition = Math.max(newPercentage, leftHandlePosition);
const newTimeRight = (rightHandlePosition / 100) * videoDuration;
videoElement.currentTime = newTimeRight;
dragEnd = newTimeRight;
}
const startTime = (leftHandlePosition / 100) * videoDuration;
const endTime = (rightHandlePosition / 100) * videoDuration;
trimmedDuration = endTime - startTime;
leftHandlePosition = leftHandlePosition;
rightHandlePosition = rightHandlePosition;
}
};
const moveHandle = (e: KeyboardEvent): void => {
if (dragging) {
// Calculate the movement distance as a percentage of the video duration
const distance = (1 / videoDuration) * 100;
if (e.key === "ArrowLeft") {
drag({ clientX: 0 }, -distance);
} else if (e.key === "ArrowRight") {
drag({ clientX: 0 }, distance);
}
}
};
const generateThumbnail = (): void => {
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
if (!ctx) return;
canvas.width = videoElement.videoWidth;
canvas.height = videoElement.videoHeight;
ctx.drawImage(videoElement, 0, 0, canvas.width, canvas.height);
const thumbnail: string = canvas.toDataURL("image/jpeg", 0.7);
thumbnails = [...thumbnails, thumbnail];
};
onMount(() => {
const loadMetadata = (): void => {
videoDuration = videoElement.duration;
const interval = videoDuration / numberOfThumbnails;
let captures = 0;
const onSeeked = (): void => {
generateThumbnail();
captures++;
if (captures < numberOfThumbnails) {
videoElement.currentTime += interval;
} else {
videoElement.removeEventListener("seeked", onSeeked);
}
};
videoElement.addEventListener("seeked", onSeeked);
videoElement.currentTime = 0;
};
if (videoElement.readyState >= 1) {
loadMetadata();
} else {
videoElement.addEventListener("loadedmetadata", loadMetadata);
}
});
onDestroy(() => {
window.removeEventListener("mousemove", drag);
window.removeEventListener("mouseup", stopDragging);
window.removeEventListener("keydown", moveHandle);
if (intervalId !== undefined) {
clearInterval(intervalId);
}
});
onMount(() => {
window.addEventListener("mousemove", drag);
window.addEventListener("mouseup", stopDragging);
window.addEventListener("keydown", moveHandle);
});
</script>
<div class="container">
{#if loadingTimelineValue}
<div class="load-wrap">
<span aria-label="loading timeline" class="loader" />
</div>
{:else}
<div id="timeline" class="thumbnail-wrapper">
<button
aria-label="start drag handle for trimming video"
class="handle left"
onmousedown={() => startDragging("left")}
onblur={stopDragging}
onkeydown={(e) => {
if (e.key === "ArrowLeft" || e.key == "ArrowRight") {
startDragging("left");
}
}}
style="left: {leftHandlePosition}%;"
></button>
<div
class="opaque-layer"
style="left: {leftHandlePosition}%; right: {100 - rightHandlePosition}%"
></div>
{#each thumbnails as thumbnail, i (i)}
<img src={thumbnail} alt={`frame-${i}`} draggable="false" />
{/each}
<button
aria-label="end drag handle for trimming video"
class="handle right"
onmousedown={() => startDragging("right")}
onblur={stopDragging}
onkeydown={(e) => {
if (e.key === "ArrowLeft" || e.key == "ArrowRight") {
startDragging("right");
}
}}
style="left: {rightHandlePosition}%;"
></button>
</div>
{/if}
</div>
<style>
.load-wrap {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
}
.loader {
display: flex;
position: relative;
background-color: var(--border-color-accent-subdued);
animation: shadowPulse 2s linear infinite;
box-shadow:
-24px 0 var(--border-color-accent-subdued),
24px 0 var(--border-color-accent-subdued);
margin: var(--spacing-md);
border-radius: 50%;
width: 10px;
height: 10px;
scale: 0.5;
}
@keyframes shadowPulse {
33% {
box-shadow:
-24px 0 var(--border-color-accent-subdued),
24px 0 #fff;
background: #fff;
}
66% {
box-shadow:
-24px 0 #fff,
24px 0 #fff;
background: var(--border-color-accent-subdued);
}
100% {
box-shadow:
-24px 0 #fff,
24px 0 var(--border-color-accent-subdued);
background: #fff;
}
}
.container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
margin: var(--spacing-lg) var(--spacing-lg) 0 var(--spacing-lg);
}
#timeline {
display: flex;
height: var(--size-10);
flex: 1;
position: relative;
}
img {
flex: 1 1 auto;
min-width: 0;
object-fit: cover;
height: var(--size-12);
border: 1px solid var(--block-border-color);
user-select: none;
z-index: 1;
}
.handle {
width: 3px;
background-color: var(--color-accent);
cursor: ew-resize;
height: var(--size-12);
z-index: 3;
position: absolute;
}
.opaque-layer {
background-color: rgba(230, 103, 40, 0.25);
border: 1px solid var(--color-accent);
height: var(--size-12);
position: absolute;
z-index: 2;
}
</style>