diff --git a/langs/en.json b/langs/en.json index 1b2195d26..60d0cc1d2 100644 --- a/langs/en.json +++ b/langs/en.json @@ -63,16 +63,6 @@ "overwrite": "Overwrite in OpenStreetMap", "title": "Structured data loaded from the external website" }, - "failedImages": { - "confirmDelete": "Permanently delete this image", - "confirmDeleteTitle": "Delete this image?", - "delete": "Delete this image", - "intro": "The following images failed to upload", - "menu": "Failed images ({count})", - "noFailedImages": "There are currently no failed images", - "retry": "Retry uploading this image", - "retryAll": "Retry uploading all images" - }, "favourite": { "reload": "Reload the data" }, @@ -612,6 +602,15 @@ }, "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." }, + "imageQueue": { + "confirmDelete": "Permanently delete this image", + "confirmDeleteTitle": "Delete this image?", + "delete": "Delete this image", + "intro": "The following images are queued for upload", + "menu": "Image upload queue ({count})", + "noFailedImages": "There are currently no images in the upload queue", + "retryAll": "Retry uploading all images" + }, "importInspector": { "title": "Inspect and manage import notes" }, diff --git a/scripts/generateLayouts.ts b/scripts/generateLayouts.ts index 79a7469aa..d1f20ff41 100644 --- a/scripts/generateLayouts.ts +++ b/scripts/generateLayouts.ts @@ -426,7 +426,7 @@ class GenerateLayouts extends Script { const csp: Record = { "default-src": "'self'", "child-src": "'self' blob: ", - "img-src": "* data:", // maplibre depends on 'data:' to load + "img-src": "* data: blob:", // maplibre depends on 'data:' to load; 'blob:' is needed "report-to": "https://report.mapcomplete.org/csp", "worker-src": "'self' blob:", // Vite somehow loads the worker via a 'blob' "style-src": "'self' 'unsafe-inline'", // unsafe-inline is needed to change the default background pin colours diff --git a/src/Logic/ImageProviders/EmergencyImageBackup.ts b/src/Logic/ImageProviders/EmergencyImageBackup.ts deleted file mode 100644 index 329e1731c..000000000 --- a/src/Logic/ImageProviders/EmergencyImageBackup.ts +++ /dev/null @@ -1,112 +0,0 @@ -import { IdbLocalStorage } from "../Web/IdbLocalStorage" -import { Store, UIEventSource } from "../UIEventSource" -import ThemeViewState from "../../Models/ThemeViewState" -import LinkImageAction from "../Osm/Actions/LinkImageAction" -import { WithImageState } from "../../Models/ThemeViewState/WithImageState" - -export interface FailedImageArgs { - readonly featureId: string, - readonly author: string, - readonly blob: File, - readonly targetKey: string | undefined, - readonly noblur: boolean, - readonly ignoreGps: boolean, - readonly lastGpsLocation: GeolocationCoordinates, - readonly layoutId: string - readonly date: number -} - -export default class EmergencyImageBackup { - - public static readonly singleton = new EmergencyImageBackup() - private readonly _failedImages: UIEventSource - - public readonly failedImages: Store - - private readonly _isUploading: UIEventSource = new UIEventSource(false) - public readonly isUploading: Store = this._isUploading - - private constructor() { - this._failedImages = IdbLocalStorage.Get("failed-images-backup", { defaultValue: [] }) - this.failedImages = this._failedImages - } - - public addFailedImage(args: FailedImageArgs) { - this._failedImages.data.push(args) - this._failedImages.ping() - } - - public delete(img: FailedImageArgs) { - const index = this._failedImages.data.indexOf(img) - if (index < 0) { - return - } - this._failedImages.data.splice(index, 1) - this._failedImages.ping() - } - - /** - * Retries uploading the given image - * Returns 'true' if the image got correctly uploaded and linked (or upload is no longer necessary, e.g. deleted iem) - * @param state - * @param i - */ - public async retryUploading(state: ThemeViewState, i: FailedImageArgs): Promise { - this._isUploading.set(true) - try { - - const feature = await state.osmObjectDownloader.DownloadObjectAsync(i.featureId) - if (feature === "deleted") { - return true - } - - const asGeojson = feature.asGeoJson() - const uploadResult = await state.imageUploadManager.uploadImageWithLicense( - i.featureId, - i.author, - i.blob, - i.targetKey, - i.noblur, - asGeojson, - { - ignoreGps: i.ignoreGps, - noBackup: true,// Don't save this _again_ - overwriteGps: i.lastGpsLocation - } - ) - if (!uploadResult) { - // Upload failed again - return false - } - state.featureProperties.trackFeature(asGeojson) - const properties = state.featureProperties.getStore(i.featureId) - // Upload successful, time to link this to the image - const action = new LinkImageAction( - i.featureId, - uploadResult.key, - uploadResult.value, - properties, - { - theme: i.layoutId, - changeType: "add-image" - } - ) - - await state.changes.applyAction(action) - await state.changes.flushChanges("delayed image upload link") - this.delete(i) - return true - } finally { - this._isUploading.set(false) - } - } - - public async retryAll(state: WithImageState) { - for (const img of [...this._failedImages.data]) { - await this.retryUploading(state, img) - /*this._isUploading.setData(true) - await Utils.waitFor(2000) - this._isUploading.set(false)*/ - } - } -} diff --git a/src/Logic/ImageProviders/ImageUploadManager.ts b/src/Logic/ImageProviders/ImageUploadManager.ts index 593bdf8a1..08971c121 100644 --- a/src/Logic/ImageProviders/ImageUploadManager.ts +++ b/src/Logic/ImageProviders/ImageUploadManager.ts @@ -1,35 +1,53 @@ import { ImageUploader, UploadResult } from "./ImageUploader" import LinkImageAction from "../Osm/Actions/LinkImageAction" import FeaturePropertiesStore from "../FeatureSource/Actors/FeaturePropertiesStore" -import { OsmId, OsmTags } from "../../Models/OsmFeature" +import { NoteId, OsmId, OsmTags } from "../../Models/OsmFeature" import ThemeConfig from "../../Models/ThemeConfig/ThemeConfig" import { Store, UIEventSource } from "../UIEventSource" import { OsmConnection } from "../Osm/OsmConnection" import { Changes } from "../Osm/Changes" import Translations from "../../UI/i18n/Translations" import { Translation } from "../../UI/i18n/Translation" -import { IndexedFeatureSource } from "../FeatureSource/FeatureSource" -import { GeoOperations } from "../GeoOperations" import { Feature } from "geojson" -import EmergencyImageBackup from "./EmergencyImageBackup" +import ImageUploadQueue, { ImageUploadArguments } from "./ImageUploadQueue" +import { GeoOperations } from "../GeoOperations" +import NoteCommentElement from "../../UI/Popup/Notes/NoteCommentElement" +import OsmObjectDownloader from "../Osm/OsmObjectDownloader" /** * The ImageUploadManager has a */ export class ImageUploadManager { + private readonly _queue: ImageUploadQueue = ImageUploadQueue.singleton private readonly _uploader: ImageUploader private readonly _featureProperties: FeaturePropertiesStore private readonly _theme: ThemeConfig - private readonly _indexedFeatures: IndexedFeatureSource private readonly _gps: Store - 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 - public readonly isUploading: Store + + /** + * Keeps track of the _features_ for which an upload was successfull. Only used to give an indication. + * Every time an image is uploaded, the featureID is added to the list. Not persisted (and should not be) + */ + private readonly _successfull: UIEventSource = new UIEventSource([]) + public readonly successfull: Store = this._successfull + /** + * Keeps track of the _features_ for which an upload failed. Only used to give an indication to the user. + * Every time an image upload fails, the featureID is added to the list. Not persisted (and should not be) + */ + private readonly _fails: UIEventSource = new UIEventSource([]) + public readonly fails: Store = this._fails.map(args => args.map(a => a.featureId)) + /** + * FeatureIDs of queued items + */ + public readonly queued: Store = this._queue.imagesInQueue.map(queue => queue.map(q => q.featureId)) + public readonly queuedArgs = this._queue.imagesInQueue + /** + * The feature for which an upload is currently running + */ + public readonly _isUploading: UIEventSource = new UIEventSource(undefined) + public readonly isUploading: Store = this._isUploading private readonly _reportError: ( message: string | Error | XMLHttpRequest, extramessage?: string @@ -42,7 +60,6 @@ export class ImageUploadManager { osmConnection: OsmConnection, changes: Changes, gpsLocation: Store, - allFeatures: IndexedFeatureSource, reportError: ( message: string | Error | XMLHttpRequest, extramessage?: string @@ -53,41 +70,8 @@ export class ImageUploadManager { this._theme = layout this._osmConnection = osmConnection this._changes = changes - this._indexedFeatures = allFeatures this._gps = gpsLocation this._reportError = reportError - - const failed = this.getCounterFor(this._uploadFailed, "*") - const done = this.getCounterFor(this._uploadFinished, "*") - - this.isUploading = this.getCounterFor(this._uploadStarted, "*").map( - (startedCount) => { - return startedCount > failed.data + done.data - }, - [failed, done] - ) - } - - /** - * 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) - } } public canBeUploaded(file: File): true | { error: Translation } { @@ -108,110 +92,187 @@ export class ImageUploadManager { /** * Uploads the given image, applies the correct title and license for the known user. - * Will then add this image to the OSM-feature or the OSM-note + * Will then add this image to the OSM-feature or the OSM-note automatically, based on the ID of the feature. + * Note: the image will actually be added to the queue. If the image-upload fails, this will be attempted when visiting MC again * @param file a jpg file to upload * @param tagsStore The tags of the feature * @param targetKey Use this key to save the attribute under. Default: 'image' * @param noblur if true, then the api call will indicate that the image is already blurred. The server won't apply blurring in this case * @param feature the feature this image is about. Will be used as fallback to get the GPS-coordinates */ - public async uploadImageAndApply( + public uploadImageAndApply( file: File, tagsStore: UIEventSource, targetKey: string, noblur: boolean, - feature: Feature - ): Promise { + feature: Feature, + options: { + ignoreGPS: boolean | false + } + ): void { const canBeUploaded = this.canBeUploaded(file) if (canBeUploaded !== true) { throw canBeUploaded.error } - const tags = tagsStore.data + const tags: OsmTags = tagsStore.data + const featureId = tags.id - const featureId = tags.id - const author = this._osmConnection.userDetails.data.name + const author = this._osmConnection?.userDetails?.data?.name ?? "Anonymous" // Might be a note upload - const uploadResult = await this.uploadImageWithLicense( - featureId, - author, - file, - targetKey, - noblur, - feature - ) - if (!uploadResult) { - return + /** + * The location to upload the image with. + * Note that EXIF-data will always be trusted _more_ by the uploader + */ + let location: [number, number] = GeoOperations.centerpointCoordinates(feature) + if (this._gps.data && !options?.ignoreGPS) { + location = [this._gps.data.longitude, this._gps.data.latitude] } - const properties = this._featureProperties.getStore(featureId) - const action = new LinkImageAction( - featureId, - uploadResult.key, - uploadResult.value, - properties, - { - theme: tags?.data?.["_orig_theme"] ?? this._theme.id, - changeType: "add-image" - } - ) + const args: ImageUploadArguments = { + location, + date: new Date().getTime(), + layoutId: this._theme.id, + author, blob: file, featureId, noblur, targetKey + } + console.log("Args are", args) + + this._queue.add(args) + this.uploadQueue() - await this._changes.applyAction(action) } /** - * Uploads an image; returns undefined if the image upload failed. - * Errors are handled internally + * Attempts to upload all items in the queue + */ + private uploadingAll = false + + public async uploadQueue() { + if (this.uploadingAll) { + return + } + const queue = this._queue.imagesInQueue.data ?? [] + if (queue.length === 0) { + return + } + console.log("Checking image upload queue and uploading if needed") + this.uploadingAll = true + try { + for (const imageToUpload of queue) { + await this.handleQueueItem(imageToUpload) + } + } catch (e) { + console.error("Error while handling the queue:", e) + await this._reportError("Image Upload Manager: queue stopped working:", e) + } finally { + this.uploadingAll = false + } + } + + /** + * Handles a queue item: + * - starts upload + * - indicates that the upload is busy + * - Applies the action to the correct element + * - indicates failure + * @private + */ + private async handleQueueItem(args: ImageUploadArguments): Promise { + console.log("Handling queue item", args) + if (!args) { + return + } + this._isUploading.set(args.featureId) + + let result: UploadResult = undefined + let attempts = 2 + while (attempts > 0 && result === undefined) { + attempts-- + const doReport = attempts == 0 + result = await this.attemptSingleUpload(args, doReport) + if (!result) { + console.log("Upload attempt failed, attempts left:", attempts) + } + } + this._isUploading.set(undefined) + if (result === undefined) { + this._fails.data.push(args) + this._fails.ping() + return + } + this._fails.set(this._fails.data.filter(a => a !== args)) + let properties: UIEventSource> = this._featureProperties.getStore(args.featureId) + + if (args.featureId.startsWith("note/")) { + // This is an OSM-note + const url = result.absoluteUrl + await this._osmConnection.addCommentToNote(args.featureId, url) + const properties: UIEventSource> = this._featureProperties.getStore(args.featureId) + if (properties) { + // Properties will not be defined if the note isn't loaded, but that is no problem as the below code is only relevant if the note is shown + NoteCommentElement.addCommentTo(url, properties, { + osmConnection: this._osmConnection + }) + } + } else { + if (properties === undefined) { + const downloaded = await new OsmObjectDownloader(this._osmConnection.Backend(), this._changes).DownloadObjectAsync(args.featureId) + if (downloaded === "deleted") { + this._queue.delete(args) + return + } + properties = new UIEventSource(downloaded.tags) + } + const action = new LinkImageAction( + args.featureId, + result.key, + result.value, + properties, + { + theme: properties?.data?.["_orig_theme"] ?? this._theme.id, + changeType: "add-image" + } + ) + await this._changes.applyAction(action) + await this._changes.flushChanges("Image upload completed") + } + + this._queue.delete(args) + + } + + /** + * Attempts to upload the image (once). + * Returns 'undefined' if failed * @param featureId * @param author * @param blob * @param targetKey * @param noblur - * @param feature - * @param options + * @param lastGpsLocation + * @param ignoreGps + * @param layoutId + * @param date + * @param reportOnFail If set, reports an error to the mapcomplete server so that pietervdvn can fix it + * @private */ - public async uploadImageWithLicense( - featureId: string, - author: string, - blob: File, - targetKey: string | undefined, - noblur: boolean, - feature: Feature, - options?: { - ignoreGps?: boolean, - noBackup?: boolean, - overwriteGps?: GeolocationCoordinates - - } + private async attemptSingleUpload( + { + featureId, + author, + blob, + targetKey, + noblur, + location + }: ImageUploadArguments, + reportOnFail: boolean ): Promise { - this.increaseCountFor(this._uploadStarted, featureId) + let key: string let value: string let absoluteUrl: string - let location: [number, number] = undefined - if (options?.overwriteGps) { - location = [options.overwriteGps.longitude, options.overwriteGps.latitude] - } else if (this._gps.data && !options?.ignoreGps) { - location = [this._gps.data.longitude, this._gps.data.latitude] - } - { - feature ??= this._indexedFeatures.featuresById.data.get(featureId) - if (feature === undefined) { - throw "ImageUploadManager: no feature given and no feature found in the indexedFeature. Cannot upload this image" - } - const featureCenterpoint = GeoOperations.centerpointCoordinates(feature) - if ( - location === undefined || - location?.some((l) => l === undefined) || - GeoOperations.distanceBetween(location, featureCenterpoint) > 150 - ) { - /* GPS location is either unknown or very far away from the photographed location. - * Default to the centerpoint - */ - location = featureCenterpoint - } - } + try { ({ key, value, absoluteUrl } = await this._uploader.uploadImage( blob, @@ -220,27 +281,9 @@ export class ImageUploadManager { noblur )) } catch (e) { - this.increaseCountFor(this._uploadRetried, featureId) - console.error("Could not upload image, trying again:", e) - try { - ({ key, value, absoluteUrl } = await this._uploader.uploadImage( - blob, - location, - author, - noblur - )) - this.increaseCountFor(this._uploadRetriedSuccess, featureId) - } catch (e) { - console.error("Could again not upload image due to", e) - this.increaseCountFor(this._uploadFailed, featureId) - if (!options?.noBackup) { - EmergencyImageBackup.singleton.addFailedImage({ - blob, author, noblur, featureId, targetKey, ignoreGps: options?.ignoreGps, - layoutId: this._theme.id, - lastGpsLocation: this._gps.data, - date: new Date().getTime() - }) - } + console.error("Could again not upload image due to", e) + if (reportOnFail) { + await this._reportError( e, JSON.stringify({ @@ -250,37 +293,15 @@ export class ImageUploadManager { targetKey }) ) - return undefined } + return undefined } - console.log("Uploading image done, creating action for", featureId, " targetkey is", targetKey, "key is", key) key = targetKey ?? key if (targetKey && targetKey.indexOf(key) < 0) { // This is a non-standard key, so we use the image link directly value = absoluteUrl } - this.increaseCountFor(this._uploadFinished, featureId) return { key, absoluteUrl, value } } - 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(global.data + 1) - } - } } diff --git a/src/Logic/ImageProviders/ImageUploadQueue.ts b/src/Logic/ImageProviders/ImageUploadQueue.ts new file mode 100644 index 000000000..de3aa8cf7 --- /dev/null +++ b/src/Logic/ImageProviders/ImageUploadQueue.ts @@ -0,0 +1,46 @@ +import { IdbLocalStorage } from "../Web/IdbLocalStorage" +import { Store, UIEventSource } from "../UIEventSource" + +export interface ImageUploadArguments { + readonly featureId: string, + readonly author: string, + readonly blob: File, + readonly targetKey: string | undefined, + readonly noblur: boolean, + readonly location: [number, number], + readonly layoutId: string + readonly date: number +} + +/** + * The 'imageUploadQueue' keeps track of all images that should still be uploaded. + * It is backed up in the indexedDB as to not drop images in case of connection problems + */ +export default class ImageUploadQueue { + + public static readonly singleton = new ImageUploadQueue() + private readonly _imagesInQueue: UIEventSource + + public readonly imagesInQueue: Store + + private constructor() { + this._imagesInQueue = IdbLocalStorage.Get("failed-images-backup", { defaultValue: [] }) + this.imagesInQueue = this._imagesInQueue + } + + public add(args: ImageUploadArguments) { + this._imagesInQueue.data.push(args) + console.log("Got args", args) + this._imagesInQueue.ping() + } + + public delete(img: ImageUploadArguments) { + const index = this._imagesInQueue.data.indexOf(img) + if (index < 0) { + return + } + this._imagesInQueue.data.splice(index, 1) + this._imagesInQueue.ping() + } + +} diff --git a/src/Logic/ImageProviders/Panoramax.ts b/src/Logic/ImageProviders/Panoramax.ts index 4f0204a1b..c00b86871 100644 --- a/src/Logic/ImageProviders/Panoramax.ts +++ b/src/Logic/ImageProviders/Panoramax.ts @@ -263,7 +263,7 @@ export class PanoramaxUploader implements ImageUploader { } console.log("Tags are", tags) } catch (e) { - console.error("Could not read EXIF-tags") + console.warn("Could not read EXIF-tags") } const p = this.panoramax diff --git a/src/Logic/Osm/OsmObject.ts b/src/Logic/Osm/OsmObject.ts index 1c6994181..6a7e4687a 100644 --- a/src/Logic/Osm/OsmObject.ts +++ b/src/Logic/Osm/OsmObject.ts @@ -19,9 +19,8 @@ export abstract class OsmObject { public changed: boolean = false timestamp: Date - protected constructor(type: string, id: number) { + protected constructor(type: "node" | "way" | "relation", id: number) { this.id = id - // @ts-ignore this.type = type this.tags = { id: `${this.type}/${id}`, diff --git a/src/Logic/State/UserRelatedState.ts b/src/Logic/State/UserRelatedState.ts index eaa5d6ab4..f67bf23b3 100644 --- a/src/Logic/State/UserRelatedState.ts +++ b/src/Logic/State/UserRelatedState.ts @@ -90,7 +90,6 @@ export class OptionallySyncedHistory { this.syncedBackingStore = Stores.fromArray( Utils.TimesT(maxHistory, (i) => { const pref = osmconnection.getPreference(key + "-hist-" + i + "-") - pref.addCallbackAndRun(v => console.trace(">>> pref", pref.tag, " is now ", v)) return UIEventSource.asObject(pref, undefined) })) diff --git a/src/Models/OsmFeature.ts b/src/Models/OsmFeature.ts index d8ff8928e..e163a471c 100644 --- a/src/Models/OsmFeature.ts +++ b/src/Models/OsmFeature.ts @@ -3,6 +3,8 @@ import { Feature, Geometry } from "geojson" export type RelationId = `relation/${number}` export type WayId = `way/${number}` export type NodeId = `node/${number}` +export type NoteId = `node/${number}` + export type OsmId = NodeId | WayId | RelationId export type OsmTags = Record & { id: string } diff --git a/src/Models/ThemeViewState/WithImageState.ts b/src/Models/ThemeViewState/WithImageState.ts index f9d0f1dcf..0c7799f25 100644 --- a/src/Models/ThemeViewState/WithImageState.ts +++ b/src/Models/ThemeViewState/WithImageState.ts @@ -9,7 +9,6 @@ import ThemeViewStateHashActor from "../../Logic/Web/ThemeViewStateHashActor" import PendingChangesUploader from "../../Logic/Actors/PendingChangesUploader" import { WithGuiState } from "./WithGuiState" import { SpecialVisualizationState } from "../../UI/SpecialVisualization" -import EmergencyImageBackup from "../../Logic/ImageProviders/EmergencyImageBackup" export class WithImageState extends WithGuiState implements SpecialVisualizationState { readonly imageUploadManager: ImageUploadManager @@ -30,7 +29,6 @@ export class WithImageState extends WithGuiState implements SpecialVisualization this.osmConnection, this.changes, this.geolocation.geolocationState.currentGPSLocation, - this.indexedFeatures, this.reportError ) const longAgo = new Date() @@ -45,7 +43,7 @@ export class WithImageState extends WithGuiState implements SpecialVisualization }) this.osmConnection.userDetails.addCallbackAndRunD(() => { - EmergencyImageBackup.singleton.retryAll(this) + this.imageUploadManager.uploadQueue() }) } @@ -57,7 +55,7 @@ export class WithImageState extends WithGuiState implements SpecialVisualization featureSwitches: this.featureSwitches, selectedElement: this.selectedElement, indexedFeatures: this.indexedFeatures, - guistate: this.guistate, + guistate: this.guistate }) new PendingChangesUploader(this.changes, this.selectedElement, this.imageUploadManager) } diff --git a/src/UI/BigComponents/MenuDrawer.svelte b/src/UI/BigComponents/MenuDrawer.svelte index 6ba902f22..f73259aff 100644 --- a/src/UI/BigComponents/MenuDrawer.svelte +++ b/src/UI/BigComponents/MenuDrawer.svelte @@ -61,9 +61,9 @@ import Hotkeys from "../Base/Hotkeys" import { ArrowTrendingUp } from "@babeard/svelte-heroicons/solid/ArrowTrendingUp" import ArrowTopRightOnSquare from "@babeard/svelte-heroicons/mini/ArrowTopRightOnSquare" - import FailedImagesView from "../Image/FailedImagesView.svelte" import { PhotoIcon } from "@babeard/svelte-heroicons/outline" - import EmergencyImageBackup from "../../Logic/ImageProviders/EmergencyImageBackup" + import ImageUploadQueue from "../../Logic/ImageProviders/ImageUploadQueue" + import QueuedImagesView from "../Image/QueuedImagesView.svelte" export let state: { favourites: FavouritesFeatureSource @@ -100,7 +100,7 @@ } }) let isAndroid = AndroidPolyfill.inAndroid - let nrOfFailedImages = EmergencyImageBackup.singleton.failedImages + let nrOfFailedImages = ImageUploadQueue.singleton.imagesInQueue let failedImagesOpen = pg.failedImages @@ -165,9 +165,9 @@ - + - + {/if} diff --git a/src/UI/Image/FailedImage.svelte b/src/UI/Image/FailedImage.svelte deleted file mode 100644 index 3e22320b8..000000000 --- a/src/UI/Image/FailedImage.svelte +++ /dev/null @@ -1,88 +0,0 @@ - - -
- - - {failedImage.featureId} {failedImage.layoutId} - {#if $isUploading || _state === "retrying"} - - - - {:else if _state === "idle" || _state === "failed"} - - {#if _state === "failed"} - - {/if} - {:else if _state === "success"} -
- -
- {/if} - - - - - - - - -
- -
- -
- -
- confirmDelete.set(false)}> - - - -
-
-
-
-
diff --git a/src/UI/Image/FailedImagesView.svelte b/src/UI/Image/FailedImagesView.svelte deleted file mode 100644 index 1a6d5a55c..000000000 --- a/src/UI/Image/FailedImagesView.svelte +++ /dev/null @@ -1,41 +0,0 @@ - - -
- {#if $failed.length === 0} - - {:else} -
- -
- - {#if $isUploading} - - {:else} - - {/if} -
- {#each $failed as failedImage (failedImage.date + failedImage.featureId)} - - {/each} -
- {/if} -
diff --git a/src/UI/Image/QueuedImage.svelte b/src/UI/Image/QueuedImage.svelte new file mode 100644 index 000000000..3a3fc4aa1 --- /dev/null +++ b/src/UI/Image/QueuedImage.svelte @@ -0,0 +1,65 @@ + + +
+ + + {imageArguments.featureId} {imageArguments.layoutId} + + + + + + + + +
+ +
+ +
+ +
+ confirmDelete.set(false)}> + + + +
+
+
+
+
diff --git a/src/UI/Image/QueuedImagesView.svelte b/src/UI/Image/QueuedImagesView.svelte new file mode 100644 index 000000000..39228bfe3 --- /dev/null +++ b/src/UI/Image/QueuedImagesView.svelte @@ -0,0 +1,42 @@ + + +
+ {#if $queued.length === 0} + + {:else} +
+ +
+ + + + {#if $isUploading} + + {:else} + + {/if} +
+ {#each $queued as i (i.date + i.featureId)} + + {/each} +
+ {/if} +
diff --git a/src/UI/Image/UploadImage.svelte b/src/UI/Image/UploadImage.svelte index 30d85ca03..fd2f0c579 100644 --- a/src/UI/Image/UploadImage.svelte +++ b/src/UI/Image/UploadImage.svelte @@ -14,7 +14,6 @@ import LoginButton from "../Base/LoginButton.svelte" import { Translation } from "../i18n/Translation" import LayerConfig from "../../Models/ThemeConfig/LayerConfig" - import NoteCommentElement from "../Popup/Notes/NoteCommentElement" import type { Feature } from "geojson" import Camera from "@babeard/svelte-heroicons/mini/Camera" @@ -38,7 +37,7 @@ let errors = new UIEventSource([]) - async function handleFiles(files: FileList, ignoreGps: boolean = false) { + async function handleFiles(files: FileList, ignoreGPS: boolean = false) { const errs = [] for (let i = 0; i < files.length; i++) { const file = files.item(i) @@ -49,31 +48,7 @@ errs.push(canBeUploaded.error) continue } - - if (layer?.id === "note") { - const uploadResult = await state?.imageUploadManager.uploadImageWithLicense( - tags.data.id, - state.osmConnection.userDetails.data?.name ?? "Anonymous", - file, - "image", - noBlur, - feature, - { - ignoreGps - } - ) - if (!uploadResult) { - return - } - const url = uploadResult.absoluteUrl - await state.osmConnection.addCommentToNote(tags.data.id, url) - NoteCommentElement.addCommentTo(url, >tags, { - osmConnection: state.osmConnection, - }) - return - } - - await state?.imageUploadManager?.uploadImageAndApply(file, tags, targetKey, noBlur, feature) + await state?.imageUploadManager?.uploadImageAndApply(file, tags, targetKey, noBlur, feature, { ignoreGPS }) } catch (e) { console.error(e) state.reportError(e, "Could not upload image") @@ -100,7 +75,7 @@ {/each} = undefined - export let featureId = tags?.data?.id + export let featureId: OsmId | NoteId | "*" = tags?.data?.id ?? "*" if (featureId === undefined) { throw "No tags or featureID given" } export let showThankYou: boolean = true - const { uploadStarted, uploadFinished, retried, failed } = - state.imageUploadManager.getCountsFor(featureId) + + /* + Number of images uploaded succesfully + */ + function getCount(input: Store): Store { + if (featureId == "*") { + return input.map(inp => inp.length) + } + return input.map(success => success.filter(item => item === featureId).length) + } + + let successfull = getCount(state.imageUploadManager.successfull) + /* Number of failed uploads */ + let failed = getCount(state.imageUploadManager.fails) + + let pending = getCount(state.imageUploadManager.queued) const t = Translations.t.image const debugging = state.featureSwitches.featureSwitchIsDebugging let dismissed = 0 + failed.addCallbackAndRun(failed => { + dismissed = Math.min(failed, dismissed) + }) {#if $debugging}
- Started {$uploadStarted} Done {$uploadFinished} Retry {$retried} Err {$failed} + Pending {$pending} Done {$successfull} Err {$failed}
{/if} -{#if dismissed === $uploadStarted} - -{:else if $uploadStarted === 1} - {#if $uploadFinished === 1} - {#if showThankYou} - - {/if} - {:else if $failed === 1} - (dismissed = $failed)} /> - {:else if $retried === 1} -
- - - -
- {:else} -
- + +{#if $pending - $failed > 0} +
+ + {#if $pending - $failed === 1} - -
- {/if} -{:else if $uploadStarted > 1} - {#if $uploadFinished + $failed === $uploadStarted} - {#if $uploadFinished === 0} - - {:else if showThankYou} - - {/if} - {:else if $uploadFinished === 0} - - + {:else if $pending - $failed > 1} + + {/if} - {:else if $uploadFinished > 0} - - - - {/if} - {#if $failed > 0} - (dismissed = $failed)} /> +
+{/if} + +{#if $failed > dismissed} + (dismissed = $failed)} /> +{/if} + +{#if showThankYou} + {#if $successfull === 1} + + {:else if $successfull > 1} + {/if} {/if} diff --git a/src/UI/Popup/Notes/NoteCommentElement.ts b/src/UI/Popup/Notes/NoteCommentElement.ts index fa0773bff..d8a69e624 100644 --- a/src/UI/Popup/Notes/NoteCommentElement.ts +++ b/src/UI/Popup/Notes/NoteCommentElement.ts @@ -9,7 +9,7 @@ export default class NoteCommentElement { */ public static addCommentTo( txt: string, - tags: UIEventSource, + tags: UIEventSource>, state: { osmConnection: { userDetails: Store<{ name: string; uid: number }> } } ) { const comments: any[] = JSON.parse(tags.data["comments"]) diff --git a/src/UI/Search/ThemeResults.svelte b/src/UI/Search/ThemeResults.svelte index d8a329063..623497268 100644 --- a/src/UI/Search/ThemeResults.svelte +++ b/src/UI/Search/ThemeResults.svelte @@ -11,11 +11,12 @@ import { TrashIcon } from "@babeard/svelte-heroicons/mini" import { CogIcon } from "@rgossiaux/svelte-heroicons/solid" import Tr from "../Base/Tr.svelte" + import { Utils } from "../../Utils.ts" export let state: SpecialVisualizationState let searchTerm = state.searchState.searchTerm let recentThemes = state.userRelatedState.recentlyVisitedThemes.value.map((themes) => - themes.filter((th) => th !== state.theme.id).slice(0, 6) + Utils.Dedup(themes.filter((th) => th !== state.theme.id).slice(0, 6)) ) let themeResults = state.searchState.themeSuggestions