UX: show loading icon if images are being loaded

This commit is contained in:
Pieter Vander Vennet 2025-01-17 16:01:40 +01:00
parent d7509c8d6f
commit 32993df92a
7 changed files with 97 additions and 57 deletions

View file

@ -1136,18 +1136,6 @@ input[type="range"].range-lg::-moz-range-thumb {
right: 0px;
}
.left-24 {
left: 6rem;
}
.right-24 {
right: 6rem;
}
.top-56 {
top: 14rem;
}
.bottom-0 {
bottom: 0px;
}
@ -1332,10 +1320,6 @@ input[type="range"].range-lg::-moz-range-thumb {
margin: 0.5rem;
}
.m-8 {
margin: 2rem;
}
.m-0\.5 {
margin: 0.125rem;
}
@ -1356,6 +1340,10 @@ input[type="range"].range-lg::-moz-range-thumb {
margin: 1.25rem;
}
.m-8 {
margin: 2rem;
}
.m-14 {
margin: 3.5rem;
}
@ -1694,18 +1682,14 @@ input[type="range"].range-lg::-moz-range-thumb {
height: 2.25rem;
}
.h-24 {
height: 6rem;
.h-screen {
height: 100vh;
}
.h-full {
height: 100%;
}
.h-screen {
height: 100vh;
}
.h-fit {
height: -webkit-fit-content;
height: -moz-fit-content;
@ -1749,6 +1733,10 @@ input[type="range"].range-lg::-moz-range-thumb {
height: 0.75rem;
}
.h-80 {
height: 20rem;
}
.h-modal {
height: calc(100% - 2rem);
}
@ -1785,10 +1773,6 @@ input[type="range"].range-lg::-moz-range-thumb {
height: 16rem;
}
.h-80 {
height: 20rem;
}
.h-20 {
height: 5rem;
}
@ -1797,6 +1781,10 @@ input[type="range"].range-lg::-moz-range-thumb {
height: 9rem;
}
.h-24 {
height: 6rem;
}
.h-96 {
height: 24rem;
}
@ -2007,6 +1995,10 @@ input[type="range"].range-lg::-moz-range-thumb {
width: 0.75rem;
}
.w-60 {
width: 15rem;
}
.w-11 {
width: 2.75rem;
}
@ -2023,11 +2015,6 @@ input[type="range"].range-lg::-moz-range-thumb {
width: 3.5rem;
}
.w-max {
width: -webkit-max-content;
width: max-content;
}
.w-48 {
width: 12rem;
}
@ -3060,11 +3047,20 @@ input[type="range"].range-lg::-moz-range-thumb {
border-color: rgb(209 213 219 / var(--tw-border-opacity));
}
.border-transparent {
border-color: transparent;
}
.border-gray-600 {
--tw-border-opacity: 1;
border-color: rgb(75 85 99 / var(--tw-border-opacity));
}
.border-red-500 {
--tw-border-opacity: 1;
border-color: rgb(240 82 82 / var(--tw-border-opacity));
}
.border-gray-800 {
--tw-border-opacity: 1;
border-color: rgb(31 41 55 / var(--tw-border-opacity));
@ -3170,11 +3166,6 @@ input[type="range"].range-lg::-moz-range-thumb {
border-color: rgb(14 159 110 / var(--tw-border-opacity));
}
.border-red-500 {
--tw-border-opacity: 1;
border-color: rgb(240 82 82 / var(--tw-border-opacity));
}
.border-gray-700 {
--tw-border-opacity: 1;
border-color: rgb(55 65 81 / var(--tw-border-opacity));
@ -3185,10 +3176,6 @@ input[type="range"].range-lg::-moz-range-thumb {
border-color: rgb(239 86 47 / var(--tw-border-opacity));
}
.border-transparent {
border-color: transparent;
}
.border-red-300 {
--tw-border-opacity: 1;
border-color: rgb(248 180 180 / var(--tw-border-opacity));
@ -3262,6 +3249,11 @@ input[type="range"].range-lg::-moz-range-thumb {
background-color: rgb(249 128 128 / var(--tw-bg-opacity));
}
.bg-gray-400 {
--tw-bg-opacity: 1;
background-color: rgb(156 163 175 / var(--tw-bg-opacity));
}
.bg-slate-400 {
--tw-bg-opacity: 1;
background-color: rgb(148 163 184 / var(--tw-bg-opacity));
@ -3494,11 +3486,6 @@ input[type="range"].range-lg::-moz-range-thumb {
background-color: rgb(254 121 93 / var(--tw-bg-opacity));
}
.bg-gray-400 {
--tw-bg-opacity: 1;
background-color: rgb(156 163 175 / var(--tw-bg-opacity));
}
.bg-pink-500 {
--tw-bg-opacity: 1;
background-color: rgb(231 70 148 / var(--tw-bg-opacity));

View file

@ -22,7 +22,7 @@ export default class AllImageProviders {
...WikimediaImageProvider.commonsPrefixes,
...Mapillary.valuePrefixes,
...AllImageProviders.dontLoadFromPrefixes,
"Category:",
"Category:"
])
private static ImageAttributionSource: ImageProvider[] = [
@ -31,7 +31,7 @@ export default class AllImageProviders {
WikidataImageProvider.singleton,
WikimediaImageProvider.singleton,
Panoramax.singleton,
AllImageProviders.genericImageProvider,
AllImageProviders.genericImageProvider
]
public static apiUrls: string[] = [].concat(
...AllImageProviders.ImageAttributionSource.map((src) => src.apiUrls())
@ -44,7 +44,7 @@ export default class AllImageProviders {
mapillary: Mapillary.singleton,
wikidata: WikidataImageProvider.singleton,
wikimedia: WikimediaImageProvider.singleton,
panoramax: Panoramax.singleton,
panoramax: Panoramax.singleton
}
public static byName(name: string) {
@ -67,6 +67,28 @@ export default class AllImageProviders {
}
private static readonly _cachedImageStores: Record<string, Store<ProvidedImage[]>> = {}
/**
* Does a guess on the number of images that are probably there.
* Will simply count all image tags
*
* AllImageProviders.estimateNumberOfImages({image:"abc", "mapillary": "123", "panoramax:0"}) // => 3
*
*/
public static estimateNumberOfImages(tags: Record<string, string>, prefixes: string[] = undefined): number {
let count = 0
const allPrefixes = prefixes ?? [].concat(...AllImageProviders.ImageAttributionSource.map(s => s.defaultKeyPrefixes))
for (const k in tags) {
for (const prefix of allPrefixes) {
if (k === prefix || k.startsWith(prefix + ":")) {
count++
}
}
}
return count
}
/**
* Tries to extract all image data for this image. Cached on tags?.data?.id
*/
@ -108,7 +130,7 @@ export default class AllImageProviders {
*/
public static loadImagesFrom(urls: string[]): Store<ProvidedImage[]> {
const tags = {
id: urls.join(";"),
id: urls.join(";")
}
for (let i = 0; i < urls.length; i++) {
tags["image:" + i] = urls[i]

View file

@ -149,7 +149,7 @@ export default class PanoramaxImageProvider extends ImageProvider {
)
}
Stores.Chronic(1500, () => hasLoading(source.data)).addCallback(() => {
Stores.Chronic(5000, () => hasLoading(source.data)).addCallback(() => {
super.getRelevantUrlsFor(tags, prefixes).then((data) => {
source.set(data)
return !hasLoading(data)

View file

@ -0,0 +1,11 @@
<script lang="ts">
import Loading from "./Loading.svelte"
</script>
<div class="relative w-60 h-80">
<div class="animate-pulse w-full h-full bg-gray-400">
</div>
<div class="w-full h-full absolute top-0 flex items-center justify-center">
<Loading />
</div>
</div>

View file

@ -17,6 +17,7 @@
import Translations from "../i18n/Translations"
import Tr from "../Base/Tr.svelte"
import DotMenu from "../Base/DotMenu.svelte"
import LoadingPlaceholder from "../Base/LoadingPlaceholder.svelte"
export let image: Partial<ProvidedImage>
let fallbackImage: string = undefined
@ -111,7 +112,11 @@
<slot name="dot-menu-actions" />
</DotMenu>
{/if}
{#if !loaded}
<LoadingPlaceholder />
{/if}
<img
class:hidden={!loaded}
bind:this={imgEl}
on:load={() => (loaded = true)}
class={imgClass ?? ""}

View file

@ -3,14 +3,28 @@
import type { ProvidedImage } from "../../Logic/ImageProviders/ImageProvider"
import type { SpecialVisualizationState } from "../SpecialVisualization"
import DeletableImage from "./DeletableImage.svelte"
import Loading from "../Base/Loading.svelte"
import LoadingPlaceholder from "../Base/LoadingPlaceholder.svelte"
export let images: Store<ProvidedImage[]>
export let state: SpecialVisualizationState
export let tags: UIEventSource<Record<string, string>>
</script>
<div class="flex w-full space-x-2 overflow-x-auto" style="scroll-snap-type: x proximity">
{#each $images as image (image.url)}
<DeletableImage {image} {state} {tags} />
{/each}
</div>
export let estimated: Store<number>
images.addCallbackAndRun(imgs => {
console.log(">>><<< imgs are", imgs)
})
</script>
{#if $estimated > 0 && $images.length < 1}
<LoadingPlaceholder />
{:else}
<div class="w-full overflow-x-auto" style="scroll-snap-type: x proximity">
<div class="flex space-x-2">
{#each $images as image (image.url)}
<DeletableImage {image} {state} {tags} />
{/each}
</div>
</div>
{/if}

View file

@ -717,7 +717,8 @@ export default class SpecialVisualizations {
imagePrefixes = [].concat(...args.map((a) => a.split(",")))
}
const images = AllImageProviders.loadImagesFor(tags, imagePrefixes)
return new SvelteUIElement(ImageCarousel, { state, tags, images })
const estimated = tags.mapD(tags => AllImageProviders.estimateNumberOfImages(tags, imagePrefixes))
return new SvelteUIElement(ImageCarousel, { state, tags, images, estimated })
},
},
{