forked from MapComplete/MapComplete
		
	Merge develop
This commit is contained in:
		
						commit
						d1e7eba2db
					
				
					 19 changed files with 554 additions and 448 deletions
				
			
		|  | @ -1,6 +1,7 @@ | |||
| <script lang="ts"> | ||||
|   import { ImmutableStore, Store } from "../../Logic/UIEventSource" | ||||
|   import Icon from "../Map/Icon.svelte" | ||||
|   import { Utils } from "../../Utils" | ||||
| 
 | ||||
|   export let text: Store<string> | ||||
|   export let href: Store<string> | ||||
|  | @ -13,7 +14,7 @@ | |||
| </script> | ||||
| 
 | ||||
| <a | ||||
|   href={$href} | ||||
|   href={Utils.prepareHref($href)} | ||||
|   aria-label={$ariaLabel} | ||||
|   title={$ariaLabel} | ||||
|   target={$newTab ? "_blank" : undefined} | ||||
|  |  | |||
|  | @ -1,6 +1,10 @@ | |||
| <script lang="ts"> | ||||
|   import { Utils } from "../../Utils" | ||||
| 
 | ||||
|   export let text: string | ||||
|   export let href: string | ||||
| 
 | ||||
| 
 | ||||
|   export let classnames: string = undefined | ||||
|   export let download: string = undefined | ||||
|   export let ariaLabel: string = undefined | ||||
|  | @ -9,7 +13,7 @@ | |||
| </script> | ||||
| 
 | ||||
| <a | ||||
|   {href} | ||||
|   href={Utils.prepareHref(href)} | ||||
|   aria-label={ariaLabel} | ||||
|   title={ariaLabel} | ||||
|   target={newTab ? "_blank" : undefined} | ||||
|  |  | |||
|  | @ -15,6 +15,7 @@ | |||
|   import AccordionSingle from "../Flowbite/AccordionSingle.svelte" | ||||
|   import GlobeAlt from "@babeard/svelte-heroicons/mini/GlobeAlt" | ||||
|   import { ComparisonState } from "./ComparisonState" | ||||
|   import LoginToggle from "../Base/LoginToggle.svelte" | ||||
| 
 | ||||
|   export let externalData: Store< | ||||
|     | { success: { content: Record<string, string> } } | ||||
|  | @ -45,35 +46,38 @@ | |||
|   let enableLogin = state.featureSwitches.featureSwitchEnableLogin | ||||
| </script> | ||||
| 
 | ||||
| {#if !$sourceUrl || !$enableLogin} | ||||
|   <!-- empty block --> | ||||
| {:else if $externalData === undefined} | ||||
|   <Loading /> | ||||
| {:else if $externalData["error"] !== undefined} | ||||
|   <div class="subtle low-interaction rounded p-2 px-4 italic"> | ||||
|     <Tr t={Translations.t.external.error} /> | ||||
|   </div> | ||||
| {:else if $propertyKeysExternal.length === 0 && $knownImages.size + $unknownImages.length === 0} | ||||
|   <Tr cls="subtle" t={t.noDataLoaded} /> | ||||
| {:else if !$hasDifferencesAtStart} | ||||
| <LoginToggle {state} silentFail> | ||||
| 
 | ||||
|   {#if !$sourceUrl || !$enableLogin} | ||||
|     <!-- empty block --> | ||||
|   {:else if $externalData === undefined} | ||||
|     <Loading /> | ||||
|   {:else if $externalData["error"] !== undefined} | ||||
|     <div class="subtle low-interaction rounded p-2 px-4 italic"> | ||||
|       <Tr t={Translations.t.external.error} /> | ||||
|     </div> | ||||
|   {:else if $propertyKeysExternal.length === 0 && $knownImages.size + $unknownImages.length === 0} | ||||
|     <Tr cls="subtle" t={t.noDataLoaded} /> | ||||
|   {:else if !$hasDifferencesAtStart} | ||||
|   <span class="subtle text-sm"> | ||||
|     <Tr t={t.allIncluded.Subs({ source: $sourceUrl })} /> | ||||
|   </span> | ||||
| {:else if $comparisonState !== undefined} | ||||
|   <AccordionSingle expanded={!collapsed}> | ||||
|   {:else if $comparisonState !== undefined} | ||||
|     <AccordionSingle expanded={!collapsed}> | ||||
|     <span slot="header" class="flex"> | ||||
|       <GlobeAlt class="h-6 w-6" /> | ||||
|       <Tr t={Translations.t.external.title} /> | ||||
|     </span> | ||||
|     <ComparisonTable | ||||
|       externalProperties={$externalData["success"]} | ||||
|       {state} | ||||
|       {feature} | ||||
|       {layer} | ||||
|       {tags} | ||||
|       {readonly} | ||||
|       sourceUrl={$sourceUrl} | ||||
|       comparisonState={$comparisonState} | ||||
|     /> | ||||
|   </AccordionSingle> | ||||
| {/if} | ||||
|       <ComparisonTable | ||||
|         externalProperties={$externalData["success"]} | ||||
|         {state} | ||||
|         {feature} | ||||
|         {layer} | ||||
|         {tags} | ||||
|         {readonly} | ||||
|         sourceUrl={$sourceUrl} | ||||
|         comparisonState={$comparisonState} | ||||
|       /> | ||||
|     </AccordionSingle> | ||||
|   {/if} | ||||
| </LoginToggle> | ||||
|  |  | |||
|  | @ -6,6 +6,7 @@ | |||
|   import type { ProvidedImage } from "../../Logic/ImageProviders/ImageProvider" | ||||
|   import { Mapillary } from "../../Logic/ImageProviders/Mapillary" | ||||
|   import { UIEventSource } from "../../Logic/UIEventSource" | ||||
|   import { MagnifyingGlassPlusIcon } from "@babeard/svelte-heroicons/outline" | ||||
| 
 | ||||
|   export let image: Partial<ProvidedImage> | ||||
|   let fallbackImage: string = undefined | ||||
|  | @ -16,25 +17,37 @@ | |||
|   let imgEl: HTMLImageElement | ||||
|   export let imgClass: string = undefined | ||||
|   export let previewedImage: UIEventSource<ProvidedImage> = undefined | ||||
|   export let attributionFormat: "minimal" | "medium" | "large" = "medium" | ||||
|   let canZoom = previewedImage !== undefined // We check if there is a SOURCE, not if there is data in it! | ||||
|   let loaded = false | ||||
| </script> | ||||
| 
 | ||||
| <div class="relative shrink-0"> | ||||
|   <img | ||||
|     bind:this={imgEl} | ||||
|     class={imgClass ?? ""} | ||||
|     class:cursor-pointer={previewedImage !== undefined} | ||||
|     on:click={() => { | ||||
|   <div class="relative w-fit"> | ||||
|     <img | ||||
|       bind:this={imgEl} | ||||
|       on:load={() => loaded = true} | ||||
|       class={imgClass ?? ""} | ||||
|       class:cursor-zoom-in={previewedImage !== undefined} | ||||
|       on:click={() => { | ||||
|       previewedImage?.setData(image) | ||||
|     }} | ||||
|     on:error={() => { | ||||
|       on:error={() => { | ||||
|       if (fallbackImage) { | ||||
|         imgEl.src = fallbackImage | ||||
|       } | ||||
|     }} | ||||
|     src={image.url} | ||||
|   /> | ||||
|       src={image.url} | ||||
|     /> | ||||
| 
 | ||||
|     {#if canZoom && loaded} | ||||
|       <div class="absolute right-0 top-0 bg-black-transparent rounded-bl-full"> | ||||
|       <MagnifyingGlassPlusIcon class="w-8 h-8 pl-3 pb-3 cursor-zoom-in" color="white" /> | ||||
|       </div> | ||||
|     {/if} | ||||
| 
 | ||||
|   </div> | ||||
|   <div class="absolute bottom-0 left-0"> | ||||
|     <ImageAttribution {image} /> | ||||
|     <ImageAttribution {image} {attributionFormat} /> | ||||
|   </div> | ||||
| </div> | ||||
|  |  | |||
|  | @ -4,11 +4,15 @@ | |||
|   import { Store, UIEventSource } from "../../Logic/UIEventSource" | ||||
|   import ToSvelte from "../Base/ToSvelte.svelte" | ||||
|   import { EyeIcon } from "@rgossiaux/svelte-heroicons/solid" | ||||
|   import Tr from "../Base/Tr.svelte" | ||||
|   import Translations from "../i18n/Translations" | ||||
| 
 | ||||
|   /** | ||||
|    * A small element showing the attribution of a single image | ||||
|    */ | ||||
|   export let image: Partial<ProvidedImage> & { id: string; url: string } | ||||
|   export let attributionFormat: "minimal" | "medium" | "large" = "medium" | ||||
| 
 | ||||
|   let license: Store<LicenseInfo> = UIEventSource.FromPromise( | ||||
|     image.provider?.DownloadAttribution(image) | ||||
|   ) | ||||
|  | @ -16,50 +20,59 @@ | |||
| </script> | ||||
| 
 | ||||
| {#if $license !== undefined} | ||||
|   <div class="no-images flex items-center rounded-lg bg-black p-0.5 pl-3 pr-3 text-sm text-white"> | ||||
|   <div class="no-images flex items-center rounded-lg bg-black-transparent p-0.5 px-3 text-sm text-white"> | ||||
|     {#if icon !== undefined} | ||||
|       <div class="mr-2 h-6 w-6"> | ||||
|         <ToSvelte construct={icon} /> | ||||
|       </div> | ||||
|     {/if} | ||||
| 
 | ||||
|     <div class="flex flex-col"> | ||||
|       {#if $license.title} | ||||
|         {#if $license.informationLocation} | ||||
|           <a href={$license.informationLocation.href} target="_blank" rel="noopener nofollower"> | ||||
|             {$license.title} | ||||
|           </a> | ||||
|         {:else} | ||||
|           $license.title | ||||
|     <div class="flex gap-x-2" class:flex-col={attributionFormat !== "minimal"}> | ||||
|       {#if attributionFormat !== "minimal" } | ||||
|         {#if $license.title} | ||||
|           {#if $license.informationLocation} | ||||
|             <a href={$license.informationLocation.href} target="_blank" rel="noopener nofollower"> | ||||
|               {$license.title} | ||||
|             </a> | ||||
|           {:else} | ||||
|             $license.title | ||||
|           {/if} | ||||
|         {/if} | ||||
|       {/if} | ||||
| 
 | ||||
|       {#if $license.artist} | ||||
|         <div class="font-bold"> | ||||
|           {@html $license.artist} | ||||
|         </div> | ||||
|         {#if attributionFormat === "large"} | ||||
|           <Tr t={Translations.t.general.attribution.madeBy.Subs({author: $license.artist})} /> | ||||
|         {:else} | ||||
|           <div class="font-bold"> | ||||
|             {@html $license.artist} | ||||
|           </div> | ||||
|         {/if} | ||||
|       {/if} | ||||
| 
 | ||||
|       <div class="flex w-full justify-between gap-x-1"> | ||||
|         {#if $license.license !== undefined || $license.licenseShortName !== undefined} | ||||
|           <div> | ||||
|             {$license?.license ?? $license?.licenseShortName} | ||||
|           </div> | ||||
|         {/if} | ||||
| 
 | ||||
|         {#if $license.views} | ||||
|           <div class="flex justify-around self-center"> | ||||
|             <EyeIcon class="h-4 w-4 pr-1" /> | ||||
|             {$license.views} | ||||
|           </div> | ||||
|         {/if} | ||||
|       </div> | ||||
| 
 | ||||
|       {#if $license.date} | ||||
|         <div> | ||||
|           {$license.date.toLocaleDateString()} | ||||
|         </div> | ||||
|       {/if} | ||||
| 
 | ||||
|       {#if attributionFormat !== "minimal"} | ||||
|         <div class="flex w-full justify-between gap-x-1"> | ||||
|           {#if ($license.license !== undefined || $license.licenseShortName !== undefined)} | ||||
|             <div> | ||||
|               {$license?.license ?? $license?.licenseShortName} | ||||
|             </div> | ||||
|           {/if} | ||||
| 
 | ||||
|           {#if $license.views} | ||||
|             <div class="flex justify-around self-center text-xs"> | ||||
|               <EyeIcon class="h-4 w-4 pr-1" /> | ||||
|               {$license.views} | ||||
|             </div> | ||||
|           {/if} | ||||
|         </div> | ||||
|       {/if} | ||||
| 
 | ||||
|     </div> | ||||
|   </div> | ||||
| {/if} | ||||
|  |  | |||
|  | @ -38,9 +38,9 @@ | |||
|     class="pointer-events-none absolute bottom-0 left-0 flex w-full flex-wrap items-end justify-between" | ||||
|   > | ||||
|     <div | ||||
|       class="pointer-events-auto m-1 w-fit opacity-50 transition-colors duration-200 hover:opacity-100" | ||||
|       class="pointer-events-auto m-1 w-fit transition-colors duration-200" | ||||
|     > | ||||
|       <ImageAttribution {image} /> | ||||
|       <ImageAttribution {image} attributionFormat="large"/> | ||||
|     </div> | ||||
| 
 | ||||
|     <button | ||||
|  |  | |||
|  | @ -14,6 +14,8 @@ | |||
|   import AttributedImage from "./AttributedImage.svelte" | ||||
|   import SpecialTranslation from "../Popup/TagRendering/SpecialTranslation.svelte" | ||||
|   import LoginToggle from "../Base/LoginToggle.svelte" | ||||
|   import ImagePreview from "./ImagePreview.svelte" | ||||
|   import FloatOver from "../Base/FloatOver.svelte" | ||||
| 
 | ||||
|   export let tags: UIEventSource<OsmTags> | ||||
|   export let state: SpecialVisualizationState | ||||
|  | @ -31,7 +33,7 @@ | |||
|     key: undefined, | ||||
|     provider: AllImageProviders.byName(image.provider), | ||||
|     date: new Date(image.date), | ||||
|     id: Object.values(image.osmTags)[0], | ||||
|     id: Object.values(image.osmTags)[0] | ||||
|   } | ||||
| 
 | ||||
|   async function applyLink(isLinked: boolean) { | ||||
|  | @ -42,7 +44,7 @@ | |||
|     if (isLinked) { | ||||
|       const action = new LinkImageAction(currentTags.id, key, url, tags, { | ||||
|         theme: tags.data._orig_theme ?? state.layout.id, | ||||
|         changeType: "link-image", | ||||
|         changeType: "link-image" | ||||
|       }) | ||||
|       await state.changes.applyAction(action) | ||||
|     } else { | ||||
|  | @ -51,24 +53,26 @@ | |||
|         if (v === url) { | ||||
|           const action = new ChangeTagAction(currentTags.id, new Tag(k, ""), currentTags, { | ||||
|             theme: tags.data._orig_theme ?? state.layout.id, | ||||
|             changeType: "remove-image", | ||||
|             changeType: "remove-image" | ||||
|           }) | ||||
|           state.changes.applyAction(action) | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   isLinked.addCallback((isLinked) => applyLink(isLinked)) | ||||
| 
 | ||||
| </script> | ||||
| 
 | ||||
| <div class="flex w-fit shrink-0 flex-col"> | ||||
|   <div class="cursor-zoom-in" on:click={() => state.previewedImage.setData(providedImage)}> | ||||
|     <AttributedImage | ||||
|       image={providedImage} | ||||
|       imgClass="max-h-64 w-auto" | ||||
|       previewedImage={state.previewedImage} | ||||
|     /> | ||||
|   </div> | ||||
| <div class="flex w-fit shrink-0 flex-col rounded-lg overflow-hidden" class:border-interactive={$isLinked} | ||||
|      style="border-width: 2px"> | ||||
|   <AttributedImage | ||||
|     image={providedImage} | ||||
|     imgClass="max-h-64 w-auto" | ||||
|     previewedImage={state.previewedImage} | ||||
|     attributionFormat="minimal" | ||||
|   /> | ||||
|   <LoginToggle {state} silentFail={true}> | ||||
|     {#if linkable} | ||||
|       <label> | ||||
|  |  | |||
|  | @ -60,7 +60,7 @@ | |||
|       <Tr t={Translations.t.image.nearby.noNearbyImages} cls="alert" /> | ||||
|     {/if} | ||||
|   {:else} | ||||
|     <div class="flex w-full space-x-1 overflow-x-auto" style="scroll-snap-type: x proximity"> | ||||
|     <div class="flex w-full space-x-4 overflow-x-auto" style="scroll-snap-type: x proximity"> | ||||
|       {#each $result as image (image.pictureUrl)} | ||||
|         <span class="w-fit shrink-0" style="scroll-snap-align: start"> | ||||
|           <LinkableImage {tags} {image} {state} {feature} {layer} {linkable} /> | ||||
|  |  | |||
|  | @ -352,6 +352,7 @@ | |||
|           {/if} | ||||
|         </legend> | ||||
| 
 | ||||
|         <!-- Search menu --> | ||||
|         {#if config.mappings?.length >= 8 || hideMappingsUnlessSearchedFor} | ||||
|           <div class="sticky flex w-full" aria-hidden="true"> | ||||
|             <Search class="h-6 w-6" /> | ||||
|  | @ -369,6 +370,7 @@ | |||
|           {/if} | ||||
|         {/if} | ||||
| 
 | ||||
|         <!-- Actual options--> | ||||
|         {#if config?.freeform?.key && !(config?.mappings?.filter((m) => m.hideInAnswer != true)?.length > 0)} | ||||
|           <!-- There are no options to choose from, simply show the input element: fill out the text field --> | ||||
|           <FreeformInput | ||||
|  | @ -384,7 +386,7 @@ | |||
|           /> | ||||
|         {:else if config.mappings !== undefined && !config.multiAnswer} | ||||
|           <!-- Simple radiobuttons as mapping --> | ||||
|           <div class="flex flex-col"> | ||||
|           <div class="flex flex-col no-bold"> | ||||
|             {#each config.mappings as mapping, i (mapping.then)} | ||||
|               <!-- Even though we have a list of 'mappings' already, we still iterate over the list as to keep the original indices--> | ||||
|               <TagRenderingMappingInput | ||||
|  | @ -432,7 +434,7 @@ | |||
|           </div> | ||||
|         {:else if config.mappings !== undefined && config.multiAnswer} | ||||
|           <!-- Multiple answers can be chosen: checkboxes --> | ||||
|           <div class="flex flex-col"> | ||||
|           <div class="flex flex-col no-bold"> | ||||
|             {#each config.mappings as mapping, i (mapping.then)} | ||||
|               <TagRenderingMappingInput | ||||
|                 {mapping} | ||||
|  | @ -475,6 +477,8 @@ | |||
|             {/if} | ||||
|           </div> | ||||
|         {/if} | ||||
| 
 | ||||
|         <!-- Save and cancel buttons, in a logintoggle --> | ||||
|         <LoginToggle {state}> | ||||
|           <Loading slot="loading" /> | ||||
|           <SubtleButton slot="not-logged-in" on:click={() => state?.osmConnection?.AttemptLogin()}> | ||||
|  |  | |||
										
											
												File diff suppressed because it is too large
												Load diff
											
										
									
								
							|  | @ -3,7 +3,6 @@ import { Translation, TypedTranslation } from "./Translation" | |||
| import BaseUIElement from "../BaseUIElement" | ||||
| import CompiledTranslations from "../../assets/generated/CompiledTranslations" | ||||
| import LanguageUtils from "../../Utils/LanguageUtils" | ||||
| import { ClickableToggle } from "../Input/Toggle" | ||||
| import { Store } from "../../Logic/UIEventSource" | ||||
| import Locale from "./Locale" | ||||
| import { Utils } from "../../Utils" | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue