275 Zeilen
7.3 KiB
JavaScript
275 Zeilen
7.3 KiB
JavaScript
/**
|
|
* 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; |