forked from MapComplete/MapComplete
		
	Chore: rework image uploading, should work better now
This commit is contained in:
		
							parent
							
								
									6f5b0622a5
								
							
						
					
					
						commit
						94ba18785d
					
				
					 17 changed files with 548 additions and 238 deletions
				
			
		|  | @ -344,8 +344,8 @@ | |||
|         }, | ||||
|         "useSearch": "Use the search above to see presets", | ||||
|         "useSearchForMore": "Use the search function to search within {total} more values…", | ||||
|         "waitingForGeopermission": "Waiting for your permission to use the geolocation...", | ||||
|         "waitingForLocation": "Searching your current location...", | ||||
|         "waitingForGeopermission": "Waiting for your permission to use the geolocation…", | ||||
|         "waitingForLocation": "Searching your current location…", | ||||
|         "weekdays": { | ||||
|             "abbreviations": { | ||||
|                 "friday": "Fri", | ||||
|  | @ -416,6 +416,22 @@ | |||
|         "pleaseLogin": "Please log in to add a picture", | ||||
|         "respectPrivacy": "Do not photograph people nor license plates. Do not upload Google Maps, Google Streetview or other copyrighted sources.", | ||||
|         "toBig": "Your image is too large as it is {actual_size}. Please use images of at most {max_size}", | ||||
|         "upload": { | ||||
|             "failReasons": "You might have lost connection to the internet", | ||||
|             "failReasonsAdvanced": "Alternatively, make sure your browser and extensions do not block third-party API's.", | ||||
|             "multiple": { | ||||
|                 "done": "{count} images are successfully uploaded. Thank you!", | ||||
|                 "partiallyDone": "{count} images are getting uploaded, {done} images are done…", | ||||
|                 "someFailed": "Sorry, we could not upload {count} images", | ||||
|                 "uploading": "{count} images are getting uploaded…" | ||||
|             }, | ||||
|             "one": { | ||||
|                 "done": "Your image was successfully uploaded. Thank you!", | ||||
|                 "failed": "Sorry, we could not upload your image", | ||||
|                 "retrying": "Your image is getting uploaded again…", | ||||
|                 "uploading": "Your image is getting uploaded…" | ||||
|             } | ||||
|         }, | ||||
|         "uploadDone": "Your picture has been added. Thanks for helping out!", | ||||
|         "uploadFailed": "Could not upload your picture. Are you connected to the Internet, and allow third party API's? The Brave browser or the uMatrix plugin might block them.", | ||||
|         "uploadMultipleDone": "{count} pictures have been added. Thanks for helping out!", | ||||
|  |  | |||
|  | @ -2682,26 +2682,6 @@ a.link-underline { | |||
|   } | ||||
| } | ||||
| 
 | ||||
| @media (prefers-reduced-motion: reduce) { | ||||
|   @-webkit-keyframes spin { | ||||
|     to { | ||||
|       -webkit-transform: rotate(360deg); | ||||
|               transform: rotate(360deg); | ||||
|     } | ||||
|   } | ||||
|   @keyframes spin { | ||||
|     to { | ||||
|       -webkit-transform: rotate(360deg); | ||||
|               transform: rotate(360deg); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   .motion-reduce\:animate-spin { | ||||
|     -webkit-animation: spin 1s linear infinite; | ||||
|             animation: spin 1s linear infinite; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| @media (max-width: 480px) { | ||||
|   .max-\[480px\]\:w-full { | ||||
|     width: 100%; | ||||
|  |  | |||
|  | @ -7,7 +7,7 @@ import { OsmTags } from "../../../Models/OsmFeature" | |||
|  */ | ||||
| export default class FeaturePropertiesStore { | ||||
|     private readonly _elements = new Map<string, UIEventSource<Record<string, string>>>() | ||||
| 
 | ||||
|     public readonly aliases = new Map<string, string>() | ||||
|     constructor(...sources: FeatureSource[]) { | ||||
|         for (const source of sources) { | ||||
|             this.trackFeatureSource(source) | ||||
|  | @ -92,7 +92,6 @@ export default class FeaturePropertiesStore { | |||
|         }) | ||||
|     } | ||||
| 
 | ||||
|     // noinspection JSUnusedGlobalSymbols
 | ||||
|     public addAlias(oldId: string, newId: string): void { | ||||
|         if (newId === undefined) { | ||||
|             // We removed the node/way/relation with type 'type' and id 'oldId' on openstreetmap!
 | ||||
|  | @ -112,6 +111,7 @@ export default class FeaturePropertiesStore { | |||
|         } | ||||
|         element.data.id = newId | ||||
|         this._elements.set(newId, element) | ||||
|         this.aliases.set(newId, oldId) | ||||
|         element.ping() | ||||
|     } | ||||
| 
 | ||||
|  |  | |||
|  | @ -0,0 +1,150 @@ | |||
| import { ImageUploader } from "./ImageUploader"; | ||||
| import LinkImageAction from "../Osm/Actions/LinkImageAction"; | ||||
| import FeaturePropertiesStore from "../FeatureSource/Actors/FeaturePropertiesStore"; | ||||
| import { OsmId, OsmTags } from "../../Models/OsmFeature"; | ||||
| import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"; | ||||
| import { Store, UIEventSource } from "../UIEventSource"; | ||||
| import { OsmConnection } from "../Osm/OsmConnection"; | ||||
| import { Changes } from "../Osm/Changes"; | ||||
| import Translations from "../../UI/i18n/Translations"; | ||||
| 
 | ||||
| 
 | ||||
| /** | ||||
|  * The ImageUploadManager has a | ||||
|  */ | ||||
| export class ImageUploadManager { | ||||
| 
 | ||||
|   private readonly _uploader: ImageUploader; | ||||
|   private readonly _featureProperties: FeaturePropertiesStore; | ||||
|   private readonly _layout: LayoutConfig; | ||||
| 
 | ||||
|   private readonly _uploadStarted: Map<string, UIEventSource<number>> = new Map(); | ||||
|   private readonly _uploadFinished: Map<string, UIEventSource<number>> = new Map(); | ||||
|   private readonly _uploadFailed: Map<string, UIEventSource<number>> = new Map(); | ||||
|   private readonly _uploadRetried: Map<string, UIEventSource<number>> = new Map(); | ||||
|   private readonly _uploadRetriedSuccess: Map<string, UIEventSource<number>> = new Map(); | ||||
|   private readonly _osmConnection: OsmConnection; | ||||
|   private readonly _changes: Changes; | ||||
| 
 | ||||
|   constructor(layout: LayoutConfig, uploader: ImageUploader, featureProperties: FeaturePropertiesStore, osmConnection: OsmConnection, changes: Changes) { | ||||
|     this._uploader = uploader; | ||||
|     this._featureProperties = featureProperties; | ||||
|     this._layout = layout; | ||||
|     this._osmConnection = osmConnection; | ||||
|     this._changes = changes; | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    * Gets various counters. | ||||
|    * Note that counters can only increase | ||||
|    * If a retry was a success, both 'retrySuccess' _and_ 'uploadFinished' will be increased | ||||
|    * @param featureId: the id of the feature you want information for. '*' has a global counter | ||||
|    */ | ||||
|   public getCountsFor(featureId: string | "*"): { | ||||
|     retried: Store<number>; | ||||
|     uploadStarted: Store<number>; | ||||
|     retrySuccess: Store<number>; | ||||
|     failed: Store<number>; | ||||
|     uploadFinished: Store<number> | ||||
|   } { | ||||
|     return { | ||||
|       uploadStarted: this.getCounterFor(this._uploadStarted, featureId), | ||||
|       uploadFinished: this.getCounterFor(this._uploadFinished, featureId), | ||||
|       retried: this.getCounterFor(this._uploadRetried, featureId), | ||||
|       failed: this.getCounterFor(this._uploadFailed, featureId), | ||||
|       retrySuccess: this.getCounterFor(this._uploadRetriedSuccess, featureId) | ||||
| 
 | ||||
|     }; | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    * Uploads the given image, applies the correct title and license for the known user | ||||
|    */ | ||||
|   public async uploadImageAndApply(file: File, tags: OsmTags) { | ||||
| 
 | ||||
|       const sizeInBytes = file.size | ||||
|     const featureId = <OsmId> tags.id | ||||
|       console.log(file.name + " has a size of " + sizeInBytes + " Bytes, attaching to", tags.id) | ||||
|       const self = this | ||||
|       if (sizeInBytes > this._uploader.maxFileSizeInMegabytes * 1000000) { | ||||
|           this.increaseCountFor(this._uploadStarted, featureId) | ||||
|           this.increaseCountFor(this._uploadFailed, featureId) | ||||
|           throw( | ||||
|             Translations.t.image.toBig.Subs({ | ||||
|                 actual_size: Math.floor(sizeInBytes / 1000000) + "MB", | ||||
|                 max_size: self._uploader.maxFileSizeInMegabytes + "MB", | ||||
|             }).txt | ||||
|           ) | ||||
|       } | ||||
| 
 | ||||
| 
 | ||||
|     const licenseStore = this._osmConnection?.GetPreference("pictures-license", "CC0"); | ||||
|     const license = licenseStore?.data ?? "CC0"; | ||||
| 
 | ||||
|     const matchingLayer = this._layout?.getMatchingLayer(tags); | ||||
| 
 | ||||
|     const title = | ||||
|       matchingLayer?.title?.GetRenderValue(tags)?.Subs(tags)?.textFor("en") ?? | ||||
|       tags.name ?? | ||||
|       "https//osm.org/" + tags.id; | ||||
|     const description = [ | ||||
|       "author:" + this._osmConnection.userDetails.data.name, | ||||
|       "license:" + license, | ||||
|       "osmid:" + tags.id | ||||
|     ].join("\n"); | ||||
| 
 | ||||
|     console.log("Upload done, creating ") | ||||
|     const action = await this.uploadImageWithLicense(featureId, title, description, file); | ||||
|     await this._changes.applyAction(action); | ||||
|   } | ||||
| 
 | ||||
|   private async uploadImageWithLicense( | ||||
|     featureId: OsmId, | ||||
|     title: string, description: string, blob: File | ||||
|   ): Promise<LinkImageAction> { | ||||
|     this.increaseCountFor(this._uploadStarted, featureId); | ||||
|     const properties = this._featureProperties.getStore(featureId); | ||||
|     let key: string; | ||||
|     let value: string; | ||||
|     try { | ||||
|       ({ key, value } = await this._uploader.uploadImage(title, description, blob)); | ||||
|     } catch (e) { | ||||
|       this.increaseCountFor(this._uploadRetried, featureId); | ||||
|       console.error("Could not upload image, trying again:", e); | ||||
|       try { | ||||
| 
 | ||||
|         ({ key, value } = await this._uploader.uploadImage(title, description, blob)); | ||||
|         this.increaseCountFor(this._uploadRetriedSuccess, featureId); | ||||
|       } catch (e) { | ||||
|         console.error("Could again not upload image due to", e); | ||||
|         this.increaseCountFor(this._uploadFailed, featureId); | ||||
|       } | ||||
| 
 | ||||
|     } | ||||
|     console.log("Uploading done, creating action for", featureId) | ||||
|     const action = new LinkImageAction(featureId, key, value, properties, { | ||||
|       theme: this._layout.id, | ||||
|       changeType: "add-image" | ||||
|     }); | ||||
|     this.increaseCountFor(this._uploadFinished, featureId); | ||||
|     return action; | ||||
|   } | ||||
| 
 | ||||
|   private getCounterFor(collection: Map<string, UIEventSource<number>>, key: string | "*") { | ||||
|     if (this._featureProperties.aliases.has(key)) { | ||||
|       key = this._featureProperties.aliases.get(key); | ||||
|     } | ||||
|     if (!collection.has(key)) { | ||||
|       collection.set(key, new UIEventSource<number>(0)); | ||||
|     } | ||||
|     return collection.get(key); | ||||
|   } | ||||
| 
 | ||||
|   private increaseCountFor(collection: Map<string, UIEventSource<number>>, key: string | "*") { | ||||
|     const counter = this.getCounterFor(collection, key); | ||||
|     counter.setData(counter.data + 1); | ||||
|     const global = this.getCounterFor(collection, "*"); | ||||
|     global.setData(counter.data + 1); | ||||
|   } | ||||
| 
 | ||||
| } | ||||
|  | @ -0,0 +1,15 @@ | |||
| export interface ImageUploader { | ||||
|   maxFileSizeInMegabytes?: number; | ||||
|     /** | ||||
|      * Uploads the 'blob' as image, with some metadata. | ||||
|      * Returns the URL to be linked + the appropriate key to add this to OSM | ||||
|      * @param title | ||||
|      * @param description | ||||
|      * @param blob | ||||
|      */ | ||||
|     uploadImage( | ||||
|         title: string, | ||||
|         description: string, | ||||
|         blob: File | ||||
|     ): Promise<{ key: string, value: string }>; | ||||
| } | ||||
|  | @ -1,60 +1,30 @@ | |||
| import ImageProvider, { ProvidedImage } from "./ImageProvider" | ||||
| import BaseUIElement from "../../UI/BaseUIElement" | ||||
| import { Utils } from "../../Utils" | ||||
| import Constants from "../../Models/Constants" | ||||
| import { LicenseInfo } from "./LicenseInfo" | ||||
| import ImageProvider, { ProvidedImage } from "./ImageProvider"; | ||||
| import BaseUIElement from "../../UI/BaseUIElement"; | ||||
| import { Utils } from "../../Utils"; | ||||
| import Constants from "../../Models/Constants"; | ||||
| import { LicenseInfo } from "./LicenseInfo"; | ||||
| import { ImageUploader } from "./ImageUploader"; | ||||
| 
 | ||||
| export class Imgur extends ImageProvider { | ||||
| export class Imgur extends ImageProvider implements ImageUploader{ | ||||
|     public static readonly defaultValuePrefix = ["https://i.imgur.com"] | ||||
|     public static readonly singleton = new Imgur() | ||||
|     public readonly defaultKeyPrefixes: string[] = ["image"] | ||||
| 
 | ||||
|     public readonly  maxFileSizeInMegabytes = 10 | ||||
|     private constructor() { | ||||
|         super() | ||||
|     } | ||||
| 
 | ||||
|     static uploadMultiple( | ||||
|     /** | ||||
|      * Uploads an image, returns the URL where to find the image | ||||
|      * @param title | ||||
|      * @param description | ||||
|      * @param blob | ||||
|      */ | ||||
|     public async uploadImage( | ||||
|         title: string, | ||||
|         description: string, | ||||
|         blobs: FileList, | ||||
|         handleSuccessfullUpload: (imageURL: string) => Promise<void>, | ||||
|         allDone: () => void, | ||||
|         onFail: (reason: string) => void, | ||||
|         offset: number = 0 | ||||
|     ) { | ||||
|         if (blobs.length == offset) { | ||||
|             allDone() | ||||
|             return | ||||
|         } | ||||
|         const blob = blobs.item(offset) | ||||
|         const self = this | ||||
|         this.uploadImage( | ||||
|             title, | ||||
|             description, | ||||
|             blob, | ||||
|             async (imageUrl) => { | ||||
|                 await handleSuccessfullUpload(imageUrl) | ||||
|                 self.uploadMultiple( | ||||
|                     title, | ||||
|                     description, | ||||
|                     blobs, | ||||
|                     handleSuccessfullUpload, | ||||
|                     allDone, | ||||
|                     onFail, | ||||
|                     offset + 1 | ||||
|                 ) | ||||
|             }, | ||||
|             onFail | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
|     static uploadImage( | ||||
|         title: string, | ||||
|         description: string, | ||||
|         blob: File, | ||||
|         handleSuccessfullUpload: (imageURL: string) => Promise<void>, | ||||
|         onFail: (reason: string) => void | ||||
|     ) { | ||||
|         blob: File | ||||
|     ): Promise<{ key: string, value: string }> { | ||||
|         const apiUrl = "https://api.imgur.com/3/image" | ||||
|         const apiKey = Constants.ImgurApiKey | ||||
| 
 | ||||
|  | @ -63,6 +33,7 @@ export class Imgur extends ImageProvider { | |||
|         formData.append("title", title) | ||||
|         formData.append("description", description) | ||||
| 
 | ||||
| 
 | ||||
|         const settings: RequestInit = { | ||||
|             method: "POST", | ||||
|             body: formData, | ||||
|  | @ -74,17 +45,9 @@ export class Imgur extends ImageProvider { | |||
|         } | ||||
| 
 | ||||
|         // Response contains stringified JSON
 | ||||
|         // Image URL available at response.data.link
 | ||||
|         fetch(apiUrl, settings) | ||||
|             .then(async function (response) { | ||||
|                 const content = await response.json() | ||||
|                 await handleSuccessfullUpload(content.data.link) | ||||
|             }) | ||||
|             .catch((reason) => { | ||||
|                 console.log("Uploading to IMGUR failed", reason) | ||||
|                 // @ts-ignore
 | ||||
|                 onFail(reason) | ||||
|             }) | ||||
|         const response = await fetch(apiUrl, settings) | ||||
|         const content = await response.json() | ||||
|         return { key: "image", value: content.data.link } | ||||
|     } | ||||
| 
 | ||||
|     SourceIcon(): BaseUIElement { | ||||
|  |  | |||
|  | @ -1,11 +1,20 @@ | |||
| import ChangeTagAction from "./ChangeTagAction" | ||||
| import { Tag } from "../../Tags/Tag" | ||||
| import ChangeTagAction from "./ChangeTagAction"; | ||||
| import { Tag } from "../../Tags/Tag"; | ||||
| import OsmChangeAction from "./OsmChangeAction"; | ||||
| import { Changes } from "../Changes"; | ||||
| import { ChangeDescription } from "./ChangeDescription"; | ||||
| import { Store } from "../../UIEventSource"; | ||||
| 
 | ||||
| export default class LinkImageAction extends OsmChangeAction { | ||||
|     private readonly _proposedKey: "image" | "mapillary" | "wiki_commons" | string; | ||||
|     private readonly _url: string; | ||||
|     private readonly _currentTags: Store<Record<string, string>>; | ||||
|     private readonly _meta: { theme: string; changeType: "add-image" | "link-image" }; | ||||
| 
 | ||||
| export default class LinkPicture extends ChangeTagAction { | ||||
|     /** | ||||
|      * Adds a link to an image | ||||
|      * Adds an image-link to a feature | ||||
|      * @param elementId | ||||
|      * @param proposedKey: a key which might be used, typically `image`. If the key is already used with a different URL, `key+":0"` will be used instead (or a higher number if needed) | ||||
|      * @param proposedKey a key which might be used, typically `image`. If the key is already used with a different URL, `key+":0"` will be used instead (or a higher number if needed) | ||||
|      * @param url | ||||
|      * @param currentTags | ||||
|      * @param meta | ||||
|  | @ -15,18 +24,31 @@ export default class LinkPicture extends ChangeTagAction { | |||
|         elementId: string, | ||||
|         proposedKey: "image" | "mapillary" | "wiki_commons" | string, | ||||
|         url: string, | ||||
|         currentTags: Record<string, string>, | ||||
|         currentTags: Store<Record<string, string>>, | ||||
|         meta: { | ||||
|             theme: string | ||||
|             changeType: "add-image" | "link-image" | ||||
|         } | ||||
|     ) { | ||||
|         let key = proposedKey | ||||
|         super(elementId, true) | ||||
|         this._proposedKey = proposedKey; | ||||
|         this._url = url; | ||||
|         this._currentTags = currentTags; | ||||
|         this._meta = meta; | ||||
|     } | ||||
| 
 | ||||
|     protected CreateChangeDescriptions(): Promise<ChangeDescription[]> { | ||||
|         let key = this._proposedKey | ||||
|         let i = 0 | ||||
|         const currentTags = this._currentTags.data | ||||
|         const url = this._url | ||||
|         while (currentTags[key] !== undefined && currentTags[key] !== url) { | ||||
|             key = proposedKey + ":" + i | ||||
|             key = this._proposedKey + ":" + i | ||||
|             i++ | ||||
|         } | ||||
|         super(elementId, new Tag(key, url), currentTags, meta) | ||||
|         const tagChangeAction = new ChangeTagAction ( this.mainObjectId, new Tag(key, url), currentTags, this._meta) | ||||
|         return tagChangeAction.CreateChangeDescriptions() | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
| } | ||||
|  |  | |||
|  | @ -19,6 +19,9 @@ export default abstract class OsmChangeAction { | |||
|     constructor(mainObjectId: string, trackStatistics: boolean = true) { | ||||
|         this.trackStatistics = trackStatistics | ||||
|         this.mainObjectId = mainObjectId | ||||
|         if(mainObjectId === undefined || mainObjectId === null){ | ||||
|             throw "OsmObject received '"+mainObjectId+"' as mainObjectId" | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     public async Perform(changes: Changes) { | ||||
|  |  | |||
|  | @ -1,22 +1,22 @@ | |||
| import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" | ||||
| import { OsmConnection } from "../Osm/OsmConnection" | ||||
| import { MangroveIdentity } from "../Web/MangroveReviews" | ||||
| import { Store, Stores, UIEventSource } from "../UIEventSource" | ||||
| import StaticFeatureSource from "../FeatureSource/Sources/StaticFeatureSource" | ||||
| import { FeatureSource } from "../FeatureSource/FeatureSource" | ||||
| import { Feature } from "geojson" | ||||
| import { Utils } from "../../Utils" | ||||
| import translators from "../../assets/translators.json" | ||||
| import codeContributors from "../../assets/contributors.json" | ||||
| import LayerConfig from "../../Models/ThemeConfig/LayerConfig" | ||||
| import { LayerConfigJson } from "../../Models/ThemeConfig/Json/LayerConfigJson" | ||||
| import usersettings from "../../../src/assets/generated/layers/usersettings.json" | ||||
| import Locale from "../../UI/i18n/Locale" | ||||
| import LinkToWeblate from "../../UI/Base/LinkToWeblate" | ||||
| import FeatureSwitchState from "./FeatureSwitchState" | ||||
| import Constants from "../../Models/Constants" | ||||
| import { QueryParameters } from "../Web/QueryParameters" | ||||
| import { ThemeMetaTagging } from "./UserSettingsMetaTagging" | ||||
| import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"; | ||||
| import { OsmConnection } from "../Osm/OsmConnection"; | ||||
| import { MangroveIdentity } from "../Web/MangroveReviews"; | ||||
| import { Store, Stores, UIEventSource } from "../UIEventSource"; | ||||
| import StaticFeatureSource from "../FeatureSource/Sources/StaticFeatureSource"; | ||||
| import { FeatureSource } from "../FeatureSource/FeatureSource"; | ||||
| import { Feature } from "geojson"; | ||||
| import { Utils } from "../../Utils"; | ||||
| import translators from "../../assets/translators.json"; | ||||
| import codeContributors from "../../assets/contributors.json"; | ||||
| import LayerConfig from "../../Models/ThemeConfig/LayerConfig"; | ||||
| import { LayerConfigJson } from "../../Models/ThemeConfig/Json/LayerConfigJson"; | ||||
| import usersettings from "../../../src/assets/generated/layers/usersettings.json"; | ||||
| import Locale from "../../UI/i18n/Locale"; | ||||
| import LinkToWeblate from "../../UI/Base/LinkToWeblate"; | ||||
| import FeatureSwitchState from "./FeatureSwitchState"; | ||||
| import Constants from "../../Models/Constants"; | ||||
| import { QueryParameters } from "../Web/QueryParameters"; | ||||
| import { ThemeMetaTagging } from "./UserSettingsMetaTagging"; | ||||
| import { MapProperties } from "../../Models/MapProperties"; | ||||
| 
 | ||||
| /** | ||||
|  | @ -43,7 +43,7 @@ export default class UserRelatedState { | |||
|     public readonly homeLocation: FeatureSource | ||||
|     public readonly language: UIEventSource<string> | ||||
|     public readonly preferredBackgroundLayer: UIEventSource<string | "photo" | "map" | "osmbasedmap" | undefined> | ||||
|     public readonly preferredBackgroundLayerForTheme: UIEventSource<string | "photo" | "map" | "osmbasedmap" | undefined> | ||||
|     public readonly imageLicense : UIEventSource<string> | ||||
|     /** | ||||
|      * The number of seconds that the GPS-locations are stored in memory. | ||||
|      * Time in seconds | ||||
|  | @ -108,6 +108,9 @@ export default class UserRelatedState { | |||
|             documentation: "The ID of a layer or layer category that MapComplete uses by default" | ||||
|         }) | ||||
| 
 | ||||
|         this.imageLicense =  this.osmConnection.GetPreference("pictures-license", "CC0", { | ||||
|             documentation: "The license under which new images are uploaded" | ||||
|         }) | ||||
|         this.installedUserThemes = this.InitInstalledUserThemes() | ||||
| 
 | ||||
|         this.homeLocation = this.initHomeLocation() | ||||
|  |  | |||
|  | @ -51,6 +51,8 @@ import ThemeViewStateHashActor from "../Logic/Web/ThemeViewStateHashActor"; | |||
| import NoElementsInViewDetector, { FeatureViewState } from "../Logic/Actors/NoElementsInViewDetector"; | ||||
| import FilteredLayer from "./FilteredLayer"; | ||||
| import { PreferredRasterLayerSelector } from "../Logic/Actors/PreferredRasterLayerSelector"; | ||||
| import { ImageUploadManager } from "../Logic/ImageProviders/ImageUploadManager"; | ||||
| import { Imgur } from "../Logic/ImageProviders/Imgur"; | ||||
| 
 | ||||
| /** | ||||
|  * | ||||
|  | @ -99,6 +101,8 @@ export default class ThemeViewState implements SpecialVisualizationState { | |||
|     readonly userRelatedState: UserRelatedState; | ||||
|     readonly geolocation: GeoLocationHandler; | ||||
| 
 | ||||
|     readonly imageUploadManager: ImageUploadManager | ||||
| 
 | ||||
|     readonly lastClickObject: WritableFeatureSource; | ||||
|     readonly overlayLayerStates: ReadonlyMap< | ||||
|         string, | ||||
|  | @ -168,6 +172,7 @@ export default class ThemeViewState implements SpecialVisualizationState { | |||
| 
 | ||||
|         this.availableLayers = AvailableRasterLayers.layersAvailableAt(this.mapProperties.location); | ||||
| 
 | ||||
| 
 | ||||
|         const self = this; | ||||
|         this.layerState = new LayerState(this.osmConnection, layout.layers, layout.id); | ||||
| 
 | ||||
|  | @ -323,6 +328,7 @@ export default class ThemeViewState implements SpecialVisualizationState { | |||
|         this.perLayerFiltered = this.showNormalDataOn(this.map); | ||||
| 
 | ||||
|         this.hasDataInView = new NoElementsInViewDetector(this).hasFeatureInView; | ||||
|         this.imageUploadManager = new ImageUploadManager(layout, Imgur.singleton, this.featureProperties, this.osmConnection, this.changes) | ||||
| 
 | ||||
|         this.initActors(); | ||||
|         this.addLastClick(lastClick); | ||||
|  |  | |||
|  | @ -0,0 +1,40 @@ | |||
| <script lang="ts"> | ||||
| 
 | ||||
|   import { createEventDispatcher } from "svelte"; | ||||
|   import { twMerge } from "tailwind-merge"; | ||||
| 
 | ||||
|   export let accept: string; | ||||
|   export let multiple: boolean = true; | ||||
| 
 | ||||
|   const dispatcher = createEventDispatcher<{ submit: FileList }>(); | ||||
|   export let cls: string = ""; | ||||
|   let drawAttention = false; | ||||
|   let inputElement: HTMLInputElement; | ||||
|   let id = Math.random() * 1000000000 + ""; | ||||
| </script> | ||||
| 
 | ||||
| <form> | ||||
|   <label class={twMerge(cls, drawAttention ? "glowing-shadow" : "")} for={"fileinput"+id}> | ||||
|     <slot /> | ||||
| 
 | ||||
|   </label> | ||||
|   <input {accept} bind:this={inputElement} class="hidden" id={"fileinput"  + id} {multiple} name="file-input" | ||||
|          on:change|preventDefault={() => { | ||||
|   drawAttention = false;  | ||||
|   dispatcher("submit", inputElement.files)}} | ||||
|           | ||||
|          on:dragend={ () => {drawAttention = false}} | ||||
|          on:dragover|preventDefault|stopPropagation={(e) => { | ||||
|            console.log("Dragging over!") | ||||
|              drawAttention = true | ||||
|              e.dataTransfer.drop = "copy" | ||||
|            }} | ||||
|          on:dragstart={ () => {drawAttention = false}} | ||||
|          on:drop|preventDefault|stopPropagation={(e) => { | ||||
|            console.log("Got a 'drop'") | ||||
|           drawAttention = false | ||||
|           dispatcher("submit", e.dataTransfer.files) | ||||
|         }} | ||||
|          type="file" | ||||
|   > | ||||
| </form> | ||||
|  | @ -1,9 +1,12 @@ | |||
| <script> | ||||
|   import ToSvelte from "./ToSvelte.svelte" | ||||
|   import Svg from "../../Svg" | ||||
| <script lang="ts"> | ||||
|   import ToSvelte from "./ToSvelte.svelte"; | ||||
|   import Svg from "../../Svg"; | ||||
|   import { twMerge } from "tailwind-merge"; | ||||
| 
 | ||||
|   export let cls : string = undefined | ||||
| </script> | ||||
| 
 | ||||
| <div class="flex p-1 pl-2"> | ||||
| <div class={twMerge( "flex p-1 pl-2", cls)}> | ||||
|   <div class="min-w-6 h-6 w-6 animate-spin self-center"> | ||||
|     <ToSvelte construct={Svg.loading_svg()} /> | ||||
|   </div> | ||||
|  |  | |||
|  | @ -15,8 +15,9 @@ import Loading from "../Base/Loading" | |||
| import { LoginToggle } from "../Popup/LoginButton" | ||||
| import Constants from "../../Models/Constants" | ||||
| import { SpecialVisualizationState } from "../SpecialVisualization" | ||||
| import exp from "constants"; | ||||
| 
 | ||||
| export class ImageUploadFlow extends Toggle { | ||||
| export class ImageUploadFlow extends Combine { | ||||
|     private static readonly uploadCountsPerId = new Map<string, UIEventSource<number>>() | ||||
| 
 | ||||
|     constructor( | ||||
|  | @ -129,7 +130,7 @@ export class ImageUploadFlow extends Toggle { | |||
|             uploader.uploadMany(title, description, filelist) | ||||
|         }) | ||||
| 
 | ||||
|         const uploadFlow: BaseUIElement = new Combine([ | ||||
|         super([ | ||||
|             new VariableUiElement( | ||||
|                 uploader.queue | ||||
|                     .map((q) => q.length) | ||||
|  | @ -183,17 +184,9 @@ export class ImageUploadFlow extends Toggle { | |||
|                     }) | ||||
|                     .SetClass("underline"), | ||||
|             ]).SetStyle("font-size:small;"), | ||||
|         ]).SetClass("flex flex-col image-upload-flow mt-4 mb-8 text-center leading-none") | ||||
|         ]) | ||||
|         this.SetClass("flex flex-col image-upload-flow mt-4 mb-8 text-center leading-none") | ||||
| 
 | ||||
| 
 | ||||
|         super( | ||||
|             new LoginToggle( | ||||
|                 /*We can show the actual upload button!*/ | ||||
|                 uploadFlow, | ||||
|                 /* User not logged in*/ t.pleaseLogin.Clone(), | ||||
|                 state | ||||
|             ), | ||||
|             undefined /* Nothing as the user badge is disabled*/, | ||||
|             state?.featureSwitchUserbadge | ||||
|         ) | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -0,0 +1,73 @@ | |||
| <script lang="ts">/** | ||||
|  * Shows an 'upload'-button which will start the upload for this feature | ||||
|  */ | ||||
| 
 | ||||
| import type { SpecialVisualizationState } from "../SpecialVisualization"; | ||||
| import { Store } from "../../Logic/UIEventSource"; | ||||
| import type { OsmTags } from "../../Models/OsmFeature"; | ||||
| import LoginToggle from "../Base/LoginToggle.svelte"; | ||||
| import Translations from "../i18n/Translations"; | ||||
| import Tr from "../Base/Tr.svelte"; | ||||
| import UploadingImageCounter from "./UploadingImageCounter.svelte"; | ||||
| import FileSelector from "../Base/FileSelector.svelte"; | ||||
| import ToSvelte from "../Base/ToSvelte.svelte"; | ||||
| import Svg from "../../Svg"; | ||||
| 
 | ||||
| export let state: SpecialVisualizationState; | ||||
| 
 | ||||
| export let tags: Store<OsmTags>; | ||||
| export let image: string = undefined; | ||||
| if (image === "") { | ||||
|   image = undefined; | ||||
| } | ||||
| export let labelText: string = undefined; | ||||
| const t = Translations.t.image; | ||||
| 
 | ||||
| let licenseStore = state.userRelatedState.imageLicense; | ||||
| 
 | ||||
| function handleFiles(files: FileList) { | ||||
|   for (let i = 0; i < files.length; i++) { | ||||
|     const file = files.item(i); | ||||
|     console.log("Got file", file.name) | ||||
|     try { | ||||
|       state.imageUploadManager.uploadImageAndApply(file, tags.data); | ||||
|     } catch (e) { | ||||
|       alert(e); | ||||
|     } | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| </script> | ||||
| 
 | ||||
| 
 | ||||
| <LoginToggle {state}> | ||||
| 
 | ||||
|   <Tr slot="not-logged-in" t={t.pleaseLogin} /> | ||||
|   <div class="flex flex-col"> | ||||
| 
 | ||||
|     <UploadingImageCounter {state} {tags} /> | ||||
|     <FileSelector accept="image/*" cls="button border-2 text-2xl" multiple={true} | ||||
|                   on:submit={e => handleFiles(e.detail)}> | ||||
|       <div class="flex items-center"> | ||||
| 
 | ||||
|         {#if image !== undefined} | ||||
|           <img src={image} /> | ||||
|         {:else} | ||||
|           <ToSvelte construct={ Svg.camera_plus_svg().SetClass("block w-12 h-12 p-1 text-4xl ")} /> | ||||
|         {/if} | ||||
|         {#if labelText} | ||||
|           {labelText} | ||||
|         {:else} | ||||
|           <Tr t={t.addPicture} /> | ||||
|         {/if} | ||||
|       </div> | ||||
|     </FileSelector> | ||||
|     <div class="text-sm"> | ||||
|       <Tr t={t.respectPrivacy} /> | ||||
|       <a class="cursor-pointer" on:click={() => {state.guistate.openUsersettings("picture-license")}}> | ||||
|         <Tr t={t.currentLicense.Subs({license: $licenseStore})} /> | ||||
|       </a> | ||||
|     </div> | ||||
|   </div> | ||||
| 
 | ||||
| </LoginToggle> | ||||
|  | @ -1,31 +1,67 @@ | |||
| <script lang="ts">/** | ||||
|  * Shows an 'upload'-button which will start the upload for this feature | ||||
|  * Shows information about how much images are uploaded for the given feature | ||||
|  */ | ||||
| 
 | ||||
| import type { SpecialVisualizationState } from "../SpecialVisualization"; | ||||
| import type { Feature } from "geojson"; | ||||
| import { Store } from "../../Logic/UIEventSource"; | ||||
| import type { OsmTags } from "../../Models/OsmFeature"; | ||||
| import { ImageUploader } from "../../Logic/ImageProviders/ImageUploader"; | ||||
| import LoginToggle from "../Base/LoginToggle.svelte"; | ||||
| import Translations from "../i18n/Translations"; | ||||
| import Tr from "../Base/Tr.svelte"; | ||||
| import { ImageUploadManager } from "../../Logic/ImageProviders/ImageUploadManager"; | ||||
| import Loading from "../Base/Loading.svelte"; | ||||
| 
 | ||||
| export let state: SpecialVisualizationState; | ||||
| export let feature: Feature; | ||||
| 
 | ||||
| export let tags: Store<OsmTags>; | ||||
| export let state: SpecialVisualizationState; | ||||
| export let lon: number; | ||||
| export let lat: number; | ||||
| const t = Translations.t.image | ||||
| const featureId = tags.data.id; | ||||
| const { | ||||
|   uploadStarted, | ||||
|   uploadFinished, | ||||
|   retried, | ||||
|   failed | ||||
| } = state.imageUploadManager.getCountsFor(featureId); | ||||
| const t = Translations.t.image; | ||||
| 
 | ||||
| </script> | ||||
| 
 | ||||
| {#if $uploadStarted == 1} | ||||
|   {#if $uploadFinished == 1 } | ||||
|     <Tr cls="thanks" t={t.upload.one.done} /> | ||||
|   {:else if $failed == 1} | ||||
|     <div class="flex flex-col alert"> | ||||
|       <Tr cls="self-center" t={t.upload.one.failed} /> | ||||
|       <Tr t={t.upload.failReasons} /> | ||||
|       <Tr t={t.upload.failReasonsAdvanced} /> | ||||
|     </div> | ||||
|   {:else if $retried == 1} | ||||
|     <Loading cls="alert"> | ||||
|       <Tr t={t.upload.one.retrying} /> | ||||
|     </Loading> | ||||
|   {:else } | ||||
|     <Loading cls="alert"> | ||||
|       <Tr t={t.upload.one.uploading} /> | ||||
|     </Loading> | ||||
|   {/if} | ||||
| {:else if $uploadStarted > 1} | ||||
|   {#if ($uploadFinished + $failed) == $uploadStarted && $uploadFinished > 0} | ||||
|     <Tr cls="thanks" t={t.upload.multiple.done.Subs({count: $uploadFinished})} /> | ||||
|   {:else if $uploadFinished == 0} | ||||
|     <Loading cls="alert"> | ||||
|       <Tr t={t.upload.multiple.uploading.Subs({count: $uploadStarted})} /> | ||||
|     </Loading> | ||||
|   {:else if $uploadFinished > 0} | ||||
|     <Loading cls="alert"> | ||||
|       <Tr t={t.upload.multiple.partiallyDone.Subs({count: $uploadStarted -  $uploadFinished, done: $uploadFinished})} /> | ||||
|     </Loading> | ||||
|   {/if} | ||||
|   {#if $failed > 0} | ||||
|     <div class="flex flex-col alert"> | ||||
|       {#if failed === 1} | ||||
|         <Tr cls="self-center" t={t.upload.one.failed} /> | ||||
|       {:else} | ||||
|         <Tr cls="self-center" t={t.upload.multiple.someFailed.Subs({count: $failed})} /> | ||||
| 
 | ||||
| <LoginToggle> | ||||
|    | ||||
|   <Tr slot="not-logged-in" t={t.pleaseLogin}/> | ||||
|    | ||||
| </LoginToggle> | ||||
|       {/if} | ||||
|       <Tr t={t.upload.failReasons} /> | ||||
|       <Tr t={t.upload.failReasonsAdvanced} /> | ||||
|     </div> | ||||
|   {/if} | ||||
| {/if} | ||||
|  |  | |||
|  | @ -1,113 +1,117 @@ | |||
| import { Store, UIEventSource } from "../Logic/UIEventSource" | ||||
| import BaseUIElement from "./BaseUIElement" | ||||
| import LayoutConfig from "../Models/ThemeConfig/LayoutConfig" | ||||
| import { IndexedFeatureSource, WritableFeatureSource } from "../Logic/FeatureSource/FeatureSource" | ||||
| import { OsmConnection } from "../Logic/Osm/OsmConnection" | ||||
| import { Changes } from "../Logic/Osm/Changes" | ||||
| import { ExportableMap, MapProperties } from "../Models/MapProperties" | ||||
| import LayerState from "../Logic/State/LayerState" | ||||
| import { Feature, Geometry, Point } from "geojson" | ||||
| import FullNodeDatabaseSource from "../Logic/FeatureSource/TiledFeatureSource/FullNodeDatabaseSource" | ||||
| import { MangroveIdentity } from "../Logic/Web/MangroveReviews" | ||||
| import { GeoIndexedStoreForLayer } from "../Logic/FeatureSource/Actors/GeoIndexedStore" | ||||
| import LayerConfig from "../Models/ThemeConfig/LayerConfig" | ||||
| import FeatureSwitchState from "../Logic/State/FeatureSwitchState" | ||||
| import { MenuState } from "../Models/MenuState" | ||||
| import OsmObjectDownloader from "../Logic/Osm/OsmObjectDownloader" | ||||
| import { RasterLayerPolygon } from "../Models/RasterLayers" | ||||
| import { Store, UIEventSource } from "../Logic/UIEventSource"; | ||||
| import BaseUIElement from "./BaseUIElement"; | ||||
| import LayoutConfig from "../Models/ThemeConfig/LayoutConfig"; | ||||
| import { IndexedFeatureSource, WritableFeatureSource } from "../Logic/FeatureSource/FeatureSource"; | ||||
| import { OsmConnection } from "../Logic/Osm/OsmConnection"; | ||||
| import { Changes } from "../Logic/Osm/Changes"; | ||||
| import { ExportableMap, MapProperties } from "../Models/MapProperties"; | ||||
| import LayerState from "../Logic/State/LayerState"; | ||||
| import { Feature, Geometry, Point } from "geojson"; | ||||
| import FullNodeDatabaseSource from "../Logic/FeatureSource/TiledFeatureSource/FullNodeDatabaseSource"; | ||||
| import { MangroveIdentity } from "../Logic/Web/MangroveReviews"; | ||||
| import { GeoIndexedStoreForLayer } from "../Logic/FeatureSource/Actors/GeoIndexedStore"; | ||||
| import LayerConfig from "../Models/ThemeConfig/LayerConfig"; | ||||
| import FeatureSwitchState from "../Logic/State/FeatureSwitchState"; | ||||
| import { MenuState } from "../Models/MenuState"; | ||||
| import OsmObjectDownloader from "../Logic/Osm/OsmObjectDownloader"; | ||||
| import { RasterLayerPolygon } from "../Models/RasterLayers"; | ||||
| import { ImageUploadManager } from "../Logic/ImageProviders/ImageUploadManager"; | ||||
| 
 | ||||
| /** | ||||
|  * The state needed to render a special Visualisation. | ||||
|  */ | ||||
| export interface SpecialVisualizationState { | ||||
|     readonly guistate: MenuState | ||||
|     readonly layout: LayoutConfig | ||||
|     readonly featureSwitches: FeatureSwitchState | ||||
|   readonly guistate: MenuState; | ||||
|   readonly layout: LayoutConfig; | ||||
|   readonly featureSwitches: FeatureSwitchState; | ||||
| 
 | ||||
|     readonly layerState: LayerState | ||||
|     readonly featureProperties: { getStore(id: string): UIEventSource<Record<string, string>> } | ||||
|   readonly layerState: LayerState; | ||||
|   readonly featureProperties: { getStore(id: string): UIEventSource<Record<string, string>> }; | ||||
| 
 | ||||
|     readonly indexedFeatures: IndexedFeatureSource | ||||
|   readonly indexedFeatures: IndexedFeatureSource; | ||||
| 
 | ||||
|     /** | ||||
|      * Some features will create a new element that should be displayed. | ||||
|      * These can be injected by appending them to this featuresource (and pinging it) | ||||
|      */ | ||||
|     readonly newFeatures: WritableFeatureSource | ||||
|   /** | ||||
|    * Some features will create a new element that should be displayed. | ||||
|    * These can be injected by appending them to this featuresource (and pinging it) | ||||
|    */ | ||||
|   readonly newFeatures: WritableFeatureSource; | ||||
| 
 | ||||
|     readonly historicalUserLocations: WritableFeatureSource<Feature<Point>> | ||||
|   readonly historicalUserLocations: WritableFeatureSource<Feature<Point>>; | ||||
| 
 | ||||
|     readonly osmConnection: OsmConnection | ||||
|     readonly featureSwitchUserbadge: Store<boolean> | ||||
|     readonly featureSwitchIsTesting: Store<boolean> | ||||
|     readonly changes: Changes | ||||
|     readonly osmObjectDownloader: OsmObjectDownloader | ||||
|     /** | ||||
|      * State of the main map | ||||
|      */ | ||||
|     readonly mapProperties: MapProperties & ExportableMap | ||||
|   readonly osmConnection: OsmConnection; | ||||
|   readonly featureSwitchUserbadge: Store<boolean>; | ||||
|   readonly featureSwitchIsTesting: Store<boolean>; | ||||
|   readonly changes: Changes; | ||||
|   readonly osmObjectDownloader: OsmObjectDownloader; | ||||
|   /** | ||||
|    * State of the main map | ||||
|    */ | ||||
|   readonly mapProperties: MapProperties & ExportableMap; | ||||
| 
 | ||||
|     readonly selectedElement: UIEventSource<Feature> | ||||
|     /** | ||||
|      * Works together with 'selectedElement' to indicate what properties should be displayed | ||||
|      */ | ||||
|     readonly selectedLayer: UIEventSource<LayerConfig> | ||||
|     readonly selectedElementAndLayer: Store<{ feature: Feature; layer: LayerConfig }> | ||||
|   readonly selectedElement: UIEventSource<Feature>; | ||||
|   /** | ||||
|    * Works together with 'selectedElement' to indicate what properties should be displayed | ||||
|    */ | ||||
|   readonly selectedLayer: UIEventSource<LayerConfig>; | ||||
|   readonly selectedElementAndLayer: Store<{ feature: Feature; layer: LayerConfig }>; | ||||
| 
 | ||||
|     /** | ||||
|      * If data is currently being fetched from external sources | ||||
|      */ | ||||
|     readonly dataIsLoading: Store<boolean> | ||||
|     /** | ||||
|      * Only needed for 'ReplaceGeometryAction' | ||||
|      */ | ||||
|     readonly fullNodeDatabase?: FullNodeDatabaseSource | ||||
|   /** | ||||
|    * If data is currently being fetched from external sources | ||||
|    */ | ||||
|   readonly dataIsLoading: Store<boolean>; | ||||
|   /** | ||||
|    * Only needed for 'ReplaceGeometryAction' | ||||
|    */ | ||||
|   readonly fullNodeDatabase?: FullNodeDatabaseSource; | ||||
| 
 | ||||
|     readonly perLayer: ReadonlyMap<string, GeoIndexedStoreForLayer> | ||||
|     readonly userRelatedState: { | ||||
|         readonly showTags: UIEventSource<"no" | undefined | "always" | "yes" | "full"> | ||||
|         readonly mangroveIdentity: MangroveIdentity | ||||
|         readonly showAllQuestionsAtOnce: UIEventSource<boolean> | ||||
|         readonly preferencesAsTags: Store<Record<string, string>> | ||||
|         readonly language: UIEventSource<string> | ||||
|     } | ||||
|     readonly lastClickObject: WritableFeatureSource | ||||
|   readonly perLayer: ReadonlyMap<string, GeoIndexedStoreForLayer>; | ||||
|   readonly userRelatedState: { | ||||
|     readonly imageLicense: UIEventSource<string>; | ||||
|     readonly showTags: UIEventSource<"no" | undefined | "always" | "yes" | "full"> | ||||
|     readonly mangroveIdentity: MangroveIdentity | ||||
|     readonly showAllQuestionsAtOnce: UIEventSource<boolean> | ||||
|     readonly preferencesAsTags: Store<Record<string, string>> | ||||
|     readonly language: UIEventSource<string> | ||||
|   }; | ||||
|   readonly lastClickObject: WritableFeatureSource; | ||||
| 
 | ||||
|     readonly availableLayers: Store<RasterLayerPolygon[]> | ||||
|   readonly availableLayers: Store<RasterLayerPolygon[]>; | ||||
| 
 | ||||
|   readonly imageUploadManager: ImageUploadManager; | ||||
| } | ||||
| 
 | ||||
| export interface SpecialVisualization { | ||||
|     readonly funcName: string | ||||
|     readonly docs: string | BaseUIElement | ||||
|     readonly example?: string | ||||
|   readonly funcName: string; | ||||
|   readonly docs: string | BaseUIElement; | ||||
|   readonly example?: string; | ||||
| 
 | ||||
|     /** | ||||
|      * Indicates that this special visualisation will make requests to the 'alLNodesDatabase' and that it thus should be included | ||||
|      */ | ||||
|     readonly needsNodeDatabase?: boolean | ||||
|     readonly args: { | ||||
|         name: string | ||||
|         defaultValue?: string | ||||
|         doc: string | ||||
|         required?: false | boolean | ||||
|     }[] | ||||
|     readonly getLayerDependencies?: (argument: string[]) => string[] | ||||
|   /** | ||||
|    * Indicates that this special visualisation will make requests to the 'alLNodesDatabase' and that it thus should be included | ||||
|    */ | ||||
|   readonly needsNodeDatabase?: boolean; | ||||
|   readonly args: { | ||||
|     name: string | ||||
|     defaultValue?: string | ||||
|     doc: string | ||||
|     required?: false | boolean | ||||
|   }[]; | ||||
|   readonly getLayerDependencies?: (argument: string[]) => string[]; | ||||
| 
 | ||||
|     structuredExamples?(): { feature: Feature<Geometry, Record<string, string>>; args: string[] }[] | ||||
|   structuredExamples?(): { feature: Feature<Geometry, Record<string, string>>; args: string[] }[]; | ||||
| 
 | ||||
|     constr( | ||||
|         state: SpecialVisualizationState, | ||||
|         tagSource: UIEventSource<Record<string, string>>, | ||||
|         argument: string[], | ||||
|         feature: Feature, | ||||
|         layer: LayerConfig | ||||
|     ): BaseUIElement | ||||
|   constr( | ||||
|     state: SpecialVisualizationState, | ||||
|     tagSource: UIEventSource<Record<string, string>>, | ||||
|     argument: string[], | ||||
|     feature: Feature, | ||||
|     layer: LayerConfig | ||||
|   ): BaseUIElement; | ||||
| } | ||||
| 
 | ||||
| export type RenderingSpecification = | ||||
|     | string | ||||
|     | { | ||||
|           func: SpecialVisualization | ||||
|           args: string[] | ||||
|           style: string | ||||
|       } | ||||
|   | string | ||||
|   | { | ||||
|   func: SpecialVisualization | ||||
|   args: string[] | ||||
|   style: string | ||||
| } | ||||
|  |  | |||
|  | @ -35,7 +35,6 @@ import LiveQueryHandler from "../Logic/Web/LiveQueryHandler" | |||
| import { SubtleButton } from "./Base/SubtleButton" | ||||
| import Svg from "../Svg" | ||||
| import NoteCommentElement from "./Popup/NoteCommentElement" | ||||
| import ImgurUploader from "../Logic/ImageProviders/ImgurUploader" | ||||
| import FileSelectorButton from "./Input/FileSelectorButton" | ||||
| import { LoginToggle } from "./Popup/LoginButton" | ||||
| import Toggle from "./Input/Toggle" | ||||
|  | @ -74,6 +73,7 @@ import FediverseValidator from "./InputElement/Validators/FediverseValidator" | |||
| import SendEmail from "./Popup/SendEmail.svelte" | ||||
| import NearbyImages from "./Popup/NearbyImages.svelte" | ||||
| import NearbyImagesCollapsed from "./Popup/NearbyImagesCollapsed.svelte" | ||||
| import UploadImage from "./Image/UploadImage.svelte"; | ||||
| 
 | ||||
| class NearbyImageVis implements SpecialVisualization { | ||||
|     // Class must be in SpecialVisualisations due to weird cyclical import that breaks the tests
 | ||||
|  | @ -616,16 +616,19 @@ export default class SpecialVisualizations { | |||
|                     { | ||||
|                         name: "image-key", | ||||
|                         doc: "Image tag to add the URL to (or image-tag:0, image-tag:1 when multiple images are added)", | ||||
|                         defaultValue: "image", | ||||
|                         required: false | ||||
|                     }, | ||||
|                     { | ||||
|                         name: "label", | ||||
|                         doc: "The text to show on the button", | ||||
|                         defaultValue: "Add image", | ||||
|                         required: false | ||||
|                     }, | ||||
|                 ], | ||||
|                 constr: (state, tags, args) => { | ||||
|                     return new ImageUploadFlow(tags, state, args[0], args[1]) | ||||
|                     return new SvelteUIElement(UploadImage, { | ||||
|                         state,tags, labelText: args[1], image: args[0] | ||||
|                     }) | ||||
|                    // return new ImageUploadFlow(tags, state, args[0], args[1])
 | ||||
|                 }, | ||||
|             }, | ||||
|             { | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue