forked from MapComplete/MapComplete
		
	UX: show loading icon if images are being loaded
This commit is contained in:
		
							parent
							
								
									d7509c8d6f
								
							
						
					
					
						commit
						32993df92a
					
				
					 7 changed files with 97 additions and 57 deletions
				
			
		|  | @ -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)); | ||||
|  |  | |||
|  | @ -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] | ||||
|  |  | |||
|  | @ -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) | ||||
|  |  | |||
							
								
								
									
										11
									
								
								src/UI/Base/LoadingPlaceholder.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								src/UI/Base/LoadingPlaceholder.svelte
									
										
									
									
									
										Normal 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> | ||||
|  | @ -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 ?? ""} | ||||
|  |  | |||
|  | @ -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"> | ||||
|   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> | ||||
|   </div> | ||||
| {/if} | ||||
|  |  | |||
|  | @ -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 }) | ||||
|                 }, | ||||
|             }, | ||||
|             { | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue