Feature(offline): better support for making changes while offline

This commit is contained in:
Pieter Vander Vennet 2025-08-05 23:49:15 +02:00
parent f671cd342f
commit 7155cd7f61
8 changed files with 89 additions and 10 deletions

View file

@ -430,10 +430,10 @@
} }
}, },
{ {
"id": "favourite_icon", "condition": "_favourite=yes",
"description": "Only for rendering", "description": "Only for rendering",
"icon": "circle:white;heart:red", "icon": "circle:white;heart:red",
"condition": "_favourite=yes", "id": "favourite_icon",
"metacondition": "__showTimeSensitiveIcons!=no" "metacondition": "__showTimeSensitiveIcons!=no"
}, },
{ {

View file

@ -18,7 +18,7 @@
"allFilteredAway": "No feature in view meets all filters", "allFilteredAway": "No feature in view meets all filters",
"loadingData": "Loading data…", "loadingData": "Loading data…",
"noData": "There are no relevant features in the current view", "noData": "There are no relevant features in the current view",
"noDataOffline": "No data is loaded and you are offline", "noDataOffline": "No data is loaded and you are offline",
"ready": "Done!", "ready": "Done!",
"retrying": "Loading data failed. Trying again in {count} seconds…", "retrying": "Loading data failed. Trying again in {count} seconds…",
"zoomIn": "Zoom in to view or edit the data" "zoomIn": "Zoom in to view or edit the data"
@ -639,6 +639,7 @@
"uploading": "{count} images are being uploaded…" "uploading": "{count} images are being uploaded…"
}, },
"noBlur": "Images will not be blurred. Do not photograph people", "noBlur": "Images will not be blurred. Do not photograph people",
"offline": "You are currently offline. Uploading images be attempted when your internet is back",
"one": { "one": {
"done": "Your image was successfully uploaded. Thank you!", "done": "Your image was successfully uploaded. Thank you!",
"failed": "Sorry, we could not upload your image", "failed": "Sorry, we could not upload your image",
@ -653,7 +654,7 @@
"confirmDeleteTitle": "Delete this image?", "confirmDeleteTitle": "Delete this image?",
"delete": "Delete this image", "delete": "Delete this image",
"intro": "The following images are queued for upload", "intro": "The following images are queued for upload",
"menu": "Image upload queue ({count})", "menu": "Pending changes and image uploads ({count})",
"noFailedImages": "There are currently no images in the upload queue", "noFailedImages": "There are currently no images in the upload queue",
"retryAll": "Retry uploading all images" "retryAll": "Retry uploading all images"
}, },

View file

@ -19,6 +19,7 @@ import MarkdownUtils from "../../Utils/MarkdownUtils"
import FeaturePropertiesStore from "../FeatureSource/Actors/FeaturePropertiesStore" import FeaturePropertiesStore from "../FeatureSource/Actors/FeaturePropertiesStore"
import { Feature, Point } from "geojson" import { Feature, Point } from "geojson"
import { Lists } from "../../Utils/Lists" import { Lists } from "../../Utils/Lists"
import { IsOnline } from "../Web/IsOnline"
/** /**
* Handles all changes made to OSM. * Handles all changes made to OSM.
@ -287,6 +288,10 @@ export class Changes {
if (this.pendingChanges.data.length === 0) { if (this.pendingChanges.data.length === 0) {
return return
} }
if(!IsOnline.isOnline.data){
// No use to upload, we aren't connected anyway
return
}
if (this.isUploading.data) { if (this.isUploading.data) {
console.log("Is already uploading... Abort") console.log("Is already uploading... Abort")
return return

View file

@ -64,6 +64,10 @@
import { GlobeEuropeAfrica } from "@babeard/svelte-heroicons/solid/GlobeEuropeAfrica" import { GlobeEuropeAfrica } from "@babeard/svelte-heroicons/solid/GlobeEuropeAfrica"
import { onDestroy } from "svelte" import { onDestroy } from "svelte"
import Avatar from "../Base/Avatar.svelte" import Avatar from "../Base/Avatar.svelte"
import { SpecialVisualizationSvelte } from "../SpecialVisualization"
import ThemeViewState from "../../Models/ThemeViewState"
import { Changes } from "../../Logic/Osm/Changes"
import PendingChangesView from "./PendingChangesView.svelte"
export let state: { export let state: {
favourites: FavouritesFeatureSource favourites: FavouritesFeatureSource
@ -73,6 +77,7 @@
featureSwitches: Partial<FeatureSwitchState> featureSwitches: Partial<FeatureSwitchState>
mapProperties?: MapProperties mapProperties?: MapProperties
userRelatedState?: UserRelatedState userRelatedState?: UserRelatedState
changes?: Changes
} }
let userdetails = state.osmConnection.userDetails let userdetails = state.osmConnection.userDetails
@ -81,6 +86,7 @@
let featureSwitches = state.featureSwitches let featureSwitches = state.featureSwitches
let showHome = featureSwitches?.featureSwitchBackToThemeOverview let showHome = featureSwitches?.featureSwitchBackToThemeOverview
let pg = state.guistate.pageStates let pg = state.guistate.pageStates
let pendingChanges = state?.changes?.pendingChanges
export let onlyLink: boolean export let onlyLink: boolean
const t = Translations.t.general.menu const t = Translations.t.general.menu
let shown = new UIEventSource(state.guistate.pageStates.menu.data || !onlyLink) let shown = new UIEventSource(state.guistate.pageStates.menu.data || !onlyLink)
@ -164,12 +170,14 @@
/> />
</Page> </Page>
{#if $nrOfFailedImages.length > 0 || $failedImagesOpen} {#if $nrOfFailedImages.length > 0 || $failedImagesOpen || $pendingChanges?.length > 0 }
<Page {onlyLink} shown={pg.failedImages} bodyPadding="p-0 pb-4"> <Page {onlyLink} shown={pg.failedImages} bodyPadding="p-0 pb-4">
<svelte:fragment slot="header"> <svelte:fragment slot="header">
<PhotoIcon /> <PhotoIcon />
<Tr t={Translations.t.imageQueue.menu.Subs({ count: $nrOfFailedImages.length })} /> <Tr
t={Translations.t.imageQueue.menu.Subs({ count: ($nrOfFailedImages?.length ?? 0) + ($pendingChanges?.length ?? 0) })} />
</svelte:fragment> </svelte:fragment>
<PendingChangesView {state} />
<QueuedImagesView {state} /> <QueuedImagesView {state} />
</Page> </Page>
{/if} {/if}

View file

@ -0,0 +1,58 @@
<script lang="ts">
import { Changes } from "../../Logic/Osm/Changes"
import type { SpecialVisualizationState } from "../SpecialVisualization"
export let state: { changes: Changes } & SpecialVisualizationState
let pending = state.changes.pendingChanges
let backend = state.osmConnection.Backend()
let debug = state.featureSwitches.featureSwitchIsDebugging
</script>
{#if $pending?.length > 0}
<div class="p-4">
<h3>Pending changes</h3>
There are currently {$pending.length} pending changes:
<table class="gap-x-2">
<tr>
<th>
Theme
</th>
<th>
Type
</th>
<th>
Object
</th>
</tr>
{#each $pending as change}
<tr>
<td>{change.meta.theme}</td>
<td>{change.meta.changeType}</td>
<td>
<a href={`${backend}/${change.type}/${change.id}`} target="_blank">
{change.type}/{change.id}
</a>
</td>
</tr>
{/each}
</table>
{#if $debug}
{#each $pending as change}
{JSON.stringify(change)}
{/each}
{/if}
</div>
{/if}
<style>
td {
padding-left: 2rem;
padding-right: 2rem;
}
</style>

View file

@ -8,9 +8,11 @@
import type { ImageUploadArguments } from "../../Logic/ImageProviders/ImageUploadQueue" import type { ImageUploadArguments } from "../../Logic/ImageProviders/ImageUploadQueue"
import { Store } from "../../Logic/UIEventSource" import { Store } from "../../Logic/UIEventSource"
import UploadingImageCounter from "./UploadingImageCounter.svelte" import UploadingImageCounter from "./UploadingImageCounter.svelte"
import { IsOnline } from "../../Logic/Web/IsOnline"
export let state: WithImageState export let state: WithImageState
let queued: Store<ImageUploadArguments[]> = state.imageUploadManager.queuedArgs let queued: Store<ImageUploadArguments[]> = state.imageUploadManager.queuedArgs
let isUploading = state.imageUploadManager.isUploading let isUploading = state.imageUploadManager.isUploading
let online = IsOnline.isOnline
const t = Translations.t const t = Translations.t
const q = t.imageQueue const q = t.imageQueue
</script> </script>
@ -27,7 +29,7 @@
{#if $isUploading} {#if $isUploading}
<Loading /> <Loading />
{:else} {:else if $online}
<button class="primary" on:click={() => state.imageUploadManager.uploadQueue()}> <button class="primary" on:click={() => state.imageUploadManager.uploadQueue()}>
<ArrowPathIcon class="m-1 h-8 w-8" /> <ArrowPathIcon class="m-1 h-8 w-8" />
<Tr t={q.retryAll} /> <Tr t={q.retryAll} />

View file

@ -66,7 +66,7 @@
let maintenanceBusy = false let maintenanceBusy = false
</script> </script>
<LoginToggle {state}> <LoginToggle {state} offline>
<LoginButton clss="small w-full" osmConnection={state.osmConnection} slot="not-logged-in"> <LoginButton clss="small w-full" osmConnection={state.osmConnection} slot="not-logged-in">
<Tr t={Translations.t.image.pleaseLogin} /> <Tr t={Translations.t.image.pleaseLogin} />
</LoginButton> </LoginButton>

View file

@ -12,6 +12,7 @@
import Tr from "../Base/Tr.svelte" import Tr from "../Base/Tr.svelte"
import Loading from "../Base/Loading.svelte" import Loading from "../Base/Loading.svelte"
import UploadFailedMessage from "./UploadFailedMessage.svelte" import UploadFailedMessage from "./UploadFailedMessage.svelte"
import { IsOnline } from "../../Logic/Web/IsOnline"
export let state: SpecialVisualizationState export let state: SpecialVisualizationState
export let tags: Store<OsmTags> = undefined export let tags: Store<OsmTags> = undefined
@ -59,6 +60,7 @@
failed.addCallbackAndRun((failed) => { failed.addCallbackAndRun((failed) => {
dismissed = Math.min(failed, dismissed) dismissed = Math.min(failed, dismissed)
}) })
let online = IsOnline.isOnline
let progress = state.imageUploadManager.progressCurrentImage let progress = state.imageUploadManager.progressCurrentImage
</script> </script>
@ -91,8 +93,11 @@
</Loading> </Loading>
</div> </div>
{/if} {/if}
{#if !$online}
{#if $failed > dismissed} <div class="alert">
<Tr t={t.upload.offline} />
</div>
{:else if $failed > dismissed}
<UploadFailedMessage failed={$failed} on:click={() => (dismissed = $failed)} {state} /> <UploadFailedMessage failed={$failed} on:click={() => (dismissed = $failed)} {state} />
{/if} {/if}