136 lines
2.8 KiB
Svelte
136 lines
2.8 KiB
Svelte
|
<script lang="ts">
|
||
|
import { debounce, throttle } from "lodash";
|
||
|
import Icon from "../../utils/Icon.svelte";
|
||
|
import Spinner from "../../utils/Spinner.svelte";
|
||
|
export let address: string;
|
||
|
export let detail: boolean;
|
||
|
|
||
|
enum State {
|
||
|
LOADING = "loading",
|
||
|
PREVIEW = "preview",
|
||
|
PREVIEWING = "previewing",
|
||
|
PLAYING = "playing",
|
||
|
ERRORED = "errored",
|
||
|
}
|
||
|
let state = State.PREVIEW;
|
||
|
|
||
|
let videoEl: HTMLVideoElement;
|
||
|
|
||
|
const seek = throttle((progress: number) => {
|
||
|
if (state === State.PREVIEWING) {
|
||
|
videoEl.currentTime = videoEl.duration * progress;
|
||
|
}
|
||
|
}, 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() {
|
||
|
state = State.PLAYING;
|
||
|
videoEl.play();
|
||
|
}
|
||
|
</script>
|
||
|
|
||
|
<div class="video-viewer {state}">
|
||
|
{#if state === State.LOADING}
|
||
|
<Spinner />
|
||
|
|
||
|
<img
|
||
|
src="api/thumb/{address}"
|
||
|
alt={address}
|
||
|
on:load={() => (state = State.PREVIEW)}
|
||
|
on:error={() => (state = State.ERRORED)}
|
||
|
/>
|
||
|
{:else}
|
||
|
<div class="player" style="--icon-size: {detail ? 100 : 32}px">
|
||
|
<!-- svelte-ignore a11y-media-has-caption -->
|
||
|
<video
|
||
|
preload={detail ? "auto" : "metadata"}
|
||
|
src="api/raw/{address}"
|
||
|
poster="api/thumb/{address}"
|
||
|
on:mousemove={updatePreviewPosition}
|
||
|
on:mouseleave={resetPreview}
|
||
|
on:click={startPlaying}
|
||
|
controls={state === State.PLAYING}
|
||
|
bind:this={videoEl}
|
||
|
/>
|
||
|
<div class="play-icon">
|
||
|
<div class="icon">
|
||
|
<Icon plain name="play" />
|
||
|
</div>
|
||
|
</div>
|
||
|
</div>
|
||
|
{/if}
|
||
|
</div>
|
||
|
|
||
|
<style lang="scss">
|
||
|
.video-viewer {
|
||
|
video {
|
||
|
width: 100%;
|
||
|
max-height: 100%;
|
||
|
|
||
|
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%);
|
||
|
|
||
|
$size: var(--icon-size);
|
||
|
|
||
|
width: $size;
|
||
|
height: $size;
|
||
|
|
||
|
color: white;
|
||
|
font-size: $size;
|
||
|
line-height: 1;
|
||
|
|
||
|
border-radius: calc($size / 4);
|
||
|
border: .07em solid white;
|
||
|
|
||
|
.icon {
|
||
|
margin-left: .04em;
|
||
|
}
|
||
|
|
||
|
opacity: 0;
|
||
|
transition: opacity 0.2s;
|
||
|
|
||
|
pointer-events: none;
|
||
|
}
|
||
|
|
||
|
&.preview {
|
||
|
video {
|
||
|
filter: brightness(0.75);
|
||
|
}
|
||
|
.play-icon {
|
||
|
opacity: 1;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
&.previewing video {
|
||
|
cursor: pointer;
|
||
|
}
|
||
|
}
|
||
|
</style>
|