diff --git a/public/waiter/.htaccess b/public/waiter/.htaccess
new file mode 100644
index 0000000..4f2da59
--- /dev/null
+++ b/public/waiter/.htaccess
@@ -0,0 +1,9 @@
+
+ AddType application/manifest+json .webmanifest
+
+
+
+
+ Header set Cache-Control "no-cache"
+
+
diff --git a/public/waiter/app.css b/public/waiter/app.css
index b12d87d..a029392 100644
--- a/public/waiter/app.css
+++ b/public/waiter/app.css
@@ -87,7 +87,8 @@ body {
50% { opacity: 1; transform: scale(1.1); }
}
-.notify-banner {
+.notify-banner,
+.install-banner {
display: flex;
align-items: center;
justify-content: space-between;
@@ -99,13 +100,21 @@ body {
margin-bottom: 16px;
}
-.notify-banner p {
+.install-banner {
+ background: linear-gradient(135deg, #14532d 0%, #1e293b 100%);
+ border-color: #22c55e;
+}
+
+.notify-banner p,
+.install-banner p {
margin-top: 4px;
font-size: 0.82rem;
color: #cbd5e1;
line-height: 1.4;
}
+.notify-banner {
+
.btn {
border: none;
border-radius: 10px;
diff --git a/public/waiter/app.js b/public/waiter/app.js
index ec89bbb..9f0cd4d 100644
--- a/public/waiter/app.js
+++ b/public/waiter/app.js
@@ -7,6 +7,8 @@ const syncDot = document.getElementById('syncDot');
const syncLabel = document.getElementById('syncLabel');
const notifyBanner = document.getElementById('notifyBanner');
const enableNotifyBtn = document.getElementById('enableNotifyBtn');
+const installBanner = document.getElementById('installBanner');
+const installAppBtn = document.getElementById('installAppBtn');
const statPending = document.getElementById('statPending');
const statWaiter = document.getElementById('statWaiter');
@@ -18,6 +20,49 @@ let feedInitialized = false;
let pollTimer = null;
let lastPayload = '';
let swRegistration = null;
+let deferredInstallPrompt = null;
+
+function isStandaloneDisplay() {
+ return window.matchMedia('(display-mode: standalone)').matches
+ || window.navigator.standalone === true;
+}
+
+function updateInstallBanner() {
+ if (!installBanner) {
+ return;
+ }
+
+ if (isStandaloneDisplay() || !deferredInstallPrompt) {
+ installBanner.classList.add('hidden');
+ return;
+ }
+
+ installBanner.classList.remove('hidden');
+}
+
+function setupInstallPrompt() {
+ window.addEventListener('beforeinstallprompt', (event) => {
+ event.preventDefault();
+ deferredInstallPrompt = event;
+ updateInstallBanner();
+ });
+
+ window.addEventListener('appinstalled', () => {
+ deferredInstallPrompt = null;
+ updateInstallBanner();
+ });
+}
+
+async function promptInstallApp() {
+ if (!deferredInstallPrompt) {
+ return;
+ }
+
+ deferredInstallPrompt.prompt();
+ await deferredInstallPrompt.userChoice;
+ deferredInstallPrompt = null;
+ updateInstallBanner();
+}
function escapeHtml(value) {
return String(value ?? '')
@@ -230,6 +275,10 @@ enableNotifyBtn?.addEventListener('click', () => {
requestNotifications();
});
+installAppBtn?.addEventListener('click', () => {
+ promptInstallApp();
+});
+
document.addEventListener('visibilitychange', () => {
if (!document.hidden) {
pollFeed();
@@ -237,13 +286,9 @@ document.addEventListener('visibilitychange', () => {
});
(async function init() {
+ setupInstallPrompt();
+ updateInstallBanner();
updateNotifyBanner();
-
- if ('Notification' in window && Notification.permission === 'granted') {
- await registerServiceWorker();
- } else if ('Notification' in window && Notification.permission === 'default') {
- notifyBanner.classList.remove('hidden');
- }
-
+ await registerServiceWorker();
startPolling();
})();
diff --git a/public/waiter/icons/icon-192.png b/public/waiter/icons/icon-192.png
new file mode 100644
index 0000000..d237666
Binary files /dev/null and b/public/waiter/icons/icon-192.png differ
diff --git a/public/waiter/icons/icon-512.png b/public/waiter/icons/icon-512.png
new file mode 100644
index 0000000..06dc118
Binary files /dev/null and b/public/waiter/icons/icon-512.png differ
diff --git a/public/waiter/icons/icon-maskable-512.png b/public/waiter/icons/icon-maskable-512.png
new file mode 100644
index 0000000..a2dbaa2
Binary files /dev/null and b/public/waiter/icons/icon-maskable-512.png differ
diff --git a/public/waiter/index.php b/public/waiter/index.php
index 3fc2419..eb314d4 100644
--- a/public/waiter/index.php
+++ b/public/waiter/index.php
@@ -9,9 +9,14 @@ $vJs = publicAssetVersion($waiterDir, 'app.js');
+
+
+
Kelner – wezwania
+
+
@@ -27,6 +32,14 @@ $vJs = publicAssetVersion($waiterDir, 'app.js');
+
+
+
Zainstaluj aplikację
+
Dodaj panel kelnera na ekran główny telefonu — szybszy dostęp bez paska adresu.
+
+
+
+
Powiadomienia wyłączone
diff --git a/public/waiter/manifest.webmanifest b/public/waiter/manifest.webmanifest
new file mode 100644
index 0000000..7406dd7
--- /dev/null
+++ b/public/waiter/manifest.webmanifest
@@ -0,0 +1,33 @@
+{
+ "id": "/app/public/waiter/",
+ "name": "Panel kelnera – Biesiada",
+ "short_name": "Kelner",
+ "description": "Wezwania kelnera i prośby o rachunek",
+ "start_url": "./index.php",
+ "scope": "./",
+ "display": "standalone",
+ "orientation": "portrait",
+ "theme_color": "#0f172a",
+ "background_color": "#0f172a",
+ "lang": "pl",
+ "icons": [
+ {
+ "src": "icons/icon-192.png",
+ "sizes": "192x192",
+ "type": "image/png",
+ "purpose": "any"
+ },
+ {
+ "src": "icons/icon-512.png",
+ "sizes": "512x512",
+ "type": "image/png",
+ "purpose": "any"
+ },
+ {
+ "src": "icons/icon-maskable-512.png",
+ "sizes": "512x512",
+ "type": "image/png",
+ "purpose": "maskable"
+ }
+ ]
+}
diff --git a/public/waiter/sw.js b/public/waiter/sw.js
index 34e47dc..7332578 100644
--- a/public/waiter/sw.js
+++ b/public/waiter/sw.js
@@ -1,3 +1,5 @@
+const START_URL = './index.php';
+
self.addEventListener('install', (event) => {
event.waitUntil(self.skipWaiting());
});
@@ -6,6 +8,27 @@ self.addEventListener('activate', (event) => {
event.waitUntil(self.clients.claim());
});
+self.addEventListener('fetch', (event) => {
+ event.respondWith(fetch(event.request));
+});
+
+self.addEventListener('notificationclick', (event) => {
+ event.notification.close();
+ event.waitUntil(
+ clients.matchAll({ type: 'window', includeUncontrolled: true }).then((list) => {
+ for (const client of list) {
+ if ('focus' in client) {
+ return client.focus();
+ }
+ }
+ if (clients.openWindow) {
+ return clients.openWindow(START_URL);
+ }
+ return undefined;
+ })
+ );
+});
+
self.addEventListener('message', (event) => {
const data = event.data;
if (!data || data.type !== 'notify') {
diff --git a/scripts/generate_waiter_pwa_icons.php b/scripts/generate_waiter_pwa_icons.php
new file mode 100644
index 0000000..02ca0c5
--- /dev/null
+++ b/scripts/generate_waiter_pwa_icons.php
@@ -0,0 +1,69 @@
+