Feature: image upload now uses the indexedDB-backed queue (formerly known as EmergencyBackup), rework (and simplify) counter logic (fix #2186; fix #1942; helps #2022)

This commit is contained in:
Pieter Vander Vennet 2025-04-07 02:53:21 +02:00
parent 55c015ad84
commit 3d3a72a70a
19 changed files with 402 additions and 503 deletions

View file

@ -1,88 +0,0 @@
<script lang="ts">
import type { FailedImageArgs } from "../../Logic/ImageProviders/EmergencyImageBackup"
import ThemeViewState from "../../Models/ThemeViewState"
import EmergencyImageBackup from "../../Logic/ImageProviders/EmergencyImageBackup"
import Loading from "../Base/Loading.svelte"
import { TrashIcon } from "@babeard/svelte-heroicons/mini"
import Popup from "../Base/Popup.svelte"
import { UIEventSource } from "../../Logic/UIEventSource"
import Page from "../Base/Page.svelte"
import BackButton from "../Base/BackButton.svelte"
import Tr from "../Base/Tr.svelte"
import Translations from "../i18n/Translations"
let emergencyBackup = EmergencyImageBackup.singleton
let isUploading = emergencyBackup.isUploading
export let state: ThemeViewState
let _state: "idle" | "retrying" | "failed" | "success" = "idle"
export let failedImage: FailedImageArgs
let confirmDelete = new UIEventSource(false)
async function retry() {
_state = "retrying"
const success = await emergencyBackup.retryUploading(state, failedImage)
if (success) {
_state = "success"
} else {
_state = "failed"
}
}
function del() {
emergencyBackup.delete(failedImage)
}
const t = Translations.t
</script>
<div class="low-interaction rounded border-interactive w-fit p-2 m-1 flex flex-col">
<img class="max-w-64 w-auto max-h-64 w-auto" src={URL.createObjectURL(failedImage.blob)} />
{failedImage.featureId} {failedImage.layoutId}
{#if $isUploading || _state === "retrying"}
<Loading>
<Tr t={t.image.upload.one.uploading} />
</Loading>
{:else if _state === "idle" || _state === "failed"}
<button on:click={() => retry()}>
<Tr t={t.failedImages.retry} />
</button>
{#if _state === "failed"}
<span class="alert"><Tr t={t.image.upload.one.failed} /></span>
{/if}
{:else if _state === "success"}
<div class="thanks">
<Tr t={t.image.upload.one.done} />
</div>
{/if}
<button class="as-link self-end" on:click={() => {confirmDelete.set(true)}}>
<TrashIcon class="w-4" />
<Tr t={t.failedImages.delete} />
</button>
<Popup shown={confirmDelete} dismissable={true}>
<Page shown={confirmDelete}>
<svelte:fragment slot="header">
<TrashIcon class="w-8 m-1" />
<Tr t={t.failedImages.confirmDeleteTitle} />
</svelte:fragment>
<div class="flex flex-col ">
<div class="flex justify-center">
<img class="max-w-128 w-auto max-h-128 w-auto" src={URL.createObjectURL(failedImage.blob)} />
</div>
<div class="flex w-full">
<BackButton clss="w-full" on:click={() => confirmDelete.set(false)}>
<Tr t={t.general.back} />
</BackButton>
<button on:click={() => del()} class="primary w-full">
<TrashIcon class="w-8 m-1" />
<Tr t={t.failedImages.confirmDelete} />
</button>
</div>
</div>
</Page>
</Popup>
</div>

View file

@ -1,41 +0,0 @@
<script lang="ts">
import EmergencyImageBackup from "../../Logic/ImageProviders/EmergencyImageBackup"
import ThemeViewState from "../../Models/ThemeViewState"
import FailedImage from "./FailedImage.svelte"
import { ArrowPathIcon } from "@babeard/svelte-heroicons/mini"
import Loading from "../Base/Loading.svelte"
import { WithImageState } from "../../Models/ThemeViewState/WithImageState"
import Tr from "../Base/Tr.svelte"
import Translations from "../i18n/Translations"
let emergencyBackup = EmergencyImageBackup.singleton
let failed = emergencyBackup.failedImages
export let state: WithImageState
let isUploading = emergencyBackup.isUploading
const t = Translations.t
</script>
<div class="m-4 flex flex-col">
{#if $failed.length === 0}
<Tr t={t.failedImages.noFailedImages} />
{:else}
<div>
<Tr t={t.failedImages.intro} />
</div>
{#if $isUploading}
<Loading />
{:else}
<button class="primary" on:click={() => emergencyBackup.retryAll(state)}>
<ArrowPathIcon class="w-8 h-8 m-1" />
<Tr t={t.failedImages.retryAll} />
</button>
{/if}
<div class="flex flex-wrap">
{#each $failed as failedImage (failedImage.date + failedImage.featureId)}
<FailedImage {failedImage} {state} />
{/each}
</div>
{/if}
</div>

View file

@ -0,0 +1,65 @@
<script lang="ts">
import type { ImageUploadArguments } from "../../Logic/ImageProviders/ImageUploadQueue"
import ImageUploadQueue from "../../Logic/ImageProviders/ImageUploadQueue"
import { TrashIcon } from "@babeard/svelte-heroicons/mini"
import Popup from "../Base/Popup.svelte"
import { UIEventSource } from "../../Logic/UIEventSource"
import Page from "../Base/Page.svelte"
import BackButton from "../Base/BackButton.svelte"
import Tr from "../Base/Tr.svelte"
import Translations from "../i18n/Translations"
let queue = ImageUploadQueue.singleton
export let imageArguments: ImageUploadArguments
let confirmDelete = new UIEventSource(false)
function del() {
queue.delete(imageArguments)
}
const t = Translations.t
let src = undefined
try{
src = URL.createObjectURL(imageArguments.blob)
}catch (e) {
console.error("Could not create an ObjectURL for blob", imageArguments.blob)
}
</script>
<div class="low-interaction rounded border-interactive w-fit p-2 m-1 flex flex-col">
<img class="max-w-64 w-auto max-h-64 w-auto" {src} />
{imageArguments.featureId} {imageArguments.layoutId}
<button class="as-link self-end" on:click={() => {confirmDelete.set(true)}}>
<TrashIcon class="w-4" />
<Tr t={t.imageQueue.delete} />
</button>
<Popup shown={confirmDelete} dismissable={true}>
<Page shown={confirmDelete}>
<svelte:fragment slot="header">
<TrashIcon class="w-8 m-1" />
<Tr t={t.imageQueue.confirmDeleteTitle} />
</svelte:fragment>
<div class="flex flex-col ">
<div class="flex justify-center">
<img class="max-w-128 w-auto max-h-128 w-auto" src={URL.createObjectURL(imageArguments.blob)} />
</div>
<div class="flex w-full">
<BackButton clss="w-full" on:click={() => confirmDelete.set(false)}>
<Tr t={t.general.back} />
</BackButton>
<button on:click={() => del()} class="primary w-full">
<TrashIcon class="w-8 m-1" />
<Tr t={t.imageQueue.confirmDelete} />
</button>
</div>
</div>
</Page>
</Popup>
</div>

View file

@ -0,0 +1,42 @@
<script lang="ts">
import QueuedImage from "./QueuedImage.svelte"
import { ArrowPathIcon } from "@babeard/svelte-heroicons/mini"
import Loading from "../Base/Loading.svelte"
import { WithImageState } from "../../Models/ThemeViewState/WithImageState"
import Tr from "../Base/Tr.svelte"
import Translations from "../i18n/Translations"
import type { ImageUploadArguments } from "../../Logic/ImageProviders/ImageUploadQueue"
import { Store } from "../../Logic/UIEventSource"
import UploadingImageCounter from "./UploadingImageCounter.svelte"
export let state: WithImageState
let queued: Store<ImageUploadArguments[]> = state.imageUploadManager.queuedArgs
let isUploading = state.imageUploadManager.isUploading
const t = Translations.t
const q = t.imageQueue
</script>
<div class="m-4 flex flex-col">
{#if $queued.length === 0}
<Tr t={q.noFailedImages} />
{:else}
<div>
<Tr t={q.intro} />
</div>
<UploadingImageCounter {state}/>
{#if $isUploading}
<Loading />
{:else}
<button class="primary" on:click={() => state.imageUploadManager.uploadQueue()}>
<ArrowPathIcon class="w-8 h-8 m-1" />
<Tr t={q.retryAll} />
</button>
{/if}
<div class="flex flex-wrap">
{#each $queued as i (i.date + i.featureId)}
<QueuedImage imageArguments={i} />
{/each}
</div>
{/if}
</div>

View file

@ -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<Translation[]>([])
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, <UIEventSource<OsmTags>>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 @@
<Tr t={error} cls="alert" />
{/each}
<FileSelector
accept="image/*"
accept=".jpg,.jpeg,image/jpeg"
capture="environment"
cls="button border-2 flex flex-col"
multiple={true}

View file

@ -7,76 +7,69 @@
import type { SpecialVisualizationState } from "../SpecialVisualization"
import { Store } from "../../Logic/UIEventSource"
import type { OsmTags } from "../../Models/OsmFeature"
import type { NoteId, OsmTags, OsmId } from "../../Models/OsmFeature"
import Translations from "../i18n/Translations"
import Tr from "../Base/Tr.svelte"
import Loading from "../Base/Loading.svelte"
import { XCircleIcon } from "@babeard/svelte-heroicons/solid"
import UploadFailedMessage from "./UploadFailedMessage.svelte"
export let state: SpecialVisualizationState
export let tags: Store<OsmTags> = 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<string[]>): Store<number> {
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)
})
</script>
{#if $debugging}
<div class="low-interaction">
Started {$uploadStarted} Done {$uploadFinished} Retry {$retried} Err {$failed}
Pending {$pending} Done {$successfull} Err {$failed}
</div>
{/if}
{#if dismissed === $uploadStarted}
<!-- We don't show anything as we ignore this number of failed items-->
{:else if $uploadStarted === 1}
{#if $uploadFinished === 1}
{#if showThankYou}
<Tr cls="thanks" t={t.upload.one.done} />
{/if}
{:else if $failed === 1}
<UploadFailedMessage failed={$failed} on:click={() => (dismissed = $failed)} />
{:else if $retried === 1}
<div class="alert">
<Loading>
<Tr t={t.upload.one.retrying} />
</Loading>
</div>
{:else}
<div class="alert">
<Loading>
{#if $pending - $failed > 0}
<div class="alert">
<Loading>
{#if $pending - $failed === 1}
<Tr t={t.upload.one.uploading} />
</Loading>
</div>
{/if}
{:else if $uploadStarted > 1}
{#if $uploadFinished + $failed === $uploadStarted}
{#if $uploadFinished === 0}
<!-- pass -->
{:else if showThankYou}
<Tr cls="thanks" t={t.upload.multiple.done.Subs({ count: $uploadFinished })} />
{/if}
{:else if $uploadFinished === 0}
<Loading cls="alert">
<Tr t={t.upload.multiple.uploading.Subs({ count: $uploadStarted })} />
{:else if $pending - $failed > 1}
<Tr t={t.upload.multiple.uploading.Subs({count: $pending})} />
{/if}
</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}
<UploadFailedMessage failed={$failed} on:click={() => (dismissed = $failed)} />
</div>
{/if}
{#if $failed > dismissed}
<UploadFailedMessage failed={$failed} on:click={() => (dismissed = $failed)} />
{/if}
{#if showThankYou}
{#if $successfull === 1}
<Tr cls="thanks" t={t.upload.one.done} />
{:else if $successfull > 1}
<Tr cls="thanks" t={t.upload.multiple.done.Subs({count: $successfull})} />
{/if}
{/if}