From 06aa8a34061ec1e607b8f0de4ab8cf12213b8dae Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Sun, 3 Aug 2025 16:35:38 +0200 Subject: [PATCH 1/6] Feature: offline: more features to be able to work fully offline --- assets/layers/icons/icons.json | 4 +- assets/layers/last_click/last_click.json | 4 +- assets/layers/note/note.json | 4 +- .../osm_community_index.json | 12 +++--- assets/layers/usersettings/usersettings.json | 40 +++++++++++++++++++ index.html | 2 - langs/en.json | 1 + langs/layers/en.json | 6 +++ scripts/generateIncludedImages.ts | 4 +- scripts/generateLayerOverview.ts | 15 +++++-- scripts/prepareServiceWorker.ts | 2 +- src/InstallServiceWorker.ts | 24 ++++++----- src/Logic/Osm/OsmConnection.ts | 18 +++++---- src/UI/Base/LoginToggle.svelte | 21 ++++++---- src/UI/BigComponents/MenuDrawerIndex.svelte | 8 ++-- src/UI/Popup/ClearCaches.svelte | 2 +- .../TagRendering/TagRenderingEditable.svelte | 2 +- .../TagRendering/TagRenderingQuestion.svelte | 4 +- src/UI/SingleThemeGui.svelte | 22 ++++++++++ .../SettingsVisualisations.ts | 26 +++++++++++- src/all_themes_index.ts | 3 +- src/service-worker/SWGenerated.ts | 2 +- src/service-worker/index.ts | 37 ++++++++++++++++- 23 files changed, 203 insertions(+), 60 deletions(-) diff --git a/assets/layers/icons/icons.json b/assets/layers/icons/icons.json index 93e9e91ec0..54ebd45d54 100644 --- a/assets/layers/icons/icons.json +++ b/assets/layers/icons/icons.json @@ -430,10 +430,10 @@ } }, { - "condition": "_favourite=yes", + "id": "favourite_icon", "description": "Only for rendering", "icon": "circle:white;heart:red", - "id": "favourite_icon", + "condition": "_favourite=yes", "metacondition": "__showTimeSensitiveIcons!=no" }, { diff --git a/assets/layers/last_click/last_click.json b/assets/layers/last_click/last_click.json index 6aaa191159..3185c513fb 100644 --- a/assets/layers/last_click/last_click.json +++ b/assets/layers/last_click/last_click.json @@ -217,8 +217,8 @@ }, { "id": "debug", - "metacondition": "__featureSwitchIsDebugging=true", - "render": "{all_tags()}" + "render": "{all_tags()}", + "metacondition": "__featureSwitchIsDebugging=true" } ], "filter": [ diff --git a/assets/layers/note/note.json b/assets/layers/note/note.json index eedef08c68..b1dd36f327 100644 --- a/assets/layers/note/note.json +++ b/assets/layers/note/note.json @@ -114,9 +114,9 @@ "lineRendering": [], "tagRenderings": [ { - "classes": "p-0", "id": "conversation", - "render": "{visualize_note_comments()}" + "render": "{visualize_note_comments()}", + "classes": "p-0" }, { "id": "add_image", diff --git a/assets/layers/osm_community_index/osm_community_index.json b/assets/layers/osm_community_index/osm_community_index.json index 150fc6ecd9..1c605ca388 100644 --- a/assets/layers/osm_community_index/osm_community_index.json +++ b/assets/layers/osm_community_index/osm_community_index.json @@ -66,16 +66,16 @@ ], "tagRenderings": [ { - "condition": "level=country", - "description": "The name of the country", "id": "country_name", - "render": "{nameEn} {emojiFlag}" + "description": "The name of the country", + "render": "{nameEn} {emojiFlag}", + "condition": "level=country" }, { - "condition": "_community_links~*", - "description": "Community Links (Discord, meetups, Slack groups, IRC channels, mailing lists etc...)", "id": "community_links", - "render": "{_community_links}" + "description": "Community Links (Discord, meetups, Slack groups, IRC channels, mailing lists etc...)", + "render": "{_community_links}", + "condition": "_community_links~*" } ], "filter": [ diff --git a/assets/layers/usersettings/usersettings.json b/assets/layers/usersettings/usersettings.json index a82beeaaf0..907b37c2d8 100644 --- a/assets/layers/usersettings/usersettings.json +++ b/assets/layers/usersettings/usersettings.json @@ -1898,6 +1898,46 @@ "render": { "*": "{storage_all_tags()}" } + }, + { + "id": "debug_serviceworker_accordeon", + "render": { + "special": { + "header": "debug_serviceworker_accordeon_title", + "labels": "debug_serviceworker", + "type": "group" + } + }, + "condition": "mapcomplete-show_debug=yes" + }, + { + "id": "debug_serviceworker_accordeon_title", + "labels": [ + "hidden" + ], + "render": { + "en": "Debug information about the service worker" + } + }, + { + "id": "expl", + "labels": [ + "debug_serviceworker", + "hidden" + ], + "render": { + "en": "To clear the service worker data, use the 'clear caches' button" + } + }, + { + "id": "service_worker_tags", + "labels": [ + "debug_serviceworker", + "hidden" + ], + "render": { + "*": "{serviceworker_all_tags()}" + } } ], "allowMove": false diff --git a/index.html b/index.html index 04cf21dcf6..796ba73ec3 100644 --- a/index.html +++ b/index.html @@ -43,8 +43,6 @@ - - diff --git a/langs/en.json b/langs/en.json index c5b3db2541..1c368e3f5c 100644 --- a/langs/en.json +++ b/langs/en.json @@ -335,6 +335,7 @@ "next": "Next", "noTagsSelected": "No tags selected", "number": "number", + "offline": "Your device is offline", "openTheMap": "Open the map", "openTheMapReason": "to view, edit and add information", "opening_hours": { diff --git a/langs/layers/en.json b/langs/layers/en.json index c9632acd73..7ccbab67b5 100644 --- a/langs/layers/en.json +++ b/langs/layers/en.json @@ -14095,6 +14095,9 @@ "debug_accordeon_title": { "render": "Debug information" }, + "debug_serviceworker_accordeon_title": { + "render": "Debug information about the service worker" + }, "debug_storage_accordeon_title": { "render": "Debug information about local storage" }, @@ -14105,6 +14108,9 @@ } } }, + "expl": { + "render": "To clear the service worker data, use the 'clear caches' button" + }, "fixate-north": { "mappings": { "0": { diff --git a/scripts/generateIncludedImages.ts b/scripts/generateIncludedImages.ts index 69d15f0b97..6b4972b1fd 100644 --- a/scripts/generateIncludedImages.ts +++ b/scripts/generateIncludedImages.ts @@ -1,7 +1,7 @@ import * as fs from "fs" import Script from "./Script" -function genImages(dryrun = false) { +function genImages() { console.log("Generating images") const dir = fs.readdirSync("./assets/svg") for (const path of dir) { @@ -64,7 +64,7 @@ class GenerateIncludedImages extends Script { super("Converts all images from assets/svg into svelte-classes.") } - async main(args: string[]): Promise { + async main(): Promise { genImages() } } diff --git a/scripts/generateLayerOverview.ts b/scripts/generateLayerOverview.ts index 503c504546..15af107cbe 100644 --- a/scripts/generateLayerOverview.ts +++ b/scripts/generateLayerOverview.ts @@ -8,7 +8,7 @@ import { DoesImageExist, PrevalidateTheme, ValidateLayer, - ValidateThemeEnsemble, + ValidateThemeEnsemble } from "../src/Models/ThemeConfig/Conversion/Validation" import { Translation } from "../src/UI/i18n/Translation" import { OrderLayer, PrepareLayer } from "../src/Models/ThemeConfig/Conversion/PrepareLayer" @@ -19,7 +19,7 @@ import { DesugaringStep, Each, Fuse, - On, + On } from "../src/Models/ThemeConfig/Conversion/Conversion" import { Utils } from "../src/Utils" import Script from "./Script" @@ -182,7 +182,7 @@ class LayerBuilder extends Conversion> { return `./assets/layers/${id}/${id}.json` } - writeLayer(layer: LayerConfigJson) { + public writeLayer(layer: LayerConfigJson) { if (layer.labels?.some((l) => this._labelBlacklist.has(l))) { console.log("Not writing layer " + layer.id + ", censored") return @@ -191,6 +191,15 @@ class LayerBuilder extends Conversion> { if (!existsSync(LayerOverviewUtils.layerPath)) { mkdirSync(LayerOverviewUtils.layerPath) } + + const usedImages = Lists.dedup(new ExtractImages(true, new Set(this._desugaringState.tagRenderings.keys())) + .convertStrict({ layers: [layer], id: "dummy", icon: undefined, title: undefined }) + .map((x) => x.path)) + usedImages.sort() + + layer["_usedImages"] = usedImages + + writeFileSync(LayerBuilder.targetPath(layer.id), JSON.stringify(layer, null, " "), { encoding: "utf8", }) diff --git a/scripts/prepareServiceWorker.ts b/scripts/prepareServiceWorker.ts index 71f3ebbc7b..d43184e665 100644 --- a/scripts/prepareServiceWorker.ts +++ b/scripts/prepareServiceWorker.ts @@ -9,7 +9,7 @@ class PrepareServiceWorker extends Script { } public async main() { - const v = Constants.vNumber + const v = Constants.vNumber + "-" + new Date().getTime() writeFileSync("./src/service-worker/SWGenerated.ts", ["export class SWGenerated {", "// generated by scripts/prepareServiceWorker.ts", diff --git a/src/InstallServiceWorker.ts b/src/InstallServiceWorker.ts index 85a0d76a76..c7fdbfbd25 100644 --- a/src/InstallServiceWorker.ts +++ b/src/InstallServiceWorker.ts @@ -1,13 +1,17 @@ -export {} -window.addEventListener("load", async () => { - if (!("serviceWorker" in navigator)) { - console.log("Service workers are not supported") - return - } - try { +export class InstallServiceWorker { + + static async installServiceWorker() { + if (!("serviceWorker" in navigator)) { + throw ("Service workers are not supported") + } await navigator.serviceWorker.register("/service-worker.js", { type: "module" }) console.log("Service worker registration successful") - } catch (err) { - console.error("Service worker registration failed", err) + } -}) + + static async precache(assets: string[]) { + if (assets?.length > 0) { + await fetch("./service-worker/precache?assets=" + assets.join(";")) + } + } +} diff --git a/src/Logic/Osm/OsmConnection.ts b/src/Logic/Osm/OsmConnection.ts index 733c4e5ec7..0f44110cdd 100644 --- a/src/Logic/Osm/OsmConnection.ts +++ b/src/Logic/Osm/OsmConnection.ts @@ -8,6 +8,7 @@ import Constants from "../../Models/Constants" import { Feature, Point } from "geojson" import { AndroidPolyfill } from "../Web/AndroidPolyfill" import { QueryParameters } from "../Web/QueryParameters" +import { IsOnline } from "../Web/IsOnline" interface OsmUserInfo { id: number @@ -131,6 +132,7 @@ export class OsmConnection { * Details of the currently logged-in user; undefined if not logged in */ public userDetails: UIEventSource + public isLoggedIn: Store public gpxServiceIsOnline: UIEventSource = new UIEventSource( "unknown" @@ -182,7 +184,7 @@ export class OsmConnection { this._oauth_config.oauth_secret = import.meta.env.VITE_OSM_OAUTH_SECRET } - this.userDetails = new UIEventSource(undefined, "userDetails") + this.userDetails = UIEventSource.asObject(LocalStorageSource.get("user_details"), undefined) if (options.fakeUser) { const ud = this.userDetails.data ud.csCount = 5678 @@ -197,13 +199,7 @@ export class OsmConnection { } this.updateCapabilities() - this.isLoggedIn = this.userDetails.map( - (user) => - !!user && - (this.apiIsOnline.data === "unknown" || this.apiIsOnline.data === "online"), - [this.apiIsOnline] - ) - + this.isLoggedIn = this.userDetails.map((user) => !!user) this._dryRun = options.dryRun ?? new UIEventSource(false) if (options?.shared_cookie) { @@ -284,6 +280,9 @@ export class OsmConnection { } public async AttemptLogin() { + if (!IsOnline.isOnline.data) { + return + } this.updateCapabilities() if (this.loadingStatus.data !== "logged-in") { this.loadingStatus.setData("loading") @@ -308,6 +307,9 @@ export class OsmConnection { } private async loadUserInfo() { + if (!IsOnline.isOnline.data) { + return + } try { const result = await this.interact("user/details.json") if (result === null) { diff --git a/src/UI/Base/LoginToggle.svelte b/src/UI/Base/LoginToggle.svelte index d4fa4fc6f3..859f8fb0c3 100644 --- a/src/UI/Base/LoginToggle.svelte +++ b/src/UI/Base/LoginToggle.svelte @@ -14,10 +14,15 @@ osmConnection: OsmConnection featureSwitches?: { featureSwitchEnableLogin?: UIEventSource } } + /** + * Do show this element when in offline mode + */ + export let offline = false /** * If set, 'loading' will act as if we are already logged in. */ - export let ignoreLoading: boolean = false + export let ignoreLoading: boolean = offline // If it works in offline mode, it'll work while we are logging in too + /** * If set and the OSM-api fails, do _not_ show any error messages nor the successful state, just hide. * Will still show the "not-logged-in"-slot @@ -32,23 +37,26 @@ unknown: t.loginFailedUnreachableMode, readonly: t.loginFailedReadonlyMode, } - const apiState: Store = + const apiState: Store = state?.osmConnection?.apiIsOnline ?? new ImmutableStore("online") const online = IsOnline.isOnline + let loggedIn = state?.osmConnection?.isLoggedIn {#if $badge} - {#if !$online} + {#if !$online && !offline} {#if !hiddenFail}
- Your device is offline +
{/if} {:else if !ignoreLoading && !hiddenFail && $loadingStatus === "loading"} - {:else if ($loadingStatus === "error" || $apiState === "readonly" || $apiState === "offline")} + {:else if $loggedIn} + + {:else if ($loadingStatus === "error" || $apiState === "readonly" || $apiState === "offline" || $apiState === "unreachable")} {#if !hiddenFail}
@@ -63,8 +71,7 @@
{/if} - {:else if $loadingStatus === "logged-in"} - + {:else if $loadingStatus === "not-attempted"} {/if} diff --git a/src/UI/BigComponents/MenuDrawerIndex.svelte b/src/UI/BigComponents/MenuDrawerIndex.svelte index 8090687357..d9d0871e69 100644 --- a/src/UI/BigComponents/MenuDrawerIndex.svelte +++ b/src/UI/BigComponents/MenuDrawerIndex.svelte @@ -77,11 +77,9 @@ let usersettingslayer = new LayerConfig(usersettings, "usersettings", true) - let theme = state.theme let featureSwitches = state.featureSwitches let showHome = featureSwitches?.featureSwitchBackToThemeOverview let pg = state.guistate.pageStates - let location = state.mapProperties?.location export let onlyLink: boolean const t = Translations.t.general.menu let shown = new UIEventSource(state.guistate.pageStates.menu.data || !onlyLink) @@ -133,7 +131,7 @@ - +
{#if $userdetails.img} @@ -143,7 +141,7 @@ {/if}
- {$userdetails.name} + {$userdetails?.name ?? ''}
@@ -281,7 +279,7 @@ - {#if !state.theme} + {#if !state?.theme} diff --git a/src/UI/Popup/ClearCaches.svelte b/src/UI/Popup/ClearCaches.svelte index 0bc83e5e18..2adcffe7c7 100644 --- a/src/UI/Popup/ClearCaches.svelte +++ b/src/UI/Popup/ClearCaches.svelte @@ -4,7 +4,7 @@ function clearCaches() { IdbLocalStorage.clearAll() - Utils.download("./service-worker-clear") + Utils.download("./service-worker/clear_caches.json") window.location.reload() } export let msg: string diff --git a/src/UI/Popup/TagRendering/TagRenderingEditable.svelte b/src/UI/Popup/TagRendering/TagRenderingEditable.svelte index 45d84ea9e0..593488436b 100644 --- a/src/UI/Popup/TagRendering/TagRenderingEditable.svelte +++ b/src/UI/Popup/TagRendering/TagRenderingEditable.svelte @@ -128,7 +128,7 @@ {layer} extraClasses="my-2" /> - {#if (!editingEnabled || $editingEnabled) && $apiState !== "readonly" && $apiState !== "offline"} + {#if !editingEnabled || $editingEnabled} {#if layer?.isNormal()} - + {#if $disabledInTheme.indexOf(config.id) >= 0} @@ -538,7 +538,7 @@ {/if} - +
{#if config.alwaysForceSaveButton}
- + {#if $recentThemes.length > 2}

diff --git a/src/UI/Base/Avatar.svelte b/src/UI/Base/Avatar.svelte new file mode 100644 index 0000000000..47834b9d35 --- /dev/null +++ b/src/UI/Base/Avatar.svelte @@ -0,0 +1,20 @@ + + +{#if !$userdetails.img || !($loaded || $isOnline)} + +{:else} + avatar {loaded.set(true)}} /> +{/if} diff --git a/src/UI/BigComponents/MenuDrawerIndex.svelte b/src/UI/BigComponents/MenuDrawerIndex.svelte index d9d0871e69..3093909aa3 100644 --- a/src/UI/BigComponents/MenuDrawerIndex.svelte +++ b/src/UI/BigComponents/MenuDrawerIndex.svelte @@ -63,6 +63,7 @@ import OfflineManagement from "./OfflineManagement.svelte" import { GlobeEuropeAfrica } from "@babeard/svelte-heroicons/solid/GlobeEuropeAfrica" import { onDestroy } from "svelte" + import Avatar from "../Base/Avatar.svelte" export let state: { favourites: FavouritesFeatureSource @@ -134,11 +135,7 @@
- {#if $userdetails.img} - avatar - {:else} - - {/if} +
{$userdetails?.name ?? ''} diff --git a/src/UI/BigComponents/StateIndicator.svelte b/src/UI/BigComponents/StateIndicator.svelte index bb465a9437..7d00ea0e88 100644 --- a/src/UI/BigComponents/StateIndicator.svelte +++ b/src/UI/BigComponents/StateIndicator.svelte @@ -3,6 +3,7 @@ import Translations from "../i18n/Translations" import Tr from "../Base/Tr.svelte" import Loading from "../Base/Loading.svelte" + import { IsOnline } from "../../Logic/Web/IsOnline" export let state: ThemeViewState /** @@ -14,6 +15,7 @@ let dataIsLoading = state.dataIsLoading let currentState = state.hasDataInView + let online = IsOnline.isOnline const t = Translations.t.centerMessage const showingSearch = state.searchState.showSearchDrawer @@ -34,6 +36,10 @@
+{:else if $currentState === "no-data" && !$online} +
+ +
{:else if $currentState === "no-data"}
diff --git a/src/UI/BigComponents/WelcomeBack.svelte b/src/UI/BigComponents/WelcomeBack.svelte index 4541f987a1..80714b11d0 100644 --- a/src/UI/BigComponents/WelcomeBack.svelte +++ b/src/UI/BigComponents/WelcomeBack.svelte @@ -3,6 +3,7 @@ import { fade } from "svelte/transition" import { OsmConnection } from "../../Logic/Osm/OsmConnection" import { onDestroy } from "svelte" + import Avatar from "../Base/Avatar.svelte" let open = false export let state: { osmConnection: OsmConnection } @@ -28,9 +29,7 @@ > {#if $username !== undefined}
- {#if $userdetails.img} - avatar - {/if} +
Welcome back
From 7155cd7f6184073bf641f1524ae438a803710b94 Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Tue, 5 Aug 2025 23:49:15 +0200 Subject: [PATCH 3/6] Feature(offline): better support for making changes while offline --- assets/layers/icons/icons.json | 4 +- langs/en.json | 5 +- src/Logic/Osm/Changes.ts | 5 ++ src/UI/BigComponents/MenuDrawerIndex.svelte | 12 +++- .../BigComponents/PendingChangesView.svelte | 58 +++++++++++++++++++ src/UI/Image/QueuedImagesView.svelte | 4 +- src/UI/Image/UploadImage.svelte | 2 +- src/UI/Image/UploadingImageCounter.svelte | 9 ++- 8 files changed, 89 insertions(+), 10 deletions(-) create mode 100644 src/UI/BigComponents/PendingChangesView.svelte diff --git a/assets/layers/icons/icons.json b/assets/layers/icons/icons.json index 54ebd45d54..93e9e91ec0 100644 --- a/assets/layers/icons/icons.json +++ b/assets/layers/icons/icons.json @@ -430,10 +430,10 @@ } }, { - "id": "favourite_icon", + "condition": "_favourite=yes", "description": "Only for rendering", "icon": "circle:white;heart:red", - "condition": "_favourite=yes", + "id": "favourite_icon", "metacondition": "__showTimeSensitiveIcons!=no" }, { diff --git a/langs/en.json b/langs/en.json index f9fbf3379f..0f25afbfe3 100644 --- a/langs/en.json +++ b/langs/en.json @@ -18,7 +18,7 @@ "allFilteredAway": "No feature in view meets all filters", "loadingData": "Loading data…", "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!", "retrying": "Loading data failed. Trying again in {count} seconds…", "zoomIn": "Zoom in to view or edit the data" @@ -639,6 +639,7 @@ "uploading": "{count} images are being uploaded…" }, "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": { "done": "Your image was successfully uploaded. Thank you!", "failed": "Sorry, we could not upload your image", @@ -653,7 +654,7 @@ "confirmDeleteTitle": "Delete this image?", "delete": "Delete this image", "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", "retryAll": "Retry uploading all images" }, diff --git a/src/Logic/Osm/Changes.ts b/src/Logic/Osm/Changes.ts index 14dae7e0cf..bd2b942df0 100644 --- a/src/Logic/Osm/Changes.ts +++ b/src/Logic/Osm/Changes.ts @@ -19,6 +19,7 @@ import MarkdownUtils from "../../Utils/MarkdownUtils" import FeaturePropertiesStore from "../FeatureSource/Actors/FeaturePropertiesStore" import { Feature, Point } from "geojson" import { Lists } from "../../Utils/Lists" +import { IsOnline } from "../Web/IsOnline" /** * Handles all changes made to OSM. @@ -287,6 +288,10 @@ export class Changes { if (this.pendingChanges.data.length === 0) { return } + if(!IsOnline.isOnline.data){ + // No use to upload, we aren't connected anyway + return + } if (this.isUploading.data) { console.log("Is already uploading... Abort") return diff --git a/src/UI/BigComponents/MenuDrawerIndex.svelte b/src/UI/BigComponents/MenuDrawerIndex.svelte index 3093909aa3..a8952cfdcc 100644 --- a/src/UI/BigComponents/MenuDrawerIndex.svelte +++ b/src/UI/BigComponents/MenuDrawerIndex.svelte @@ -64,6 +64,10 @@ import { GlobeEuropeAfrica } from "@babeard/svelte-heroicons/solid/GlobeEuropeAfrica" import { onDestroy } from "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: { favourites: FavouritesFeatureSource @@ -73,6 +77,7 @@ featureSwitches: Partial mapProperties?: MapProperties userRelatedState?: UserRelatedState + changes?: Changes } let userdetails = state.osmConnection.userDetails @@ -81,6 +86,7 @@ let featureSwitches = state.featureSwitches let showHome = featureSwitches?.featureSwitchBackToThemeOverview let pg = state.guistate.pageStates + let pendingChanges = state?.changes?.pendingChanges export let onlyLink: boolean const t = Translations.t.general.menu let shown = new UIEventSource(state.guistate.pageStates.menu.data || !onlyLink) @@ -164,12 +170,14 @@ /> - {#if $nrOfFailedImages.length > 0 || $failedImagesOpen} + {#if $nrOfFailedImages.length > 0 || $failedImagesOpen || $pendingChanges?.length > 0 } - + + {/if} diff --git a/src/UI/BigComponents/PendingChangesView.svelte b/src/UI/BigComponents/PendingChangesView.svelte new file mode 100644 index 0000000000..3aa888c9ef --- /dev/null +++ b/src/UI/BigComponents/PendingChangesView.svelte @@ -0,0 +1,58 @@ + +{#if $pending?.length > 0} +
+ +

Pending changes

+ + There are currently {$pending.length} pending changes: + + + + + + + + {#each $pending as change} + + + + + + + {/each} +
+ Theme + + Type + + Object +
{change.meta.theme}{change.meta.changeType} + + {change.type}/{change.id} + +
+ + {#if $debug} + {#each $pending as change} + {JSON.stringify(change)} + {/each} + {/if} +
+ +{/if} + + + diff --git a/src/UI/Image/QueuedImagesView.svelte b/src/UI/Image/QueuedImagesView.svelte index 3c813ffa59..ab2e988475 100644 --- a/src/UI/Image/QueuedImagesView.svelte +++ b/src/UI/Image/QueuedImagesView.svelte @@ -8,9 +8,11 @@ import type { ImageUploadArguments } from "../../Logic/ImageProviders/ImageUploadQueue" import { Store } from "../../Logic/UIEventSource" import UploadingImageCounter from "./UploadingImageCounter.svelte" + import { IsOnline } from "../../Logic/Web/IsOnline" export let state: WithImageState let queued: Store = state.imageUploadManager.queuedArgs let isUploading = state.imageUploadManager.isUploading + let online = IsOnline.isOnline const t = Translations.t const q = t.imageQueue @@ -27,7 +29,7 @@ {#if $isUploading} - {:else} + {:else if $online}
{/if} - -{#if $failed > dismissed} +{#if !$online} +
+ +
+{:else if $failed > dismissed} (dismissed = $failed)} {state} /> {/if} From f1da97285fe68c72b13299ebc97c28428fc95230 Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Tue, 5 Aug 2025 23:55:10 +0200 Subject: [PATCH 4/6] Feature(offline): don't attempt to upload images if offline --- src/Logic/ImageProviders/ImageUploadManager.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Logic/ImageProviders/ImageUploadManager.ts b/src/Logic/ImageProviders/ImageUploadManager.ts index 88bad0bcce..86e4cf82bb 100644 --- a/src/Logic/ImageProviders/ImageUploadManager.ts +++ b/src/Logic/ImageProviders/ImageUploadManager.ts @@ -16,6 +16,7 @@ import OsmObjectDownloader from "../Osm/OsmObjectDownloader" import ExifReader from "exifreader" import { Utils } from "../../Utils" import { Lists } from "../../Utils/Lists" +import { IsOnline } from "../Web/IsOnline" /** * The ImageUploadManager has a @@ -172,6 +173,9 @@ export class ImageUploadManager { if (this.uploadingAll) { return } + if(!IsOnline.isOnline){ + return + } try { let queue: ImageUploadArguments[] const failed: Set = new Set() From 561e4cb00990cb1e1e38f9459ff858256b9acfaf Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Fri, 8 Aug 2025 13:19:49 +0200 Subject: [PATCH 5/6] Feature(offline): more offline hardening --- src/Models/ThemeConfig/TagRenderingConfig.ts | 22 ++++-- src/UI/Comparison/ComparisonTool.svelte | 73 ++++++++++---------- src/UI/Image/UploadingImageCounter.svelte | 2 +- src/UI/Popup/DeleteFlow/DeleteFlowState.ts | 6 +- src/UI/Popup/DeleteFlow/DeleteWizard.svelte | 11 ++- src/UI/Popup/MarkAsFavourite.svelte | 48 +++++++------ src/UI/SingleThemeGui.svelte | 10 +++ 7 files changed, 103 insertions(+), 69 deletions(-) diff --git a/src/Models/ThemeConfig/TagRenderingConfig.ts b/src/Models/ThemeConfig/TagRenderingConfig.ts index c4bbdb7f1b..f034e3d1aa 100644 --- a/src/Models/ThemeConfig/TagRenderingConfig.ts +++ b/src/Models/ThemeConfig/TagRenderingConfig.ts @@ -19,6 +19,7 @@ import LayerConfig from "./LayerConfig" import ComparingTag from "../../Logic/Tags/ComparingTag" import { Unit } from "../Unit" import { Lists } from "../../Utils/Lists" +import { IsOnline } from "../../Logic/Web/IsOnline" export interface Mapping { readonly if: UploadableTag @@ -1154,10 +1155,10 @@ export class TagRenderingConfigUtils { const extraMappings = tags.bindD((tags) => { const country = tags._country if (country === undefined) { - return undefined + return undefined } const center = GeoOperations.centerpointCoordinates(feature) - return UIEventSource.fromPromise( + return UIEventSource.fromPromiseWithErr( NameSuggestionIndex.generateMappings( config.freeform.key, tags, @@ -1167,7 +1168,20 @@ export class TagRenderingConfigUtils { ) ) }) - return extraMappings.mapD((extraMappings) => { + return extraMappings.map((extraMappingsErr) => { + if(extraMappingsErr?.["error"]){ + console.log("Could not download the NSI: ", extraMappingsErr["error"]) + return config + } + const extraMappings = extraMappingsErr?.success + if(extraMappings === undefined){ + if(!IsOnline.isOnline.data){ + // The 'extraMappings' will still attempt to download the NSI - it might be in the service worker's cache + // As such, if they happen to come through anyway, they'll be shown + return config + } + return undefined + } if (extraMappings.length == 0) { return config } @@ -1187,6 +1201,6 @@ export class TagRenderingConfigUtils { }) ?? [] clone.mappings = [...oldMappingsCloned, ...extraMappings] return clone - }) + }, [IsOnline.isOnline]) } } diff --git a/src/UI/Comparison/ComparisonTool.svelte b/src/UI/Comparison/ComparisonTool.svelte index 58f90f27ca..fb8b3fff01 100644 --- a/src/UI/Comparison/ComparisonTool.svelte +++ b/src/UI/Comparison/ComparisonTool.svelte @@ -13,9 +13,10 @@ import Translations from "../i18n/Translations" import Tr from "../Base/Tr.svelte" import AccordionSingle from "../Flowbite/AccordionSingle.svelte" - import GlobeAlt from "@babeard/svelte-heroicons/mini/GlobeAlt" import { ComparisonState } from "./ComparisonState" import LoginToggle from "../Base/LoginToggle.svelte" + import { IsOnline } from "../../Logic/Web/IsOnline" + import GlobeAlt from "@babeard/svelte-heroicons/mini/GlobeAlt" export let externalData: Store< | { success: { content: Record } } @@ -33,7 +34,7 @@ * A switch that signals that the information should be downloaded. * The actual 'download' code is _not_ implemented here */ - export let downloadInformation : UIEventSource + export let downloadInformation: UIEventSource export let collapsed: boolean const t = Translations.t.external @@ -48,45 +49,47 @@ let propertyKeysExternal = comparisonState.mapD((ct) => ct.propertyKeysExternal) let hasDifferencesAtStart = comparisonState.mapD((ct) => ct.hasDifferencesAtStart) let enableLogin = state.featureSwitches.featureSwitchEnableLogin + const online = IsOnline.isOnline - - - {#if !$sourceUrl || !$enableLogin} - +{#if $online} + + {#if !$sourceUrl || !$enableLogin} + {:else if !$downloadInformation} - - {:else if $externalData === undefined} -
- -
- {:else if $externalData["error"] !== undefined} -
- -
- {:else if $propertyKeysExternal.length === 0 && $knownImages.size + $unknownImages.length === 0} - - {:else if !$hasDifferencesAtStart} + + {:else if $externalData === undefined} +
+ +
+ {:else if $externalData["error"] !== undefined} +
+ +
+ {:else if $propertyKeysExternal.length === 0 && $knownImages.size + $unknownImages.length === 0} + + {:else if !$hasDifferencesAtStart} - {:else if $comparisonState !== undefined} - + {:else if $comparisonState !== undefined} + - - - {/if} -
+ + + {/if} +
+{/if} diff --git a/src/UI/Image/UploadingImageCounter.svelte b/src/UI/Image/UploadingImageCounter.svelte index e8aac6e49d..4e214a4d24 100644 --- a/src/UI/Image/UploadingImageCounter.svelte +++ b/src/UI/Image/UploadingImageCounter.svelte @@ -93,7 +93,7 @@
{/if} -{#if !$online} +{#if !$online && $pending > 0}
diff --git a/src/UI/Popup/DeleteFlow/DeleteFlowState.ts b/src/UI/Popup/DeleteFlow/DeleteFlowState.ts index 53f8152905..6f750e7f5a 100644 --- a/src/UI/Popup/DeleteFlow/DeleteFlowState.ts +++ b/src/UI/Popup/DeleteFlow/DeleteFlowState.ts @@ -17,20 +17,18 @@ export class DeleteFlowState { private readonly _id: OsmId private readonly _allowDeletionAtChangesetCount: number private readonly _osmConnection: OsmConnection - private readonly state: SpecialVisualizationState constructor( id: OsmId, state: SpecialVisualizationState, allowDeletionAtChangesetCount?: number ) { - this.state = state this.objectDownloader = state.osmObjectDownloader this._id = id this._osmConnection = state.osmConnection this._allowDeletionAtChangesetCount = allowDeletionAtChangesetCount ?? Number.MAX_VALUE - this.CheckDeleteability(false) + this.checkDeleteability(false) } /** @@ -39,7 +37,7 @@ export class DeleteFlowState { * @constructor * @private */ - public CheckDeleteability(useTheInternet: boolean): void { + public checkDeleteability(useTheInternet: boolean): void { console.log("Checking deleteability (internet?", useTheInternet, ")") const t = Translations.t.delete const id = this._id diff --git a/src/UI/Popup/DeleteFlow/DeleteWizard.svelte b/src/UI/Popup/DeleteFlow/DeleteWizard.svelte index 11dc23a4fb..27c9ce11e1 100644 --- a/src/UI/Popup/DeleteFlow/DeleteWizard.svelte +++ b/src/UI/Popup/DeleteFlow/DeleteWizard.svelte @@ -22,6 +22,7 @@ import Invalid from "../../../assets/svg/Invalid.svelte" import { And } from "../../../Logic/Tags/And" import type { UploadableTag } from "../../../Logic/Tags/TagTypes" + import { IsOnline } from "../../../Logic/Web/IsOnline" export let state: SpecialVisualizationState export let deleteConfig: DeleteConfig @@ -39,9 +40,10 @@ const canBeDeletedReason = deleteAbility.canBeDeletedReason const hasSoftDeletion = deleteConfig.softDeletionTags !== undefined + const online = IsOnline.isOnline let currentState: "confirm" | "applying" | "deleted" = "confirm" $: { - deleteAbility.CheckDeleteability(true) + deleteAbility.checkDeleteability(true) } const t = Translations.t.delete @@ -97,8 +99,10 @@ currentState = "deleted" } - - +{#if !$online} +
You are offline. Deleting points is not possible
+ {:else} + {#if $canBeDeleted === false && !hasSoftDeletion}
@@ -171,3 +175,4 @@ {/if} + {/if} diff --git a/src/UI/Popup/MarkAsFavourite.svelte b/src/UI/Popup/MarkAsFavourite.svelte index b6df673a67..e15f28d3c9 100644 --- a/src/UI/Popup/MarkAsFavourite.svelte +++ b/src/UI/Popup/MarkAsFavourite.svelte @@ -7,41 +7,45 @@ import LoginToggle from "../Base/LoginToggle.svelte" import type { Feature } from "geojson" import LayerConfig from "../../Models/ThemeConfig/LayerConfig" + import { IsOnline } from "../../Logic/Web/IsOnline" + import { Store } from "../../Logic/UIEventSource.js" /** * A full-blown 'mark as favourite'-button */ export let state: SpecialVisualizationState export let feature: Feature - export let tags: Record + export let tags: Store> export let layer: LayerConfig let isFavourite = tags?.map((tags) => tags._favourite === "yes") const t = Translations.t.favouritePoi + const online = IsOnline.isOnline function markFavourite(isFavourite: boolean) { state.favourites.markAsFavourite(feature, layer.id, state.theme.id, tags, isFavourite) } - - - {#if $isFavourite} -
- +
+ + {:else} + -
- - {:else} - - {/if} - + {/if} + +{/if} diff --git a/src/UI/SingleThemeGui.svelte b/src/UI/SingleThemeGui.svelte index a7e83c5d08..7b0a76b614 100644 --- a/src/UI/SingleThemeGui.svelte +++ b/src/UI/SingleThemeGui.svelte @@ -71,6 +71,16 @@ window.requestIdleCallback(() => { InstallServiceWorker.precache(layer["_usedImages"]?.filter(i => i.startsWith("./"))) }) + + // The NSI + window.requestIdleCallback(() => { + InstallServiceWorker.precache( + [Constants.nsiLogosEndpoint + "nsi.min.json", + Constants.nsiLogosEndpoint + "featureCollection.min.json", + ], + ) + }) + } }).catch(e => console.error("Could not install service worker:", e)) From 07a181aa1e8f758e88a91bcd55eaca1788a558b0 Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Thu, 14 Aug 2025 14:45:44 +0200 Subject: [PATCH 6/6] Offline: don't attempt to load reviews when offline --- src/Logic/Web/MangroveReviews.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Logic/Web/MangroveReviews.ts b/src/Logic/Web/MangroveReviews.ts index 38ee66321d..d043b539f6 100644 --- a/src/Logic/Web/MangroveReviews.ts +++ b/src/Logic/Web/MangroveReviews.ts @@ -5,6 +5,7 @@ import { Feature, Position } from "geojson" import { GeoOperations } from "../GeoOperations" import { SpecialVisualizationState } from "../../UI/SpecialVisualization" import { WithUserRelatedState } from "../../Models/ThemeViewState/WithUserRelatedState" +import { IsOnline } from "./IsOnline" export interface ReviewCollection { readonly subjectUri?: Store @@ -238,11 +239,14 @@ export default class FeatureReviews implements ReviewCollection { if (!loadingAllowed.data) { return } + if (!IsOnline.isOnline.data) { + return + } const reviews = await MangroveReviews.getReviews({ sub }) console.debug("Got reviews for", feature, reviews, sub) this.addReviews(reviews.reviews, this._name.data) }, - [this._name, loadingAllowed] + [this._name, loadingAllowed, IsOnline.isOnline] ) /* We also construct all subject queries _without_ encoding the name to work around a previous bug * See https://github.com/giggls/opencampsitemap/issues/30