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",
"metacondition": "__featureSwitchIsDebugging=true",
"render": "{all_tags()}"
"render": "{all_tags()}",
"metacondition": "__featureSwitchIsDebugging=true"
}
],
"filter": [

View file

@ -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",

View file

@ -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": [

View file

@ -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

View file

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

View file

@ -18,6 +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",
"ready": "Done!",
"retrying": "Loading data failed. Trying again in {count} seconds…",
"zoomIn": "Zoom in to view or edit the data"
@ -335,6 +336,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": {
@ -637,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",
@ -651,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"
},
@ -958,4 +961,4 @@
"startsWithQ": "A wikidata identifier starts with Q and is followed by a number"
}
}
}
}

View file

@ -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": {

View file

@ -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<void> {
async main(): Promise<void> {
genImages()
}
}

View file

@ -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<object, Map<string, LayerConfigJson>> {
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<object, Map<string, LayerConfigJson>> {
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",
})

View file

@ -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",

View file

@ -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(";"))
}
}
}

View file

@ -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<ImageUploadArguments> = new Set()

View file

@ -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

View file

@ -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<UserDetails | undefined>
public isLoggedIn: Store<boolean>
public gpxServiceIsOnline: UIEventSource<OsmServiceState> = new UIEventSource<OsmServiceState>(
"unknown"
@ -182,7 +184,7 @@ export class OsmConnection {
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) {
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<boolean>(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) {

View file

@ -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<string>
@ -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

View file

@ -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])
}
}

View file

@ -204,7 +204,7 @@
</div>
</div>
<LoginToggle {state}>
<LoginToggle {state} offline>
{#if $recentThemes.length > 2}
<div class="my-4">
<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
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.
*/
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<string> =
const apiState: Store<OsmServiceState> =
state?.osmConnection?.apiIsOnline ?? new ImmutableStore<OsmServiceState>("online")
const online = IsOnline.isOnline
let loggedIn = state?.osmConnection?.isLoggedIn
</script>
{#if $badge}
{#if !$online}
{#if !$online && !offline}
{#if !hiddenFail}
<div class="alert">
Your device is offline
<Tr t={t.offline} />
</div>
{/if}
{:else if !ignoreLoading && !hiddenFail && $loadingStatus === "loading"}
<slot name="loading">
<Loading />
</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}
<slot name="error">
<div class="alert flex flex-col items-center">
@ -63,8 +71,7 @@
</div>
</slot>
{/if}
{:else if $loadingStatus === "logged-in"}
<slot />
{:else if $loadingStatus === "not-attempted"}
<slot name="not-logged-in" />
{/if}

View file

@ -63,6 +63,11 @@
import OfflineManagement from "./OfflineManagement.svelte"
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
@ -72,16 +77,16 @@
featureSwitches: Partial<FeatureSwitchState>
mapProperties?: MapProperties
userRelatedState?: UserRelatedState
changes?: Changes
}
let userdetails = state.osmConnection.userDetails
let usersettingslayer = new LayerConfig(<LayerConfigJson>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
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)
@ -133,17 +138,13 @@
<!-- User related: avatar, settings, favourits, logout -->
<SidebarUnit>
<LoginToggle {state}>
<LoginToggle {state} offline>
<LoginButton osmConnection={state.osmConnection} slot="not-logged-in" />
<div class="flex items-center gap-x-4 w-full m-2">
{#if $userdetails.img}
<img alt="avatar" src={$userdetails.img} class="h-12 w-12 rounded-full" />
{:else}
<UserCircle class="h-14 w-14" color="gray"/>
{/if}
<Avatar userdetails={state.osmConnection.userDetails} />
<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} />
</div>
</div>
@ -169,12 +170,14 @@
/>
</Page>
{#if $nrOfFailedImages.length > 0 || $failedImagesOpen}
{#if $nrOfFailedImages.length > 0 || $failedImagesOpen || $pendingChanges?.length > 0 }
<Page {onlyLink} shown={pg.failedImages} bodyPadding="p-0 pb-4">
<svelte:fragment slot="header">
<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>
<PendingChangesView {state} />
<QueuedImagesView {state} />
</Page>
{/if}
@ -281,7 +284,7 @@
<Tr t={Translations.t.inspector.menu} />
</a>
{#if !state.theme}
{#if !state?.theme}
<a class="flex" href={($isAndroid ? "https://mapcomplete.org" : ".") +`/statistics.html`}
target="_blank">
<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 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
</script>
@ -34,6 +36,10 @@
<Tr t={Translations.t.centerMessage.loadingData} />
</Loading>
</div>
{:else if $currentState === "no-data" && !$online}
<div class="alert w-fit p-4">
<Tr t={t.noDataOffline} />
</div>
{:else if $currentState === "no-data"}
<div class="alert w-fit p-4">
<Tr t={t.noData} />

View file

@ -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}
<div style="width: max-content" class="flex items-center">
{#if $userdetails.img}
<img src={$userdetails.img} alt="avatar" class="mr-4 h-10 w-10 rounded-full" />
{/if}
<Avatar {userdetails} />
<div>
<div>Welcome back</div>
<div class="normal-background" style="width: max-content">

View file

@ -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<string, string> } }
@ -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<boolean>
export let downloadInformation: UIEventSource<boolean>
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
</script>
<LoginToggle {state} silentFail>
{#if !$sourceUrl || !$enableLogin}
<!-- empty block -->
{#if $online}
<LoginToggle {state} hiddenFail>
{#if !$sourceUrl || !$enableLogin}
<!-- empty block -->
{:else if !$downloadInformation}
<button on:click={() => downloadInformation.set(true)}>
Attempt to download information from the website {$sourceUrl}
</button>
{:else if $externalData === undefined}
<div class="flex justify-center">
<Loading />
</div>
{:else if $externalData["error"] !== undefined}
<div class="subtle low-interaction rounded p-2 px-4 italic">
<Tr t={Translations.t.external.error} />
</div>
{:else if $propertyKeysExternal.length === 0 && $knownImages.size + $unknownImages.length === 0}
<Tr cls="subtle" t={t.noDataLoaded} />
{:else if !$hasDifferencesAtStart}
<button on:click={() => downloadInformation.set(true)}>
Attempt to download information from the website {$sourceUrl}
</button>
{:else if $externalData === undefined}
<div class="flex justify-center">
<Loading />
</div>
{:else if $externalData["error"] !== undefined}
<div class="subtle low-interaction rounded p-2 px-4 italic">
<Tr t={Translations.t.external.error} />
</div>
{:else if $propertyKeysExternal.length === 0 && $knownImages.size + $unknownImages.length === 0}
<Tr cls="subtle" t={t.noDataLoaded} />
{:else if !$hasDifferencesAtStart}
<span class="subtle text-sm">
<Tr t={t.allIncluded.Subs({ source: $sourceUrl })} />
</span>
{:else if $comparisonState !== undefined}
<AccordionSingle expanded={!collapsed}>
{:else if $comparisonState !== undefined}
<AccordionSingle expanded={!collapsed}>
<span slot="header" class="flex">
<GlobeAlt class="h-6 w-6" />
<Tr t={Translations.t.external.title} />
</span>
<ComparisonTable
externalProperties={$externalData["success"]}
{state}
{feature}
{layer}
{tags}
{readonly}
sourceUrl={$sourceUrl}
comparisonState={$comparisonState}
/>
</AccordionSingle>
{/if}
</LoginToggle>
<ComparisonTable
externalProperties={$externalData["success"]}
{state}
{feature}
{layer}
{tags}
{readonly}
sourceUrl={$sourceUrl}
comparisonState={$comparisonState}
/>
</AccordionSingle>
{/if}
</LoginToggle>
{/if}

View file

@ -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<ImageUploadArguments[]> = state.imageUploadManager.queuedArgs
let isUploading = state.imageUploadManager.isUploading
let online = IsOnline.isOnline
const t = Translations.t
const q = t.imageQueue
</script>
@ -27,7 +29,7 @@
{#if $isUploading}
<Loading />
{:else}
{:else if $online}
<button class="primary" on:click={() => state.imageUploadManager.uploadQueue()}>
<ArrowPathIcon class="m-1 h-8 w-8" />
<Tr t={q.retryAll} />

View file

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

View file

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

View file

@ -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

View file

@ -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

View file

@ -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"
}
</script>
<LoginToggle ignoreLoading={true} {state} silentFail>
{#if !$online}
<div class="subtle">You are offline. Deleting points is not possible</div>
{:else}
<LoginToggle ignoreLoading={true} {state} hiddenFail>
{#if $canBeDeleted === false && !hasSoftDeletion}
<div class="low-interaction subtle flex gap-x-1 rounded p-2 text-sm italic">
<div class="relative h-fit">
@ -171,3 +175,4 @@
</AccordionSingle>
{/if}
</LoginToggle>
{/if}

View file

@ -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<string, string>
export let tags: Store<Record<string, string>>
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)
}
</script>
<LoginToggle ignoreLoading={true} {state}>
{#if $isFavourite}
<div class="flex h-fit items-start">
<button class="w-full" on:click={() => markFavourite(false)}>
<HeartSolidIcon class="mr-2 w-16 shrink-0" on:click={() => markFavourite(false)} />
<div class="flex flex-col items-start">
<Tr t={t.button.unmark} />
<Tr cls="normal-font subtle" t={t.button.unmarkNotDeleted} />
{#if $online}
<LoginToggle ignoreLoading hiddenFail {state}>
{#if $isFavourite}
<div class="flex h-fit items-start">
<button class="w-full" on:click={() => markFavourite(false)}>
<HeartSolidIcon class="mr-2 w-16 shrink-0" on:click={() => markFavourite(false)} />
<div class="flex flex-col items-start">
<Tr t={t.button.unmark} />
<Tr cls="normal-font subtle" t={t.button.unmarkNotDeleted} />
</div>
</button>
</div>
<Tr cls="font-bold thanks m-2 p-2 block" t={t.button.isFavourite} />
{:else}
<button class="w-full" on:click={() => markFavourite(true)}>
<HeartOutlineIcon class="mr-2 w-16 shrink-0" on:click={() => markFavourite(true)} />
<div class="flex w-full flex-col items-start">
<Tr t={t.button.markAsFavouriteTitle} />
<Tr cls="normal-font subtle" t={t.button.markDescription} />
</div>
</button>
</div>
<Tr cls="font-bold thanks m-2 p-2 block" t={t.button.isFavourite} />
{:else}
<button class="w-full" on:click={() => markFavourite(true)}>
<HeartOutlineIcon class="mr-2 w-16 shrink-0" on:click={() => markFavourite(true)} />
<div class="flex w-full flex-col items-start">
<Tr t={t.button.markAsFavouriteTitle} />
<Tr cls="normal-font subtle" t={t.button.markDescription} />
</div>
</button>
{/if}
</LoginToggle>
{/if}
</LoginToggle>
{/if}

View file

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

View file

@ -359,7 +359,7 @@
{#if question !== undefined && $apiState !== "readonly" && $apiState !== "offline"}
<div class={clss}>
{#if layer?.isNormal()}
<LoginToggle {state}>
<LoginToggle {state} offline hiddenFail>
<DotMenu hideBackground={true} open={menuIsOpened}>
<SidebarUnit>
{#if $disabledInTheme.indexOf(config.id) >= 0}
@ -538,7 +538,7 @@
{/if}
<!-- 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">
{#if config.alwaysForceSaveButton}
<button

View file

@ -13,6 +13,8 @@
import { WithSearchState } from "../Models/ThemeViewState/WithSearchState"
import ThemeConfig from "../Models/ThemeConfig/ThemeConfig"
import { AndroidPolyfill } from "../Logic/Web/AndroidPolyfill"
import { InstallServiceWorker } from "../InstallServiceWorker"
import type { LayerConfigJson } from "../Models/ThemeConfig/Json/LayerConfigJson"
function webgl_support() {
try {
@ -51,6 +53,36 @@
let availableLayers = UIEventSource.fromPromise(getAvailableLayers())
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>
{#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"
group = "settings"
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 {
funcName = "clear_caches"
docs =
@ -234,7 +255,8 @@ export class SettingsVisualisations {
new DisabledQuestionsVis(),
new GyroscopeAllTags(),
new GpsAllTags(),
new StorageAlLTags(),
new StorageAllTags(),
new ServiceWorkerAllTags(),
new ClearCachesVis(),
new LoginButtonVis(),
new QrLogin(),

View file

@ -1,5 +1,6 @@
import { QueryParameters } from "./Logic/Web/QueryParameters"
import AllThemesGui from "./UI/AllThemesGui.svelte"
import { InstallServiceWorker } from "./InstallServiceWorker"
const theme = QueryParameters.GetQueryParameter("layout", 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
)
}
InstallServiceWorker.installServiceWorker().catch(e => console.error(e))
new AllThemesGui({
target: document.getElementById("main"),
})

View file

@ -1,4 +1,4 @@
export class SWGenerated {
// 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)
}
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 {
private readonly _endpoints: Record<string, (event: FetchEvent) => void>
private readonly _subpaths: Record<string, (event: FetchEvent, rest: string) => void>
@ -71,12 +79,33 @@ class 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) => {
event.respondWith(
listCachedRequests().then(cached =>
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")) {
return // Installation of a new version, we don't interfere
}
console.log("Intercepting event", event.request.url)
if (url.indexOf("/service-worker/") >= 0) {
allOffline.route(event)
return
@ -99,6 +127,11 @@ self.addEventListener("fetch", (event) => {
respondFromCache(event)
return
}
if (urlObj.hostname === "data.mapcomplete.org") {
respondFromCache(event)
return
}
})
self.addEventListener("install", () => self.skipWaiting())