Updater: APK direkt in App herunterladen und installieren [apk]
All checks were successful
Build APK / build-apk (push) Successful in 1m42s
All checks were successful
Build APK / build-apk (push) Successful in 1m42s
Ersetzt den Browser-Umweg (window.open) durch einen echten In-App-Installer: das native Plugin lädt die APK streamend herunter (Fortschritts-Events updateProgress 0–100 %), prüft die Installationsberechtigung (Android 8+) und öffnet den Paketinstaller über den vorhandenen FileProvider. Versionsvergleich jetzt numerisch (YYYYMMDD-HHMM) statt lexikografisch. Banner ist schließbar; Einstellungsseite zeigt separaten Fortschrittsbalken. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
8d7353cbff
commit
34356f25ef
5 changed files with 344 additions and 19 deletions
|
|
@ -2,7 +2,12 @@ package de.data_it_solution.netdiag
|
||||||
|
|
||||||
import android.Manifest
|
import android.Manifest
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
import android.net.wifi.WifiManager
|
import android.net.wifi.WifiManager
|
||||||
|
import android.os.Build
|
||||||
|
import android.provider.Settings
|
||||||
|
import androidx.core.content.FileProvider
|
||||||
import com.getcapacitor.JSArray
|
import com.getcapacitor.JSArray
|
||||||
import com.getcapacitor.JSObject
|
import com.getcapacitor.JSObject
|
||||||
import com.getcapacitor.Plugin
|
import com.getcapacitor.Plugin
|
||||||
|
|
@ -19,12 +24,15 @@ import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import java.io.BufferedReader
|
import java.io.BufferedReader
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import java.io.FileOutputStream
|
||||||
import java.io.FileReader
|
import java.io.FileReader
|
||||||
import java.net.DatagramPacket
|
import java.net.DatagramPacket
|
||||||
import java.net.DatagramSocket
|
import java.net.DatagramSocket
|
||||||
|
import java.net.HttpURLConnection
|
||||||
import java.net.InetAddress
|
import java.net.InetAddress
|
||||||
import java.net.InetSocketAddress
|
import java.net.InetSocketAddress
|
||||||
import java.net.Socket
|
import java.net.Socket
|
||||||
|
import java.net.URL
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -475,6 +483,93 @@ class NetDiagScannerPlugin : Plugin() {
|
||||||
var maxMs = 0.0
|
var maxMs = 0.0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* --------------------------------------------------------------------- */
|
||||||
|
/* App-Update: APK herunterladen und Paketinstaller öffnen */
|
||||||
|
/* --------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lädt die neue APK vom (authentifizierten) Update-Proxy des netdiag-Moduls
|
||||||
|
* herunter und öffnet den Android-Paketinstaller. Der Download-Fortschritt
|
||||||
|
* wird laufend als `updateProgress`-Event (0–100 %) gemeldet.
|
||||||
|
*
|
||||||
|
* Vor Android 8 genügt die globale Einstellung „Unbekannte Quellen". Ab
|
||||||
|
* Android 8 muss die App einzeln berechtigt sein — fehlt das Recht, wird
|
||||||
|
* der passende Einstellungs-Dialog geöffnet und der Aufruf abgewiesen.
|
||||||
|
*/
|
||||||
|
@PluginMethod
|
||||||
|
fun installUpdate(call: PluginCall) {
|
||||||
|
val url = call.getString("url") ?: return call.reject("url fehlt")
|
||||||
|
io.launch {
|
||||||
|
try {
|
||||||
|
// Ab Android 8: App braucht das Recht, Pakete zu installieren
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O &&
|
||||||
|
!context.packageManager.canRequestPackageInstalls()
|
||||||
|
) {
|
||||||
|
val perm = Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES)
|
||||||
|
.setData(Uri.parse("package:${context.packageName}"))
|
||||||
|
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
|
context.startActivity(perm)
|
||||||
|
call.reject("Bitte erlauben, dass NetDiag Apps installieren darf, dann erneut tippen")
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
|
||||||
|
val apk = File(context.cacheDir, "NetDiag-update.apk")
|
||||||
|
downloadApk(url, apk)
|
||||||
|
|
||||||
|
val uri = FileProvider.getUriForFile(
|
||||||
|
context, "${context.packageName}.fileprovider", apk
|
||||||
|
)
|
||||||
|
val install = Intent(Intent.ACTION_VIEW)
|
||||||
|
.setDataAndType(uri, "application/vnd.android.package-archive")
|
||||||
|
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||||
|
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
|
context.startActivity(install)
|
||||||
|
resolve(call, JSObject().put("started", true))
|
||||||
|
} catch (e: Exception) {
|
||||||
|
call.reject("installUpdate: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** APK streamend in `target` laden und dabei `updateProgress`-Events senden. */
|
||||||
|
private fun downloadApk(url: String, target: File) {
|
||||||
|
val conn = (URL(url).openConnection() as HttpURLConnection).apply {
|
||||||
|
connectTimeout = 15_000
|
||||||
|
readTimeout = 120_000
|
||||||
|
instanceFollowRedirects = true
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
val code = conn.responseCode
|
||||||
|
if (code != 200) throw Exception("Download fehlgeschlagen (HTTP $code)")
|
||||||
|
val total = conn.contentLength.toLong() // -1 wenn unbekannt
|
||||||
|
var read = 0L
|
||||||
|
var lastPct = -1
|
||||||
|
conn.inputStream.use { input ->
|
||||||
|
FileOutputStream(target).use { out ->
|
||||||
|
val buf = ByteArray(64 * 1024)
|
||||||
|
while (true) {
|
||||||
|
val n = input.read(buf)
|
||||||
|
if (n < 0) break
|
||||||
|
out.write(buf, 0, n)
|
||||||
|
read += n
|
||||||
|
if (total > 0) {
|
||||||
|
val pct = (read * 100 / total).toInt()
|
||||||
|
if (pct != lastPct) {
|
||||||
|
lastPct = pct
|
||||||
|
notifyListeners(
|
||||||
|
"updateProgress", JSObject().put("percent", pct)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (target.length() < 1024) throw Exception("APK unvollständig empfangen")
|
||||||
|
} finally {
|
||||||
|
conn.disconnect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* --------------------------------------------------------------------- */
|
/* --------------------------------------------------------------------- */
|
||||||
/* Hilfsfunktionen */
|
/* Hilfsfunktionen */
|
||||||
/* --------------------------------------------------------------------- */
|
/* --------------------------------------------------------------------- */
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,12 @@ package de.data_it_solution.netdiag
|
||||||
|
|
||||||
import android.Manifest
|
import android.Manifest
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
import android.net.wifi.WifiManager
|
import android.net.wifi.WifiManager
|
||||||
|
import android.os.Build
|
||||||
|
import android.provider.Settings
|
||||||
|
import androidx.core.content.FileProvider
|
||||||
import com.getcapacitor.JSArray
|
import com.getcapacitor.JSArray
|
||||||
import com.getcapacitor.JSObject
|
import com.getcapacitor.JSObject
|
||||||
import com.getcapacitor.Plugin
|
import com.getcapacitor.Plugin
|
||||||
|
|
@ -19,12 +24,15 @@ import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import java.io.BufferedReader
|
import java.io.BufferedReader
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import java.io.FileOutputStream
|
||||||
import java.io.FileReader
|
import java.io.FileReader
|
||||||
import java.net.DatagramPacket
|
import java.net.DatagramPacket
|
||||||
import java.net.DatagramSocket
|
import java.net.DatagramSocket
|
||||||
|
import java.net.HttpURLConnection
|
||||||
import java.net.InetAddress
|
import java.net.InetAddress
|
||||||
import java.net.InetSocketAddress
|
import java.net.InetSocketAddress
|
||||||
import java.net.Socket
|
import java.net.Socket
|
||||||
|
import java.net.URL
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -426,6 +434,93 @@ class NetDiagScannerPlugin : Plugin() {
|
||||||
var maxMs = 0.0
|
var maxMs = 0.0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* --------------------------------------------------------------------- */
|
||||||
|
/* App-Update: APK herunterladen und Paketinstaller öffnen */
|
||||||
|
/* --------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lädt die neue APK vom (authentifizierten) Update-Proxy des netdiag-Moduls
|
||||||
|
* herunter und öffnet den Android-Paketinstaller. Der Download-Fortschritt
|
||||||
|
* wird laufend als `updateProgress`-Event (0–100 %) gemeldet.
|
||||||
|
*
|
||||||
|
* Vor Android 8 genügt die globale Einstellung „Unbekannte Quellen". Ab
|
||||||
|
* Android 8 muss die App einzeln berechtigt sein — fehlt das Recht, wird
|
||||||
|
* der passende Einstellungs-Dialog geöffnet und der Aufruf abgewiesen.
|
||||||
|
*/
|
||||||
|
@PluginMethod
|
||||||
|
fun installUpdate(call: PluginCall) {
|
||||||
|
val url = call.getString("url") ?: return call.reject("url fehlt")
|
||||||
|
io.launch {
|
||||||
|
try {
|
||||||
|
// Ab Android 8: App braucht das Recht, Pakete zu installieren
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O &&
|
||||||
|
!context.packageManager.canRequestPackageInstalls()
|
||||||
|
) {
|
||||||
|
val perm = Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES)
|
||||||
|
.setData(Uri.parse("package:${context.packageName}"))
|
||||||
|
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
|
context.startActivity(perm)
|
||||||
|
call.reject("Bitte erlauben, dass NetDiag Apps installieren darf, dann erneut tippen")
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
|
||||||
|
val apk = File(context.cacheDir, "NetDiag-update.apk")
|
||||||
|
downloadApk(url, apk)
|
||||||
|
|
||||||
|
val uri = FileProvider.getUriForFile(
|
||||||
|
context, "${context.packageName}.fileprovider", apk
|
||||||
|
)
|
||||||
|
val install = Intent(Intent.ACTION_VIEW)
|
||||||
|
.setDataAndType(uri, "application/vnd.android.package-archive")
|
||||||
|
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||||
|
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
|
context.startActivity(install)
|
||||||
|
resolve(call, JSObject().put("started", true))
|
||||||
|
} catch (e: Exception) {
|
||||||
|
call.reject("installUpdate: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** APK streamend in `target` laden und dabei `updateProgress`-Events senden. */
|
||||||
|
private fun downloadApk(url: String, target: File) {
|
||||||
|
val conn = (URL(url).openConnection() as HttpURLConnection).apply {
|
||||||
|
connectTimeout = 15_000
|
||||||
|
readTimeout = 120_000
|
||||||
|
instanceFollowRedirects = true
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
val code = conn.responseCode
|
||||||
|
if (code != 200) throw Exception("Download fehlgeschlagen (HTTP $code)")
|
||||||
|
val total = conn.contentLength.toLong() // -1 wenn unbekannt
|
||||||
|
var read = 0L
|
||||||
|
var lastPct = -1
|
||||||
|
conn.inputStream.use { input ->
|
||||||
|
FileOutputStream(target).use { out ->
|
||||||
|
val buf = ByteArray(64 * 1024)
|
||||||
|
while (true) {
|
||||||
|
val n = input.read(buf)
|
||||||
|
if (n < 0) break
|
||||||
|
out.write(buf, 0, n)
|
||||||
|
read += n
|
||||||
|
if (total > 0) {
|
||||||
|
val pct = (read * 100 / total).toInt()
|
||||||
|
if (pct != lastPct) {
|
||||||
|
lastPct = pct
|
||||||
|
notifyListeners(
|
||||||
|
"updateProgress", JSObject().put("percent", pct)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (target.length() < 1024) throw Exception("APK unvollständig empfangen")
|
||||||
|
} finally {
|
||||||
|
conn.disconnect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* --------------------------------------------------------------------- */
|
/* --------------------------------------------------------------------- */
|
||||||
/* Hilfsfunktionen */
|
/* Hilfsfunktionen */
|
||||||
/* --------------------------------------------------------------------- */
|
/* --------------------------------------------------------------------- */
|
||||||
|
|
|
||||||
|
|
@ -6,12 +6,15 @@
|
||||||
* (update.php): der Server prüft die Registry, die App spricht nur ihren
|
* (update.php): der Server prüft die Registry, die App spricht nur ihren
|
||||||
* eigenen, authentifizierten Endpunkt an.
|
* eigenen, authentifizierten Endpunkt an.
|
||||||
*
|
*
|
||||||
|
* Installiert wird direkt in der App: das native Plugin lädt die APK (mit
|
||||||
|
* Fortschritt) und öffnet den Android-Paketinstaller — kein Browser-Umweg.
|
||||||
|
*
|
||||||
* checkForUpdate() verschluckt Fehler NICHT — bei einem Problem wirft es mit
|
* checkForUpdate() verschluckt Fehler NICHT — bei einem Problem wirft es mit
|
||||||
* Klartext-Grund. So kann der Aufrufer „wirklich aktuell" von „Prüfung
|
* Klartext-Grund. So kann der Aufrufer „wirklich aktuell" von „Prüfung
|
||||||
* fehlgeschlagen" unterscheiden und eine echte Meldung zeigen.
|
* fehlgeschlagen" unterscheiden und eine echte Meldung zeigen.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Capacitor } from '@capacitor/core';
|
import { Capacitor, registerPlugin, type PluginListenerHandle } from '@capacitor/core';
|
||||||
import { checkAppUpdate, updateDownloadUrl } from './api';
|
import { checkAppUpdate, updateDownloadUrl } from './api';
|
||||||
|
|
||||||
/** Aktuelle Build-Version (von der CI injiziert, im Dev 'dev') */
|
/** Aktuelle Build-Version (von der CI injiziert, im Dev 'dev') */
|
||||||
|
|
@ -22,6 +25,39 @@ export interface UpdateInfo {
|
||||||
downloadUrl: string;
|
downloadUrl: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Fortschritts-Callback für den APK-Download (0–100 %) */
|
||||||
|
export type UpdateProgressCb = (percent: number) => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Natives Plugin (Kotlin-Klasse `NetDiagScanner`). `installUpdate` lädt die
|
||||||
|
* APK herunter und feuert den Paketinstaller; der Download-Fortschritt kommt
|
||||||
|
* als `updateProgress`-Event.
|
||||||
|
*/
|
||||||
|
interface NetDiagUpdaterPlugin {
|
||||||
|
installUpdate(opts: { url: string }): Promise<{ started: boolean }>;
|
||||||
|
addListener(
|
||||||
|
eventName: 'updateProgress',
|
||||||
|
cb: (data: { percent: number }) => void,
|
||||||
|
): Promise<PluginListenerHandle>;
|
||||||
|
}
|
||||||
|
const updaterPlugin = registerPlugin<NetDiagUpdaterPlugin>('NetDiagScanner');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Versionsvergleich für das Build-Format `YYYYMMDD-HHMM`. Verglichen wird
|
||||||
|
* numerisch (alle Ziffern aneinandergereiht) statt lexikografisch — robust
|
||||||
|
* gegen unterschiedliche Längen. Liefert false, sobald eine Seite keine
|
||||||
|
* Ziffern enthält (z. B. der Dev-Build `dev`).
|
||||||
|
*/
|
||||||
|
export function isNewer(remote: string, local: string): boolean {
|
||||||
|
const digits = (v: string) => Number(v.replace(/\D/g, ''));
|
||||||
|
const r = digits(remote);
|
||||||
|
const l = digits(local);
|
||||||
|
if (!Number.isSafeInteger(r) || !Number.isSafeInteger(l) || r === 0 || l === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return r > l;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Prüfen, ob eine neuere APK verfügbar ist.
|
* Prüfen, ob eine neuere APK verfügbar ist.
|
||||||
*
|
*
|
||||||
|
|
@ -37,16 +73,36 @@ export async function checkForUpdate(): Promise<UpdateInfo | null> {
|
||||||
if (!version) {
|
if (!version) {
|
||||||
throw new Error('Server lieferte keine Versionsinfo');
|
throw new Error('Server lieferte keine Versionsinfo');
|
||||||
}
|
}
|
||||||
if (version > APP_VERSION) {
|
if (isNewer(version, APP_VERSION)) {
|
||||||
return { version, downloadUrl: updateDownloadUrl() };
|
return { version, downloadUrl: updateDownloadUrl() };
|
||||||
}
|
}
|
||||||
return null; // aktuell — nachweislich geprüft
|
return null; // aktuell — nachweislich geprüft
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update-APK im System öffnen. Android lädt die Datei herunter; der Nutzer
|
* Update direkt in der App installieren: APK herunterladen (mit Fortschritt)
|
||||||
* bestätigt anschließend die Installation.
|
* und den Android-Paketinstaller öffnen. Im Browser-Dev gibt es keinen
|
||||||
|
* nativen Installer — dort wird die APK ersatzweise im Browser geöffnet.
|
||||||
|
*
|
||||||
|
* @throws Error mit Klartext-Grund, wenn Download oder Installation scheitern
|
||||||
|
* (z. B. fehlendes Recht „Apps installieren").
|
||||||
*/
|
*/
|
||||||
export function openUpdate(info: UpdateInfo): void {
|
export async function installUpdate(
|
||||||
|
info: UpdateInfo,
|
||||||
|
onProgress?: UpdateProgressCb,
|
||||||
|
): Promise<void> {
|
||||||
|
if (!Capacitor.isNativePlatform()) {
|
||||||
window.open(info.downloadUrl, '_system');
|
window.open(info.downloadUrl, '_system');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let handle: PluginListenerHandle | undefined;
|
||||||
|
if (onProgress) {
|
||||||
|
handle = await updaterPlugin.addListener('updateProgress', (d) => onProgress(d.percent));
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await updaterPlugin.installUpdate({ url: info.downloadUrl });
|
||||||
|
} finally {
|
||||||
|
await handle?.remove();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@
|
||||||
import { toast } from '$lib/toast.svelte';
|
import { toast } from '$lib/toast.svelte';
|
||||||
import { initDb } from '$lib/db';
|
import { initDb } from '$lib/db';
|
||||||
import { registerBackListener, removeBackListener } from '$lib/backButton.svelte';
|
import { registerBackListener, removeBackListener } from '$lib/backButton.svelte';
|
||||||
import { checkForUpdate, openUpdate, type UpdateInfo } from '$lib/updater';
|
import { checkForUpdate, installUpdate, type UpdateInfo } from '$lib/updater';
|
||||||
import { initDebugLog } from '$lib/debuglog.svelte';
|
import { initDebugLog } from '$lib/debuglog.svelte';
|
||||||
import Toast from '$lib/components/Toast.svelte';
|
import Toast from '$lib/components/Toast.svelte';
|
||||||
|
|
||||||
|
|
@ -16,9 +16,26 @@
|
||||||
|
|
||||||
let booted = $state(false);
|
let booted = $state(false);
|
||||||
let updateInfo = $state<UpdateInfo | null>(null);
|
let updateInfo = $state<UpdateInfo | null>(null);
|
||||||
|
let updateDismissed = $state(false);
|
||||||
|
let updateBusy = $state(false);
|
||||||
|
let updatePercent = $state(0);
|
||||||
|
|
||||||
const HOME = '/auftraege/';
|
const HOME = '/auftraege/';
|
||||||
|
|
||||||
|
// Update herunterladen und Installer öffnen — Fortschritt im Banner
|
||||||
|
async function runUpdate() {
|
||||||
|
if (!updateInfo || updateBusy) return;
|
||||||
|
updateBusy = true;
|
||||||
|
updatePercent = 0;
|
||||||
|
try {
|
||||||
|
await installUpdate(updateInfo, (p) => (updatePercent = p));
|
||||||
|
// Erfolg: Android übernimmt die Installation, Banner bleibt bei 100 %
|
||||||
|
} catch (e) {
|
||||||
|
toast.show(e instanceof Error ? e.message : 'Update fehlgeschlagen', 'error', 6000);
|
||||||
|
updateBusy = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
initDebugLog(); // zuerst — damit auch Startfehler erfasst werden
|
initDebugLog(); // zuerst — damit auch Startfehler erfasst werden
|
||||||
await auth.init();
|
await auth.init();
|
||||||
|
|
@ -70,13 +87,36 @@
|
||||||
<span>NetDiag startet …</span>
|
<span>NetDiag startet …</span>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
{#if updateInfo}
|
{#if updateInfo && !updateDismissed}
|
||||||
<button
|
<div class="bg-sky-700 text-sm text-white safe-top">
|
||||||
class="bg-sky-700 px-4 pb-2 text-sm text-white safe-top"
|
{#if updateBusy}
|
||||||
onclick={() => updateInfo && openUpdate(updateInfo)}
|
<div class="px-4 pb-2">
|
||||||
>
|
<div class="flex justify-between">
|
||||||
|
<span>Update wird geladen …</span>
|
||||||
|
<span class="font-mono">{updatePercent}%</span>
|
||||||
|
</div>
|
||||||
|
<div class="mt-1 h-1.5 overflow-hidden rounded bg-sky-900">
|
||||||
|
<div
|
||||||
|
class="h-full rounded bg-white transition-all"
|
||||||
|
style="width:{updatePercent}%"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="flex items-center">
|
||||||
|
<button class="flex-1 px-4 pb-2 text-left" onclick={runUpdate}>
|
||||||
Neue Version {updateInfo.version} verfügbar — tippen zum Aktualisieren
|
Neue Version {updateInfo.version} verfügbar — tippen zum Aktualisieren
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
class="px-4 pb-2 text-lg leading-none"
|
||||||
|
aria-label="Hinweis ausblenden"
|
||||||
|
onclick={() => (updateDismissed = true)}
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{@render children()}
|
{@render children()}
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
||||||
|
|
@ -5,9 +5,12 @@
|
||||||
import { getServerUrl } from '$lib/api';
|
import { getServerUrl } from '$lib/api';
|
||||||
import { sync } from '$lib/sync.svelte';
|
import { sync } from '$lib/sync.svelte';
|
||||||
import { toast } from '$lib/toast.svelte';
|
import { toast } from '$lib/toast.svelte';
|
||||||
import { APP_VERSION, checkForUpdate, openUpdate } from '$lib/updater';
|
import { APP_VERSION, checkForUpdate, installUpdate, type UpdateInfo } from '$lib/updater';
|
||||||
|
|
||||||
let checking = $state(false);
|
let checking = $state(false);
|
||||||
|
let available = $state<UpdateInfo | null>(null);
|
||||||
|
let installing = $state(false);
|
||||||
|
let percent = $state(0);
|
||||||
|
|
||||||
// Sync-Status in Klartext
|
// Sync-Status in Klartext
|
||||||
const syncLabel = $derived(
|
const syncLabel = $derived(
|
||||||
|
|
@ -25,9 +28,8 @@
|
||||||
async function checkUpdate() {
|
async function checkUpdate() {
|
||||||
checking = true;
|
checking = true;
|
||||||
try {
|
try {
|
||||||
const upd = await checkForUpdate();
|
available = await checkForUpdate();
|
||||||
if (upd) openUpdate(upd);
|
if (!available) toast.show('App ist aktuell', 'success');
|
||||||
else toast.show('App ist aktuell', 'success');
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Echte Fehlermeldung statt stillem „aktuell"
|
// Echte Fehlermeldung statt stillem „aktuell"
|
||||||
toast.show(
|
toast.show(
|
||||||
|
|
@ -42,6 +44,19 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function runInstall() {
|
||||||
|
if (!available || installing) return;
|
||||||
|
installing = true;
|
||||||
|
percent = 0;
|
||||||
|
try {
|
||||||
|
await installUpdate(available, (p) => (percent = p));
|
||||||
|
// Erfolg: Android übernimmt die Installation
|
||||||
|
} catch (e) {
|
||||||
|
toast.show(e instanceof Error ? e.message : 'Update fehlgeschlagen', 'error', 6000);
|
||||||
|
installing = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function logout() {
|
async function logout() {
|
||||||
await auth.logout();
|
await auth.logout();
|
||||||
sync.stop();
|
sync.stop();
|
||||||
|
|
@ -64,10 +79,34 @@
|
||||||
<button
|
<button
|
||||||
class="mt-3 w-full rounded bg-zinc-800 px-3 py-2 text-sm active:bg-zinc-700 disabled:opacity-50"
|
class="mt-3 w-full rounded bg-zinc-800 px-3 py-2 text-sm active:bg-zinc-700 disabled:opacity-50"
|
||||||
onclick={checkUpdate}
|
onclick={checkUpdate}
|
||||||
disabled={checking}
|
disabled={checking || installing}
|
||||||
>
|
>
|
||||||
{checking ? 'Prüfe …' : 'Auf Update prüfen'}
|
{checking ? 'Prüfe …' : 'Auf Update prüfen'}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
{#if available}
|
||||||
|
{#if installing}
|
||||||
|
<div class="mt-3">
|
||||||
|
<div class="flex justify-between text-xs text-zinc-400">
|
||||||
|
<span>Update wird geladen …</span>
|
||||||
|
<span class="font-mono">{percent}%</span>
|
||||||
|
</div>
|
||||||
|
<div class="mt-1 h-1.5 overflow-hidden rounded bg-zinc-800">
|
||||||
|
<div
|
||||||
|
class="h-full rounded bg-sky-500 transition-all"
|
||||||
|
style="width:{percent}%"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<button
|
||||||
|
class="mt-2 w-full rounded bg-sky-700 px-3 py-2 text-sm font-semibold text-white active:bg-sky-800"
|
||||||
|
onclick={runInstall}
|
||||||
|
>
|
||||||
|
Version {available.version} installieren
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- Synchronisierung -->
|
<!-- Synchronisierung -->
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue