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; |   right: 0px; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .left-24 { |  | ||||||
|   left: 6rem; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .right-24 { |  | ||||||
|   right: 6rem; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .top-56 { |  | ||||||
|   top: 14rem; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .bottom-0 { | .bottom-0 { | ||||||
|   bottom: 0px; |   bottom: 0px; | ||||||
| } | } | ||||||
|  | @ -1332,10 +1320,6 @@ input[type="range"].range-lg::-moz-range-thumb { | ||||||
|   margin: 0.5rem; |   margin: 0.5rem; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .m-8 { |  | ||||||
|   margin: 2rem; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .m-0\.5 { | .m-0\.5 { | ||||||
|   margin: 0.125rem; |   margin: 0.125rem; | ||||||
| } | } | ||||||
|  | @ -1356,6 +1340,10 @@ input[type="range"].range-lg::-moz-range-thumb { | ||||||
|   margin: 1.25rem; |   margin: 1.25rem; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | .m-8 { | ||||||
|  |   margin: 2rem; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| .m-14 { | .m-14 { | ||||||
|   margin: 3.5rem; |   margin: 3.5rem; | ||||||
| } | } | ||||||
|  | @ -1694,18 +1682,14 @@ input[type="range"].range-lg::-moz-range-thumb { | ||||||
|   height: 2.25rem; |   height: 2.25rem; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .h-24 { | .h-screen { | ||||||
|   height: 6rem; |   height: 100vh; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .h-full { | .h-full { | ||||||
|   height: 100%; |   height: 100%; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .h-screen { |  | ||||||
|   height: 100vh; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .h-fit { | .h-fit { | ||||||
|   height: -webkit-fit-content; |   height: -webkit-fit-content; | ||||||
|   height: -moz-fit-content; |   height: -moz-fit-content; | ||||||
|  | @ -1749,6 +1733,10 @@ input[type="range"].range-lg::-moz-range-thumb { | ||||||
|   height: 0.75rem; |   height: 0.75rem; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | .h-80 { | ||||||
|  |   height: 20rem; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| .h-modal { | .h-modal { | ||||||
|   height: calc(100% - 2rem); |   height: calc(100% - 2rem); | ||||||
| } | } | ||||||
|  | @ -1785,10 +1773,6 @@ input[type="range"].range-lg::-moz-range-thumb { | ||||||
|   height: 16rem; |   height: 16rem; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .h-80 { |  | ||||||
|   height: 20rem; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .h-20 { | .h-20 { | ||||||
|   height: 5rem; |   height: 5rem; | ||||||
| } | } | ||||||
|  | @ -1797,6 +1781,10 @@ input[type="range"].range-lg::-moz-range-thumb { | ||||||
|   height: 9rem; |   height: 9rem; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | .h-24 { | ||||||
|  |   height: 6rem; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| .h-96 { | .h-96 { | ||||||
|   height: 24rem; |   height: 24rem; | ||||||
| } | } | ||||||
|  | @ -2007,6 +1995,10 @@ input[type="range"].range-lg::-moz-range-thumb { | ||||||
|   width: 0.75rem; |   width: 0.75rem; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | .w-60 { | ||||||
|  |   width: 15rem; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| .w-11 { | .w-11 { | ||||||
|   width: 2.75rem; |   width: 2.75rem; | ||||||
| } | } | ||||||
|  | @ -2023,11 +2015,6 @@ input[type="range"].range-lg::-moz-range-thumb { | ||||||
|   width: 3.5rem; |   width: 3.5rem; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .w-max { |  | ||||||
|   width: -webkit-max-content; |  | ||||||
|   width: max-content; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .w-48 { | .w-48 { | ||||||
|   width: 12rem; |   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-color: rgb(209 213 219 / var(--tw-border-opacity)); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | .border-transparent { | ||||||
|  |   border-color: transparent; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| .border-gray-600 { | .border-gray-600 { | ||||||
|   --tw-border-opacity: 1; |   --tw-border-opacity: 1; | ||||||
|   border-color: rgb(75 85 99 / var(--tw-border-opacity)); |   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 { | .border-gray-800 { | ||||||
|   --tw-border-opacity: 1; |   --tw-border-opacity: 1; | ||||||
|   border-color: rgb(31 41 55 / var(--tw-border-opacity)); |   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-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 { | .border-gray-700 { | ||||||
|   --tw-border-opacity: 1; |   --tw-border-opacity: 1; | ||||||
|   border-color: rgb(55 65 81 / var(--tw-border-opacity)); |   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-color: rgb(239 86 47 / var(--tw-border-opacity)); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .border-transparent { |  | ||||||
|   border-color: transparent; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .border-red-300 { | .border-red-300 { | ||||||
|   --tw-border-opacity: 1; |   --tw-border-opacity: 1; | ||||||
|   border-color: rgb(248 180 180 / var(--tw-border-opacity)); |   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)); |   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 { | .bg-slate-400 { | ||||||
|   --tw-bg-opacity: 1; |   --tw-bg-opacity: 1; | ||||||
|   background-color: rgb(148 163 184 / var(--tw-bg-opacity)); |   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)); |   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 { | .bg-pink-500 { | ||||||
|   --tw-bg-opacity: 1; |   --tw-bg-opacity: 1; | ||||||
|   background-color: rgb(231 70 148 / var(--tw-bg-opacity)); |   background-color: rgb(231 70 148 / var(--tw-bg-opacity)); | ||||||
|  |  | ||||||
|  | @ -22,7 +22,7 @@ export default class AllImageProviders { | ||||||
|         ...WikimediaImageProvider.commonsPrefixes, |         ...WikimediaImageProvider.commonsPrefixes, | ||||||
|         ...Mapillary.valuePrefixes, |         ...Mapillary.valuePrefixes, | ||||||
|         ...AllImageProviders.dontLoadFromPrefixes, |         ...AllImageProviders.dontLoadFromPrefixes, | ||||||
|         "Category:", |         "Category:" | ||||||
|     ]) |     ]) | ||||||
| 
 | 
 | ||||||
|     private static ImageAttributionSource: ImageProvider[] = [ |     private static ImageAttributionSource: ImageProvider[] = [ | ||||||
|  | @ -31,7 +31,7 @@ export default class AllImageProviders { | ||||||
|         WikidataImageProvider.singleton, |         WikidataImageProvider.singleton, | ||||||
|         WikimediaImageProvider.singleton, |         WikimediaImageProvider.singleton, | ||||||
|         Panoramax.singleton, |         Panoramax.singleton, | ||||||
|         AllImageProviders.genericImageProvider, |         AllImageProviders.genericImageProvider | ||||||
|     ] |     ] | ||||||
|     public static apiUrls: string[] = [].concat( |     public static apiUrls: string[] = [].concat( | ||||||
|         ...AllImageProviders.ImageAttributionSource.map((src) => src.apiUrls()) |         ...AllImageProviders.ImageAttributionSource.map((src) => src.apiUrls()) | ||||||
|  | @ -44,7 +44,7 @@ export default class AllImageProviders { | ||||||
|         mapillary: Mapillary.singleton, |         mapillary: Mapillary.singleton, | ||||||
|         wikidata: WikidataImageProvider.singleton, |         wikidata: WikidataImageProvider.singleton, | ||||||
|         wikimedia: WikimediaImageProvider.singleton, |         wikimedia: WikimediaImageProvider.singleton, | ||||||
|         panoramax: Panoramax.singleton, |         panoramax: Panoramax.singleton | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public static byName(name: string) { |     public static byName(name: string) { | ||||||
|  | @ -67,6 +67,28 @@ export default class AllImageProviders { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private static readonly _cachedImageStores: Record<string, Store<ProvidedImage[]>> = {} |     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 |      * 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[]> { |     public static loadImagesFrom(urls: string[]): Store<ProvidedImage[]> { | ||||||
|         const tags = { |         const tags = { | ||||||
|             id: urls.join(";"), |             id: urls.join(";") | ||||||
|         } |         } | ||||||
|         for (let i = 0; i < urls.length; i++) { |         for (let i = 0; i < urls.length; i++) { | ||||||
|             tags["image:" + i] = urls[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) => { |             super.getRelevantUrlsFor(tags, prefixes).then((data) => { | ||||||
|                 source.set(data) |                 source.set(data) | ||||||
|                 return !hasLoading(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 Translations from "../i18n/Translations" | ||||||
|   import Tr from "../Base/Tr.svelte" |   import Tr from "../Base/Tr.svelte" | ||||||
|   import DotMenu from "../Base/DotMenu.svelte" |   import DotMenu from "../Base/DotMenu.svelte" | ||||||
|  |   import LoadingPlaceholder from "../Base/LoadingPlaceholder.svelte" | ||||||
| 
 | 
 | ||||||
|   export let image: Partial<ProvidedImage> |   export let image: Partial<ProvidedImage> | ||||||
|   let fallbackImage: string = undefined |   let fallbackImage: string = undefined | ||||||
|  | @ -111,7 +112,11 @@ | ||||||
|           <slot name="dot-menu-actions" /> |           <slot name="dot-menu-actions" /> | ||||||
|         </DotMenu> |         </DotMenu> | ||||||
|       {/if} |       {/if} | ||||||
|  |       {#if !loaded} | ||||||
|  |         <LoadingPlaceholder /> | ||||||
|  |       {/if} | ||||||
|       <img |       <img | ||||||
|  |         class:hidden={!loaded} | ||||||
|         bind:this={imgEl} |         bind:this={imgEl} | ||||||
|         on:load={() => (loaded = true)} |         on:load={() => (loaded = true)} | ||||||
|         class={imgClass ?? ""} |         class={imgClass ?? ""} | ||||||
|  |  | ||||||
|  | @ -3,14 +3,28 @@ | ||||||
|   import type { ProvidedImage } from "../../Logic/ImageProviders/ImageProvider" |   import type { ProvidedImage } from "../../Logic/ImageProviders/ImageProvider" | ||||||
|   import type { SpecialVisualizationState } from "../SpecialVisualization" |   import type { SpecialVisualizationState } from "../SpecialVisualization" | ||||||
|   import DeletableImage from "./DeletableImage.svelte" |   import DeletableImage from "./DeletableImage.svelte" | ||||||
|  |   import Loading from "../Base/Loading.svelte" | ||||||
|  |   import LoadingPlaceholder from "../Base/LoadingPlaceholder.svelte" | ||||||
| 
 | 
 | ||||||
|   export let images: Store<ProvidedImage[]> |   export let images: Store<ProvidedImage[]> | ||||||
|   export let state: SpecialVisualizationState |   export let state: SpecialVisualizationState | ||||||
|   export let tags: UIEventSource<Record<string, string>> |   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)} |       {#each $images as image (image.url)} | ||||||
|         <DeletableImage {image} {state} {tags} /> |         <DeletableImage {image} {state} {tags} /> | ||||||
|       {/each} |       {/each} | ||||||
| </div> |     </div> | ||||||
|  |   </div> | ||||||
|  | {/if} | ||||||
|  |  | ||||||
|  | @ -717,7 +717,8 @@ export default class SpecialVisualizations { | ||||||
|                         imagePrefixes = [].concat(...args.map((a) => a.split(","))) |                         imagePrefixes = [].concat(...args.map((a) => a.split(","))) | ||||||
|                     } |                     } | ||||||
|                     const images = AllImageProviders.loadImagesFor(tags, imagePrefixes) |                     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