Logo für Webseiten-Tab implementiert
Dieser Commit ist enthalten in:
committet von
Server Deploy
Ursprung
ef153789cc
Commit
5b1f8b1cfe
275
frontend/js/pwa.js
Normale Datei
275
frontend/js/pwa.js
Normale Datei
@ -0,0 +1,275 @@
|
||||
/**
|
||||
* TASKMATE - PWA Module
|
||||
* =====================
|
||||
* Progressive Web App Features
|
||||
*/
|
||||
|
||||
class PWAManager {
|
||||
constructor() {
|
||||
this.deferredPrompt = null;
|
||||
this.installButton = null;
|
||||
this.isInstalled = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize PWA features
|
||||
*/
|
||||
init() {
|
||||
// Check if already installed
|
||||
this.checkInstallStatus();
|
||||
|
||||
// Listen for install prompt
|
||||
window.addEventListener('beforeinstallprompt', (e) => {
|
||||
console.log('[PWA] Install prompt available');
|
||||
e.preventDefault();
|
||||
this.deferredPrompt = e;
|
||||
this.showInstallButton();
|
||||
});
|
||||
|
||||
// Listen for app installed
|
||||
window.addEventListener('appinstalled', () => {
|
||||
console.log('[PWA] App installed');
|
||||
this.isInstalled = true;
|
||||
this.hideInstallButton();
|
||||
this.showInstallSuccess();
|
||||
});
|
||||
|
||||
// Check for iOS
|
||||
if (this.isIOS() && !this.isInStandaloneMode()) {
|
||||
this.showIOSInstallInstructions();
|
||||
}
|
||||
|
||||
// Update online/offline status
|
||||
this.updateOnlineStatus();
|
||||
window.addEventListener('online', () => this.updateOnlineStatus());
|
||||
window.addEventListener('offline', () => this.updateOnlineStatus());
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if app is already installed
|
||||
*/
|
||||
checkInstallStatus() {
|
||||
// Check for display-mode: standalone
|
||||
if (window.matchMedia('(display-mode: standalone)').matches) {
|
||||
this.isInstalled = true;
|
||||
console.log('[PWA] Already running in standalone mode');
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for iOS standalone
|
||||
if (window.navigator.standalone) {
|
||||
this.isInstalled = true;
|
||||
console.log('[PWA] Already running in iOS standalone mode');
|
||||
return;
|
||||
}
|
||||
|
||||
// Check URL parameters (for TWA)
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
if (urlParams.get('mode') === 'twa') {
|
||||
this.isInstalled = true;
|
||||
console.log('[PWA] Running as TWA');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show install button
|
||||
*/
|
||||
showInstallButton() {
|
||||
if (this.isInstalled) return;
|
||||
|
||||
// Create install button if not exists
|
||||
if (!this.installButton) {
|
||||
this.createInstallButton();
|
||||
}
|
||||
|
||||
this.installButton.classList.remove('hidden');
|
||||
|
||||
// Show install banner after delay
|
||||
setTimeout(() => {
|
||||
if (!this.isInstalled && this.deferredPrompt) {
|
||||
this.showInstallBanner();
|
||||
}
|
||||
}, 30000); // 30 seconds
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide install button
|
||||
*/
|
||||
hideInstallButton() {
|
||||
if (this.installButton) {
|
||||
this.installButton.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create install button in header
|
||||
*/
|
||||
createInstallButton() {
|
||||
this.installButton = document.createElement('button');
|
||||
this.installButton.className = 'btn btn-primary install-button hidden';
|
||||
this.installButton.innerHTML = `
|
||||
<svg viewBox="0 0 24 24" width="16" height="16">
|
||||
<path d="M12 2v10m0 0l-4-4m4 4l4-4M3 12v7a2 2 0 002 2h14a2 2 0 002-2v-7"
|
||||
stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round"/>
|
||||
</svg>
|
||||
<span>App installieren</span>
|
||||
`;
|
||||
|
||||
// Insert before user menu
|
||||
const headerActions = document.querySelector('.header-actions');
|
||||
if (headerActions) {
|
||||
headerActions.insertBefore(this.installButton, headerActions.firstChild);
|
||||
}
|
||||
|
||||
// Handle click
|
||||
this.installButton.addEventListener('click', () => this.handleInstallClick());
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle install button click
|
||||
*/
|
||||
async handleInstallClick() {
|
||||
if (!this.deferredPrompt) return;
|
||||
|
||||
// Show the install prompt
|
||||
this.deferredPrompt.prompt();
|
||||
|
||||
// Wait for the user to respond
|
||||
const { outcome } = await this.deferredPrompt.userChoice;
|
||||
console.log(`[PWA] User response: ${outcome}`);
|
||||
|
||||
// Clear the deferred prompt
|
||||
this.deferredPrompt = null;
|
||||
|
||||
// Hide button if accepted
|
||||
if (outcome === 'accepted') {
|
||||
this.hideInstallButton();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show install banner
|
||||
*/
|
||||
showInstallBanner() {
|
||||
const banner = document.createElement('div');
|
||||
banner.className = 'pwa-install-banner';
|
||||
banner.innerHTML = `
|
||||
<div class="install-banner-content">
|
||||
<div class="install-banner-icon">
|
||||
<svg viewBox="0 0 24 24" width="32" height="32">
|
||||
<path d="M12 2v10m0 0l-4-4m4 4l4-4M3 12v7a2 2 0 002 2h14a2 2 0 002-2v-7"
|
||||
stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="install-banner-text">
|
||||
<h3>TaskMate App installieren</h3>
|
||||
<p>Installiere TaskMate für schnelleren Zugriff und Offline-Nutzung</p>
|
||||
</div>
|
||||
<div class="install-banner-actions">
|
||||
<button class="btn btn-secondary" data-dismiss>Später</button>
|
||||
<button class="btn btn-primary" data-install>Installieren</button>
|
||||
</div>
|
||||
<button class="install-banner-close" data-dismiss>×</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(banner);
|
||||
|
||||
// Animate in
|
||||
setTimeout(() => banner.classList.add('show'), 100);
|
||||
|
||||
// Handle buttons
|
||||
banner.querySelector('[data-install]').addEventListener('click', () => {
|
||||
this.handleInstallClick();
|
||||
this.dismissBanner(banner);
|
||||
});
|
||||
|
||||
banner.querySelectorAll('[data-dismiss]').forEach(btn => {
|
||||
btn.addEventListener('click', () => this.dismissBanner(banner));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Dismiss install banner
|
||||
*/
|
||||
dismissBanner(banner) {
|
||||
banner.classList.remove('show');
|
||||
setTimeout(() => banner.remove(), 300);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show install success message
|
||||
*/
|
||||
showInstallSuccess() {
|
||||
window.dispatchEvent(new CustomEvent('toast:show', {
|
||||
detail: {
|
||||
message: 'TaskMate wurde erfolgreich installiert!',
|
||||
type: 'success'
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if iOS
|
||||
*/
|
||||
isIOS() {
|
||||
return /iPhone|iPad|iPod/.test(navigator.userAgent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if in standalone mode
|
||||
*/
|
||||
isInStandaloneMode() {
|
||||
return window.navigator.standalone ||
|
||||
window.matchMedia('(display-mode: standalone)').matches;
|
||||
}
|
||||
|
||||
/**
|
||||
* Show iOS install instructions
|
||||
*/
|
||||
showIOSInstallInstructions() {
|
||||
// Only show once per session
|
||||
if (sessionStorage.getItem('ios-install-shown')) return;
|
||||
|
||||
const instructions = document.createElement('div');
|
||||
instructions.className = 'ios-install-instructions';
|
||||
instructions.innerHTML = `
|
||||
<div class="ios-install-content">
|
||||
<h3>TaskMate installieren</h3>
|
||||
<p>Tippe auf <span class="ios-share-icon">⬆</span> und wähle "Zum Home-Bildschirm"</p>
|
||||
<button class="btn btn-primary" data-dismiss>Verstanden</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(instructions);
|
||||
|
||||
// Animate in
|
||||
setTimeout(() => instructions.classList.add('show'), 100);
|
||||
|
||||
// Handle dismiss
|
||||
instructions.querySelector('[data-dismiss]').addEventListener('click', () => {
|
||||
instructions.classList.remove('show');
|
||||
setTimeout(() => instructions.remove(), 300);
|
||||
sessionStorage.setItem('ios-install-shown', 'true');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update online/offline status
|
||||
*/
|
||||
updateOnlineStatus() {
|
||||
const isOnline = navigator.onLine;
|
||||
document.body.classList.toggle('offline', !isOnline);
|
||||
|
||||
// Update offline banner
|
||||
const offlineBanner = document.getElementById('offline-banner');
|
||||
if (offlineBanner) {
|
||||
offlineBanner.classList.toggle('hidden', isOnline);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Export
|
||||
const pwaManager = new PWAManager();
|
||||
export default pwaManager;
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren