[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
This commit is contained in:
parent
1b823bcce4
commit
164cdcd105
8 changed files with 229 additions and 163 deletions
|
@ -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)}
|
||||
|
|
|
@ -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}
|
||||
{#if fragment}
|
||||
<FragmentViewer {address} {detail} />
|
||||
<img
|
||||
src="api/thumb/{address}"
|
||||
alt="Thumbnail for {address}..."
|
||||
on:load={() => (imageLoaded = address)}
|
||||
on:error={() => (handled = false)}
|
||||
/>
|
||||
</div>
|
||||
{/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;
|
||||
}
|
||||
}
|
||||
|
||||
video {
|
||||
background: rgba(128, 128, 128, 128);
|
||||
.hashbadge {
|
||||
font-size: 64px;
|
||||
opacity: 0.25;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
|
|
143
webui/src/components/display/BlobViewer.svelte
Normal file
143
webui/src/components/display/BlobViewer.svelte
Normal 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>
|
|
@ -18,7 +18,6 @@
|
|||
<Spinner />
|
||||
{/if}
|
||||
{#if $entity}
|
||||
<UpLink to={{ entity: objectAddress }}>
|
||||
<img
|
||||
class="preview-image"
|
||||
class:imageLoaded
|
||||
|
@ -30,7 +29,6 @@
|
|||
on:load={() => (imageLoaded = true)}
|
||||
draggable="false"
|
||||
/>
|
||||
</UpLink>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
|
@ -45,6 +43,7 @@
|
|||
|
||||
img {
|
||||
max-width: 100%;
|
||||
min-height: 0;
|
||||
&.imageLoaded {
|
||||
border: 2px dashed colors.$yellow;
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
<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;
|
||||
|
|
|
@ -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>
|
|
@ -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
|
||||
|
||||
|
|
Loading…
Reference in a new issue