264 lines
5.9 KiB
Svelte
264 lines
5.9 KiB
Svelte
<script lang="ts">
|
|
import { throttle } from "lodash";
|
|
import Spinner from "../../utils/Spinner.svelte";
|
|
import Icon from "../../utils/Icon.svelte";
|
|
import { useEntity } from "../../../lib/entity";
|
|
import { i18n } from "../../../i18n";
|
|
import { createEventDispatcher } from "svelte";
|
|
import api from "../../../lib/api";
|
|
const dispatch = createEventDispatcher();
|
|
|
|
export let address: string;
|
|
export let detail: boolean;
|
|
|
|
const { entity } = useEntity(address);
|
|
|
|
enum State {
|
|
LOADING = "loading",
|
|
PREVIEW = "preview",
|
|
PREVIEWING = "previewing",
|
|
PLAYING = "playing",
|
|
ERRORED = "errored",
|
|
}
|
|
let state = State.LOADING;
|
|
let supported = true;
|
|
|
|
$: if (state == State.PREVIEW) dispatch("loaded");
|
|
|
|
$: {
|
|
if ($entity && videoEl) {
|
|
const mime = $entity.get("FILE_MIME");
|
|
if (mime) {
|
|
supported = Boolean(videoEl.canPlayType(mime as string));
|
|
}
|
|
}
|
|
}
|
|
|
|
let videoEl: HTMLVideoElement;
|
|
let currentTime: number;
|
|
|
|
let timeCodeWidth: number;
|
|
let timeCodeLeft: string;
|
|
let timeCodeSize: string;
|
|
|
|
const seek = throttle((progress: number) => {
|
|
if (state === State.PREVIEWING && videoEl.duration) {
|
|
currentTime = videoEl.duration * progress;
|
|
|
|
if (timeCodeWidth) {
|
|
let timeCodeLeftPx = Math.min(
|
|
Math.max(videoEl.clientWidth * progress, timeCodeWidth / 2),
|
|
videoEl.clientWidth - timeCodeWidth / 2
|
|
);
|
|
timeCodeLeft = `${timeCodeLeftPx}px`;
|
|
timeCodeSize = `${videoEl.clientHeight / 9}px`;
|
|
}
|
|
}
|
|
}, 100);
|
|
|
|
function updatePreviewPosition(ev: MouseEvent) {
|
|
if (state === State.PREVIEW || state === State.PREVIEWING) {
|
|
state = State.PREVIEWING;
|
|
const bcr = videoEl.getBoundingClientRect();
|
|
const progress = (ev.clientX - bcr.x) / bcr.width;
|
|
seek(progress);
|
|
}
|
|
}
|
|
|
|
function resetPreview() {
|
|
if (state === State.PREVIEWING) {
|
|
state = State.PREVIEW;
|
|
videoEl.load();
|
|
}
|
|
}
|
|
|
|
function startPlaying() {
|
|
if (detail) {
|
|
state = State.PLAYING;
|
|
videoEl.play();
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<div class="video-viewer {state}" class:detail class:unsupported={!supported}>
|
|
<div class="player" style="--icon-size: {detail ? 100 : 32}px">
|
|
{#if state === State.LOADING}
|
|
<Spinner />
|
|
{/if}
|
|
{#if state === State.LOADING || (!detail && state === State.PREVIEW)}
|
|
<!-- svelte-ignore a11y-mouse-events-have-key-events -->
|
|
<img
|
|
class="thumb"
|
|
src="{api.apiUrl}/thumb/{address}?mime=video"
|
|
alt="Preview for {address}"
|
|
loading="lazy"
|
|
on:load={() => (state = State.PREVIEW)}
|
|
on:mouseover={() => (state = State.PREVIEWING)}
|
|
on:error={() => (state = State.ERRORED)}
|
|
/>
|
|
{:else}
|
|
<!-- svelte-ignore a11y-media-has-caption -->
|
|
<video
|
|
preload={detail ? "auto" : "metadata"}
|
|
src="{api.apiUrl}/raw/{address}"
|
|
poster="{api.apiUrl}/thumb/{address}?mime=video"
|
|
on:mousemove={updatePreviewPosition}
|
|
on:mouseleave={resetPreview}
|
|
on:click|preventDefault={startPlaying}
|
|
controls={state === State.PLAYING}
|
|
bind:this={videoEl}
|
|
bind:currentTime
|
|
/>
|
|
{#if !supported}
|
|
<div class="unsupported-message">
|
|
<div class="label">
|
|
{$i18n.t("UNSUPPORTED FORMAT")}
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
{/if}
|
|
<div class="play-icon">
|
|
<Icon plain border name="play" />
|
|
</div>
|
|
<div
|
|
class="timecode"
|
|
bind:clientWidth={timeCodeWidth}
|
|
style:left={timeCodeLeft}
|
|
style:font-size={timeCodeSize}
|
|
>
|
|
{#if videoEl?.duration && currentTime}
|
|
{#if videoEl.duration > 3600}{String(
|
|
Math.floor(currentTime / 3600)
|
|
).padStart(2, "0")}:{/if}{String(
|
|
Math.floor((currentTime % 3600) / 60)
|
|
).padStart(2, "0")}:{String(
|
|
Math.floor((currentTime % 3600) % 60)
|
|
).padStart(2, "0")}
|
|
{:else if supported}
|
|
<Spinner />
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<style lang="scss">
|
|
.video-viewer {
|
|
min-width: 0;
|
|
min-height: 0;
|
|
|
|
&,
|
|
.player {
|
|
display: flex;
|
|
align-items: center;
|
|
min-height: 0;
|
|
flex-direction: column;
|
|
width: 100%;
|
|
}
|
|
|
|
img,
|
|
video {
|
|
width: 100%;
|
|
max-height: 100%;
|
|
min-height: 0;
|
|
object-fit: contain;
|
|
|
|
// background: rgba(128, 128, 128, 128);
|
|
|
|
transition: filter 0.2s;
|
|
}
|
|
|
|
.player {
|
|
position: relative;
|
|
}
|
|
|
|
.play-icon {
|
|
position: absolute;
|
|
top: 50%;
|
|
left: 50%;
|
|
transform: translate(-50%, -50%);
|
|
|
|
color: white;
|
|
font-size: var(--icon-size);
|
|
|
|
opacity: 0;
|
|
transition: opacity 0.2s;
|
|
|
|
pointer-events: none;
|
|
}
|
|
|
|
.timecode {
|
|
display: none;
|
|
pointer-events: none;
|
|
|
|
position: absolute;
|
|
top: 50%;
|
|
left: var(--left);
|
|
transform: translate(-50%, -50%);
|
|
|
|
font-feature-settings: "tnum", "zero";
|
|
font-weight: bold;
|
|
color: white;
|
|
opacity: 0.66;
|
|
}
|
|
|
|
&.unsupported.detail {
|
|
.play-icon {
|
|
display: none;
|
|
}
|
|
}
|
|
|
|
.unsupported-message {
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
background: rgba(1, 1, 1, 0.7);
|
|
pointer-events: none;
|
|
|
|
.label {
|
|
position: absolute;
|
|
top: 50%;
|
|
left: 0;
|
|
transform: translateY(-50%);
|
|
|
|
width: 100%;
|
|
text-align: center;
|
|
|
|
font-weight: bold;
|
|
|
|
color: darkred;
|
|
}
|
|
}
|
|
|
|
&.loading {
|
|
.player > * {
|
|
position: absolute;
|
|
left: 50%;
|
|
top: 50%;
|
|
transform: translate(-50%, -50%);
|
|
}
|
|
}
|
|
|
|
&.standby,
|
|
&.preview {
|
|
img,
|
|
video {
|
|
filter: brightness(0.75);
|
|
}
|
|
.play-icon {
|
|
opacity: 0.8;
|
|
}
|
|
}
|
|
|
|
&.previewing {
|
|
.timecode {
|
|
display: block;
|
|
}
|
|
video {
|
|
cursor: pointer;
|
|
}
|
|
}
|
|
}
|
|
</style>
|