157 lines
3.1 KiB
Svelte
157 lines
3.1 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.duration) {
|
|
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() {
|
|
if (detail) {
|
|
state = State.PLAYING;
|
|
videoEl.play();
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<div class="video-viewer {state}">
|
|
{#if state === State.LOADING}
|
|
<Spinner />
|
|
|
|
<img
|
|
class="thumb"
|
|
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 {
|
|
min-width: 0;
|
|
min-height: 0;
|
|
|
|
&,
|
|
.player {
|
|
display: flex;
|
|
align-items: center;
|
|
min-height: 0;
|
|
flex-direction: column;
|
|
}
|
|
|
|
video {
|
|
width: 100%;
|
|
max-height: 100%;
|
|
min-height: 0;
|
|
|
|
// 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: 0.07em solid white;
|
|
|
|
.icon {
|
|
margin-left: 0.04em;
|
|
}
|
|
|
|
opacity: 0;
|
|
transition: opacity 0.2s;
|
|
|
|
pointer-events: none;
|
|
}
|
|
|
|
&.preview {
|
|
video {
|
|
filter: brightness(0.75);
|
|
}
|
|
.play-icon {
|
|
opacity: 1;
|
|
}
|
|
}
|
|
|
|
.loading img {
|
|
min-width: 640px;
|
|
min-height: 480px;
|
|
background: gray;
|
|
}
|
|
|
|
&.previewing video {
|
|
cursor: pointer;
|
|
}
|
|
}
|
|
</style>
|