Logo für Webseiten-Tab implementiert

Dieser Commit ist enthalten in:
hendrik_gebhardt@gmx.de
2026-01-10 16:47:02 +00:00
committet von Server Deploy
Ursprung ef153789cc
Commit 5b1f8b1cfe
53 geänderte Dateien mit 2377 neuen und 46 gelöschten Zeilen

275
frontend/js/pwa.js Normale Datei
Datei anzeigen

@ -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>&times;</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;