Compare commits

...
Sign in to create a new pull request.

6 commits

38 changed files with 431 additions and 144 deletions

View file

@ -217,8 +217,8 @@
}, },
{ {
"id": "debug", "id": "debug",
"metacondition": "__featureSwitchIsDebugging=true", "render": "{all_tags()}",
"render": "{all_tags()}" "metacondition": "__featureSwitchIsDebugging=true"
} }
], ],
"filter": [ "filter": [

View file

@ -114,9 +114,9 @@
"lineRendering": [], "lineRendering": [],
"tagRenderings": [ "tagRenderings": [
{ {
"classes": "p-0",
"id": "conversation", "id": "conversation",
"render": "{visualize_note_comments()}" "render": "{visualize_note_comments()}",
"classes": "p-0"
}, },
{ {
"id": "add_image", "id": "add_image",

View file

@ -66,16 +66,16 @@
], ],
"tagRenderings": [ "tagRenderings": [
{ {
"condition": "level=country",
"description": "The name of the country",
"id": "country_name", "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", "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": [ "filter": [

View file

@ -1898,6 +1898,46 @@
"render": { "render": {
"*": "{storage_all_tags()}" "*": "{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 "allowMove": false

View file

@ -43,8 +43,6 @@
<script src="./src/Logic/Web/AndroidPolyfill.ts" type="module"></script> <script src="./src/Logic/Web/AndroidPolyfill.ts" type="module"></script>
<script type="module" src="./src/all_themes_index.ts"></script> <script type="module" src="./src/all_themes_index.ts"></script>
<script async src="./src/InstallServiceWorker.ts" type="module"></script>
</body> </body>
</html> </html>

View file

@ -18,6 +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",
"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"
@ -335,6 +336,7 @@
"next": "Next", "next": "Next",
"noTagsSelected": "No tags selected", "noTagsSelected": "No tags selected",
"number": "number", "number": "number",
"offline": "Your device is offline",
"openTheMap": "Open the map", "openTheMap": "Open the map",
"openTheMapReason": "to view, edit and add information", "openTheMapReason": "to view, edit and add information",
"opening_hours": { "opening_hours": {
@ -637,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",
@ -651,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

@ -14095,6 +14095,9 @@
"debug_accordeon_title": { "debug_accordeon_title": {
"render": "Debug information" "render": "Debug information"
}, },
"debug_serviceworker_accordeon_title": {
"render": "Debug information about the service worker"
},
"debug_storage_accordeon_title": { "debug_storage_accordeon_title": {
"render": "Debug information about local storage" "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": { "fixate-north": {
"mappings": { "mappings": {
"0": { "0": {

View file

@ -1,7 +1,7 @@
import * as fs from "fs" import * as fs from "fs"
import Script from "./Script" import Script from "./Script"
function genImages(dryrun = false) { function genImages() {
console.log("Generating images") console.log("Generating images")
const dir = fs.readdirSync("./assets/svg") const dir = fs.readdirSync("./assets/svg")
for (const path of dir) { for (const path of dir) {
@ -64,7 +64,7 @@ class GenerateIncludedImages extends Script {
super("Converts all images from assets/svg into svelte-classes.") super("Converts all images from assets/svg into svelte-classes.")
} }
async main(args: string[]): Promise<void> { async main(): Promise<void> {
genImages() genImages()
} }
} }

View file

@ -8,7 +8,7 @@ import {
DoesImageExist, DoesImageExist,
PrevalidateTheme, PrevalidateTheme,
ValidateLayer, ValidateLayer,
ValidateThemeEnsemble, ValidateThemeEnsemble
} from "../src/Models/ThemeConfig/Conversion/Validation" } from "../src/Models/ThemeConfig/Conversion/Validation"
import { Translation } from "../src/UI/i18n/Translation" import { Translation } from "../src/UI/i18n/Translation"
import { OrderLayer, PrepareLayer } from "../src/Models/ThemeConfig/Conversion/PrepareLayer" import { OrderLayer, PrepareLayer } from "../src/Models/ThemeConfig/Conversion/PrepareLayer"
@ -19,7 +19,7 @@ import {
DesugaringStep, DesugaringStep,
Each, Each,
Fuse, Fuse,
On, On
} from "../src/Models/ThemeConfig/Conversion/Conversion" } from "../src/Models/ThemeConfig/Conversion/Conversion"
import { Utils } from "../src/Utils" import { Utils } from "../src/Utils"
import Script from "./Script" import Script from "./Script"
@ -182,7 +182,7 @@ class LayerBuilder extends Conversion<object, Map<string, LayerConfigJson>> {
return `./assets/layers/${id}/${id}.json` return `./assets/layers/${id}/${id}.json`
} }
writeLayer(layer: LayerConfigJson) { public writeLayer(layer: LayerConfigJson) {
if (layer.labels?.some((l) => this._labelBlacklist.has(l))) { if (layer.labels?.some((l) => this._labelBlacklist.has(l))) {
console.log("Not writing layer " + layer.id + ", censored") console.log("Not writing layer " + layer.id + ", censored")
return return
@ -191,6 +191,15 @@ class LayerBuilder extends Conversion<object, Map<string, LayerConfigJson>> {
if (!existsSync(LayerOverviewUtils.layerPath)) { if (!existsSync(LayerOverviewUtils.layerPath)) {
mkdirSync(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, " "), { writeFileSync(LayerBuilder.targetPath(layer.id), JSON.stringify(layer, null, " "), {
encoding: "utf8", encoding: "utf8",
}) })

View file

@ -9,7 +9,7 @@ class PrepareServiceWorker extends Script {
} }
public async main() { public async main() {
const v = Constants.vNumber const v = Constants.vNumber + "-" + new Date().getTime()
writeFileSync("./src/service-worker/SWGenerated.ts", writeFileSync("./src/service-worker/SWGenerated.ts",
["export class SWGenerated {", ["export class SWGenerated {",
"// generated by scripts/prepareServiceWorker.ts", "// generated by scripts/prepareServiceWorker.ts",

View file

@ -1,13 +1,17 @@
export {} export class InstallServiceWorker {
window.addEventListener("load", async () => {
static async installServiceWorker() {
if (!("serviceWorker" in navigator)) { if (!("serviceWorker" in navigator)) {
console.log("Service workers are not supported") throw ("Service workers are not supported")
return
} }
try {
await navigator.serviceWorker.register("/service-worker.js", { type: "module" }) await navigator.serviceWorker.register("/service-worker.js", { type: "module" })
console.log("Service worker registration successful") 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(";"))
}
}
} }
})

View file

@ -16,6 +16,7 @@ import OsmObjectDownloader from "../Osm/OsmObjectDownloader"
import ExifReader from "exifreader" import ExifReader from "exifreader"
import { Utils } from "../../Utils" import { Utils } from "../../Utils"
import { Lists } from "../../Utils/Lists" import { Lists } from "../../Utils/Lists"
import { IsOnline } from "../Web/IsOnline"
/** /**
* The ImageUploadManager has a * The ImageUploadManager has a
@ -172,6 +173,9 @@ export class ImageUploadManager {
if (this.uploadingAll) { if (this.uploadingAll) {
return return
} }
if(!IsOnline.isOnline){
return
}
try { try {
let queue: ImageUploadArguments[] let queue: ImageUploadArguments[]
const failed: Set<ImageUploadArguments> = new Set() const failed: Set<ImageUploadArguments> = new Set()

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

@ -8,6 +8,7 @@ import Constants from "../../Models/Constants"
import { Feature, Point } from "geojson" import { Feature, Point } from "geojson"
import { AndroidPolyfill } from "../Web/AndroidPolyfill" import { AndroidPolyfill } from "../Web/AndroidPolyfill"
import { QueryParameters } from "../Web/QueryParameters" import { QueryParameters } from "../Web/QueryParameters"
import { IsOnline } from "../Web/IsOnline"
interface OsmUserInfo { interface OsmUserInfo {
id: number id: number
@ -131,6 +132,7 @@ export class OsmConnection {
* Details of the currently logged-in user; undefined if not logged in * Details of the currently logged-in user; undefined if not logged in
*/ */
public userDetails: UIEventSource<UserDetails | undefined> public userDetails: UIEventSource<UserDetails | undefined>
public isLoggedIn: Store<boolean> public isLoggedIn: Store<boolean>
public gpxServiceIsOnline: UIEventSource<OsmServiceState> = new UIEventSource<OsmServiceState>( public gpxServiceIsOnline: UIEventSource<OsmServiceState> = new UIEventSource<OsmServiceState>(
"unknown" "unknown"
@ -182,7 +184,7 @@ export class OsmConnection {
this._oauth_config.oauth_secret = import.meta.env.VITE_OSM_OAUTH_SECRET this._oauth_config.oauth_secret = import.meta.env.VITE_OSM_OAUTH_SECRET
} }
this.userDetails = new UIEventSource<UserDetails>(undefined, "userDetails") this.userDetails = UIEventSource.asObject<UserDetails>(LocalStorageSource.get("user_details"), undefined)
if (options.fakeUser) { if (options.fakeUser) {
const ud = this.userDetails.data const ud = this.userDetails.data
ud.csCount = 5678 ud.csCount = 5678
@ -197,13 +199,7 @@ export class OsmConnection {
} }
this.updateCapabilities() this.updateCapabilities()
this.isLoggedIn = this.userDetails.map( this.isLoggedIn = this.userDetails.map((user) => !!user)
(user) =>
!!user &&
(this.apiIsOnline.data === "unknown" || this.apiIsOnline.data === "online"),
[this.apiIsOnline]
)
this._dryRun = options.dryRun ?? new UIEventSource<boolean>(false) this._dryRun = options.dryRun ?? new UIEventSource<boolean>(false)
if (options?.shared_cookie) { if (options?.shared_cookie) {
@ -284,6 +280,9 @@ export class OsmConnection {
} }
public async AttemptLogin() { public async AttemptLogin() {
if (!IsOnline.isOnline.data) {
return
}
this.updateCapabilities() this.updateCapabilities()
if (this.loadingStatus.data !== "logged-in") { if (this.loadingStatus.data !== "logged-in") {
this.loadingStatus.setData("loading") this.loadingStatus.setData("loading")
@ -308,6 +307,9 @@ export class OsmConnection {
} }
private async loadUserInfo() { private async loadUserInfo() {
if (!IsOnline.isOnline.data) {
return
}
try { try {
const result = await this.interact("user/details.json") const result = await this.interact("user/details.json")
if (result === null) { if (result === null) {

View file

@ -5,6 +5,7 @@ import { Feature, Position } from "geojson"
import { GeoOperations } from "../GeoOperations" import { GeoOperations } from "../GeoOperations"
import { SpecialVisualizationState } from "../../UI/SpecialVisualization" import { SpecialVisualizationState } from "../../UI/SpecialVisualization"
import { WithUserRelatedState } from "../../Models/ThemeViewState/WithUserRelatedState" import { WithUserRelatedState } from "../../Models/ThemeViewState/WithUserRelatedState"
import { IsOnline } from "./IsOnline"
export interface ReviewCollection { export interface ReviewCollection {
readonly subjectUri?: Store<string> readonly subjectUri?: Store<string>
@ -238,11 +239,14 @@ export default class FeatureReviews implements ReviewCollection {
if (!loadingAllowed.data) { if (!loadingAllowed.data) {
return return
} }
if (!IsOnline.isOnline.data) {
return
}
const reviews = await MangroveReviews.getReviews({ sub }) const reviews = await MangroveReviews.getReviews({ sub })
console.debug("Got reviews for", feature, reviews, sub) console.debug("Got reviews for", feature, reviews, sub)
this.addReviews(reviews.reviews, this._name.data) 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 /* We also construct all subject queries _without_ encoding the name to work around a previous bug
* See https://github.com/giggls/opencampsitemap/issues/30 * See https://github.com/giggls/opencampsitemap/issues/30

View file

@ -19,6 +19,7 @@ import LayerConfig from "./LayerConfig"
import ComparingTag from "../../Logic/Tags/ComparingTag" import ComparingTag from "../../Logic/Tags/ComparingTag"
import { Unit } from "../Unit" import { Unit } from "../Unit"
import { Lists } from "../../Utils/Lists" import { Lists } from "../../Utils/Lists"
import { IsOnline } from "../../Logic/Web/IsOnline"
export interface Mapping { export interface Mapping {
readonly if: UploadableTag readonly if: UploadableTag
@ -1157,7 +1158,7 @@ export class TagRenderingConfigUtils {
return undefined return undefined
} }
const center = GeoOperations.centerpointCoordinates(feature) const center = GeoOperations.centerpointCoordinates(feature)
return UIEventSource.fromPromise( return UIEventSource.fromPromiseWithErr(
NameSuggestionIndex.generateMappings( NameSuggestionIndex.generateMappings(
config.freeform.key, config.freeform.key,
tags, 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) { if (extraMappings.length == 0) {
return config return config
} }
@ -1187,6 +1201,6 @@ export class TagRenderingConfigUtils {
}) ?? [] }) ?? []
clone.mappings = [...oldMappingsCloned, ...extraMappings] clone.mappings = [...oldMappingsCloned, ...extraMappings]
return clone return clone
}) }, [IsOnline.isOnline])
} }
} }

View file

@ -204,7 +204,7 @@
</div> </div>
</div> </div>
<LoginToggle {state}> <LoginToggle {state} offline>
{#if $recentThemes.length > 2} {#if $recentThemes.length > 2}
<div class="my-4"> <div class="my-4">
<h2> <h2>

20
src/UI/Base/Avatar.svelte Normal file
View file

@ -0,0 +1,20 @@
<script lang="ts">
import type UserDetails from "../../Logic/Osm/OsmConnection"
import { Store, UIEventSource } from "../../Logic/UIEventSource"
import UserCircle from "@rgossiaux/svelte-heroicons/solid/UserCircle"
import { IsOnline } from "../../Logic/Web/IsOnline"
/**
* USer icon, if available
*/
export let userdetails: Store<UserDetails>
let loaded = new UIEventSource(false)
let isOnline = IsOnline.isOnline
</script>
{#if !$userdetails.img || !($loaded || $isOnline)}
<UserCircle class="h-14 w-14" color="gray" />
{:else}
<img alt="avatar" src={$userdetails.img} class="h-12 w-12 rounded-full" on:load={() => {loaded.set(true)}} />
{/if}

View file

@ -14,10 +14,15 @@
osmConnection: OsmConnection osmConnection: OsmConnection
featureSwitches?: { featureSwitchEnableLogin?: UIEventSource<boolean> } featureSwitches?: { featureSwitchEnableLogin?: UIEventSource<boolean> }
} }
/**
* Do show this element when in offline mode
*/
export let offline = false
/** /**
* If set, 'loading' will act as if we are already logged in. * 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. * 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 * Will still show the "not-logged-in"-slot
@ -32,23 +37,26 @@
unknown: t.loginFailedUnreachableMode, unknown: t.loginFailedUnreachableMode,
readonly: t.loginFailedReadonlyMode, readonly: t.loginFailedReadonlyMode,
} }
const apiState: Store<string> = const apiState: Store<OsmServiceState> =
state?.osmConnection?.apiIsOnline ?? new ImmutableStore<OsmServiceState>("online") state?.osmConnection?.apiIsOnline ?? new ImmutableStore<OsmServiceState>("online")
const online = IsOnline.isOnline const online = IsOnline.isOnline
let loggedIn = state?.osmConnection?.isLoggedIn
</script> </script>
{#if $badge} {#if $badge}
{#if !$online} {#if !$online && !offline}
{#if !hiddenFail} {#if !hiddenFail}
<div class="alert"> <div class="alert">
Your device is offline <Tr t={t.offline} />
</div> </div>
{/if} {/if}
{:else if !ignoreLoading && !hiddenFail && $loadingStatus === "loading"} {:else if !ignoreLoading && !hiddenFail && $loadingStatus === "loading"}
<slot name="loading"> <slot name="loading">
<Loading /> <Loading />
</slot> </slot>
{:else if ($loadingStatus === "error" || $apiState === "readonly" || $apiState === "offline")} {:else if $loggedIn}
<slot />
{:else if ($loadingStatus === "error" || $apiState === "readonly" || $apiState === "offline" || $apiState === "unreachable")}
{#if !hiddenFail} {#if !hiddenFail}
<slot name="error"> <slot name="error">
<div class="alert flex flex-col items-center"> <div class="alert flex flex-col items-center">
@ -63,8 +71,7 @@
</div> </div>
</slot> </slot>
{/if} {/if}
{:else if $loadingStatus === "logged-in"}
<slot />
{:else if $loadingStatus === "not-attempted"} {:else if $loadingStatus === "not-attempted"}
<slot name="not-logged-in" /> <slot name="not-logged-in" />
{/if} {/if}

View file

@ -63,6 +63,11 @@
import OfflineManagement from "./OfflineManagement.svelte" import OfflineManagement from "./OfflineManagement.svelte"
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 { 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
@ -72,16 +77,16 @@
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
let usersettingslayer = new LayerConfig(<LayerConfigJson>usersettings, "usersettings", true) let usersettingslayer = new LayerConfig(<LayerConfigJson>usersettings, "usersettings", true)
let theme = state.theme
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 location = state.mapProperties?.location 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)
@ -133,17 +138,13 @@
<!-- User related: avatar, settings, favourits, logout --> <!-- User related: avatar, settings, favourits, logout -->
<SidebarUnit> <SidebarUnit>
<LoginToggle {state}> <LoginToggle {state} offline>
<LoginButton osmConnection={state.osmConnection} slot="not-logged-in" /> <LoginButton osmConnection={state.osmConnection} slot="not-logged-in" />
<div class="flex items-center gap-x-4 w-full m-2"> <div class="flex items-center gap-x-4 w-full m-2">
{#if $userdetails.img} <Avatar userdetails={state.osmConnection.userDetails} />
<img alt="avatar" src={$userdetails.img} class="h-12 w-12 rounded-full" />
{:else}
<UserCircle class="h-14 w-14" color="gray"/>
{/if}
<div class="flex flex-col w-full gap-y-2"> <div class="flex flex-col w-full gap-y-2">
<b>{$userdetails.name}</b> <b>{$userdetails?.name ?? '<Username>'}</b>
<LogoutButton clss="as-link small subtle text-sm" osmConnection={state.osmConnection} /> <LogoutButton clss="as-link small subtle text-sm" osmConnection={state.osmConnection} />
</div> </div>
</div> </div>
@ -169,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}
@ -281,7 +284,7 @@
<Tr t={Translations.t.inspector.menu} /> <Tr t={Translations.t.inspector.menu} />
</a> </a>
{#if !state.theme} {#if !state?.theme}
<a class="flex" href={($isAndroid ? "https://mapcomplete.org" : ".") +`/statistics.html`} <a class="flex" href={($isAndroid ? "https://mapcomplete.org" : ".") +`/statistics.html`}
target="_blank"> target="_blank">
<ChartBar class="h-6 w-6" /> <ChartBar class="h-6 w-6" />

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

@ -3,6 +3,7 @@
import Translations from "../i18n/Translations" import Translations from "../i18n/Translations"
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 { IsOnline } from "../../Logic/Web/IsOnline"
export let state: ThemeViewState export let state: ThemeViewState
/** /**
@ -14,6 +15,7 @@
let dataIsLoading = state.dataIsLoading let dataIsLoading = state.dataIsLoading
let currentState = state.hasDataInView let currentState = state.hasDataInView
let online = IsOnline.isOnline
const t = Translations.t.centerMessage const t = Translations.t.centerMessage
const showingSearch = state.searchState.showSearchDrawer const showingSearch = state.searchState.showSearchDrawer
</script> </script>
@ -34,6 +36,10 @@
<Tr t={Translations.t.centerMessage.loadingData} /> <Tr t={Translations.t.centerMessage.loadingData} />
</Loading> </Loading>
</div> </div>
{:else if $currentState === "no-data" && !$online}
<div class="alert w-fit p-4">
<Tr t={t.noDataOffline} />
</div>
{:else if $currentState === "no-data"} {:else if $currentState === "no-data"}
<div class="alert w-fit p-4"> <div class="alert w-fit p-4">
<Tr t={t.noData} /> <Tr t={t.noData} />

View file

@ -3,6 +3,7 @@
import { fade } from "svelte/transition" import { fade } from "svelte/transition"
import { OsmConnection } from "../../Logic/Osm/OsmConnection" import { OsmConnection } from "../../Logic/Osm/OsmConnection"
import { onDestroy } from "svelte" import { onDestroy } from "svelte"
import Avatar from "../Base/Avatar.svelte"
let open = false let open = false
export let state: { osmConnection: OsmConnection } export let state: { osmConnection: OsmConnection }
@ -28,9 +29,7 @@
> >
{#if $username !== undefined} {#if $username !== undefined}
<div style="width: max-content" class="flex items-center"> <div style="width: max-content" class="flex items-center">
{#if $userdetails.img} <Avatar {userdetails} />
<img src={$userdetails.img} alt="avatar" class="mr-4 h-10 w-10 rounded-full" />
{/if}
<div> <div>
<div>Welcome back</div> <div>Welcome back</div>
<div class="normal-background" style="width: max-content"> <div class="normal-background" style="width: max-content">

View file

@ -13,9 +13,10 @@
import Translations from "../i18n/Translations" import Translations from "../i18n/Translations"
import Tr from "../Base/Tr.svelte" import Tr from "../Base/Tr.svelte"
import AccordionSingle from "../Flowbite/AccordionSingle.svelte" import AccordionSingle from "../Flowbite/AccordionSingle.svelte"
import GlobeAlt from "@babeard/svelte-heroicons/mini/GlobeAlt"
import { ComparisonState } from "./ComparisonState" import { ComparisonState } from "./ComparisonState"
import LoginToggle from "../Base/LoginToggle.svelte" import LoginToggle from "../Base/LoginToggle.svelte"
import { IsOnline } from "../../Logic/Web/IsOnline"
import GlobeAlt from "@babeard/svelte-heroicons/mini/GlobeAlt"
export let externalData: Store< export let externalData: Store<
| { success: { content: Record<string, string> } } | { success: { content: Record<string, string> } }
@ -48,9 +49,10 @@
let propertyKeysExternal = comparisonState.mapD((ct) => ct.propertyKeysExternal) let propertyKeysExternal = comparisonState.mapD((ct) => ct.propertyKeysExternal)
let hasDifferencesAtStart = comparisonState.mapD((ct) => ct.hasDifferencesAtStart) let hasDifferencesAtStart = comparisonState.mapD((ct) => ct.hasDifferencesAtStart)
let enableLogin = state.featureSwitches.featureSwitchEnableLogin let enableLogin = state.featureSwitches.featureSwitchEnableLogin
const online = IsOnline.isOnline
</script> </script>
{#if $online}
<LoginToggle {state} silentFail> <LoginToggle {state} hiddenFail>
{#if !$sourceUrl || !$enableLogin} {#if !$sourceUrl || !$enableLogin}
<!-- empty block --> <!-- empty block -->
{:else if !$downloadInformation} {:else if !$downloadInformation}
@ -90,3 +92,4 @@
</AccordionSingle> </AccordionSingle>
{/if} {/if}
</LoginToggle> </LoginToggle>
{/if}

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 && $pending > 0}
{#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}

View file

@ -4,7 +4,7 @@
function clearCaches() { function clearCaches() {
IdbLocalStorage.clearAll() IdbLocalStorage.clearAll()
Utils.download("./service-worker-clear") Utils.download("./service-worker/clear_caches.json")
window.location.reload() window.location.reload()
} }
export let msg: string export let msg: string

View file

@ -17,20 +17,18 @@ export class DeleteFlowState {
private readonly _id: OsmId private readonly _id: OsmId
private readonly _allowDeletionAtChangesetCount: number private readonly _allowDeletionAtChangesetCount: number
private readonly _osmConnection: OsmConnection private readonly _osmConnection: OsmConnection
private readonly state: SpecialVisualizationState
constructor( constructor(
id: OsmId, id: OsmId,
state: SpecialVisualizationState, state: SpecialVisualizationState,
allowDeletionAtChangesetCount?: number allowDeletionAtChangesetCount?: number
) { ) {
this.state = state
this.objectDownloader = state.osmObjectDownloader this.objectDownloader = state.osmObjectDownloader
this._id = id this._id = id
this._osmConnection = state.osmConnection this._osmConnection = state.osmConnection
this._allowDeletionAtChangesetCount = allowDeletionAtChangesetCount ?? Number.MAX_VALUE this._allowDeletionAtChangesetCount = allowDeletionAtChangesetCount ?? Number.MAX_VALUE
this.CheckDeleteability(false) this.checkDeleteability(false)
} }
/** /**
@ -39,7 +37,7 @@ export class DeleteFlowState {
* @constructor * @constructor
* @private * @private
*/ */
public CheckDeleteability(useTheInternet: boolean): void { public checkDeleteability(useTheInternet: boolean): void {
console.log("Checking deleteability (internet?", useTheInternet, ")") console.log("Checking deleteability (internet?", useTheInternet, ")")
const t = Translations.t.delete const t = Translations.t.delete
const id = this._id const id = this._id

View file

@ -22,6 +22,7 @@
import Invalid from "../../../assets/svg/Invalid.svelte" import Invalid from "../../../assets/svg/Invalid.svelte"
import { And } from "../../../Logic/Tags/And" import { And } from "../../../Logic/Tags/And"
import type { UploadableTag } from "../../../Logic/Tags/TagTypes" import type { UploadableTag } from "../../../Logic/Tags/TagTypes"
import { IsOnline } from "../../../Logic/Web/IsOnline"
export let state: SpecialVisualizationState export let state: SpecialVisualizationState
export let deleteConfig: DeleteConfig export let deleteConfig: DeleteConfig
@ -39,9 +40,10 @@
const canBeDeletedReason = deleteAbility.canBeDeletedReason const canBeDeletedReason = deleteAbility.canBeDeletedReason
const hasSoftDeletion = deleteConfig.softDeletionTags !== undefined const hasSoftDeletion = deleteConfig.softDeletionTags !== undefined
const online = IsOnline.isOnline
let currentState: "confirm" | "applying" | "deleted" = "confirm" let currentState: "confirm" | "applying" | "deleted" = "confirm"
$: { $: {
deleteAbility.CheckDeleteability(true) deleteAbility.checkDeleteability(true)
} }
const t = Translations.t.delete const t = Translations.t.delete
@ -97,8 +99,10 @@
currentState = "deleted" currentState = "deleted"
} }
</script> </script>
{#if !$online}
<LoginToggle ignoreLoading={true} {state} silentFail> <div class="subtle">You are offline. Deleting points is not possible</div>
{:else}
<LoginToggle ignoreLoading={true} {state} hiddenFail>
{#if $canBeDeleted === false && !hasSoftDeletion} {#if $canBeDeleted === false && !hasSoftDeletion}
<div class="low-interaction subtle flex gap-x-1 rounded p-2 text-sm italic"> <div class="low-interaction subtle flex gap-x-1 rounded p-2 text-sm italic">
<div class="relative h-fit"> <div class="relative h-fit">
@ -171,3 +175,4 @@
</AccordionSingle> </AccordionSingle>
{/if} {/if}
</LoginToggle> </LoginToggle>
{/if}

View file

@ -7,23 +7,26 @@
import LoginToggle from "../Base/LoginToggle.svelte" import LoginToggle from "../Base/LoginToggle.svelte"
import type { Feature } from "geojson" import type { Feature } from "geojson"
import LayerConfig from "../../Models/ThemeConfig/LayerConfig" 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 * A full-blown 'mark as favourite'-button
*/ */
export let state: SpecialVisualizationState export let state: SpecialVisualizationState
export let feature: Feature export let feature: Feature
export let tags: Record<string, string> export let tags: Store<Record<string, string>>
export let layer: LayerConfig export let layer: LayerConfig
let isFavourite = tags?.map((tags) => tags._favourite === "yes") let isFavourite = tags?.map((tags) => tags._favourite === "yes")
const t = Translations.t.favouritePoi const t = Translations.t.favouritePoi
const online = IsOnline.isOnline
function markFavourite(isFavourite: boolean) { function markFavourite(isFavourite: boolean) {
state.favourites.markAsFavourite(feature, layer.id, state.theme.id, tags, isFavourite) state.favourites.markAsFavourite(feature, layer.id, state.theme.id, tags, isFavourite)
} }
</script> </script>
{#if $online}
<LoginToggle ignoreLoading={true} {state}> <LoginToggle ignoreLoading hiddenFail {state}>
{#if $isFavourite} {#if $isFavourite}
<div class="flex h-fit items-start"> <div class="flex h-fit items-start">
<button class="w-full" on:click={() => markFavourite(false)}> <button class="w-full" on:click={() => markFavourite(false)}>
@ -45,3 +48,4 @@
</button> </button>
{/if} {/if}
</LoginToggle> </LoginToggle>
{/if}

View file

@ -128,7 +128,7 @@
{layer} {layer}
extraClasses="my-2" extraClasses="my-2"
/> />
{#if (!editingEnabled || $editingEnabled) && $apiState !== "readonly" && $apiState !== "offline"} {#if !editingEnabled || $editingEnabled}
<EditButton <EditButton
arialabel={config.editButtonAriaLabel} arialabel={config.editButtonAriaLabel}
ariaLabelledBy={answerId} ariaLabelledBy={answerId}

View file

@ -359,7 +359,7 @@
{#if question !== undefined && $apiState !== "readonly" && $apiState !== "offline"} {#if question !== undefined && $apiState !== "readonly" && $apiState !== "offline"}
<div class={clss}> <div class={clss}>
{#if layer?.isNormal()} {#if layer?.isNormal()}
<LoginToggle {state}> <LoginToggle {state} offline hiddenFail>
<DotMenu hideBackground={true} open={menuIsOpened}> <DotMenu hideBackground={true} open={menuIsOpened}>
<SidebarUnit> <SidebarUnit>
{#if $disabledInTheme.indexOf(config.id) >= 0} {#if $disabledInTheme.indexOf(config.id) >= 0}
@ -538,7 +538,7 @@
{/if} {/if}
<!-- Save and cancel buttons, in a logintoggle --> <!-- Save and cancel buttons, in a logintoggle -->
<LoginToggle {state} ignoreLoading> <LoginToggle {state} ignoreLoading offline>
<div class="flex w-full justify-end" slot="not-logged-in"> <div class="flex w-full justify-end" slot="not-logged-in">
{#if config.alwaysForceSaveButton} {#if config.alwaysForceSaveButton}
<button <button

View file

@ -13,6 +13,8 @@
import { WithSearchState } from "../Models/ThemeViewState/WithSearchState" import { WithSearchState } from "../Models/ThemeViewState/WithSearchState"
import ThemeConfig from "../Models/ThemeConfig/ThemeConfig" import ThemeConfig from "../Models/ThemeConfig/ThemeConfig"
import { AndroidPolyfill } from "../Logic/Web/AndroidPolyfill" import { AndroidPolyfill } from "../Logic/Web/AndroidPolyfill"
import { InstallServiceWorker } from "../InstallServiceWorker"
import type { LayerConfigJson } from "../Models/ThemeConfig/Json/LayerConfigJson"
function webgl_support() { function webgl_support() {
try { try {
@ -51,6 +53,36 @@
let availableLayers = UIEventSource.fromPromise(getAvailableLayers()) let availableLayers = UIEventSource.fromPromise(getAvailableLayers())
const state = new WithSearchState(theme, availableLayers) const state = new WithSearchState(theme, availableLayers)
InstallServiceWorker.installServiceWorker().then(() => {
if (theme.source._usedImages) {
window.requestIdleCallback(() => {
InstallServiceWorker.precache(theme.source._usedImages?.filter(i => i.startsWith("./")))
})
}
for (const layer of (<LayerConfigJson[]>theme.source.layers)) {
if (!Constants.isPriviliged(layer)) {
continue
}
if (!layer["_usedImages"]) {
continue
}
// The priviliged layers, which are injected, might have assets not included in the '_usedImages' of the theme
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))
</script> </script>
{#if !webgl_supported} {#if !webgl_supported}

View file

@ -79,7 +79,7 @@ class GpsAllTags extends SpecialVisualizationSvelte {
} }
} }
class StorageAlLTags extends SpecialVisualizationSvelte { class StorageAllTags extends SpecialVisualizationSvelte {
funcName = "storage_all_tags" funcName = "storage_all_tags"
group = "settings" group = "settings"
docs = "Shows the current state of storage" docs = "Shows the current state of storage"
@ -103,6 +103,27 @@ class StorageAlLTags extends SpecialVisualizationSvelte {
} }
} }
class ServiceWorkerAllTags extends SpecialVisualizationSvelte {
funcName = "serviceworker_all_tags"
group = "settings"
docs = "Shows the current state of service worker"
args = []
constr(state: SpecialVisualizationState): SvelteUIElement {
const data = {}
for (const key in localStorage) {
data[key] = localStorage[key]
}
const tags = UIEventSource.fromPromise<Record<string, any>>(
Utils.downloadJson(
"./service-worker/status.json")
)
return new SvelteUIElement(AllTagsPanel, { state, tags })
}
}
export class ClearCachesVis extends SpecialVisualizationSvelte { export class ClearCachesVis extends SpecialVisualizationSvelte {
funcName = "clear_caches" funcName = "clear_caches"
docs = docs =
@ -234,7 +255,8 @@ export class SettingsVisualisations {
new DisabledQuestionsVis(), new DisabledQuestionsVis(),
new GyroscopeAllTags(), new GyroscopeAllTags(),
new GpsAllTags(), new GpsAllTags(),
new StorageAlLTags(), new StorageAllTags(),
new ServiceWorkerAllTags(),
new ClearCachesVis(), new ClearCachesVis(),
new LoginButtonVis(), new LoginButtonVis(),
new QrLogin(), new QrLogin(),

View file

@ -1,5 +1,6 @@
import { QueryParameters } from "./Logic/Web/QueryParameters" import { QueryParameters } from "./Logic/Web/QueryParameters"
import AllThemesGui from "./UI/AllThemesGui.svelte" import AllThemesGui from "./UI/AllThemesGui.svelte"
import { InstallServiceWorker } from "./InstallServiceWorker"
const theme = QueryParameters.GetQueryParameter("layout", undefined).data ?? "" const theme = QueryParameters.GetQueryParameter("layout", undefined).data ?? ""
const customLayout = QueryParameters.GetQueryParameter("userlayout", undefined).data ?? "" const customLayout = QueryParameters.GetQueryParameter("userlayout", undefined).data ?? ""
@ -26,7 +27,7 @@ if (theme !== "") {
l.protocol + "//" + window.location.host + "/theme.html" + l.search + l.hash l.protocol + "//" + window.location.host + "/theme.html" + l.search + l.hash
) )
} }
InstallServiceWorker.installServiceWorker().catch(e => console.error(e))
new AllThemesGui({ new AllThemesGui({
target: document.getElementById("main"), target: document.getElementById("main"),
}) })

View file

@ -1,4 +1,4 @@
export class SWGenerated { export class SWGenerated {
// generated by scripts/prepareServiceWorker.ts // generated by scripts/prepareServiceWorker.ts
static vNumber = "0.54.4" static vNumber = "0.54.6-1754230245885"
} }

View file

@ -37,6 +37,14 @@ async function listCachedRequests(): Promise<string[]> {
return requests.map(req => req.url) return requests.map(req => req.url)
} }
async function clearCaches(): Promise<void> {
const cache = await caches.open(SWGenerated.vNumber)
const keys = await cache.keys()
for (const key of keys) {
await cache.delete(key)
}
}
class Router { class Router {
private readonly _endpoints: Record<string, (event: FetchEvent) => void> private readonly _endpoints: Record<string, (event: FetchEvent) => void>
private readonly _subpaths: Record<string, (event: FetchEvent, rest: string) => void> private readonly _subpaths: Record<string, (event: FetchEvent, rest: string) => void>
@ -71,12 +79,33 @@ class Router {
const allOffline = new Router({ const allOffline = new Router({
"clear_caches.Json": (event) => {
event.respondWith(
clearCaches().then(() =>
jsonResponse({ status: "ok" })
)
)
},
"precache": (event) => {
const url = new URL(event.request.url)
const assets = url.searchParams.get("assets")?.split(";")
if (assets) {
console.log("Precaching:", assets)
event.waitUntil(caches.open(SWGenerated.vNumber).then(cache =>
cache.addAll(assets)
))
}
event.respondWith(jsonResponse({ status: "ok" }))
},
"status.json": (event) => { "status.json": (event) => {
event.respondWith( event.respondWith(
listCachedRequests().then(cached => listCachedRequests().then(cached =>
jsonResponse( jsonResponse(
{ {
status: "ok", cached, status: "ok",
vnumber: SWGenerated.vNumber,
domain: selfDomain,
cached
} }
)) ))
) )
@ -89,7 +118,6 @@ self.addEventListener("fetch", (event) => {
if (url.endsWith("/service-worker.js")) { if (url.endsWith("/service-worker.js")) {
return // Installation of a new version, we don't interfere return // Installation of a new version, we don't interfere
} }
console.log("Intercepting event", event.request.url)
if (url.indexOf("/service-worker/") >= 0) { if (url.indexOf("/service-worker/") >= 0) {
allOffline.route(event) allOffline.route(event)
return return
@ -99,6 +127,11 @@ self.addEventListener("fetch", (event) => {
respondFromCache(event) respondFromCache(event)
return return
} }
if (urlObj.hostname === "data.mapcomplete.org") {
respondFromCache(event)
return
}
}) })
self.addEventListener("install", () => self.skipWaiting()) self.addEventListener("install", () => self.skipWaiting())