Feature: add emergency image backup. If uploading images fails, they are saved into local storage and uploaded later on. Part of #2111, but also #2342

This commit is contained in:
Pieter Vander Vennet 2025-04-06 15:32:58 +02:00
parent 7380841205
commit 9f3d198068
9 changed files with 313 additions and 8 deletions

View file

@ -61,6 +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"
export let state: {
favourites: FavouritesFeatureSource
@ -97,6 +100,8 @@
}
})
let isAndroid = AndroidPolyfill.inAndroid
let nrOfFailedImages = EmergencyImageBackup.singleton.failedImages
let failedImagesOpen = pg.failedImages
</script>
<div
@ -156,6 +161,16 @@
/>
</Page>
{#if $nrOfFailedImages.length > 0 || $failedImagesOpen}
<Page {onlyLink} shown={pg.failedImages} bodyPadding="p-0 pb-4">
<svelte:fragment slot="header">
<PhotoIcon />
<Tr t={Translations.t.failedImages.menu.Subs({count: $nrOfFailedImages.length})} />
</svelte:fragment>
<FailedImagesView {state} />
</Page>
{/if}
<LoginToggle {state} silentFail>
{#if state.favourites}
<Page {onlyLink} shown={pg.favourites}>

View file

@ -0,0 +1,88 @@
<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

@ -0,0 +1,41 @@
<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

@ -58,7 +58,9 @@
"image",
noBlur,
feature,
ignoreGps
{
ignoreGps
}
)
if (!uploadResult) {
return