[ui] VideoViewer has mouseover previews
parent
3b82b0ee69
commit
4a8fd90f1f
|
@ -3,6 +3,7 @@
|
|||
import Spinner from "../utils/Spinner.svelte";
|
||||
import FragmentViewer from "./blobs/FragmentViewer.svelte";
|
||||
import ModelViewer from "./blobs/ModelViewer.svelte";
|
||||
import VideoViewer from "./blobs/VideoViewer.svelte";
|
||||
import HashBadge from "./HashBadge.svelte";
|
||||
|
||||
export let address: string;
|
||||
|
@ -48,6 +49,8 @@
|
|||
/>
|
||||
{:else if fragment}
|
||||
<FragmentViewer {address} detail={false} />
|
||||
{:else if video}
|
||||
<VideoViewer {address} detail={false} />
|
||||
{:else}
|
||||
<div class="image" class:loaded={imageLoaded == address || !handled}>
|
||||
{#if handled && imageLoaded != address}
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
import ImageViewer from "./blobs/ImageViewer.svelte";
|
||||
import ModelViewer from "./blobs/ModelViewer.svelte";
|
||||
import TextViewer from "./blobs/TextViewer.svelte";
|
||||
import VideoViewer from "./blobs/VideoViewer.svelte";
|
||||
import UpLink from "./UpLink.svelte";
|
||||
|
||||
export let address: string;
|
||||
|
@ -50,24 +51,7 @@
|
|||
<AudioViewer {address} {detail} {editable} />
|
||||
{/if}
|
||||
{#if video}
|
||||
{#if imageLoaded != address}
|
||||
<Spinner />
|
||||
|
||||
<img
|
||||
src="api/thumb/{address}"
|
||||
alt={address}
|
||||
on:load={() => (imageLoaded = address)}
|
||||
on:error={() => (imageLoaded = address)}
|
||||
/>
|
||||
{:else}
|
||||
<!-- svelte-ignore a11y-media-has-caption -->
|
||||
<video
|
||||
controls
|
||||
preload="auto"
|
||||
src="api/raw/{address}"
|
||||
poster="api/thumb/{address}"
|
||||
/>
|
||||
{/if}
|
||||
<VideoViewer detail {address} />
|
||||
{/if}
|
||||
{#if image}
|
||||
<ImageViewer {address} {editable} {detail} />
|
||||
|
@ -116,7 +100,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
video,
|
||||
img,
|
||||
.text {
|
||||
width: 100%;
|
||||
|
@ -136,8 +119,4 @@
|
|||
img {
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
video {
|
||||
background: rgba(128, 128, 128, 128);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -0,0 +1,135 @@
|
|||
<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>
|
|
@ -3,6 +3,7 @@
|
|||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export let plain = false;
|
||||
export let name: string;
|
||||
|
||||
if (!loaded) {
|
||||
|
@ -14,10 +15,10 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<i class="bx bx-{name}" />
|
||||
<i class="bx bx-{name}" class:plain />
|
||||
|
||||
<style>
|
||||
.bx {
|
||||
.bx:not(.plain) {
|
||||
font-size: 115%;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
|
Loading…
Reference in New Issue