Compare commits
No commits in common. "eaad796ab655d52c248c380167e46b9872f6ad48" and "707638808c436ef760f4bc2ee39fed52093b1451" have entirely different histories.
eaad796ab6
...
707638808c
3 changed files with 27 additions and 99 deletions
18
README.md
18
README.md
|
|
@ -13,8 +13,6 @@ Mobile Progressive Web App für die Baustellen-Doku — Foto-Upload, Sprach- und
|
||||||
|
|
||||||
**Aufträge**
|
**Aufträge**
|
||||||
- ✅ Login mit Dolibarr-Credentials → zentrales SSO (HttpOnly-Cookie `awl_sso`, same-origin; Single-Sign-On/-Logout über alle AWL-Apps). WebAuthn/Passkey-Login möglich
|
- ✅ Login mit Dolibarr-Credentials → zentrales SSO (HttpOnly-Cookie `awl_sso`, same-origin; Single-Sign-On/-Logout über alle AWL-Apps). WebAuthn/Passkey-Login möglich
|
||||||
- ✅ **„Heute"-Ansicht**: offene Aufträge des Tages, gefiltert aus der normalen Auftragsliste, mit Route
|
|
||||||
für alle Termine auf einen Blick (öffnet Google Maps mit allen Adressen als Wegpunkten)
|
|
||||||
- ✅ Auftragsliste mit Suche, gefiltert auf eigene Aufträge (Multi-User)
|
- ✅ Auftragsliste mit Suche, gefiltert auf eigene Aufträge (Multi-User)
|
||||||
- ✅ Toggle "Auch abgeschlossene" (Filter in localStorage persistiert)
|
- ✅ Toggle "Auch abgeschlossene" (Filter in localStorage persistiert)
|
||||||
- ✅ Auftragsdetail mit Kunde, Adresse, Telefon (Click-to-Call)
|
- ✅ Auftragsdetail mit Kunde, Adresse, Telefon (Click-to-Call)
|
||||||
|
|
@ -71,20 +69,18 @@ Mobile Progressive Web App für die Baustellen-Doku — Foto-Upload, Sprach- und
|
||||||
- ✅ **Web Share Target** — aus WhatsApp & Co. Bilder **und** Beschreibungstext an „Baustelle" teilen:
|
- ✅ **Web Share Target** — aus WhatsApp & Co. Bilder **und** Beschreibungstext an „Baustelle" teilen:
|
||||||
anmelden (falls nötig, direkt in der Teilen-Seite), Auftrag suchen und wählen, ablegen. Bilder gehen
|
anmelden (falls nötig, direkt in der Teilen-Seite), Auftrag suchen und wählen, ablegen. Bilder gehen
|
||||||
über die normale Upload-Queue, der Text wird als Notiz (`notiz_*.txt`) im Auftrag gespeichert
|
über die normale Upload-Queue, der Text wird als Notiz (`notiz_*.txt`) im Auftrag gespeichert
|
||||||
- ✅ **Serie ohne Sucherei**: die Teilen-Seite merkt sich die letzten drei benutzten Aufträge und zeigt
|
|
||||||
sie unter „Zuletzt benutzt" ganz oben. Wurde einer davon in den letzten 30 Minuten benutzt, ist er
|
|
||||||
**vorausgewählt** — die zweite und dritte Nachricht der Kundin kosten dann nur noch einen Tipp.
|
|
||||||
Danach steht er nur noch als Schnellzugriff da, damit nichts versehentlich im Auftrag von vorgestern landet
|
|
||||||
- ✅ Nativer Android-Share-Sheet für Fotos, PDFs und Dokumente (`navigator.share`) — direkt an WhatsApp / Signal / Mail
|
- ✅ Nativer Android-Share-Sheet für Fotos, PDFs und Dokumente (`navigator.share`) — direkt an WhatsApp / Signal / Mail
|
||||||
- ✅ Mehrfachauswahl in Foto- und Dokument-Liste mit Batch-Teilen
|
- ✅ Mehrfachauswahl in Foto- und Dokument-Liste mit Batch-Teilen
|
||||||
- ✅ Android-Hardware-Back-Button schließt Modale/Ansichten sauber (History-Modal-Stack), bricht Mehrfachauswahl ab statt App zu verlassen
|
- ✅ Android-Hardware-Back-Button schließt Modale/Ansichten sauber (History-Modal-Stack), bricht Mehrfachauswahl ab statt App zu verlassen
|
||||||
- ✅ „Nochmal drücken zum Beenden"-Toast auf Top-Level-Routen
|
- ✅ „Nochmal drücken zum Beenden"-Toast auf Top-Level-Routen
|
||||||
- ✅ PIN-Schutz (optional)
|
- ✅ PIN-Schutz (optional)
|
||||||
- ✅ **Push-Benachrichtigungen via ntfy** (statt klassischem Web-Push): eigener Server/Topic in den
|
|
||||||
Einstellungen, Browser-Notification-Permission, Reconnect bei Verbindungsabbruch
|
|
||||||
- ✅ **Bild-Cache** `baustelle-media` (cache-first, LRU 300): schon angesehene Fotos sind auch ohne Netz da; überlebt Deploys, wird beim Abmelden geleert
|
- ✅ **Bild-Cache** `baustelle-media` (cache-first, LRU 300): schon angesehene Fotos sind auch ohne Netz da; überlebt Deploys, wird beim Abmelden geleert
|
||||||
- ✅ **Keine Browser-Dialoge** — `confirm`/`alert`/`prompt` sind durch eigene Modale ersetzt (`confirmDialog`, `alertDialog`, `inputDialog`); der Hardware-Zurück-Button schließt sie als Abbruch
|
- ✅ **Keine Browser-Dialoge** — `confirm`/`alert`/`prompt` sind durch eigene Modale ersetzt (`confirmDialog`, `alertDialog`, `inputDialog`); der Hardware-Zurück-Button schließt sie als Abbruch
|
||||||
|
|
||||||
|
## Geplant
|
||||||
|
|
||||||
|
- Push-Notifications für neue Aufträge
|
||||||
|
|
||||||
## Hosting
|
## Hosting
|
||||||
|
|
||||||
Deploy per Forgejo-Pipeline (Commit-Tag `[deploy]`) nach `/mnt/appdata/firma/dolibarr-202509/modules/baustelle/`. Erreichbar unter `https://awl.data-it-solution.de/custom/baustelle/`. Cache-Busting automatisch via `filemtime()` in `index.php` (`?v=<mtime>` an JS/CSS) — kein manuelles Versions-Hochzählen nötig, die PWA aktualisiert sich beim nächsten Laden.
|
Deploy per Forgejo-Pipeline (Commit-Tag `[deploy]`) nach `/mnt/appdata/firma/dolibarr-202509/modules/baustelle/`. Erreichbar unter `https://awl.data-it-solution.de/custom/baustelle/`. Cache-Busting automatisch via `filemtime()` in `index.php` (`?v=<mtime>` an JS/CSS) — kein manuelles Versions-Hochzählen nötig, die PWA aktualisiert sich beim nächsten Laden.
|
||||||
|
|
@ -118,15 +114,12 @@ Die App spricht die Bericht-API unter `/custom/bericht/api/`:
|
||||||
|
|
||||||
**Auth**
|
**Auth**
|
||||||
- `POST /auth.php` — Login mit Dolibarr-Credentials → setzt HttpOnly-Cookie `awl_sso` (zentrales SSO via awlauth). Folge-Requests authentisieren same-origin über das Cookie (kein Bearer-Token)
|
- `POST /auth.php` — Login mit Dolibarr-Credentials → setzt HttpOnly-Cookie `awl_sso` (zentrales SSO via awlauth). Folge-Requests authentisieren same-origin über das Cookie (kein Bearer-Token)
|
||||||
- `GET /verify.php` — prüft das Cookie und verlängert die Session (sliding renewal); läuft bei jedem Routenwechsel (`ensureAuth()`) und ist der Weg, wie `share.html` prüft, ob eine Anmeldung nötig ist
|
|
||||||
- `POST /logout.php` — löscht das `awl_sso`-Cookie (Single-Logout über alle AWL-Apps)
|
|
||||||
|
|
||||||
**Aufträge**
|
**Aufträge**
|
||||||
- `GET /orders.php` — Aufträge des Users (Filter: `?open=1`, `?q=suche`)
|
- `GET /orders.php` — Aufträge des Users (Filter: `?open=1`, `?q=suche`)
|
||||||
- `GET /orders.php?id=X` — Auftrag-Detail inkl. verknüpfte Berichte
|
- `GET /orders.php?id=X` — Auftrag-Detail inkl. verknüpfte Berichte
|
||||||
- `GET /orders.php?id=X&action=photos` — Anhang-Liste (je Datei `taken_at` = EXIF-Aufnahmezeit, danach sortiert)
|
- `GET /orders.php?id=X&action=photos` — Anhang-Liste (je Datei `taken_at` = EXIF-Aufnahmezeit, danach sortiert)
|
||||||
- `POST /orders.php?id=X&action=upload_photo` — Foto-Upload (multipart)
|
- `POST /orders.php?id=X&action=upload_photo` — Foto-Upload (multipart)
|
||||||
- `POST /orders.php?action=create` — Neuen Auftrag anlegen (Kunde, Ihr Zeichen, Beschreibung, Liefertermin, optional direkt freigeben)
|
|
||||||
|
|
||||||
**Kunden**
|
**Kunden**
|
||||||
- `GET /customers.php` — Kundenliste (Filter: `?q=suche`)
|
- `GET /customers.php` — Kundenliste (Filter: `?q=suche`)
|
||||||
|
|
@ -175,9 +168,6 @@ Die App spricht die Bericht-API unter `/custom/bericht/api/`:
|
||||||
- `GET /templates.php` — Bericht-Vorlagen
|
- `GET /templates.php` — Bericht-Vorlagen
|
||||||
- `GET /odt_templates.php` — ODT-Deckblatt-Vorlagen
|
- `GET /odt_templates.php` — ODT-Deckblatt-Vorlagen
|
||||||
|
|
||||||
**Konfiguration**
|
|
||||||
- `GET /config.php` — Admin-Vorgaben zur Fotoqualität (`photo_maxside`, `photo_quality`); wird beim Start und nach jedem Login neu geladen, zwischengespeichert in `localStorage` für den Offline-Fall
|
|
||||||
|
|
||||||
## Lizenz
|
## Lizenz
|
||||||
|
|
||||||
GPL v3+
|
GPL v3+
|
||||||
|
|
|
||||||
|
|
@ -150,9 +150,6 @@ Vorher endete jeder Teilen-Vorgang in einer Sackgasse:
|
||||||
`api/note.php`. Schlägt die Notiz fehl (kein Netz), bleibt der Text gesichert und der
|
`api/note.php`. Schlägt die Notiz fehl (kein Netz), bleibt der Text gesichert und der
|
||||||
Vorgang ist wiederholbar
|
Vorgang ist wiederholbar
|
||||||
- [x] Banner in der App, wenn noch etwas Geteiltes unabgelegt bereitliegt
|
- [x] Banner in der App, wenn noch etwas Geteiltes unabgelegt bereitliegt
|
||||||
- [x] **Mehrere Nachrichten hintereinander**: die letzten drei Aufträge stehen unter „Zuletzt benutzt"
|
|
||||||
oben; innerhalb von 30 Minuten ist der letzte vorausgewählt (danach nur noch Schnellzugriff —
|
|
||||||
eine Dauer-Vorauswahl würde irgendwann im falschen Auftrag landen)
|
|
||||||
|
|
||||||
## Erledigt
|
## Erledigt
|
||||||
|
|
||||||
|
|
|
||||||
105
share.html
105
share.html
|
|
@ -54,11 +54,6 @@
|
||||||
* anmelden"), sondern über das awl_sso-Cookie, geprüft an api/verify.php.
|
* anmelden"), sondern über das awl_sso-Cookie, geprüft an api/verify.php.
|
||||||
*/
|
*/
|
||||||
const SHARE_KEY = 'shared_payload';
|
const SHARE_KEY = 'shared_payload';
|
||||||
const RECENT_KEY = 'share_recent_orders';
|
|
||||||
/* Wer mehrere Sachen hintereinander teilt, will nicht jedes Mal suchen. Innerhalb dieser
|
|
||||||
* Spanne ist der zuletzt benutzte Auftrag vorausgewaehlt — danach steht er nur noch oben
|
|
||||||
* als Schnellzugriff, damit nichts versehentlich im Auftrag von vorgestern landet. */
|
|
||||||
const VORWAHL_MS = 30 * 60 * 1000;
|
|
||||||
const main = () => document.getElementById('main');
|
const main = () => document.getElementById('main');
|
||||||
|
|
||||||
window.showToast = function (msg, kind) {
|
window.showToast = function (msg, kind) {
|
||||||
|
|
@ -79,8 +74,6 @@ document.getElementById('back-btn').addEventListener('click', () => { location.h
|
||||||
let payload = null; // { title, text, url, files:[{name,type,blob}] }
|
let payload = null; // { title, text, url, files:[{name,type,blob}] }
|
||||||
let orders = [];
|
let orders = [];
|
||||||
let selectedId = null;
|
let selectedId = null;
|
||||||
let selectedRef = '';
|
|
||||||
let recent = []; // zuletzt benutzte Auftraege, neuester zuerst
|
|
||||||
let searchTimer = null;
|
let searchTimer = null;
|
||||||
|
|
||||||
function toBlob(f) {
|
function toBlob(f) {
|
||||||
|
|
@ -155,7 +148,7 @@ function renderLogin() {
|
||||||
try {
|
try {
|
||||||
await api.login(fd.get('login'), fd.get('password'));
|
await api.login(fd.get('login'), fd.get('password'));
|
||||||
showToast('Angemeldet');
|
showToast('Angemeldet');
|
||||||
await renderPicker();
|
renderPicker();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
showToast(err.message || 'Anmeldung fehlgeschlagen', 'error');
|
showToast(err.message || 'Anmeldung fehlgeschlagen', 'error');
|
||||||
}
|
}
|
||||||
|
|
@ -170,7 +163,7 @@ function renderLogin() {
|
||||||
const res = await awlauthWebAuthn.login();
|
const res = await awlauthWebAuthn.login();
|
||||||
if (res && res.user) await idb.set('user', res.user);
|
if (res && res.user) await idb.set('user', res.user);
|
||||||
showToast('Angemeldet');
|
showToast('Angemeldet');
|
||||||
await renderPicker();
|
renderPicker();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
pk.disabled = false;
|
pk.disabled = false;
|
||||||
showToast((err && err.message) || 'Anmeldung fehlgeschlagen', 'error');
|
showToast((err && err.message) || 'Anmeldung fehlgeschlagen', 'error');
|
||||||
|
|
@ -179,60 +172,6 @@ function renderLogin() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ----- Zuletzt benutzte Auftraege ----- */
|
|
||||||
async function ladeRecent() {
|
|
||||||
const liste = await idb.get(RECENT_KEY).catch(() => null);
|
|
||||||
return Array.isArray(liste) ? liste : [];
|
|
||||||
}
|
|
||||||
|
|
||||||
async function merkeAuftrag(o) {
|
|
||||||
const eintrag = { id: String(o.id), ref: o.ref || '', name: o.name || '', ort: o.ort || '', ts: Date.now() };
|
|
||||||
const rest = recent.filter(x => String(x.id) !== eintrag.id);
|
|
||||||
await idb.set(RECENT_KEY, [eintrag].concat(rest).slice(0, 3)).catch(() => {});
|
|
||||||
}
|
|
||||||
|
|
||||||
function seitdem(ts) {
|
|
||||||
const min = Math.round((Date.now() - ts) / 60000);
|
|
||||||
if (min < 1) return 'gerade eben';
|
|
||||||
if (min < 60) return 'vor ' + min + ' Min.';
|
|
||||||
const std = Math.round(min / 60);
|
|
||||||
if (std < 24) return 'vor ' + std + ' Std.';
|
|
||||||
const tage = Math.round(std / 24);
|
|
||||||
return tage === 1 ? 'gestern' : 'vor ' + tage + ' Tagen';
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Auswahl setzen — gilt ueber beide Listen hinweg (Schnellzugriff und Suchtreffer). */
|
|
||||||
function waehleAuftrag(id, ref, name, ort) {
|
|
||||||
selectedId = String(id);
|
|
||||||
selectedRef = ref || '';
|
|
||||||
document.querySelectorAll('.order-card').forEach(c => c.classList.toggle('selected', c.dataset.id === selectedId));
|
|
||||||
const btn = document.getElementById('btn-store');
|
|
||||||
if (!btn) return;
|
|
||||||
btn.disabled = false;
|
|
||||||
btn.textContent = 'In ' + (selectedRef || 'Auftrag') + ' ablegen';
|
|
||||||
btn.dataset.name = name || '';
|
|
||||||
btn.dataset.ort = ort || '';
|
|
||||||
}
|
|
||||||
|
|
||||||
function karteHtml(o) {
|
|
||||||
const kunde = o.name !== undefined ? o.name : ((o.customer && o.customer.name) || '');
|
|
||||||
const ort = o.ort !== undefined ? o.ort : (((o.customer && o.customer.zip) || '') + ' ' + ((o.customer && o.customer.town) || ''));
|
|
||||||
return `
|
|
||||||
<div class="order-card${String(o.id) === String(selectedId) ? ' selected' : ''}"
|
|
||||||
data-id="${o.id}" data-ref="${esc(o.ref)}" data-name="${esc(kunde)}" data-ort="${esc(ort.trim())}">
|
|
||||||
<div class="ref">${esc(o.ref)}</div>
|
|
||||||
${o.ref_client ? `<div class="ref-client">${esc(o.ref_client)}</div>` : ''}
|
|
||||||
<div class="name">${esc(kunde)}</div>
|
|
||||||
<div class="meta"><span>${esc(ort.trim())}</span></div>
|
|
||||||
</div>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function bindeKarten(wurzel) {
|
|
||||||
wurzel.querySelectorAll('.order-card').forEach(c => {
|
|
||||||
c.addEventListener('click', () => waehleAuftrag(c.dataset.id, c.dataset.ref, c.dataset.name, c.dataset.ort));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ----- Auswahl: Text prüfen, Auftrag wählen ----- */
|
/* ----- Auswahl: Text prüfen, Auftrag wählen ----- */
|
||||||
function defaultSubject() {
|
function defaultSubject() {
|
||||||
// Der Betreff steht spaeter im Dateinamen — deshalb kurz und an der Wortgrenze
|
// Der Betreff steht spaeter im Dateinamen — deshalb kurz und an der Wortgrenze
|
||||||
|
|
@ -251,11 +190,8 @@ function defaultText() {
|
||||||
return parts.join('\n\n');
|
return parts.join('\n\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
async function renderPicker() {
|
function renderPicker() {
|
||||||
const n = payload.files.length;
|
const n = payload.files.length;
|
||||||
recent = await ladeRecent();
|
|
||||||
// Frisch benutzt heisst: der Nutzer teilt gerade eine Serie -> vorauswaehlen
|
|
||||||
const vorwahl = recent[0] && (Date.now() - recent[0].ts) < VORWAHL_MS ? recent[0] : null;
|
|
||||||
main().innerHTML = `
|
main().innerHTML = `
|
||||||
${n ? `
|
${n ? `
|
||||||
<div class="share-section">
|
<div class="share-section">
|
||||||
|
|
@ -269,15 +205,8 @@ async function renderPicker() {
|
||||||
<textarea id="note-text" placeholder="Was ist zu tun / was hat der Kunde geschrieben?">${esc(defaultText())}</textarea>
|
<textarea id="note-text" placeholder="Was ist zu tun / was hat der Kunde geschrieben?">${esc(defaultText())}</textarea>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
${recent.length ? `
|
|
||||||
<div class="share-section">
|
<div class="share-section">
|
||||||
<h3>Zuletzt benutzt</h3>
|
<h3>Auftrag wählen</h3>
|
||||||
${recent.map(o => karteHtml(o)).join('')}
|
|
||||||
${vorwahl ? `<p class="label" style="margin-top:8px;">Vorausgewählt — zuletzt ${esc(seitdem(vorwahl.ts))} benutzt</p>` : ''}
|
|
||||||
</div>` : ''}
|
|
||||||
|
|
||||||
<div class="share-section">
|
|
||||||
<h3>${recent.length ? 'Anderer Auftrag' : 'Auftrag wählen'}</h3>
|
|
||||||
<div class="search-bar">
|
<div class="search-bar">
|
||||||
<input type="search" id="order-search" placeholder="Auftrag, Kunde oder Ihr Zeichen…" autocomplete="off">
|
<input type="search" id="order-search" placeholder="Auftrag, Kunde oder Ihr Zeichen…" autocomplete="off">
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -310,8 +239,6 @@ async function renderPicker() {
|
||||||
});
|
});
|
||||||
|
|
||||||
document.getElementById('btn-store').addEventListener('click', store);
|
document.getElementById('btn-store').addEventListener('click', store);
|
||||||
bindeKarten(main()); // Schnellzugriff klickbar machen
|
|
||||||
if (vorwahl) waehleAuftrag(vorwahl.id, vorwahl.ref, vorwahl.name, vorwahl.ort);
|
|
||||||
loadOrders('');
|
loadOrders('');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -327,8 +254,24 @@ async function loadOrders(q) {
|
||||||
box.innerHTML = '<div class="empty-state"><div class="icon">🔍</div>Kein Auftrag gefunden</div>';
|
box.innerHTML = '<div class="empty-state"><div class="icon">🔍</div>Kein Auftrag gefunden</div>';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
box.innerHTML = orders.map(o => karteHtml(o)).join('');
|
box.innerHTML = orders.map(o => `
|
||||||
bindeKarten(box);
|
<div class="order-card${String(o.id) === String(selectedId) ? ' selected' : ''}" data-id="${o.id}">
|
||||||
|
<div class="ref">${esc(o.ref)}</div>
|
||||||
|
${o.ref_client ? `<div class="ref-client">${esc(o.ref_client)}</div>` : ''}
|
||||||
|
<div class="name">${esc((o.customer && o.customer.name) || '')}</div>
|
||||||
|
<div class="meta"><span>${esc(((o.customer && o.customer.zip) || '') + ' ' + ((o.customer && o.customer.town) || ''))}</span></div>
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
|
box.querySelectorAll('.order-card').forEach(c => {
|
||||||
|
c.addEventListener('click', () => {
|
||||||
|
selectedId = c.dataset.id;
|
||||||
|
box.querySelectorAll('.order-card').forEach(x => x.classList.toggle('selected', x === c));
|
||||||
|
const btn = document.getElementById('btn-store');
|
||||||
|
btn.disabled = false;
|
||||||
|
const o = orders.find(x => String(x.id) === String(selectedId));
|
||||||
|
btn.textContent = 'In ' + ((o && o.ref) || 'Auftrag') + ' ablegen';
|
||||||
|
});
|
||||||
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
box.innerHTML = '<div class="empty-state"><div class="icon">⚠</div>' + esc(e.message || 'Fehler') + '</div>';
|
box.innerHTML = '<div class="empty-state"><div class="icon">⚠</div>' + esc(e.message || 'Fehler') + '</div>';
|
||||||
}
|
}
|
||||||
|
|
@ -376,8 +319,6 @@ async function store() {
|
||||||
}
|
}
|
||||||
|
|
||||||
await clearPayload();
|
await clearPayload();
|
||||||
// Fuer die naechste Ladung merken — dann steht der Auftrag oben und ist vorausgewaehlt
|
|
||||||
await merkeAuftrag({ id: orderId, ref: selectedRef, name: btn.dataset.name, ort: btn.dataset.ort });
|
|
||||||
await offline.syncQueue().catch(() => {});
|
await offline.syncQueue().catch(() => {});
|
||||||
showToast('Abgelegt');
|
showToast('Abgelegt');
|
||||||
location.replace('./#/orders/' + orderId);
|
location.replace('./#/orders/' + orderId);
|
||||||
|
|
@ -393,7 +334,7 @@ async function store() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
if (await isLoggedIn()) await renderPicker();
|
if (await isLoggedIn()) renderPicker();
|
||||||
else renderLogin();
|
else renderLogin();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
main().innerHTML = `<div class="empty-state"><div class="icon">📴</div>
|
main().innerHTML = `<div class="empty-state"><div class="icon">📴</div>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue