Ereignisse, Fensterkontext und Startargumente
app.context enthält alles, was der Desktop dem Fenster beim Start mitgeteilt hat: den Benutzer, die Sprache, die Darstellung, die Größenklasse, die Startargumente, die Berechtigungen und den Zustand von Backend und Speicher. Das SDK hält ihn aktuell, und app.on(event, handler) sagt der App, wenn sich etwas geändert hat. Mit app.answer antwortet sie auf die zwei Fragen, die der Desktop stellt, bevor ein Fenster verschwindet. Dieser Leitfaden zeigt, wie ein Fenster startet, reagiert und seinen Zustand speichert.
Beispiel
Abschnitt betitelt „Beispiel“// A viewer that follows its window: launch arguments, notification clicks, visibility, the connection, saved window// state, unsaved changes and updates.import { connect, type LaunchArgs, type PithyxApp } from '@pithyx/sdk';
interface WindowState { view: 'inbox' | 'archive'; scrollTop: number;}
const app: PithyxApp = await connect();const state: WindowState = { view: 'inbox', scrollTop: 0, ...(await restoreState(app)) };let dirty = false;let poll: ReturnType<typeof setInterval> | undefined;
/** What this window saved before a reload, or on another device before "Get desktop from ...". */async function restoreState(app: PithyxApp): Promise<Partial<WindowState>> { const saved = await app.session.restore(); return typeof saved === 'object' && saved !== null ? saved : {};}
function isView(value: unknown): value is WindowState['view'] { return value === 'inbox' || value === 'archive';}
/** At start from the context, later from `launch` when a `single` app is launched again. */function handleLaunch(launch: LaunchArgs) { if (launch.notification !== undefined) { // The user clicked a notification while no window of the app was open. openFromNotification(launch.notification.data, launch.notification.action); } else if (isView(launch.args)) { state.view = launch.args; } render();}
function openFromNotification(data: unknown, action: string | null) { app.log.info('Opened from a notification', { action }); if (typeof data === 'object' && data !== null && 'view' in data && isView(data.view)) { state.view = data.view; }}
function startPolling() { poll ??= setInterval(() => void refresh(), 30_000);}
function stopPolling() { clearInterval(poll); poll = undefined;}
async function refresh() { if (app.context.online) { await app.window.setTitle(state.view === 'inbox' ? 'Inbox' : 'Archive'); }}
async function markDirty(value: boolean) { dirty = value; await app.window.setDirty(value);}
async function save() { await app.session.save(state); await markDirty(false);}
function render() { document.body.dataset.view = state.view; document.body.dataset.size = app.context.sizeClass; document.scrollingElement?.scrollTo(0, state.scrollTop);}
handleLaunch(app.context.launch);app.on('launch', handleLaunch);app.on('notificationAction', ({ data, action }) => { openFromNotification(data, action); render();});
// Minimizing does not hide the frame for the browser: visibilityChange is the signal to pause work nobody sees.app.on('visibilityChange', ({ visible }) => { if (visible) { startPolling(); } else { stopPolling(); }});app.on('connectionChange', ({ online }) => { if (online) { void refresh(); }});app.on('sizeClassChange', render);
// Asked only while setDirty(true), because the app calls setDirty; the desktop waits 5 seconds.app.answer('beforeClose', () => ({ allow: !dirty }));// Sign-out, shutdown, restart or an update of Pithyx: cannot be refused, 2 seconds to save.app.answer('beforeUnload', async () => { await save(); return {};});app.on('appUpdated', ({ version }) => { app.log.info('New version installed', { version }); if (!dirty) { location.reload(); }});
addEventListener('scroll', () => { state.scrollTop = document.scrollingElement?.scrollTop ?? 0;});await markDirty(false);if (app.context.visible) { startPolling();}render();Bis deine App verbunden ist, zeigt ihr Fenster einen Platzhalter, den der Desktop zeichnet: dein Icon und deinen Namen und nach 300 ms einen laufenden Balken. Dafür schreibst du keinen Code. Während dein Backend startet (backend.status ist starting, etwa beim ersten Öffnen, nachdem es sich im Leerlauf beendet hat), sagt eine Zeile das: “window.loading.text aus dem Manifest. Ein Backend, das nicht starten kann, sagt stattdessen warum (backend.reason): Es wartet darauf, dass ein Administrator einen Pflichtordner zuordnet, oder sein Pool ist gesperrt oder fehlt.
Wenn deine App zuerst Daten lädt und ein leeres Fenster zeigen würde, setze window.loading.untilReady auf true und rufe app.window.ready() auf, sobald es etwas zu sehen gibt. Der Desktop entfernt den Platzhalter in jedem Fall nach 30 Sekunden, auch wenn ready nie kommt. Ohne untilReady tut ready() nichts. pithyx dev zeigt denselben Platzhalter.
"window": { "instances": "single", "defaultSize": { "width": 800, "height": 600 }, "minSize": { "width": 360, "height": 400 }, "loading": { "text": { "en": "Opening your lists ...", "de": "Deine Listen werden geöffnet ..." }, "untilReady": true }}Der Kontext
Abschnitt betitelt „Der Kontext“app.context zu lesen braucht keine Berechtigung und keinen Aufruf. Das SDK aktualisiert diese Felder, bevor es deine Handler aufruft: theme (themeChange), language, locale, timeZone (localeChange), sizeClass (sizeClassChange), permissions (permissionsChange), visible (visibilityChange), online (connectionChange), logLevel (logLevelChange), backend.status und backend.reason (backendStatus) und storage (storageChange). Lies das Feld, statt eine eigene Kopie zu führen.
instanceist die ID dieses Fensters, eindeutig pro Desktop-Tab.userhatid,name,displayName,groups,isAdminundtwoFactor. Das ist zur Anzeige da; ein Backend nutzt stattdessen die Header des Proxys.sizeClassistcompactunter 600 px,mediumbis 1023 px und darüberexpanded.capabilitieslistet die Methoden, die der Desktop bedient;app.supports(method)liest sie.
Startargumente
Abschnitt betitelt „Startargumente“app.context.launch sagt, warum das Fenster aufging: { args, file?, notification? }. Ein Fenster, das der Benutzer vom Desktop oder aus dem Store öffnet, hat { args: null }. Ein Klick auf eine Benachrichtigung der App, während kein Fenster der App offen ist, startet sie mit launch.notification (notification, action, data). Bei einer App mit "instances": "single" im Manifest holt ein erneuter Start das Fenster des Benutzers auf diesem Gerät nach vorn und sendet die neuen Argumente als Ereignis launch.
launch.file und das Ereignis openFile gehören zu Datei-Handlern. Wählt der Benutzer in der Dateien-App unter “Öffnen mit” deine App für eine Datei, deren Endung ein Eintrag fileHandlers deines Manifests nennt, bekommt ein neues Fenster launch.file, und ein offenes Fenster einer single-App bekommt openFile mit der Datei. Beide sind Handles, die du mit files.read liest (und mit files.write schreibst, wenn der Benutzer darf); eine App ohne files.read bekommt keine Datei. Öffne die Datei in deinem openFile-Handler, nicht nur beim Start.
Benachrichtigungen
Abschnitt betitelt „Benachrichtigungen“Ein Klick auf eine Benachrichtigung oder einen ihrer Knöpfe, während ein Fenster der App offen ist, holt dieses Fenster nach vorn und sendet notificationAction mit der ID der Benachrichtigung, der id des Knopfs (oder null für die Benachrichtigung selbst) und den data, die du notify mitgegeben hast. Das Fenster, das die Benachrichtigung gesendet hat, bekommt es, wenn es noch verbunden ist, sonst das oberste Fenster der App.
Der Desktop fügt jeder Benachrichtigung einer App einen eigenen Knopf Öffnen neben deinen actions hinzu. Nenne keine Aktion “Öffnen”; benenne die Knöpfe nach dem, was sie tun (“Antworten”, “Liste zeigen”).
Ereignisse
Abschnitt betitelt „Ereignisse“| Ereignis | Wann |
|---|---|
focus, blur |
das Fenster bekam oder verlor den Fokus |
minimize, restore |
das Fenster wurde minimiert oder wieder gezeigt |
visibilityChange |
das Fenster ist sichtbar oder nicht: minimiert, oder der Browser-Tab verborgen |
connectionChange |
der Desktop verlor oder bekam die Verbindung zur Box zurück |
sizeClassChange |
das Fenster überschritt 600 oder 1024 px |
themeChange, localeChange |
siehe Sprachen und Darstellung |
permissionsChange |
siehe Berechtigungen |
storageChange |
eine Speicherzuordnung änderte sich oder ihr Pool wurde gesperrt oder kam zurück |
backendStatus |
das Backend ist stopped, starting, running, updating oder unavailable, mit reason storage oder pool, wenn es nicht starten kann |
logLevelChange |
ein Administrator schaltete das Debug-Logging der App um |
appUpdated |
eine andere Version ist installiert (Update oder Zurücksetzen); neu laden, um sie zu nutzen, nach dem Speichern |
launch, notificationAction |
siehe oben |
app.on liefert eine Funktion, die den Handler wieder entfernt. Handler laufen in der Reihenfolge, in der sie hinzugefügt wurden. Ereignisse, die die App nicht kennt, werden ignoriert; neue Ereignisse brechen also keine App.
Zustand speichern
Abschnitt betitelt „Zustand speichern“app.session.save(state)speichert bis zu 16 KB JSON für dieses Fenster mit dem Desktop-Layout.app.session.restore()liefert es nach einem Neuladen und auf einem anderen Gerät nach „Desktop holen von …“ zurück. Größerer Zustand scheitert mitapp.storage.too_large.beforeClosewird gefragt, wenn der Benutzer das Fenster schließt. Antworte{ allow: false }, um es offen zu halten, etwa um nach ungespeicherten Änderungen zu fragen, und ruf danach selbstapp.window.close()auf, das nicht noch einmal fragt. Sobald die Appwindow.setDirtyaufgerufen hat, fragt der Desktop nur noch, solange das Fenster als geändert markiert ist, und schließt ein sauberes Fenster sofort. Der Desktop wartet 5 Sekunden und bietet dann an, das Fenster trotzdem zu schließen; ein Handler, der eine Ausnahme wirft, erlaubt das Schließen.beforeUnloadkommt vor einer Abmeldung, einem Herunterfahren, einem Neustart oder einem Pithyx-Update (reason). Es lässt sich nicht ablehnen; der Desktop wartet 2 Sekunden.
Sonderfälle
Abschnitt betitelt „Sonderfälle“- Ereignisse, die passieren, während sich das Fenster verbindet, werden nicht nachgeliefert; der Startkontext enthält schon ihr Ergebnis.
visibilityChangeist das Signal, Timer und Animationen anzuhalten: Der Frame eines minimierten Fensters bleibt für den Browser sichtbar.- Nach
connectionChangemitonline: falsescheitern Aufrufe, die die Box brauchen, bis sie zurück ist. Versuche es nicht in einer Schleife; warte aufonline: true. app.window.setTitlenimmt höchstens 200 Zeichen; ein längerer Titel scheitert mitsdk.request.invalid. Fensteraufrufe wirken nur auf das aufrufende Fenster.- Der Mock-Desktop von
pithyx devstartet immer mitlaunch: { args: null }; sendelaunch,notificationActionoder jedes andere Ereignis mit eigenen Daten aus seinem Panel.
Fehlercodes
Abschnitt betitelt „Fehlercodes“app.storage.too_large, sdk.request.invalid, sdk.request.timeout, sdk.transport.closed.