App-Lifecycle: Back-Button, Resume letzte Position, sicheres Speichern
- Resume: jede Navigation wird gemerkt; nach App-Neustart (oder wenn Android den Prozess beim App-Wechsel beendet hat) öffnet die App wieder genau dort, wo der Benutzer war — inklusive offenem Protokoll - Protokoll wird beim Verlassen der Seite (Back-Tap/Navigation) und beim Wechsel in den Hintergrund automatisch gesichert — auch Eingaben, die noch nicht per onblur gespeichert wurden, gehen nicht mehr verloren - Hardware-Backbutton schließt einen offenen Werkzeug-Dialog, statt gleich die Seite zu verlassen (neue Overlay-Registry overlay.svelte.ts) - backButton.svelte.ts: PluginListenerHandle korrekt aus @capacitor/core importiert (war fälschlich @capacitor/app — Svelte-Check-Fehler behoben) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
1a0f1dc5ca
commit
50793e4e5d
4 changed files with 88 additions and 6 deletions
|
|
@ -11,8 +11,8 @@
|
|||
* 3. Auf der Hauptroute: 1. Tap = Hinweis, 2. Tap binnen 1,8 s = App beenden
|
||||
*/
|
||||
|
||||
import { App, type PluginListenerHandle } from '@capacitor/app';
|
||||
import { Capacitor } from '@capacitor/core';
|
||||
import { App } from '@capacitor/app';
|
||||
import { Capacitor, type PluginListenerHandle } from '@capacitor/core';
|
||||
|
||||
interface BackConfig {
|
||||
/** Schließt einen offenen Overlay-Zustand. true = verarbeitet, nichts weiter tun. */
|
||||
|
|
|
|||
32
src/lib/overlay.svelte.ts
Normal file
32
src/lib/overlay.svelte.ts
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
/**
|
||||
* Registry offener Overlays (Dialoge, Sheets, Bottom-Sheets).
|
||||
*
|
||||
* Damit der Hardware-Backbutton zuerst ein offenes Overlay schließt, statt
|
||||
* gleich die Seite zu verlassen, meldet jedes Overlay beim Öffnen einen
|
||||
* Schließen-Callback an. Der Backbutton-Handler (backButton.svelte.ts) ruft
|
||||
* `closeTopOverlay()` und navigiert nur, wenn kein Overlay offen war.
|
||||
*
|
||||
* Modul-Scope, kein Svelte-State — Mehrfachregistrierung ist sicher.
|
||||
*/
|
||||
|
||||
const closers: Array<() => void> = [];
|
||||
|
||||
/**
|
||||
* Overlay anmelden, solange es geöffnet ist.
|
||||
* Gibt die Abmeldefunktion zurück (im Svelte-$effect-Cleanup aufrufen).
|
||||
*/
|
||||
export function pushOverlay(close: () => void): () => void {
|
||||
closers.push(close);
|
||||
return () => {
|
||||
const i = closers.lastIndexOf(close);
|
||||
if (i >= 0) closers.splice(i, 1);
|
||||
};
|
||||
}
|
||||
|
||||
/** Oberstes Overlay schließen. Liefert true, wenn es eines zu schließen gab. */
|
||||
export function closeTopOverlay(): boolean {
|
||||
const close = closers.pop();
|
||||
if (!close) return false;
|
||||
close();
|
||||
return true;
|
||||
}
|
||||
|
|
@ -1,13 +1,15 @@
|
|||
<script lang="ts">
|
||||
import '../app.css';
|
||||
import { onMount, onDestroy } from 'svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { goto, afterNavigate } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import { Preferences } from '@capacitor/preferences';
|
||||
import { auth } from '$lib/auth.svelte';
|
||||
import { sync } from '$lib/sync.svelte';
|
||||
import { toast } from '$lib/toast.svelte';
|
||||
import { initDb } from '$lib/db';
|
||||
import { registerBackListener, removeBackListener } from '$lib/backButton.svelte';
|
||||
import { closeTopOverlay } from '$lib/overlay.svelte';
|
||||
import { checkForUpdate, installUpdate, type UpdateInfo } from '$lib/updater';
|
||||
import { initDebugLog } from '$lib/debuglog.svelte';
|
||||
import Toast from '$lib/components/Toast.svelte';
|
||||
|
|
@ -21,6 +23,18 @@
|
|||
let updatePercent = $state(0);
|
||||
|
||||
const HOME = '/auftraege/';
|
||||
/** Preferences-Schlüssel für die zuletzt besuchte Seite (Resume nach App-Neustart) */
|
||||
const LAST_ROUTE_KEY = 'nd_last_route';
|
||||
|
||||
// Jede Navigation merken — damit die App nach einem Neustart (oder Wechsel
|
||||
// aus einer anderen App, bei dem Android den Prozess beendet hat) wieder
|
||||
// genau dort öffnet, wo der Benutzer aufgehört hat.
|
||||
afterNavigate(({ to }) => {
|
||||
const path = to?.url.pathname;
|
||||
if (path && !path.startsWith('/login')) {
|
||||
void Preferences.set({ key: LAST_ROUTE_KEY, value: path });
|
||||
}
|
||||
});
|
||||
|
||||
// Update herunterladen und Installer öffnen — Fortschritt im Banner
|
||||
async function runUpdate() {
|
||||
|
|
@ -44,7 +58,7 @@
|
|||
|
||||
// Hardware-Backbutton (Modul-Scope, Single-Instance — KB #480/#549)
|
||||
registerBackListener({
|
||||
handleOverlay: () => false,
|
||||
handleOverlay: () => closeTopOverlay(),
|
||||
isHomeRoute: () => {
|
||||
const p = $page.url.pathname;
|
||||
return p === HOME || p === '/' || p === '/login/';
|
||||
|
|
@ -53,6 +67,17 @@
|
|||
showExitHint: () => toast.show('Nochmal drücken zum Beenden'),
|
||||
});
|
||||
|
||||
// Letzte Position wiederherstellen: nur beim echten Kaltstart (App öffnet
|
||||
// auf "/" oder der Auftragsliste), nicht wenn gezielt woandershin navigiert
|
||||
// wurde. Ungültige/gelöschte Protokolle fängt die Zielseite selbst ab.
|
||||
if (auth.loggedIn) {
|
||||
const last = (await Preferences.get({ key: LAST_ROUTE_KEY })).value;
|
||||
const here = $page.url.pathname;
|
||||
if (last && last !== here && (here === '/' || here === HOME)) {
|
||||
await goto(last);
|
||||
}
|
||||
}
|
||||
|
||||
// Auf neue APK prüfen — beim Start still (kein Toast), nur Banner bei Erfolg
|
||||
try {
|
||||
updateInfo = await checkForUpdate();
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { onMount, onDestroy } from 'svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import { App } from '@capacitor/app';
|
||||
import type { PluginListenerHandle } from '@capacitor/core';
|
||||
import AppHeader from '$lib/components/AppHeader.svelte';
|
||||
import ToolDialog from '$lib/components/ToolDialog.svelte';
|
||||
import MeasurementResult from '$lib/components/MeasurementResult.svelte';
|
||||
|
|
@ -10,6 +12,7 @@
|
|||
import { addMeasurement, upsertDevice } from '$lib/protocols';
|
||||
import { sync } from '$lib/sync.svelte';
|
||||
import { toast } from '$lib/toast.svelte';
|
||||
import { pushOverlay } from '$lib/overlay.svelte';
|
||||
import { TOOLS, getTool } from '$lib/tools';
|
||||
import type { Tool } from '$lib/tools/types';
|
||||
import type { Device, Protocol } from '$lib/types';
|
||||
|
|
@ -20,6 +23,8 @@
|
|||
let activeDevice = $state<Device | undefined>(undefined);
|
||||
let saving = $state(false);
|
||||
|
||||
let appStateListener: PluginListenerHandle | null = null;
|
||||
|
||||
const protocolTools = TOOLS.filter((t) => t.scope === 'protocol');
|
||||
const deviceTools = TOOLS.filter((t) => t.scope === 'device');
|
||||
|
||||
|
|
@ -27,7 +32,7 @@
|
|||
const ampelDot = ['bg-emerald-500', 'bg-amber-400', 'bg-red-500'];
|
||||
|
||||
onMount(async () => {
|
||||
const uuid = $page.params.id;
|
||||
const uuid = $page.params.id ?? '';
|
||||
const p = await getProtocol(uuid);
|
||||
if (!p) {
|
||||
toast.show('Protokoll nicht gefunden', 'error');
|
||||
|
|
@ -35,6 +40,19 @@
|
|||
return;
|
||||
}
|
||||
protocol = p;
|
||||
|
||||
// App wechselt in den Hintergrund (anderer App-Wechsel, Display aus) →
|
||||
// sofort sichern, bevor Android den Prozess evtl. beendet.
|
||||
appStateListener = await App.addListener('appStateChange', ({ isActive }) => {
|
||||
if (!isActive) void persist();
|
||||
});
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
// Beim Verlassen der Seite (Back-Tap, Navigation) final sichern — fängt
|
||||
// auch Eingaben ab, die noch nicht per onblur gespeichert wurden.
|
||||
void persist();
|
||||
appStateListener?.remove();
|
||||
});
|
||||
|
||||
/** Protokoll als geändert markieren und lokal speichern */
|
||||
|
|
@ -45,6 +63,13 @@
|
|||
await sync.refreshPending();
|
||||
}
|
||||
|
||||
// Offenen Werkzeug-Dialog beim Hardware-Backbutton schließen, statt
|
||||
// gleich die Seite zu verlassen.
|
||||
$effect(() => {
|
||||
if (!activeTool) return;
|
||||
return pushOverlay(() => (activeTool = null));
|
||||
});
|
||||
|
||||
/** Lucide-Icon dynamisch holen (Tool-Icon-Name ist kebab-case) */
|
||||
function icon(name: string) {
|
||||
const pascal = name
|
||||
|
|
|
|||
Loading…
Reference in a new issue