diff --git a/langs/en.json b/langs/en.json index f25b61951..42432d12c 100644 --- a/langs/en.json +++ b/langs/en.json @@ -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!", diff --git a/public/css/index-tailwind-output.css b/public/css/index-tailwind-output.css index f38fd858e..da51a2a7f 100644 --- a/public/css/index-tailwind-output.css +++ b/public/css/index-tailwind-output.css @@ -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%; diff --git a/src/Logic/FeatureSource/Actors/FeaturePropertiesStore.ts b/src/Logic/FeatureSource/Actors/FeaturePropertiesStore.ts index cd7522a29..c8186a8ba 100644 --- a/src/Logic/FeatureSource/Actors/FeaturePropertiesStore.ts +++ b/src/Logic/FeatureSource/Actors/FeaturePropertiesStore.ts @@ -7,7 +7,7 @@ import { OsmTags } from "../../../Models/OsmFeature" */ export default class FeaturePropertiesStore { private readonly _elements = new Map>>() - + public readonly aliases = new Map() 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() } diff --git a/src/Logic/ImageProviders/ImageUploadManager.ts b/src/Logic/ImageProviders/ImageUploadManager.ts index e69de29bb..9bb2f9535 100644 --- a/src/Logic/ImageProviders/ImageUploadManager.ts +++ b/src/Logic/ImageProviders/ImageUploadManager.ts @@ -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> = new Map(); + private readonly _uploadFinished: Map> = new Map(); + private readonly _uploadFailed: Map> = new Map(); + private readonly _uploadRetried: Map> = new Map(); + private readonly _uploadRetriedSuccess: Map> = 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; + uploadStarted: Store; + retrySuccess: Store; + failed: Store; + uploadFinished: Store + } { + 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 = 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 { + 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>, key: string | "*") { + if (this._featureProperties.aliases.has(key)) { + key = this._featureProperties.aliases.get(key); + } + if (!collection.has(key)) { + collection.set(key, new UIEventSource(0)); + } + return collection.get(key); + } + + private increaseCountFor(collection: Map>, key: string | "*") { + const counter = this.getCounterFor(collection, key); + counter.setData(counter.data + 1); + const global = this.getCounterFor(collection, "*"); + global.setData(counter.data + 1); + } + +} diff --git a/src/Logic/ImageProviders/ImageUploader.ts b/src/Logic/ImageProviders/ImageUploader.ts index e69de29bb..3efb8d279 100644 --- a/src/Logic/ImageProviders/ImageUploader.ts +++ b/src/Logic/ImageProviders/ImageUploader.ts @@ -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 }>; +} diff --git a/src/Logic/ImageProviders/Imgur.ts b/src/Logic/ImageProviders/Imgur.ts index a7a142733..4e4a1c541 100644 --- a/src/Logic/ImageProviders/Imgur.ts +++ b/src/Logic/ImageProviders/Imgur.ts @@ -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, - 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, - 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 { diff --git a/src/Logic/Osm/Actions/LinkImageAction.ts b/src/Logic/Osm/Actions/LinkImageAction.ts index 014a836a0..1b2b90d19 100644 --- a/src/Logic/Osm/Actions/LinkImageAction.ts +++ b/src/Logic/Osm/Actions/LinkImageAction.ts @@ -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>; + 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, + currentTags: Store>, 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 { + 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() } + + } diff --git a/src/Logic/Osm/Actions/OsmChangeAction.ts b/src/Logic/Osm/Actions/OsmChangeAction.ts index 4161dc967..2bf31b02c 100644 --- a/src/Logic/Osm/Actions/OsmChangeAction.ts +++ b/src/Logic/Osm/Actions/OsmChangeAction.ts @@ -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) { diff --git a/src/Logic/State/UserRelatedState.ts b/src/Logic/State/UserRelatedState.ts index 8c276be2c..867ef45d5 100644 --- a/src/Logic/State/UserRelatedState.ts +++ b/src/Logic/State/UserRelatedState.ts @@ -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 public readonly preferredBackgroundLayer: UIEventSource - public readonly preferredBackgroundLayerForTheme: UIEventSource + public readonly imageLicense : UIEventSource /** * 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() diff --git a/src/Models/ThemeViewState.ts b/src/Models/ThemeViewState.ts index caa3b6357..b6f13012b 100644 --- a/src/Models/ThemeViewState.ts +++ b/src/Models/ThemeViewState.ts @@ -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); diff --git a/src/UI/Base/FileSelector.svelte b/src/UI/Base/FileSelector.svelte index e69de29bb..fa4dc2ad6 100644 --- a/src/UI/Base/FileSelector.svelte +++ b/src/UI/Base/FileSelector.svelte @@ -0,0 +1,40 @@ + + +
+ + { + 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" + > +
diff --git a/src/UI/Base/Loading.svelte b/src/UI/Base/Loading.svelte index 097bc4472..ff8a622d7 100644 --- a/src/UI/Base/Loading.svelte +++ b/src/UI/Base/Loading.svelte @@ -1,9 +1,12 @@ - -
+
diff --git a/src/UI/Image/ImageUploadFlow.ts b/src/UI/Image/ImageUploadFlow.ts index 5c3f6b5c6..2a90ab66a 100644 --- a/src/UI/Image/ImageUploadFlow.ts +++ b/src/UI/Image/ImageUploadFlow.ts @@ -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>() 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 - ) } } diff --git a/src/UI/Image/UploadImage.svelte b/src/UI/Image/UploadImage.svelte index e69de29bb..fa82ee34c 100644 --- a/src/UI/Image/UploadImage.svelte +++ b/src/UI/Image/UploadImage.svelte @@ -0,0 +1,73 @@ + + + + + + +
+ + + handleFiles(e.detail)}> +
+ + {#if image !== undefined} + + {:else} + + {/if} + {#if labelText} + {labelText} + {:else} + + {/if} +
+
+ +
+ +
diff --git a/src/UI/Image/UploadingImageCounter.svelte b/src/UI/Image/UploadingImageCounter.svelte index a3bfa02e5..0c1b6f777 100644 --- a/src/UI/Image/UploadingImageCounter.svelte +++ b/src/UI/Image/UploadingImageCounter.svelte @@ -1,31 +1,67 @@ +{#if $uploadStarted == 1} + {#if $uploadFinished == 1 } + + {:else if $failed == 1} +
+ + + +
+ {:else if $retried == 1} + + + + {:else } + + + + {/if} +{:else if $uploadStarted > 1} + {#if ($uploadFinished + $failed) == $uploadStarted && $uploadFinished > 0} + + {:else if $uploadFinished == 0} + + + + {:else if $uploadFinished > 0} + + + + {/if} + {#if $failed > 0} +
+ {#if failed === 1} + + {:else} + - - - - - + {/if} + + +
+ {/if} +{/if} diff --git a/src/UI/SpecialVisualization.ts b/src/UI/SpecialVisualization.ts index 4cb3aeb02..1d3575b42 100644 --- a/src/UI/SpecialVisualization.ts +++ b/src/UI/SpecialVisualization.ts @@ -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> } + readonly layerState: LayerState; + readonly featureProperties: { getStore(id: string): UIEventSource> }; - 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> + readonly historicalUserLocations: WritableFeatureSource>; - readonly osmConnection: OsmConnection - readonly featureSwitchUserbadge: Store - readonly featureSwitchIsTesting: Store - readonly changes: Changes - readonly osmObjectDownloader: OsmObjectDownloader - /** - * State of the main map - */ - readonly mapProperties: MapProperties & ExportableMap + readonly osmConnection: OsmConnection; + readonly featureSwitchUserbadge: Store; + readonly featureSwitchIsTesting: Store; + readonly changes: Changes; + readonly osmObjectDownloader: OsmObjectDownloader; + /** + * State of the main map + */ + readonly mapProperties: MapProperties & ExportableMap; - readonly selectedElement: UIEventSource - /** - * Works together with 'selectedElement' to indicate what properties should be displayed - */ - readonly selectedLayer: UIEventSource - readonly selectedElementAndLayer: Store<{ feature: Feature; layer: LayerConfig }> + readonly selectedElement: UIEventSource; + /** + * Works together with 'selectedElement' to indicate what properties should be displayed + */ + readonly selectedLayer: UIEventSource; + readonly selectedElementAndLayer: Store<{ feature: Feature; layer: LayerConfig }>; - /** - * If data is currently being fetched from external sources - */ - readonly dataIsLoading: Store - /** - * Only needed for 'ReplaceGeometryAction' - */ - readonly fullNodeDatabase?: FullNodeDatabaseSource + /** + * If data is currently being fetched from external sources + */ + readonly dataIsLoading: Store; + /** + * Only needed for 'ReplaceGeometryAction' + */ + readonly fullNodeDatabase?: FullNodeDatabaseSource; - readonly perLayer: ReadonlyMap - readonly userRelatedState: { - readonly showTags: UIEventSource<"no" | undefined | "always" | "yes" | "full"> - readonly mangroveIdentity: MangroveIdentity - readonly showAllQuestionsAtOnce: UIEventSource - readonly preferencesAsTags: Store> - readonly language: UIEventSource - } - readonly lastClickObject: WritableFeatureSource + readonly perLayer: ReadonlyMap; + readonly userRelatedState: { + readonly imageLicense: UIEventSource; + readonly showTags: UIEventSource<"no" | undefined | "always" | "yes" | "full"> + readonly mangroveIdentity: MangroveIdentity + readonly showAllQuestionsAtOnce: UIEventSource + readonly preferencesAsTags: Store> + readonly language: UIEventSource + }; + readonly lastClickObject: WritableFeatureSource; - readonly availableLayers: Store + readonly availableLayers: Store; + + 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>; args: string[] }[] + structuredExamples?(): { feature: Feature>; args: string[] }[]; - constr( - state: SpecialVisualizationState, - tagSource: UIEventSource>, - argument: string[], - feature: Feature, - layer: LayerConfig - ): BaseUIElement + constr( + state: SpecialVisualizationState, + tagSource: UIEventSource>, + argument: string[], + feature: Feature, + layer: LayerConfig + ): BaseUIElement; } export type RenderingSpecification = - | string - | { - func: SpecialVisualization - args: string[] - style: string - } + | string + | { + func: SpecialVisualization + args: string[] + style: string +} diff --git a/src/UI/SpecialVisualizations.ts b/src/UI/SpecialVisualizations.ts index 0d9e1d66f..24b712da2 100644 --- a/src/UI/SpecialVisualizations.ts +++ b/src/UI/SpecialVisualizations.ts @@ -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]) }, }, {