forked from MapComplete/MapComplete
		
	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:
		
							parent
							
								
									7380841205
								
							
						
					
					
						commit
						9f3d198068
					
				
					 9 changed files with 313 additions and 8 deletions
				
			
		|  | @ -63,6 +63,16 @@ | ||||||
|         "overwrite": "Overwrite in OpenStreetMap", |         "overwrite": "Overwrite in OpenStreetMap", | ||||||
|         "title": "Structured data loaded from the external website" |         "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": { |     "favourite": { | ||||||
|         "reload": "Reload the data" |         "reload": "Reload the data" | ||||||
|     }, |     }, | ||||||
|  |  | ||||||
							
								
								
									
										112
									
								
								src/Logic/ImageProviders/EmergencyImageBackup.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										112
									
								
								src/Logic/ImageProviders/EmergencyImageBackup.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,112 @@ | ||||||
|  | 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<FailedImageArgs[]> | ||||||
|  | 
 | ||||||
|  |     public readonly failedImages: Store<FailedImageArgs[]> | ||||||
|  | 
 | ||||||
|  |     private readonly _isUploading: UIEventSource<boolean> = new UIEventSource(false) | ||||||
|  |     public readonly isUploading: Store<boolean> = this._isUploading | ||||||
|  | 
 | ||||||
|  |     private constructor() { | ||||||
|  |         this._failedImages = IdbLocalStorage.Get<FailedImageArgs[]>("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<boolean> { | ||||||
|  |         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)*/ | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -11,6 +11,7 @@ import { Translation } from "../../UI/i18n/Translation" | ||||||
| import { IndexedFeatureSource } from "../FeatureSource/FeatureSource" | import { IndexedFeatureSource } from "../FeatureSource/FeatureSource" | ||||||
| import { GeoOperations } from "../GeoOperations" | import { GeoOperations } from "../GeoOperations" | ||||||
| import { Feature } from "geojson" | import { Feature } from "geojson" | ||||||
|  | import EmergencyImageBackup from "./EmergencyImageBackup" | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * The ImageUploadManager has a |  * The ImageUploadManager has a | ||||||
|  | @ -85,7 +86,7 @@ export class ImageUploadManager { | ||||||
|             uploadFinished: this.getCounterFor(this._uploadFinished, featureId), |             uploadFinished: this.getCounterFor(this._uploadFinished, featureId), | ||||||
|             retried: this.getCounterFor(this._uploadRetried, featureId), |             retried: this.getCounterFor(this._uploadRetried, featureId), | ||||||
|             failed: this.getCounterFor(this._uploadFailed, featureId), |             failed: this.getCounterFor(this._uploadFailed, featureId), | ||||||
|             retrySuccess: this.getCounterFor(this._uploadRetriedSuccess, featureId), |             retrySuccess: this.getCounterFor(this._uploadRetriedSuccess, featureId) | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | @ -94,10 +95,14 @@ export class ImageUploadManager { | ||||||
|         if (sizeInBytes > this._uploader.maxFileSizeInMegabytes * 1000000) { |         if (sizeInBytes > this._uploader.maxFileSizeInMegabytes * 1000000) { | ||||||
|             const error = Translations.t.image.toBig.Subs({ |             const error = Translations.t.image.toBig.Subs({ | ||||||
|                 actual_size: Math.floor(sizeInBytes / 1000000) + "MB", |                 actual_size: Math.floor(sizeInBytes / 1000000) + "MB", | ||||||
|                 max_size: this._uploader.maxFileSizeInMegabytes + "MB", |                 max_size: this._uploader.maxFileSizeInMegabytes + "MB" | ||||||
|             }) |             }) | ||||||
|             return { error } |             return { error } | ||||||
|         } |         } | ||||||
|  |         const ext = file.name.split(".").at(-1).toLowerCase() | ||||||
|  |         if (ext !== "jpg" && ext !== "jpeg") { | ||||||
|  |             return { error: new Translation({ en: "Only JPG-files are allowed" }) } | ||||||
|  |         } | ||||||
|         return true |         return true | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | @ -148,13 +153,24 @@ export class ImageUploadManager { | ||||||
|             properties, |             properties, | ||||||
|             { |             { | ||||||
|                 theme: tags?.data?.["_orig_theme"] ?? this._theme.id, |                 theme: tags?.data?.["_orig_theme"] ?? this._theme.id, | ||||||
|                 changeType: "add-image", |                 changeType: "add-image" | ||||||
|             } |             } | ||||||
|         ) |         ) | ||||||
| 
 | 
 | ||||||
|         await this._changes.applyAction(action) |         await this._changes.applyAction(action) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     /** | ||||||
|  |      * Uploads an image; returns undefined if the image upload failed. | ||||||
|  |      * Errors are handled internally | ||||||
|  |      * @param featureId | ||||||
|  |      * @param author | ||||||
|  |      * @param blob | ||||||
|  |      * @param targetKey | ||||||
|  |      * @param noblur | ||||||
|  |      * @param feature | ||||||
|  |      * @param options | ||||||
|  |      */ | ||||||
|     public async uploadImageWithLicense( |     public async uploadImageWithLicense( | ||||||
|         featureId: string, |         featureId: string, | ||||||
|         author: string, |         author: string, | ||||||
|  | @ -162,14 +178,21 @@ export class ImageUploadManager { | ||||||
|         targetKey: string | undefined, |         targetKey: string | undefined, | ||||||
|         noblur: boolean, |         noblur: boolean, | ||||||
|         feature: Feature, |         feature: Feature, | ||||||
|         ignoreGps: boolean = false |         options?: { | ||||||
|     ): Promise<UploadResult> { |             ignoreGps?: boolean, | ||||||
|  |             noBackup?: boolean, | ||||||
|  |             overwriteGps?: GeolocationCoordinates | ||||||
|  | 
 | ||||||
|  |         } | ||||||
|  |     ): Promise<UploadResult | undefined> { | ||||||
|         this.increaseCountFor(this._uploadStarted, featureId) |         this.increaseCountFor(this._uploadStarted, featureId) | ||||||
|         let key: string |         let key: string | ||||||
|         let value: string |         let value: string | ||||||
|         let absoluteUrl: string |         let absoluteUrl: string | ||||||
|         let location: [number, number] = undefined |         let location: [number, number] = undefined | ||||||
|         if (this._gps.data && !ignoreGps) { |         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] |             location = [this._gps.data.longitude, this._gps.data.latitude] | ||||||
|         } |         } | ||||||
|         { |         { | ||||||
|  | @ -210,13 +233,21 @@ export class ImageUploadManager { | ||||||
|             } catch (e) { |             } catch (e) { | ||||||
|                 console.error("Could again not upload image due to", e) |                 console.error("Could again not upload image due to", e) | ||||||
|                 this.increaseCountFor(this._uploadFailed, featureId) |                 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() | ||||||
|  |                     }) | ||||||
|  |                 } | ||||||
|                 await this._reportError( |                 await this._reportError( | ||||||
|                     e, |                     e, | ||||||
|                     JSON.stringify({ |                     JSON.stringify({ | ||||||
|                         ctx: "While uploading an image in the Image Upload Manager", |                         ctx: "While uploading an image in the Image Upload Manager", | ||||||
|                         featureId, |                         featureId, | ||||||
|                         author, |                         author, | ||||||
|                         targetKey, |                         targetKey | ||||||
|                     }) |                     }) | ||||||
|                 ) |                 ) | ||||||
|                 return undefined |                 return undefined | ||||||
|  |  | ||||||
|  | @ -26,6 +26,7 @@ export class MenuState { | ||||||
|         "about_theme", |         "about_theme", | ||||||
|         "download", |         "download", | ||||||
|         "favourites", |         "favourites", | ||||||
|  |         "failedImages", | ||||||
|         "usersettings", |         "usersettings", | ||||||
|         "share", |         "share", | ||||||
|         "menu", |         "menu", | ||||||
|  |  | ||||||
|  | @ -9,6 +9,7 @@ import ThemeViewStateHashActor from "../../Logic/Web/ThemeViewStateHashActor" | ||||||
| import PendingChangesUploader from "../../Logic/Actors/PendingChangesUploader" | import PendingChangesUploader from "../../Logic/Actors/PendingChangesUploader" | ||||||
| import { WithGuiState } from "./WithGuiState" | import { WithGuiState } from "./WithGuiState" | ||||||
| import { SpecialVisualizationState } from "../../UI/SpecialVisualization" | import { SpecialVisualizationState } from "../../UI/SpecialVisualization" | ||||||
|  | import EmergencyImageBackup from "../../Logic/ImageProviders/EmergencyImageBackup" | ||||||
| 
 | 
 | ||||||
| export class WithImageState extends WithGuiState implements SpecialVisualizationState { | export class WithImageState extends WithGuiState implements SpecialVisualizationState { | ||||||
|     readonly imageUploadManager: ImageUploadManager |     readonly imageUploadManager: ImageUploadManager | ||||||
|  | @ -42,6 +43,10 @@ export class WithImageState extends WithGuiState implements SpecialVisualization | ||||||
|                 this.selectCurrentView() |                 this.selectCurrentView() | ||||||
|             } |             } | ||||||
|         }) |         }) | ||||||
|  | 
 | ||||||
|  |         this.osmConnection.userDetails.addCallbackAndRunD(() => { | ||||||
|  |             EmergencyImageBackup.singleton.retryAll(this) | ||||||
|  |         }) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|  |  | ||||||
|  | @ -61,6 +61,9 @@ | ||||||
|   import Hotkeys from "../Base/Hotkeys" |   import Hotkeys from "../Base/Hotkeys" | ||||||
|   import { ArrowTrendingUp } from "@babeard/svelte-heroicons/solid/ArrowTrendingUp" |   import { ArrowTrendingUp } from "@babeard/svelte-heroicons/solid/ArrowTrendingUp" | ||||||
|   import ArrowTopRightOnSquare from "@babeard/svelte-heroicons/mini/ArrowTopRightOnSquare" |   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: { |   export let state: { | ||||||
|     favourites: FavouritesFeatureSource |     favourites: FavouritesFeatureSource | ||||||
|  | @ -97,6 +100,8 @@ | ||||||
|     } |     } | ||||||
|   }) |   }) | ||||||
|   let isAndroid = AndroidPolyfill.inAndroid |   let isAndroid = AndroidPolyfill.inAndroid | ||||||
|  |   let nrOfFailedImages = EmergencyImageBackup.singleton.failedImages | ||||||
|  |   let failedImagesOpen = pg.failedImages | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <div | <div | ||||||
|  | @ -156,6 +161,16 @@ | ||||||
|       /> |       /> | ||||||
|     </Page> |     </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> |     <LoginToggle {state} silentFail> | ||||||
|       {#if state.favourites} |       {#if state.favourites} | ||||||
|         <Page {onlyLink} shown={pg.favourites}> |         <Page {onlyLink} shown={pg.favourites}> | ||||||
|  |  | ||||||
							
								
								
									
										88
									
								
								src/UI/Image/FailedImage.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										88
									
								
								src/UI/Image/FailedImage.svelte
									
										
									
									
									
										Normal 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> | ||||||
							
								
								
									
										41
									
								
								src/UI/Image/FailedImagesView.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								src/UI/Image/FailedImagesView.svelte
									
										
									
									
									
										Normal 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> | ||||||
|  | @ -58,7 +58,9 @@ | ||||||
|             "image", |             "image", | ||||||
|             noBlur, |             noBlur, | ||||||
|             feature, |             feature, | ||||||
|             ignoreGps |             { | ||||||
|  |               ignoreGps | ||||||
|  |             } | ||||||
|           ) |           ) | ||||||
|           if (!uploadResult) { |           if (!uploadResult) { | ||||||
|             return |             return | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue