hmgr 0.0.20 → 0.0.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/gui/main.js +124 -7
- package/dist/gui/renderer/index.html +15 -0
- package/dist/gui/renderer/renderer.js +29 -1
- package/dist/gui/selbst-update.js +105 -0
- package/package.json +1 -1
package/dist/gui/main.js
CHANGED
|
@@ -13,6 +13,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
13
13
|
import { bauen, umgebungLesen } from "../cli/main.js";
|
|
14
14
|
import { quelleAlsText, quelleAusText } from "../adapter/quelle-text.js";
|
|
15
15
|
import { paketVersion } from "../adapter/paket-version.js";
|
|
16
|
+
import { infoPlistPfad, MeldungsPuffer, neuereVersionAufPlatte } from "./selbst-update.js";
|
|
16
17
|
import { quelleName, quelleSchluessel } from "../kern/modell.js";
|
|
17
18
|
import { kannProfile } from "../ports/harness.js";
|
|
18
19
|
import { iconFuerQuelle } from "./icons.js";
|
|
@@ -27,6 +28,8 @@ const smoketest = process.env.HMGR_GUI_SMOKETEST === '1';
|
|
|
27
28
|
// E84 Teil 1: Pfad eines im Fixture noch unbekannten Verzeichnisses, das der Smoke-Lauf per Dialog
|
|
28
29
|
// hinzufügt, um den Sprung zur neuen Karte zu belegen. Nur zur Konstruktionszeit des DOM-Auszug-
|
|
29
30
|
// Skripts gelesen (Node-Seite), nicht zur Laufzeit im Renderer.
|
|
31
|
+
/** E90-Nachtrag: Smoke-Lauf, in dem die Platten-Erkennung schon vor dem Laden der Seite feuert. */
|
|
32
|
+
const smoketestFrueh = process.env.HMGR_GUI_SMOKE_FRUEH === '1';
|
|
30
33
|
const smoketestNeuesVerzeichnis = process.env.HMGR_SMOKETEST_NEUES_VERZEICHNIS ?? '';
|
|
31
34
|
/**
|
|
32
35
|
* Nur eine hmgr-Instanz je Nutzer (E67). Grund: zwei laufende Fenster schrieben nacheinander auf
|
|
@@ -331,10 +334,18 @@ function registriereIpc() {
|
|
|
331
334
|
// Neustart-Knopf an der GUI-Update-Zeile (E80): installiert das geladene Paket sofort, statt auf
|
|
332
335
|
// das nächste Beenden zu warten. Im Smoke-Test ein No-op — dort läuft `updatePruefen` ohnehin nie,
|
|
333
336
|
// der Knopf bleibt verborgen, aber ein Absturz durch ein fehlendes Update darf trotzdem nie sein.
|
|
337
|
+
// E90: mit einem geladenen Windows-Paket `quitAndInstall`, sonst (macOS: das Bundle liegt schon
|
|
338
|
+
// ersetzt auf der Platte) ein Neustart des Prozesses. Entscheidend ist das Merker-Flag, nicht die
|
|
339
|
+
// Plattform allein.
|
|
334
340
|
ipcMain.on('hmgr:neuStarten', () => {
|
|
335
341
|
if (smoketest)
|
|
336
342
|
return;
|
|
337
|
-
|
|
343
|
+
if (geladenesPaketVorhanden) {
|
|
344
|
+
autoUpdater.quitAndInstall();
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
app.relaunch();
|
|
348
|
+
app.exit(0);
|
|
338
349
|
});
|
|
339
350
|
ipcMain.handle('hmgr:arbeitsverzeichnisHinzufuegen', async (_event, pfad) => auffangen(async () => anwendungsfaelle.arbeitsverzeichnisHinzufuegen(pfad)));
|
|
340
351
|
ipcMain.handle('hmgr:arbeitsverzeichnisVergessen', async (_event, pfad) => auffangen(async () => anwendungsfaelle.arbeitsverzeichnisVergessen(pfad)));
|
|
@@ -498,8 +509,12 @@ function fensterOeffnen() {
|
|
|
498
509
|
sandbox: true,
|
|
499
510
|
},
|
|
500
511
|
});
|
|
512
|
+
meldungsPuffer.bereitAbmelden();
|
|
513
|
+
hauptfenster.webContents.on('did-start-loading', () => meldungsPuffer.bereitAbmelden());
|
|
514
|
+
hauptfenster.webContents.on('did-finish-load', () => meldungsPuffer.bereitSetzen());
|
|
501
515
|
hauptfenster.on('closed', () => {
|
|
502
516
|
hauptfenster = null;
|
|
517
|
+
zuletztGemeldeteGuiVersion = null;
|
|
503
518
|
});
|
|
504
519
|
// Der <title> der index.html setzt den Fenstertitel beim Laden sonst zurück und überschriebe die
|
|
505
520
|
// Option oben (E72). Der DOM-Titel bleibt dabei, wie er ist, nur das Fenster folgt ihm nicht mehr.
|
|
@@ -509,7 +524,21 @@ function fensterOeffnen() {
|
|
|
509
524
|
console.log(`[renderer] ${event.message}`);
|
|
510
525
|
});
|
|
511
526
|
// Nur für den Smoke-Test (scripts/gui-smoke-test.ts): ein DOM-Auszug im Log, danach Ende.
|
|
512
|
-
if (smoketest) {
|
|
527
|
+
if (smoketest && smoketestFrueh) {
|
|
528
|
+
// E90-Nachtrag: die Platten-Erkennung feuert schon vor dem Laden der Seite (direkt nach der
|
|
529
|
+
// Fenstererzeugung); die Meldung muss trotzdem ankommen. Kein großer DOM-Auszug, nur das Modal.
|
|
530
|
+
const fenster = hauptfenster;
|
|
531
|
+
plattenVersionPruefen(true);
|
|
532
|
+
fenster.webContents.once('did-finish-load', () => {
|
|
533
|
+
setTimeout(() => {
|
|
534
|
+
void fenster.webContents
|
|
535
|
+
.executeJavaScript(NEUSTART_AUSZUG_SKRIPT)
|
|
536
|
+
.then((auszug) => console.log(`[smoke] Neustart-Modal früh: ${String(auszug)}`))
|
|
537
|
+
.finally(() => app.quit());
|
|
538
|
+
}, 1500);
|
|
539
|
+
});
|
|
540
|
+
}
|
|
541
|
+
if (smoketest && !smoketestFrueh) {
|
|
513
542
|
const fenster = hauptfenster;
|
|
514
543
|
fenster.webContents.once('did-finish-load', () => {
|
|
515
544
|
setTimeout(() => {
|
|
@@ -1202,6 +1231,20 @@ function fensterOeffnen() {
|
|
|
1202
1231
|
// E72: der Fenstertitel steht nicht im DOM (dort steht weiter der <title> der
|
|
1203
1232
|
// index.html), er lässt sich nur hier im Main-Prozess ablesen.
|
|
1204
1233
|
.then(() => console.log(`[smoke] Fenster: ${JSON.stringify({ fenstertitel: fenster.getTitle() })}`))
|
|
1234
|
+
// E90: erst nach dem DOM-Auszug die Platten-Erkennung auslösen (Naht über
|
|
1235
|
+
// HMGR_GUI_SMOKE_PLATTENVERSION), damit die Leer-Prüfungen der Update-Zeile darin gelten.
|
|
1236
|
+
// Danach ein zweiter, kleiner Auszug nur zum Neustart-Modal.
|
|
1237
|
+
.then(async () => {
|
|
1238
|
+
if (process.env.HMGR_GUI_SMOKE_PLATTENVERSION === undefined)
|
|
1239
|
+
return;
|
|
1240
|
+
// Der Rest-Dialog (d-erfolg) öffnet über einen 90-ms-Timer nach der letzten Aktion des
|
|
1241
|
+
// Auszugs; ohne dieses Abwarten löste die Erkennung mal vor, mal nach ihm aus.
|
|
1242
|
+
await fenster.webContents.executeJavaScript(`new Promise(function (r) { var n = 0; (function w() { if (document.querySelector('dialog[open]') || ++n > 30) r(); else setTimeout(w, 50); })(); })`);
|
|
1243
|
+
plattenVersionPruefen(true);
|
|
1244
|
+
await new Promise((r) => setTimeout(r, 800));
|
|
1245
|
+
const auszug = await fenster.webContents.executeJavaScript(NEUSTART_AUSZUG_SKRIPT);
|
|
1246
|
+
console.log(`[smoke] Neustart-Modal: ${String(auszug)}`);
|
|
1247
|
+
})
|
|
1205
1248
|
.finally(() => app.quit());
|
|
1206
1249
|
}, 3000);
|
|
1207
1250
|
});
|
|
@@ -1215,7 +1258,10 @@ function fensterOeffnen() {
|
|
|
1215
1258
|
fenster.webContents.once('did-finish-load', () => {
|
|
1216
1259
|
fenster.webContents.send('hmgr:aktualisierenAnstossen', 'start');
|
|
1217
1260
|
});
|
|
1218
|
-
const intervall = setInterval(() =>
|
|
1261
|
+
const intervall = setInterval(() => {
|
|
1262
|
+
fenster.webContents.send('hmgr:aktualisierenAnstossen', 'intervall');
|
|
1263
|
+
plattenVersionPruefen();
|
|
1264
|
+
}, SECHS_STUNDEN_MS);
|
|
1219
1265
|
fenster.on('closed', () => clearInterval(intervall));
|
|
1220
1266
|
}
|
|
1221
1267
|
// Die GUI erfährt von Änderungen außerhalb ihrer selbst (E62): Fokus-Reload und Datei-Watcher.
|
|
@@ -1224,7 +1270,10 @@ function fensterOeffnen() {
|
|
|
1224
1270
|
// Aktualisieren (E44): ein Event dürfte sonst in den DOM-Auszug pfuschen.
|
|
1225
1271
|
if (!smoketest) {
|
|
1226
1272
|
const fenster = hauptfenster;
|
|
1227
|
-
fenster.on('focus', () =>
|
|
1273
|
+
fenster.on('focus', () => {
|
|
1274
|
+
neuLadenAnstossen();
|
|
1275
|
+
plattenVersionPruefen();
|
|
1276
|
+
});
|
|
1228
1277
|
void beobachtetePfade().then((pfade) => {
|
|
1229
1278
|
if (fenster.isDestroyed())
|
|
1230
1279
|
return; // Fenster schon zu, während die Pfade ermittelt wurden
|
|
@@ -1281,7 +1330,8 @@ function updatePruefen() {
|
|
|
1281
1330
|
.catch((fehler) => {
|
|
1282
1331
|
console.log(`[verlauf] ${fehler instanceof Error ? fehler.message : String(fehler)}`);
|
|
1283
1332
|
});
|
|
1284
|
-
|
|
1333
|
+
geladenesPaketVorhanden = true;
|
|
1334
|
+
selbstUpdateMelden({ art: 'gui', version: guiVersion, neuStartNoetig: 'geladen' });
|
|
1285
1335
|
});
|
|
1286
1336
|
}
|
|
1287
1337
|
void autoUpdater.checkForUpdatesAndNotify().catch((fehler) => {
|
|
@@ -1292,11 +1342,78 @@ function updatePruefen() {
|
|
|
1292
1342
|
console.log(`[updater] ${fehler instanceof Error ? fehler.message : String(fehler)}`);
|
|
1293
1343
|
}
|
|
1294
1344
|
}
|
|
1295
|
-
/**
|
|
1296
|
-
|
|
1345
|
+
/** Das Skript für den Modal-Auszug im Smoke-Test (Neustart-Modal, Sidebar-Zeile, „Später"). */
|
|
1346
|
+
const NEUSTART_AUSZUG_SKRIPT = `(async () => {
|
|
1347
|
+
var dlg = document.getElementById('d-neustart');
|
|
1348
|
+
// Ein anderer Dialog steht noch offen (Rest des ersten Auszugs): das Modal wartet
|
|
1349
|
+
// solange, und öffnet erst, wenn der andere zu ist.
|
|
1350
|
+
var anderePlatzhalter = Array.from(document.querySelectorAll('dialog[open]')).filter(function (d) { return d !== dlg; });
|
|
1351
|
+
var wartetSolangeAndererOffen = anderePlatzhalter.length > 0 && dlg?.open !== true;
|
|
1352
|
+
anderePlatzhalter.forEach(function (d) { d.close(); });
|
|
1353
|
+
await new Promise(function (r) { setTimeout(r, 300); });
|
|
1354
|
+
var offen = dlg?.open === true;
|
|
1355
|
+
var titel = document.getElementById('dn-titel')?.textContent ?? null;
|
|
1356
|
+
var zeile = document.getElementById('selbstupdate-gui');
|
|
1357
|
+
var zeileText = zeile && !zeile.hidden ? zeile.textContent : null;
|
|
1358
|
+
var neustartKnopfSichtbar = document.getElementById('selbstupdate-neustart')?.hidden === false;
|
|
1359
|
+
var knopfTexte = Array.from(dlg?.querySelectorAll('.dfuss button') ?? []).map(function (b) { return b.textContent; }).join('|');
|
|
1360
|
+
dlg?.querySelector('button[value="spaeter"]')?.click();
|
|
1361
|
+
var nachSpaeterOffen = dlg?.open === true;
|
|
1362
|
+
return JSON.stringify({ wartetSolangeAndererOffen, offen, titel, zeileText, neustartKnopfSichtbar, knopfTexte, nachSpaeterOffen });
|
|
1363
|
+
})()`;
|
|
1364
|
+
/**
|
|
1365
|
+
* Ein Hinweis für die Sidebar (E77). Ist das Fenster schon zu, verfällt er stillschweigend. Solange
|
|
1366
|
+
* die Seite nicht geladen ist, hat der Renderer seinen Listener noch nicht (E90-Nachtrag): der
|
|
1367
|
+
* Puffer hält je Art die letzte Nachricht und sendet sie nach `did-finish-load` — das Skript am
|
|
1368
|
+
* Ende des Bodys registriert `onSelbstUpdate` synchron, `did-finish-load` kommt erst danach.
|
|
1369
|
+
*/
|
|
1370
|
+
const meldungsPuffer = new MeldungsPuffer((nachricht) => {
|
|
1297
1371
|
if (hauptfenster === null || hauptfenster.isDestroyed())
|
|
1298
1372
|
return;
|
|
1299
1373
|
hauptfenster.webContents.send('hmgr:selbstUpdate', nachricht);
|
|
1374
|
+
});
|
|
1375
|
+
function selbstUpdateMelden(nachricht) {
|
|
1376
|
+
if (hauptfenster === null || hauptfenster.isDestroyed())
|
|
1377
|
+
return;
|
|
1378
|
+
meldungsPuffer.melden(nachricht);
|
|
1379
|
+
}
|
|
1380
|
+
/** Windows: ein Paket ist geladen und wartet auf `quitAndInstall` (E90, Merker für `hmgr:neuStarten`). */
|
|
1381
|
+
let geladenesPaketVorhanden = false;
|
|
1382
|
+
/** Die beim Start festgehaltene Version des laufenden Prozesses. */
|
|
1383
|
+
const laufendeVersion = paketVersion();
|
|
1384
|
+
const PLATTEN_PRUEFUNG_MIN_ABSTAND_MS = 30_000;
|
|
1385
|
+
let letztePlattenPruefung = 0;
|
|
1386
|
+
let zuletztGemeldeteGuiVersion = null;
|
|
1387
|
+
/**
|
|
1388
|
+
* E90, macOS-Weg: das App-Bundle wird im Hintergrund vom Deploy-Skript (ditto/rsync) ersetzt, der
|
|
1389
|
+
* laufende Prozess merkt das nicht. Verglichen wird die Version in `Contents/Info.plist` mit der
|
|
1390
|
+
* beim Start festgehaltenen. Die Info.plist statt der package.json in `app.asar`, weil Electron den
|
|
1391
|
+
* asar-Header je Pfad zwischenspeichert und ein ersetztes Archiv nicht neu liest. Nur gepackt und
|
|
1392
|
+
* nur auf macOS (nur dort gibt es ein Bundle mit Info.plist); gedrosselt auf einen Lauf je 30 s.
|
|
1393
|
+
* Im Smoke-Test liefert `HMGR_GUI_SMOKE_PLATTENVERSION` eine künstliche Platten-Version, sonst
|
|
1394
|
+
* läuft dort nie eine echte Prüfung.
|
|
1395
|
+
*/
|
|
1396
|
+
function plattenVersionPruefen(erzwingen = false) {
|
|
1397
|
+
let platte;
|
|
1398
|
+
if (smoketest) {
|
|
1399
|
+
if (!erzwingen)
|
|
1400
|
+
return;
|
|
1401
|
+
const kuenstlich = process.env.HMGR_GUI_SMOKE_PLATTENVERSION;
|
|
1402
|
+
platte = kuenstlich !== undefined && kuenstlich !== '' ? kuenstlich : null;
|
|
1403
|
+
}
|
|
1404
|
+
else {
|
|
1405
|
+
if (!app.isPackaged || process.platform !== 'darwin')
|
|
1406
|
+
return;
|
|
1407
|
+
const jetzt = Date.now();
|
|
1408
|
+
if (jetzt - letztePlattenPruefung < PLATTEN_PRUEFUNG_MIN_ABSTAND_MS)
|
|
1409
|
+
return;
|
|
1410
|
+
letztePlattenPruefung = jetzt;
|
|
1411
|
+
platte = neuereVersionAufPlatte(infoPlistPfad(process.execPath), laufendeVersion);
|
|
1412
|
+
}
|
|
1413
|
+
if (platte === null || platte === zuletztGemeldeteGuiVersion)
|
|
1414
|
+
return;
|
|
1415
|
+
zuletztGemeldeteGuiVersion = platte;
|
|
1416
|
+
selbstUpdateMelden({ art: 'gui', version: platte, neuStartNoetig: 'installiert' });
|
|
1300
1417
|
}
|
|
1301
1418
|
/**
|
|
1302
1419
|
* Die daneben installierte hmgr-CLI gegen npm (E77, F83=O1). Läuft auf allen Plattformen — anders
|
|
@@ -255,6 +255,21 @@
|
|
|
255
255
|
</form>
|
|
256
256
|
</dialog>
|
|
257
257
|
|
|
258
|
+
<!-- Neustart-Angebot (E90): ein Update für hmgr selbst liegt bereit. -->
|
|
259
|
+
<dialog id="d-neustart">
|
|
260
|
+
<form method="dialog">
|
|
261
|
+
<div class="dkopf">
|
|
262
|
+
<span class="etikett">Update</span>
|
|
263
|
+
<h3 id="dn-titel"></h3>
|
|
264
|
+
<p>hmgr startet dafür neu. Offene Dialoge gehen dabei verloren.</p>
|
|
265
|
+
</div>
|
|
266
|
+
<div class="dfuss">
|
|
267
|
+
<button class="knopf leise" value="spaeter">Später</button>
|
|
268
|
+
<button class="knopf signal" value="jetzt">Jetzt neu starten</button>
|
|
269
|
+
</div>
|
|
270
|
+
</form>
|
|
271
|
+
</dialog>
|
|
272
|
+
|
|
258
273
|
<dialog id="d-erfolg">
|
|
259
274
|
<form method="dialog">
|
|
260
275
|
<div class="dkopf">
|
|
@@ -2668,11 +2668,39 @@
|
|
|
2668
2668
|
die CLI installiert die GUI nicht selbst (F82). Die CLI-Zeile ist seit E82 eine Warnung, keine
|
|
2669
2669
|
bloße Ansage: eine alte CLI (vor 0.0.7, kennt --version nicht) schreibt dieselben Dateien
|
|
2670
2670
|
(state.json, verlauf.json) wie die neue GUI und kennt Bundles, Verlauf und WSL-Heime nicht. */
|
|
2671
|
+
/* Neustart-Modal (E90): je Version einmal. Nach „Später" kommt es für dieselbe Version nicht
|
|
2672
|
+
wieder (nur im Speicher, ein App-Neustart setzt es zurück). Steht gerade ein anderer Dialog
|
|
2673
|
+
offen, wartet es, bis der zu ist. */
|
|
2674
|
+
var neustartAngebotVersion = null;
|
|
2675
|
+
var neustartAbgelehnt = null;
|
|
2676
|
+
function neustartAnbieten(version) {
|
|
2677
|
+
if (version === neustartAbgelehnt) return;
|
|
2678
|
+
neustartAngebotVersion = version;
|
|
2679
|
+
neustartAngebotOeffnen();
|
|
2680
|
+
}
|
|
2681
|
+
function neustartAngebotOeffnen() {
|
|
2682
|
+
if (neustartAngebotVersion === null || neustartAngebotVersion === neustartAbgelehnt) return;
|
|
2683
|
+
if (document.querySelector('dialog[open]')) return;
|
|
2684
|
+
$('dn-titel').textContent = 'hmgr ' + neustartAngebotVersion + ' ist bereit';
|
|
2685
|
+
zeige($('d-neustart'));
|
|
2686
|
+
}
|
|
2687
|
+
$('d-neustart').addEventListener('close', function () {
|
|
2688
|
+
if ($('d-neustart').returnValue === 'jetzt') window.hmgr.neuStarten();
|
|
2689
|
+
else neustartAbgelehnt = neustartAngebotVersion;
|
|
2690
|
+
});
|
|
2691
|
+
// `close` bubbelt nicht, der Capture-Listener am Dokument sieht aber jeden geschlossenen Dialog.
|
|
2692
|
+
document.addEventListener('close', function (ev) {
|
|
2693
|
+
if (ev.target !== $('d-neustart')) setTimeout(neustartAngebotOeffnen, 0);
|
|
2694
|
+
}, true);
|
|
2695
|
+
|
|
2671
2696
|
function selbstUpdateZeigen(nachricht) {
|
|
2672
2697
|
if (nachricht.art === 'gui') {
|
|
2673
|
-
$('selbstupdate-gui').textContent =
|
|
2698
|
+
$('selbstupdate-gui').textContent = nachricht.neuStartNoetig === 'installiert'
|
|
2699
|
+
? 'hmgr ' + nachricht.version + ' ist installiert — Neustart lädt sie'
|
|
2700
|
+
: 'hmgr ' + nachricht.version + ' bereit — wird beim Beenden installiert';
|
|
2674
2701
|
$('selbstupdate-gui').hidden = false;
|
|
2675
2702
|
$('selbstupdate-neustart').hidden = false;
|
|
2703
|
+
neustartAnbieten(nachricht.version);
|
|
2676
2704
|
} else if (nachricht.art === 'cli') {
|
|
2677
2705
|
var p = $('selbstupdate-cli');
|
|
2678
2706
|
p.textContent = '';
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// Erkennung „das App-Bundle auf der Platte ist neuer als der laufende Prozess" (E90). Reine
|
|
2
|
+
// Funktionen ohne Electron-Import, damit sie ohne App testbar sind; das Verdrahten liegt in main.ts.
|
|
3
|
+
import { readFileSync } from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
function zerlegen(version) {
|
|
6
|
+
const treffer = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/.exec(version.trim());
|
|
7
|
+
if (treffer === null)
|
|
8
|
+
return null;
|
|
9
|
+
return {
|
|
10
|
+
kern: [Number(treffer[1]), Number(treffer[2]), Number(treffer[3])],
|
|
11
|
+
vorab: treffer[4] === undefined ? [] : treffer[4].split('.'),
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
/** Semver-Rangfolge: negativ, 0 oder positiv; Vorabversion ist kleiner als ihre Release. */
|
|
15
|
+
function vergleichen(a, b) {
|
|
16
|
+
for (let i = 0; i < 3; i++) {
|
|
17
|
+
const unterschied = a.kern[i] - b.kern[i];
|
|
18
|
+
if (unterschied !== 0)
|
|
19
|
+
return unterschied;
|
|
20
|
+
}
|
|
21
|
+
if (a.vorab.length === 0 && b.vorab.length === 0)
|
|
22
|
+
return 0;
|
|
23
|
+
if (a.vorab.length === 0)
|
|
24
|
+
return 1;
|
|
25
|
+
if (b.vorab.length === 0)
|
|
26
|
+
return -1;
|
|
27
|
+
for (let i = 0; i < Math.max(a.vorab.length, b.vorab.length); i++) {
|
|
28
|
+
const x = a.vorab[i];
|
|
29
|
+
const y = b.vorab[i];
|
|
30
|
+
if (x === undefined)
|
|
31
|
+
return -1;
|
|
32
|
+
if (y === undefined)
|
|
33
|
+
return 1;
|
|
34
|
+
const xZahl = /^\d+$/.test(x);
|
|
35
|
+
const yZahl = /^\d+$/.test(y);
|
|
36
|
+
if (xZahl && yZahl) {
|
|
37
|
+
if (Number(x) !== Number(y))
|
|
38
|
+
return Number(x) - Number(y);
|
|
39
|
+
}
|
|
40
|
+
else if (xZahl !== yZahl) {
|
|
41
|
+
return xZahl ? -1 : 1;
|
|
42
|
+
}
|
|
43
|
+
else if (x !== y) {
|
|
44
|
+
return x < y ? -1 : 1;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return 0;
|
|
48
|
+
}
|
|
49
|
+
/** `true`, wenn `platte` eine echt größere Version ist als `laufend`. Unlesbares gilt als „nein". */
|
|
50
|
+
export function istNeuer(platte, laufend) {
|
|
51
|
+
const p = zerlegen(platte);
|
|
52
|
+
const l = zerlegen(laufend);
|
|
53
|
+
return p !== null && l !== null && vergleichen(p, l) > 0;
|
|
54
|
+
}
|
|
55
|
+
/** `…/hmgr.app/Contents/MacOS/hmgr` → `…/hmgr.app/Contents/Info.plist`. */
|
|
56
|
+
export function infoPlistPfad(execPath) {
|
|
57
|
+
return path.join(path.dirname(execPath), '..', 'Info.plist');
|
|
58
|
+
}
|
|
59
|
+
/** Die CFBundleShortVersionString aus einer XML-Info.plist; `null` bei allem Unlesbaren. */
|
|
60
|
+
export function plistVersion(plistPfad) {
|
|
61
|
+
try {
|
|
62
|
+
const text = readFileSync(plistPfad, 'utf8');
|
|
63
|
+
const treffer = /<key>CFBundleShortVersionString<\/key>\s*<string>([^<]*)<\/string>/.exec(text);
|
|
64
|
+
return treffer?.[1]?.trim() || null;
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/** Die Platten-Version, wenn sie neuer ist als die laufende, sonst `null`. Wirft nie. */
|
|
71
|
+
export function neuereVersionAufPlatte(plistPfad, laufend) {
|
|
72
|
+
const platte = plistVersion(plistPfad);
|
|
73
|
+
return platte !== null && istNeuer(platte, laufend) ? platte : null;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Puffer für Meldungen an den Renderer (E90-Nachtrag). Solange die Seite nicht geladen ist, hat der
|
|
77
|
+
* Renderer seinen Listener noch nicht registriert, ein `webContents.send` ginge ins Leere. Bis
|
|
78
|
+
* `bereitSetzen()` wird deshalb je `art` nur die letzte Nachricht behalten und dann einmal
|
|
79
|
+
* gesendet; danach geht alles sofort durch. `bereitAbmelden()` (neues Laden der Seite) schaltet
|
|
80
|
+
* wieder auf Puffern. Ohne Electron-Import, damit ohne App testbar.
|
|
81
|
+
*/
|
|
82
|
+
export class MeldungsPuffer {
|
|
83
|
+
bereit = false;
|
|
84
|
+
wartend = new Map();
|
|
85
|
+
senden;
|
|
86
|
+
constructor(senden) {
|
|
87
|
+
this.senden = senden;
|
|
88
|
+
}
|
|
89
|
+
melden(nachricht) {
|
|
90
|
+
if (this.bereit)
|
|
91
|
+
this.senden(nachricht);
|
|
92
|
+
else
|
|
93
|
+
this.wartend.set(nachricht.art, nachricht);
|
|
94
|
+
}
|
|
95
|
+
bereitSetzen() {
|
|
96
|
+
this.bereit = true;
|
|
97
|
+
const gepuffert = [...this.wartend.values()];
|
|
98
|
+
this.wartend.clear();
|
|
99
|
+
for (const nachricht of gepuffert)
|
|
100
|
+
this.senden(nachricht);
|
|
101
|
+
}
|
|
102
|
+
bereitAbmelden() {
|
|
103
|
+
this.bereit = false;
|
|
104
|
+
}
|
|
105
|
+
}
|