Zum Inhalt springen

Ereignisse, Fensterkontext und Startargumente

Als Markdown ansehen

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.

// 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: “ wird gestartet …” in der Sprache des Benutzers, oder dein eigener 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 }
}

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.

  • instance ist die ID dieses Fensters, eindeutig pro Desktop-Tab.
  • user hat id, name, displayName, groups, isAdmin und twoFactor. Das ist zur Anzeige da; ein Backend nutzt stattdessen die Header des Proxys.
  • sizeClass ist compact unter 600 px, medium bis 1023 px und darüber expanded.
  • capabilities listet die Methoden, die der Desktop bedient; app.supports(method) liest sie.

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.

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”).

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.

  • 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 mit app.storage.too_large.
  • beforeClose wird 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 selbst app.window.close() auf, das nicht noch einmal fragt. Sobald die App window.setDirty aufgerufen 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.
  • beforeUnload kommt vor einer Abmeldung, einem Herunterfahren, einem Neustart oder einem Pithyx-Update (reason). Es lässt sich nicht ablehnen; der Desktop wartet 2 Sekunden.
  • Ereignisse, die passieren, während sich das Fenster verbindet, werden nicht nachgeliefert; der Startkontext enthält schon ihr Ergebnis.
  • visibilityChange ist das Signal, Timer und Animationen anzuhalten: Der Frame eines minimierten Fensters bleibt für den Browser sichtbar.
  • Nach connectionChange mit online: false scheitern Aufrufe, die die Box brauchen, bis sie zurück ist. Versuche es nicht in einer Schleife; warte auf online: true.
  • app.window.setTitle nimmt höchstens 200 Zeichen; ein längerer Titel scheitert mit sdk.request.invalid. Fensteraufrufe wirken nur auf das aufrufende Fenster.
  • Der Mock-Desktop von pithyx dev startet immer mit launch: { args: null }; sende launch, notificationAction oder jedes andere Ereignis mit eigenen Daten aus seinem Panel.

app.storage.too_large, sdk.request.invalid, sdk.request.timeout, sdk.transport.closed.