[ui] use blobpreview for gallery previews (fragments, models, web get thumbnails)

- modelviewer also has no-interaction mode to fix scrolling in gallery view
- fragmentviewer doesn't link directly, fix thumbnail click
feat/vaults
Tomáš Mládek 2022-03-18 21:27:24 +01:00
parent 1b823bcce4
commit 164cdcd105
8 changed files with 229 additions and 163 deletions

View File

@ -18,6 +18,7 @@
import { GROUP_TYPE_ADDR } from "upend/constants";
import { deleteEntry, putEntityAttribute, putEntry } from "../lib/api";
import Icon from "./utils/Icon.svelte";
import BlobViewer from "./display/BlobViewer.svelte";
const dispatch = createEventDispatcher();
const params = useParams();
@ -227,7 +228,7 @@
</div>
</section>
{/if}
<BlobPreview {address} {editable} {detail} />
<BlobViewer {address} {editable} {detail} />
<NotesEditor {address} {editable} on:change={onChange} />
{#if !$error}
{#if Boolean($allTypeEntries)}

View File

@ -1,15 +1,11 @@
<script lang="ts">
import { useEntity } from "../../lib/entity";
import Spinner from "../utils/Spinner.svelte";
import AudioViewer from "./blobs/AudioViewer.svelte";
import FragmentViewer from "./blobs/FragmentViewer.svelte";
import ImageViewer from "./blobs/ImageViewer.svelte";
import ModelViewer from "./blobs/ModelViewer.svelte";
import TextViewer from "./blobs/TextViewer.svelte";
import HashBadge from "./HashBadge.svelte";
export let address: string;
export let editable: boolean;
export let detail: boolean;
$: ({ entity, entityInfo } = useEntity(address));
@ -37,48 +33,10 @@
</script>
{#if handled}
<div class="preview" class:detail class:image>
{#if text}
<div class="text">
<TextViewer {address} />
</div>
{/if}
{#if audio}
<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}
{/if}
{#if image}
<ImageViewer {address} {editable} {detail} />
{/if}
{#if pdf}
<iframe
src="api/raw/{address}?inline"
title="PDF document of {address}"
/>
{/if}
<div class="preview">
{#if model}
<ModelViewer src="api/raw/{address}" />
{/if}
{#if web}
<ModelViewer lookonly src="api/raw/{address}" />
{:else if web}
{#if imageLoaded != address}
<Spinner />
{/if}
@ -88,53 +46,56 @@
on:load={() => (imageLoaded = address)}
on:error={() => (handled = false)}
/>
{:else if fragment}
<FragmentViewer {address} detail={false} />
{:else}
<div class="image" class:loaded={imageLoaded == address || !handled}>
{#if handled && imageLoaded != address}
<div class="spinner">
<Spinner centered />
</div>
{/if}
<img
src="api/thumb/{address}"
alt="Thumbnail for {address}..."
on:load={() => (imageLoaded = address)}
on:error={() => (handled = false)}
/>
</div>
{/if}
{#if fragment}
<FragmentViewer {address} {detail} />
{/if}
</div>
{:else}
<div class="hashbadge">
<HashBadge {address} />
</div>
{/if}
<style scoped lang="scss">
<style lang="scss">
.preview {
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
max-height: 25em;
&.detail {
max-height: 50vh;
}
&.detail.image {
max-height: unset;
flex-grow: 1;
min-height: 0;
}
}
video,
img,
.text {
width: 100%;
max-height: 100%;
}
iframe {
width: 100%;
}
.text {
.image {
display: flex;
margin-bottom: 1rem;
min-height: 0;
justify-content: center;
padding: 0.5em;
img {
max-width: 100%;
object-fit: contain;
}
}
img {
object-fit: contain;
}
video {
background: rgba(128, 128, 128, 128);
.hashbadge {
font-size: 64px;
opacity: 0.25;
text-align: center;
}
</style>

View File

@ -0,0 +1,143 @@
<script lang="ts">
import { useEntity } from "../../lib/entity";
import Spinner from "../utils/Spinner.svelte";
import AudioViewer from "./blobs/AudioViewer.svelte";
import FragmentViewer from "./blobs/FragmentViewer.svelte";
import ImageViewer from "./blobs/ImageViewer.svelte";
import ModelViewer from "./blobs/ModelViewer.svelte";
import TextViewer from "./blobs/TextViewer.svelte";
import UpLink from "./UpLink.svelte";
export let address: string;
export let editable: boolean;
export let detail: boolean;
$: ({ entity, entityInfo } = useEntity(address));
$: mimeType = String($entity?.get("FILE_MIME"));
$: audio = ["audio", "application/x-riff"].some((p) =>
mimeType.startsWith(p)
);
$: video = ["video", "application/x-matroska"].some((p) =>
mimeType.startsWith(p)
);
$: image = mimeType.startsWith("image");
$: text = mimeType.startsWith("text");
$: pdf = mimeType.startsWith("application/pdf");
$: model =
mimeType?.startsWith("model") ||
$entity?.identify().some((l) => l.endsWith(".stl"));
$: web = $entityInfo?.t == "Url";
$: fragment = Boolean($entity?.get("ANNOTATES"));
$: handled =
audio || video || image || text || pdf || model || web || fragment;
let imageLoaded = null;
</script>
{#if handled}
<div class="preview" class:detail class:image>
{#if text}
<div class="text">
<TextViewer {address} />
</div>
{/if}
{#if audio}
<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}
{/if}
{#if image}
<ImageViewer {address} {editable} {detail} />
{/if}
{#if pdf}
<iframe
src="api/raw/{address}?inline"
title="PDF document of {address}"
/>
{/if}
{#if model}
<ModelViewer src="api/raw/{address}" />
{/if}
{#if web}
{#if imageLoaded != address}
<Spinner />
{/if}
<img
src={String($entity?.get("OG_IMAGE"))}
alt="OpenGraph image for {$entityInfo?.t == 'Url' && $entityInfo?.c}"
on:load={() => (imageLoaded = address)}
on:error={() => (handled = false)}
/>
{/if}
{#if fragment}
<UpLink to={{ entity: String($entity.get("ANNOTATES")) }}>
<FragmentViewer {address} {detail} />
</UpLink>
{/if}
</div>
{/if}
<style scoped lang="scss">
.preview {
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
max-height: 25em;
&.detail {
max-height: 50vh;
}
&.detail.image {
max-height: unset;
flex-grow: 1;
min-height: 0;
}
}
video,
img,
.text {
width: 100%;
max-height: 100%;
}
iframe {
width: 100%;
}
.text {
display: flex;
margin-bottom: 1rem;
}
img {
object-fit: contain;
}
video {
background: rgba(128, 128, 128, 128);
}
</style>

View File

@ -18,19 +18,17 @@
<Spinner />
{/if}
{#if $entity}
<UpLink to={{ entity: objectAddress }}>
<img
class="preview-image"
class:imageLoaded
src="api/{detail ? 'raw' : 'thumb'}/{objectAddress}#{$entity?.get(
'W3C_FRAGMENT_SELECTOR'
)}"
use:xywh
alt={address}
on:load={() => (imageLoaded = true)}
draggable="false"
/>
</UpLink>
<img
class="preview-image"
class:imageLoaded
src="api/{detail ? 'raw' : 'thumb'}/{objectAddress}#{$entity?.get(
'W3C_FRAGMENT_SELECTOR'
)}"
use:xywh
alt={address}
on:load={() => (imageLoaded = true)}
draggable="false"
/>
{/if}
</div>
@ -45,6 +43,7 @@
img {
max-width: 100%;
min-height: 0;
&.imageLoaded {
border: 2px dashed colors.$yellow;
}

View File

@ -1,5 +1,6 @@
<script lang="ts">
export let src: string;
export let lookonly = false;
import { onMount } from "svelte";
let root: HTMLElement;
@ -64,10 +65,14 @@
});
</script>
<div class="modelviewer" bind:this={root} />
<div class="modelviewer" class:lookonly bind:this={root} />
<style>
.modelviewer {
width: 100%;
}
.modelviewer.lookonly {
pointer-events: none;
}
</style>

View File

@ -4,7 +4,9 @@
import type { UpEntry, UpListing } from "upend";
import { query } from "../../lib/entity";
import { defaultEntitySort, entityValueSort } from "../../util/sort";
import Thumbnail from "./gallery/Thumbnail.svelte";
import BlobPreview from "../display/BlobPreview.svelte";
import UpLink from "../display/UpLink.svelte";
import UpObject from "../display/UpObject.svelte";
export let entries: UpEntry[];
export const editable = false;
@ -91,17 +93,32 @@
<div class="gallery">
{#each sortedAttributes as entry (entry.address)}
<div class="thumbnail">
<Thumbnail
address={String(showEntities ? entry.entity : entry.value.c)}
on:resolved={(event) => {
addSortKeys(String(entry.value.c), event.detail);
}}
/>
<UpLink
to={{ entity: String(showEntities ? entry.entity : entry.value.c) }}
>
<BlobPreview
address={String(showEntities ? entry.entity : entry.value.c)}
/>
<div class="label">
<UpObject
address={String(showEntities ? entry.entity : entry.value.c)}
on:resolved={(event) => {
addSortKeys(String(entry.value.c), event.detail);
}}
/>
</div>
</UpLink>
</div>
{/each}
</div>
<style lang="scss">
.thumbnail {
border: 1px solid var(--foreground);
border-radius: 4px;
padding: 0.5em;
}
.gallery {
display: flex;
gap: 1rem;

View File

@ -1,60 +0,0 @@
<script lang="ts">
import UpObject from "../../display/UpObject.svelte";
import HashBadge from "../../display/HashBadge.svelte";
import Spinner from "../../utils/Spinner.svelte";
import UpLink from "../../display/UpLink.svelte";
export let address: string;
let loaded = "";
let handled = true;
let identity = address;
</script>
<UpLink to={{ entity: address }}>
<div class="thumbnail">
<div class="image" class:loaded={loaded == address || !handled}>
{#if handled && loaded != address}
<div class="spinner">
<Spinner centered />
</div>
{/if}
{#if handled}
<img
src="api/thumb/{address}"
alt="Thumbnail for {identity}..."
on:load={() => (loaded = address)}
on:error={() => (handled = false)}
/>
{:else}
<div class="hashbadge">
<HashBadge {address} />
</div>
{/if}
</div>
<div class="label">
<UpObject {address} on:resolved />
</div>
</div>
</UpLink>
<style lang="scss">
.thumbnail {
border: 1px solid var(--foreground);
border-radius: 4px;
padding: 0.5em;
}
.image.loaded {
text-align: center;
}
img {
max-width: 100%;
}
.hashbadge {
font-size: 64px;
opacity: 0.25;
}
</style>

View File

@ -4669,8 +4669,8 @@ __metadata:
"upend@file:../tools/upend_js::locator=svelte-app%40workspace%3A.":
version: 0.0.1
resolution: "upend@file:../tools/upend_js#../tools/upend_js::hash=a988d5&locator=svelte-app%40workspace%3A."
checksum: 11b26f7703c0c8e750b7c4667dd025d8eee1964eee1ae9a11e1b3038bda34bc28053dc5b6c31a780857b4f884d872a425a2b19c7a20ce8dbd87997b51c94fd60
resolution: "upend@file:../tools/upend_js#../tools/upend_js::hash=ce63a9&locator=svelte-app%40workspace%3A."
checksum: d9276c2767aa3a543e1b3dfc3243e3cb9b35cc3afacba8a7daae04b63edc5fd39e7fe5befa8c5a1b6b91760cb5c851ecdc547f9cf6ae6c5342867636e5bd0b18
languageName: node
linkType: hard