[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 { GROUP_TYPE_ADDR } from "upend/constants";
|
||||||
import { deleteEntry, putEntityAttribute, putEntry } from "../lib/api";
|
import { deleteEntry, putEntityAttribute, putEntry } from "../lib/api";
|
||||||
import Icon from "./utils/Icon.svelte";
|
import Icon from "./utils/Icon.svelte";
|
||||||
|
import BlobViewer from "./display/BlobViewer.svelte";
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
|
|
||||||
|
@ -227,7 +228,7 @@
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
{/if}
|
{/if}
|
||||||
<BlobPreview {address} {editable} {detail} />
|
<BlobViewer {address} {editable} {detail} />
|
||||||
<NotesEditor {address} {editable} on:change={onChange} />
|
<NotesEditor {address} {editable} on:change={onChange} />
|
||||||
{#if !$error}
|
{#if !$error}
|
||||||
{#if Boolean($allTypeEntries)}
|
{#if Boolean($allTypeEntries)}
|
||||||
|
|
|
@ -1,15 +1,11 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { useEntity } from "../../lib/entity";
|
import { useEntity } from "../../lib/entity";
|
||||||
import Spinner from "../utils/Spinner.svelte";
|
import Spinner from "../utils/Spinner.svelte";
|
||||||
import AudioViewer from "./blobs/AudioViewer.svelte";
|
|
||||||
import FragmentViewer from "./blobs/FragmentViewer.svelte";
|
import FragmentViewer from "./blobs/FragmentViewer.svelte";
|
||||||
import ImageViewer from "./blobs/ImageViewer.svelte";
|
|
||||||
import ModelViewer from "./blobs/ModelViewer.svelte";
|
import ModelViewer from "./blobs/ModelViewer.svelte";
|
||||||
import TextViewer from "./blobs/TextViewer.svelte";
|
import HashBadge from "./HashBadge.svelte";
|
||||||
|
|
||||||
export let address: string;
|
export let address: string;
|
||||||
export let editable: boolean;
|
|
||||||
export let detail: boolean;
|
|
||||||
|
|
||||||
$: ({ entity, entityInfo } = useEntity(address));
|
$: ({ entity, entityInfo } = useEntity(address));
|
||||||
|
|
||||||
|
@ -37,48 +33,10 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if handled}
|
{#if handled}
|
||||||
<div class="preview" class:detail class:image>
|
<div class="preview">
|
||||||
{#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}
|
{#if model}
|
||||||
<ModelViewer src="api/raw/{address}" />
|
<ModelViewer lookonly src="api/raw/{address}" />
|
||||||
{/if}
|
{:else if web}
|
||||||
{#if web}
|
|
||||||
{#if imageLoaded != address}
|
{#if imageLoaded != address}
|
||||||
<Spinner />
|
<Spinner />
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -88,53 +46,56 @@
|
||||||
on:load={() => (imageLoaded = address)}
|
on:load={() => (imageLoaded = address)}
|
||||||
on:error={() => (handled = false)}
|
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}
|
||||||
{#if fragment}
|
</div>
|
||||||
<FragmentViewer {address} {detail} />
|
{:else}
|
||||||
{/if}
|
<div class="hashbadge">
|
||||||
|
<HashBadge {address} />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style lang="scss">
|
||||||
.preview {
|
.preview {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
max-height: 25em;
|
max-height: 25em;
|
||||||
|
|
||||||
&.detail {
|
|
||||||
max-height: 50vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.detail.image {
|
|
||||||
max-height: unset;
|
|
||||||
flex-grow: 1;
|
|
||||||
min-height: 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
video,
|
.image {
|
||||||
img,
|
|
||||||
.text {
|
|
||||||
width: 100%;
|
|
||||||
max-height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
iframe {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.text {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
margin-bottom: 1rem;
|
min-height: 0;
|
||||||
|
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
padding: 0.5em;
|
||||||
|
|
||||||
|
img {
|
||||||
|
max-width: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
img {
|
.hashbadge {
|
||||||
object-fit: contain;
|
font-size: 64px;
|
||||||
}
|
opacity: 0.25;
|
||||||
|
text-align: center;
|
||||||
video {
|
|
||||||
background: rgba(128, 128, 128, 128);
|
|
||||||
}
|
}
|
||||||
</style>
|
</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,19 +18,17 @@
|
||||||
<Spinner />
|
<Spinner />
|
||||||
{/if}
|
{/if}
|
||||||
{#if $entity}
|
{#if $entity}
|
||||||
<UpLink to={{ entity: objectAddress }}>
|
<img
|
||||||
<img
|
class="preview-image"
|
||||||
class="preview-image"
|
class:imageLoaded
|
||||||
class:imageLoaded
|
src="api/{detail ? 'raw' : 'thumb'}/{objectAddress}#{$entity?.get(
|
||||||
src="api/{detail ? 'raw' : 'thumb'}/{objectAddress}#{$entity?.get(
|
'W3C_FRAGMENT_SELECTOR'
|
||||||
'W3C_FRAGMENT_SELECTOR'
|
)}"
|
||||||
)}"
|
use:xywh
|
||||||
use:xywh
|
alt={address}
|
||||||
alt={address}
|
on:load={() => (imageLoaded = true)}
|
||||||
on:load={() => (imageLoaded = true)}
|
draggable="false"
|
||||||
draggable="false"
|
/>
|
||||||
/>
|
|
||||||
</UpLink>
|
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -45,6 +43,7 @@
|
||||||
|
|
||||||
img {
|
img {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
|
min-height: 0;
|
||||||
&.imageLoaded {
|
&.imageLoaded {
|
||||||
border: 2px dashed colors.$yellow;
|
border: 2px dashed colors.$yellow;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
export let src: string;
|
export let src: string;
|
||||||
|
export let lookonly = false;
|
||||||
|
|
||||||
import { onMount } from "svelte";
|
import { onMount } from "svelte";
|
||||||
let root: HTMLElement;
|
let root: HTMLElement;
|
||||||
|
@ -64,10 +65,14 @@
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="modelviewer" bind:this={root} />
|
<div class="modelviewer" class:lookonly bind:this={root} />
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.modelviewer {
|
.modelviewer {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.modelviewer.lookonly {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -4,7 +4,9 @@
|
||||||
import type { UpEntry, UpListing } from "upend";
|
import type { UpEntry, UpListing } from "upend";
|
||||||
import { query } from "../../lib/entity";
|
import { query } from "../../lib/entity";
|
||||||
import { defaultEntitySort, entityValueSort } from "../../util/sort";
|
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 let entries: UpEntry[];
|
||||||
export const editable = false;
|
export const editable = false;
|
||||||
|
@ -91,17 +93,32 @@
|
||||||
<div class="gallery">
|
<div class="gallery">
|
||||||
{#each sortedAttributes as entry (entry.address)}
|
{#each sortedAttributes as entry (entry.address)}
|
||||||
<div class="thumbnail">
|
<div class="thumbnail">
|
||||||
<Thumbnail
|
<UpLink
|
||||||
address={String(showEntities ? entry.entity : entry.value.c)}
|
to={{ entity: String(showEntities ? entry.entity : entry.value.c) }}
|
||||||
on:resolved={(event) => {
|
>
|
||||||
addSortKeys(String(entry.value.c), event.detail);
|
<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>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
.thumbnail {
|
||||||
|
border: 1px solid var(--foreground);
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
.gallery {
|
.gallery {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 1rem;
|
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.":
|
"upend@file:../tools/upend_js::locator=svelte-app%40workspace%3A.":
|
||||||
version: 0.0.1
|
version: 0.0.1
|
||||||
resolution: "upend@file:../tools/upend_js#../tools/upend_js::hash=a988d5&locator=svelte-app%40workspace%3A."
|
resolution: "upend@file:../tools/upend_js#../tools/upend_js::hash=ce63a9&locator=svelte-app%40workspace%3A."
|
||||||
checksum: 11b26f7703c0c8e750b7c4667dd025d8eee1964eee1ae9a11e1b3038bda34bc28053dc5b6c31a780857b4f884d872a425a2b19c7a20ce8dbd87997b51c94fd60
|
checksum: d9276c2767aa3a543e1b3dfc3243e3cb9b35cc3afacba8a7daae04b63edc5fd39e7fe5befa8c5a1b6b91760cb5c851ecdc547f9cf6ae6c5342867636e5bd0b18
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue