Logo für Webseiten-Tab implementiert
@ -58,7 +58,11 @@
|
|||||||
"Bash(grep -A10 -B5 \"modal-header\\|modal-title\" /home/claude-dev/TaskMate/frontend/css/modal.css)",
|
"Bash(grep -A10 -B5 \"modal-header\\|modal-title\" /home/claude-dev/TaskMate/frontend/css/modal.css)",
|
||||||
"Bash(grep -A10 -B5 \"calendar-reminder\\|reminder.*calendar\" /home/claude-dev/TaskMate/frontend/css/reminders.css)",
|
"Bash(grep -A10 -B5 \"calendar-reminder\\|reminder.*calendar\" /home/claude-dev/TaskMate/frontend/css/reminders.css)",
|
||||||
"Bash(grep -A15 -B5 \"calendar.*task\\|task.*calendar\" /home/claude-dev/TaskMate/frontend/js/calendar.js)",
|
"Bash(grep -A15 -B5 \"calendar.*task\\|task.*calendar\" /home/claude-dev/TaskMate/frontend/js/calendar.js)",
|
||||||
"WebFetch(domain:admin-panel-undso.aegis-sight.de)"
|
"WebFetch(domain:admin-panel-undso.aegis-sight.de)",
|
||||||
|
"Bash(mkdir:*)",
|
||||||
|
"Bash(for:*)",
|
||||||
|
"Bash(do )",
|
||||||
|
"Bash(done)"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1,6 +1,88 @@
|
|||||||
TASKMATE - CHANGELOG
|
TASKMATE - CHANGELOG
|
||||||
====================
|
====================
|
||||||
|
|
||||||
|
================================================================================
|
||||||
|
10.01.2026 - UI: Favicon ohne schwarzen Hintergrund
|
||||||
|
================================================================================
|
||||||
|
|
||||||
|
## ÄNDERUNG
|
||||||
|
Favicon (Browser-Tab Icon) auf transparente PNG-Version umgestellt
|
||||||
|
|
||||||
|
## DETAILS
|
||||||
|
- SVG-Logo mit dunklem Hintergrund (#0A1832) entfernt
|
||||||
|
- Transparente PNG-Icons (AegisSight Logo) als Favicon gesetzt
|
||||||
|
- favicon-32x32.png und favicon-16x16.png erstellt
|
||||||
|
- Cache-Version erhöht auf 297
|
||||||
|
|
||||||
|
## DATEIEN
|
||||||
|
✅ frontend/index.html - Favicon-Links aktualisiert
|
||||||
|
✅ frontend/assets/icons/favicon-32x32.png - Erstellt
|
||||||
|
✅ frontend/assets/icons/favicon-16x16.png - Erstellt
|
||||||
|
✅ frontend/sw.js - Cache-Version 297
|
||||||
|
|
||||||
|
================================================================================
|
||||||
|
10.01.2026 - BUGFIX: Race-Condition bei 401-Fehlern nach Login
|
||||||
|
================================================================================
|
||||||
|
|
||||||
|
## PROBLEM
|
||||||
|
Nach dem Login wurden Benutzer bei bestimmten Tabs (Genehmigung, Coding,
|
||||||
|
Kontakte) sofort wieder ausgeloggt.
|
||||||
|
|
||||||
|
## URSACHE
|
||||||
|
Race-Condition: API-Requests, die VOR dem Login gestartet wurden, kamen
|
||||||
|
mit 401 zurück und lösten einen Logout aus, obwohl bereits ein neuer
|
||||||
|
gültiger Token existierte.
|
||||||
|
|
||||||
|
## LÖSUNG
|
||||||
|
✅ handleAuthFailure() prüft jetzt ob ein neuer Token existiert
|
||||||
|
- Vergleicht Token der fehlgeschlagenen Anfrage mit aktuellem Token
|
||||||
|
- Ignoriert 401 wenn neuer Token nach dem Request gesetzt wurde
|
||||||
|
- Wiederholt Request mit neuem Token (max 1x gegen Endlos-Schleifen)
|
||||||
|
✅ Cache-Version: 294
|
||||||
|
|
||||||
|
================================================================================
|
||||||
|
10.01.2026 - FEATURE: PROGRESSIVE WEB APP (PWA) & MOBILE APP SUPPORT
|
||||||
|
================================================================================
|
||||||
|
|
||||||
|
## NEUE FUNKTIONEN
|
||||||
|
Vollständige PWA-Implementierung für mobile App-Funktionalität
|
||||||
|
|
||||||
|
## IMPLEMENTIERUNG
|
||||||
|
✅ Web App Manifest erstellt (manifest.json)
|
||||||
|
- App-Name, Icons, Theme-Konfiguration
|
||||||
|
- Shortcuts für schnellen Zugriff
|
||||||
|
- Standalone Display-Mode
|
||||||
|
✅ PWA Install-Prompt implementiert
|
||||||
|
- Automatische Erkennung der Installierbarkeit
|
||||||
|
- Install-Button im Header
|
||||||
|
- Install-Banner nach 30 Sekunden
|
||||||
|
- iOS-spezifische Installationsanleitung
|
||||||
|
✅ Service Worker optimiert
|
||||||
|
- Cache-First für statische Assets
|
||||||
|
- Network-First für HTML und API
|
||||||
|
- Erweiterte Offline-Funktionalität
|
||||||
|
- Background Sync vorbereitet
|
||||||
|
✅ PWA-Module (pwa.js) erstellt
|
||||||
|
- Install-Management
|
||||||
|
- Online/Offline Status
|
||||||
|
- iOS-Detection
|
||||||
|
✅ Dokumentation für APK-Erstellung
|
||||||
|
- PWABuilder Anleitung
|
||||||
|
- Bubblewrap Setup
|
||||||
|
- TWA (Trusted Web Activity) Konfiguration
|
||||||
|
- Play Store Veröffentlichung
|
||||||
|
|
||||||
|
## TECHNISCHE DETAILS
|
||||||
|
- Neue Dateien: manifest.json, js/pwa.js, css/pwa.css
|
||||||
|
- Icons benötigt: 48x48 bis 512x512 PNG
|
||||||
|
- Service Worker Cache-Version: 293
|
||||||
|
- APK kann mit PWABuilder.com erstellt werden
|
||||||
|
|
||||||
|
## NÄCHSTE SCHRITTE
|
||||||
|
1. Icons in allen Größen generieren (siehe generate-icons.js)
|
||||||
|
2. APK mit PWABuilder erstellen und testen
|
||||||
|
3. Optional: Play Store Veröffentlichung
|
||||||
|
|
||||||
================================================================================
|
================================================================================
|
||||||
09.01.2026 - ENTFERNUNG: SIMULIERTE VERBRAUCHSANZEIGE IM CODING-MODUL
|
09.01.2026 - ENTFERNUNG: SIMULIERTE VERBRAUCHSANZEIGE IM CODING-MODUL
|
||||||
================================================================================
|
================================================================================
|
||||||
|
|||||||
325
PWA_TO_APK_ANLEITUNG.md
Normale Datei
@ -0,0 +1,325 @@
|
|||||||
|
# TaskMate PWA zu APK - Vollständige Anleitung
|
||||||
|
|
||||||
|
## Überblick
|
||||||
|
|
||||||
|
Diese Anleitung zeigt, wie Sie aus der TaskMate PWA eine Android APK erstellen, die im Google Play Store veröffentlicht werden kann.
|
||||||
|
|
||||||
|
## Voraussetzungen
|
||||||
|
|
||||||
|
- TaskMate PWA ist online verfügbar (https://taskmate.aegis-sight.de)
|
||||||
|
- Web App Manifest und Service Worker sind implementiert
|
||||||
|
- PWA Icons in allen erforderlichen Größen vorhanden
|
||||||
|
|
||||||
|
## Methode 1: PWABuilder (Einfachste Methode)
|
||||||
|
|
||||||
|
### Schritt 1: PWA Score prüfen
|
||||||
|
|
||||||
|
1. Öffnen Sie https://www.pwabuilder.com
|
||||||
|
2. Geben Sie Ihre PWA URL ein: `https://taskmate.aegis-sight.de`
|
||||||
|
3. Klicken Sie auf "Start"
|
||||||
|
4. PWABuilder analysiert Ihre App und zeigt den Score
|
||||||
|
|
||||||
|
### Schritt 2: Package für Android generieren
|
||||||
|
|
||||||
|
1. Klicken Sie auf "Package for stores"
|
||||||
|
2. Wählen Sie "Android"
|
||||||
|
3. Konfigurieren Sie die Einstellungen:
|
||||||
|
- **Package ID**: `de.aegissight.taskmate`
|
||||||
|
- **App Name**: TaskMate
|
||||||
|
- **Display Mode**: Standalone
|
||||||
|
- **Orientation**: Any
|
||||||
|
- **Fallback URL**: `https://taskmate.aegis-sight.de`
|
||||||
|
|
||||||
|
### Schritt 3: Erweiterte Optionen
|
||||||
|
|
||||||
|
- **Signing Key**:
|
||||||
|
- "None" für Test-APK
|
||||||
|
- "New" für Play Store (speichern Sie den Key sicher!)
|
||||||
|
- **Maskable Icon**: Upload des 512x512 Icons
|
||||||
|
- **Splash Screen**: Automatisch generiert
|
||||||
|
- **Settings**:
|
||||||
|
- Enable notifications: Yes
|
||||||
|
- Location permissions: No
|
||||||
|
- WebView settings: Standard
|
||||||
|
|
||||||
|
### Schritt 4: Download und Test
|
||||||
|
|
||||||
|
1. Klicken Sie auf "Download"
|
||||||
|
2. Extrahieren Sie die ZIP-Datei
|
||||||
|
3. Die APK befindet sich im Ordner
|
||||||
|
4. Installieren Sie zum Testen auf einem Android-Gerät
|
||||||
|
|
||||||
|
## Methode 2: Bubblewrap (Für Entwickler)
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Node.js und npm müssen installiert sein
|
||||||
|
npm install -g @bubblewrap/cli
|
||||||
|
```
|
||||||
|
|
||||||
|
### Projekt initialisieren
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Neues Verzeichnis erstellen
|
||||||
|
mkdir taskmate-android
|
||||||
|
cd taskmate-android
|
||||||
|
|
||||||
|
# Bubblewrap init
|
||||||
|
bubblewrap init --manifest https://taskmate.aegis-sight.de/manifest.json
|
||||||
|
```
|
||||||
|
|
||||||
|
### Konfiguration (twa-manifest.json)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"packageId": "de.aegissight.taskmate",
|
||||||
|
"host": "taskmate.aegis-sight.de",
|
||||||
|
"name": "TaskMate",
|
||||||
|
"launcherName": "TaskMate",
|
||||||
|
"display": "standalone",
|
||||||
|
"themeColor": "#000000",
|
||||||
|
"navigationColor": "#000000",
|
||||||
|
"backgroundColor": "#000000",
|
||||||
|
"enableNotifications": true,
|
||||||
|
"startUrl": "/",
|
||||||
|
"iconUrl": "https://taskmate.aegis-sight.de/assets/icons/icon-512x512.png",
|
||||||
|
"maskableIconUrl": "https://taskmate.aegis-sight.de/assets/icons/icon-512x512.png",
|
||||||
|
"appVersion": "1.0.0",
|
||||||
|
"signingKey": {
|
||||||
|
"alias": "taskmate-key",
|
||||||
|
"path": "./taskmate-key.keystore"
|
||||||
|
},
|
||||||
|
"splashScreenFadeOutDuration": 300,
|
||||||
|
"enableSiteSettingsShortcut": false,
|
||||||
|
"shortcuts": [
|
||||||
|
{
|
||||||
|
"name": "Neue Aufgabe",
|
||||||
|
"shortName": "Neue Aufgabe",
|
||||||
|
"url": "/?action=new-task",
|
||||||
|
"chosenIconUrl": "https://taskmate.aegis-sight.de/assets/icons/add-task-96x96.png"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"fallbackType": "customtabs",
|
||||||
|
"webManifestUrl": "https://taskmate.aegis-sight.de/manifest.json"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Build-Prozess
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Keystore erstellen (nur einmal)
|
||||||
|
bubblewrap build --skipPwaValidation
|
||||||
|
|
||||||
|
# APK erstellen
|
||||||
|
bubblewrap build
|
||||||
|
|
||||||
|
# Signed APK für Play Store
|
||||||
|
bubblewrap build --skipPwaValidation
|
||||||
|
```
|
||||||
|
|
||||||
|
## Methode 3: Android Studio mit TWA
|
||||||
|
|
||||||
|
### Projekt Setup
|
||||||
|
|
||||||
|
1. Erstellen Sie ein neues Android-Projekt
|
||||||
|
2. Minimum SDK: API 19 (Android 4.4)
|
||||||
|
3. Wählen Sie "No Activity"
|
||||||
|
|
||||||
|
### Dependencies (app/build.gradle)
|
||||||
|
|
||||||
|
```gradle
|
||||||
|
dependencies {
|
||||||
|
implementation 'com.google.androidbrowserhelper:androidbrowserhelper:2.5.0'
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### AndroidManifest.xml
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
package="de.aegissight.taskmate">
|
||||||
|
|
||||||
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
|
||||||
|
<application
|
||||||
|
android:allowBackup="true"
|
||||||
|
android:icon="@mipmap/ic_launcher"
|
||||||
|
android:label="@string/app_name"
|
||||||
|
android:theme="@style/Theme.AppCompat.Light.NoActionBar">
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name="com.google.androidbrowserhelper.trusted.LauncherActivity"
|
||||||
|
android:exported="true"
|
||||||
|
android:label="@string/app_name">
|
||||||
|
|
||||||
|
<meta-data
|
||||||
|
android:name="android.support.customtabs.trusted.DEFAULT_URL"
|
||||||
|
android:value="https://taskmate.aegis-sight.de" />
|
||||||
|
|
||||||
|
<meta-data
|
||||||
|
android:name="android.support.customtabs.trusted.STATUS_BAR_COLOR"
|
||||||
|
android:resource="@color/colorPrimary" />
|
||||||
|
|
||||||
|
<meta-data
|
||||||
|
android:name="android.support.customtabs.trusted.NAVIGATION_BAR_COLOR"
|
||||||
|
android:resource="@color/colorPrimary" />
|
||||||
|
|
||||||
|
<meta-data
|
||||||
|
android:name="android.support.customtabs.trusted.SPLASH_IMAGE_DRAWABLE"
|
||||||
|
android:resource="@drawable/splash" />
|
||||||
|
|
||||||
|
<meta-data
|
||||||
|
android:name="android.support.customtabs.trusted.SPLASH_SCREEN_FADE_OUT_DURATION"
|
||||||
|
android:value="300" />
|
||||||
|
|
||||||
|
<meta-data
|
||||||
|
android:name="android.support.customtabs.trusted.DISPLAY_MODE"
|
||||||
|
android:value="standalone" />
|
||||||
|
|
||||||
|
<meta-data
|
||||||
|
android:name="android.support.customtabs.trusted.SCREEN_ORIENTATION"
|
||||||
|
android:value="unspecified" />
|
||||||
|
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
|
</intent-filter>
|
||||||
|
|
||||||
|
<intent-filter android:autoVerify="true">
|
||||||
|
<action android:name="android.intent.action.VIEW"/>
|
||||||
|
<category android:name="android.intent.category.DEFAULT"/>
|
||||||
|
<category android:name="android.intent.category.BROWSABLE"/>
|
||||||
|
<data
|
||||||
|
android:scheme="https"
|
||||||
|
android:host="taskmate.aegis-sight.de"/>
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
|
||||||
|
<service
|
||||||
|
android:name="com.google.androidbrowserhelper.trusted.DelegationService"
|
||||||
|
android:enabled="true"
|
||||||
|
android:exported="true">
|
||||||
|
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.support.customtabs.trusted.TRUSTED_WEB_ACTIVITY_SERVICE"/>
|
||||||
|
<category android:name="android.intent.category.DEFAULT"/>
|
||||||
|
</intent-filter>
|
||||||
|
</service>
|
||||||
|
|
||||||
|
</application>
|
||||||
|
</manifest>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Digital Asset Links
|
||||||
|
|
||||||
|
Erstellen Sie auf Ihrem Server die Datei:
|
||||||
|
`https://taskmate.aegis-sight.de/.well-known/assetlinks.json`
|
||||||
|
|
||||||
|
```json
|
||||||
|
[{
|
||||||
|
"relation": ["delegate_permission/common.handle_all_urls"],
|
||||||
|
"target": {
|
||||||
|
"namespace": "android_app",
|
||||||
|
"package_name": "de.aegissight.taskmate",
|
||||||
|
"sha256_cert_fingerprints": [
|
||||||
|
"YOUR_APP_SIGNING_KEY_FINGERPRINT"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Icon-Generierung
|
||||||
|
|
||||||
|
Falls Icons fehlen, nutzen Sie diese Tools:
|
||||||
|
|
||||||
|
1. **Android Asset Studio**: https://romannurik.github.io/AndroidAssetStudio/
|
||||||
|
2. **Maskable.app**: https://maskable.app/editor
|
||||||
|
|
||||||
|
Benötigte Größen:
|
||||||
|
- 48x48, 72x72, 96x96, 144x144, 192x192, 512x512
|
||||||
|
- Adaptive Icons für Android 8+
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
### Lokaler Test
|
||||||
|
1. Aktivieren Sie "Unbekannte Quellen" in Android-Einstellungen
|
||||||
|
2. Übertragen Sie die APK aufs Gerät
|
||||||
|
3. Installieren und testen
|
||||||
|
|
||||||
|
### Play Console Test Track
|
||||||
|
1. Laden Sie die APK in die Google Play Console
|
||||||
|
2. Erstellen Sie eine interne Testversion
|
||||||
|
3. Fügen Sie Tester hinzu
|
||||||
|
4. Testen Sie Installation und Updates
|
||||||
|
|
||||||
|
## Veröffentlichung im Play Store
|
||||||
|
|
||||||
|
### Vorbereitung
|
||||||
|
1. Google Play Developer Account ($25 einmalig)
|
||||||
|
2. App-Beschreibung und Screenshots
|
||||||
|
3. Datenschutzerklärung
|
||||||
|
4. Altersfreigabe-Fragebogen
|
||||||
|
|
||||||
|
### Store Listing
|
||||||
|
- **Titel**: TaskMate - Aufgabenverwaltung
|
||||||
|
- **Kurzbeschreibung**: Einfache und effiziente Aufgabenverwaltung mit Kanban-Board
|
||||||
|
- **Kategorie**: Produktivität
|
||||||
|
- **Screenshots**: Mindestens 2, idealerweise 8
|
||||||
|
|
||||||
|
### APK Upload
|
||||||
|
1. Signierte APK erstellen
|
||||||
|
2. In Play Console hochladen
|
||||||
|
3. Rollout starten (gestaffelt empfohlen)
|
||||||
|
|
||||||
|
## Wartung und Updates
|
||||||
|
|
||||||
|
### Version Updates
|
||||||
|
1. Erhöhen Sie `versionCode` und `versionName`
|
||||||
|
2. Erstellen Sie neue APK
|
||||||
|
3. Laden Sie in Play Console hoch
|
||||||
|
4. Beschreiben Sie Änderungen
|
||||||
|
|
||||||
|
### PWA Updates
|
||||||
|
- Service Worker Updates werden automatisch geladen
|
||||||
|
- Manifest-Änderungen erfordern App-Update
|
||||||
|
- Cache-Versionierung beachten
|
||||||
|
|
||||||
|
## Häufige Probleme
|
||||||
|
|
||||||
|
### "Not a valid PWA"
|
||||||
|
- Prüfen Sie HTTPS
|
||||||
|
- Validieren Sie manifest.json
|
||||||
|
- Service Worker muss registriert sein
|
||||||
|
|
||||||
|
### "Digital Asset Links validation failed"
|
||||||
|
- assetlinks.json muss öffentlich erreichbar sein
|
||||||
|
- SHA256 Fingerprint muss stimmen
|
||||||
|
- Content-Type: application/json
|
||||||
|
|
||||||
|
### Icon-Probleme
|
||||||
|
- Alle Größen müssen vorhanden sein
|
||||||
|
- PNG-Format erforderlich
|
||||||
|
- Transparenz vermeiden für adaptive Icons
|
||||||
|
|
||||||
|
## Empfehlungen
|
||||||
|
|
||||||
|
1. **Verwenden Sie PWABuilder** für den Anfang
|
||||||
|
2. **Bubblewrap** für mehr Kontrolle
|
||||||
|
3. **Android Studio** nur bei speziellen Anforderungen
|
||||||
|
4. Testen Sie auf verschiedenen Geräten
|
||||||
|
5. Behalten Sie die Keystore-Datei sicher auf!
|
||||||
|
|
||||||
|
## Nächste Schritte
|
||||||
|
|
||||||
|
1. Icons generieren (falls noch nicht vorhanden)
|
||||||
|
2. PWABuilder Test durchführen
|
||||||
|
3. Test-APK erstellen und installieren
|
||||||
|
4. Bei Erfolg: Play Store Konto einrichten
|
||||||
|
5. Produktions-APK mit Signing Key erstellen
|
||||||
|
6. Im Play Store veröffentlichen
|
||||||
|
|
||||||
|
Bei Fragen oder Problemen können Sie die offizielle Dokumentation konsultieren:
|
||||||
|
- [PWABuilder Docs](https://docs.pwabuilder.com/)
|
||||||
|
- [Bubblewrap GitHub](https://github.com/GoogleChromeLabs/bubblewrap)
|
||||||
|
- [TWA Documentation](https://developers.google.com/web/android/trusted-web-activity)
|
||||||
51
create-placeholder-icons.sh
Ausführbare Datei
@ -0,0 +1,51 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Erstellt Platzhalter-Icons für TaskMate PWA
|
||||||
|
|
||||||
|
cd /home/claude-dev/TaskMate/frontend/assets/icons/
|
||||||
|
|
||||||
|
# Funktion zum Erstellen eines PNG aus SVG mit ImageMagick
|
||||||
|
create_icon() {
|
||||||
|
size=$1
|
||||||
|
echo "Erstelle icon-${size}x${size}.png..."
|
||||||
|
|
||||||
|
# Mit rsvg-convert (falls verfügbar)
|
||||||
|
if command -v rsvg-convert &> /dev/null; then
|
||||||
|
rsvg-convert -w $size -h $size taskmate-logo.svg -o icon-${size}x${size}.png
|
||||||
|
rsvg-convert -w $size -h $size taskmate-logo.svg -o icon-maskable-${size}x${size}.png
|
||||||
|
# Mit convert/ImageMagick (falls verfügbar)
|
||||||
|
elif command -v convert &> /dev/null; then
|
||||||
|
convert -background transparent -resize ${size}x${size} taskmate-logo.svg icon-${size}x${size}.png
|
||||||
|
convert -background transparent -resize ${size}x${size} taskmate-logo.svg icon-maskable-${size}x${size}.png
|
||||||
|
else
|
||||||
|
echo "Weder rsvg-convert noch ImageMagick gefunden!"
|
||||||
|
echo "Installieren Sie eines davon mit:"
|
||||||
|
echo " sudo apt-get install librsvg2-bin"
|
||||||
|
echo " oder"
|
||||||
|
echo " sudo apt-get install imagemagick"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Alle benötigten Größen
|
||||||
|
sizes=(48 72 96 128 144 152 192 384 512)
|
||||||
|
|
||||||
|
# Icons erstellen
|
||||||
|
for size in "${sizes[@]}"; do
|
||||||
|
create_icon $size
|
||||||
|
done
|
||||||
|
|
||||||
|
# Zusätzliche Icons
|
||||||
|
if command -v rsvg-convert &> /dev/null; then
|
||||||
|
echo "Erstelle Shortcut-Icons..."
|
||||||
|
rsvg-convert -w 96 -h 96 taskmate-logo.svg -o add-task-96x96.png
|
||||||
|
rsvg-convert -w 96 -h 96 taskmate-logo.svg -o calendar-96x96.png
|
||||||
|
elif command -v convert &> /dev/null; then
|
||||||
|
echo "Erstelle Shortcut-Icons..."
|
||||||
|
convert -background transparent -resize 96x96 taskmate-logo.svg add-task-96x96.png
|
||||||
|
convert -background transparent -resize 96x96 taskmate-logo.svg calendar-96x96.png
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Fertig! Icons wurden erstellt."
|
||||||
|
echo ""
|
||||||
|
echo "Nächster Schritt: Icons in Docker Container kopieren:"
|
||||||
|
echo "docker cp *.png taskmate:/app/public/assets/icons/"
|
||||||
BIN
data/taskmate.db
10
frontend/.well-known/assetlinks.json
Normale Datei
@ -0,0 +1,10 @@
|
|||||||
|
[{
|
||||||
|
"relation": ["delegate_permission/common.handle_all_urls"],
|
||||||
|
"target": {
|
||||||
|
"namespace": "android_app",
|
||||||
|
"package_name": "de.aegissight.taskmate",
|
||||||
|
"sha256_cert_fingerprints": [
|
||||||
|
"TO_BE_REPLACED_WITH_YOUR_APP_SIGNING_KEY_FINGERPRINT"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}]
|
||||||
BIN
frontend/assets/icons/add-task-96x96.png
Normale Datei
|
Nachher Breite: | Höhe: | Größe: 3.9 KiB |
BIN
frontend/assets/icons/android-launchericon-144-144.png
Normale Datei
|
Nachher Breite: | Höhe: | Größe: 5.7 KiB |
BIN
frontend/assets/icons/android-launchericon-192-192.png
Normale Datei
|
Nachher Breite: | Höhe: | Größe: 9.0 KiB |
BIN
frontend/assets/icons/android-launchericon-48-48.png
Normale Datei
|
Nachher Breite: | Höhe: | Größe: 1.7 KiB |
BIN
frontend/assets/icons/android-launchericon-512-512.png
Normale Datei
|
Nachher Breite: | Höhe: | Größe: 31 KiB |
BIN
frontend/assets/icons/android-launchericon-72-72.png
Normale Datei
|
Nachher Breite: | Höhe: | Größe: 2.7 KiB |
BIN
frontend/assets/icons/android-launchericon-96-96.png
Normale Datei
|
Nachher Breite: | Höhe: | Größe: 3.9 KiB |
BIN
frontend/assets/icons/calendar-96x96.png
Normale Datei
|
Nachher Breite: | Höhe: | Größe: 3.9 KiB |
198
frontend/assets/icons/create-icons.html
Normale Datei
@ -0,0 +1,198 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Icon Generator</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>TaskMate Icon Generator</h1>
|
||||||
|
<p>Diese Seite generiert die Icons im Browser. Klicke auf jeden Link, um die Icons herunterzuladen.</p>
|
||||||
|
|
||||||
|
<canvas id="canvas" style="border: 1px solid #ccc; display: none;"></canvas>
|
||||||
|
|
||||||
|
<div id="links"></div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const sizes = [48, 72, 96, 128, 144, 152, 192, 384, 512];
|
||||||
|
const canvas = document.getElementById('canvas');
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
const linksDiv = document.getElementById('links');
|
||||||
|
|
||||||
|
function drawIcon(size) {
|
||||||
|
canvas.width = size;
|
||||||
|
canvas.height = size;
|
||||||
|
|
||||||
|
// Black background with rounded corners
|
||||||
|
const radius = size * 0.1875; // 96/512 ratio
|
||||||
|
ctx.fillStyle = '#000000';
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(radius, 0);
|
||||||
|
ctx.lineTo(size - radius, 0);
|
||||||
|
ctx.quadraticCurveTo(size, 0, size, radius);
|
||||||
|
ctx.lineTo(size, size - radius);
|
||||||
|
ctx.quadraticCurveTo(size, size, size - radius, size);
|
||||||
|
ctx.lineTo(radius, size);
|
||||||
|
ctx.quadraticCurveTo(0, size, 0, size - radius);
|
||||||
|
ctx.lineTo(0, radius);
|
||||||
|
ctx.quadraticCurveTo(0, 0, radius, 0);
|
||||||
|
ctx.closePath();
|
||||||
|
ctx.fill();
|
||||||
|
|
||||||
|
// Draw task list
|
||||||
|
const lineWidth = size * 0.0625; // 32/512 ratio
|
||||||
|
const margin = size * 0.3125; // 160/512 ratio
|
||||||
|
const lineLength = size * 0.375; // 192/512 ratio
|
||||||
|
const spacing = size * 0.1875; // 96/512 ratio
|
||||||
|
const dotRadius = size * 0.0234; // 12/512 ratio
|
||||||
|
const dotX = size * 0.25; // 128/512 ratio
|
||||||
|
|
||||||
|
ctx.strokeStyle = '#00D4FF';
|
||||||
|
ctx.fillStyle = '#00D4FF';
|
||||||
|
ctx.lineWidth = lineWidth;
|
||||||
|
ctx.lineCap = 'round';
|
||||||
|
|
||||||
|
for (let i = 0; i < 3; i++) {
|
||||||
|
const y = margin + (i * spacing);
|
||||||
|
|
||||||
|
// Draw line
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(margin, y);
|
||||||
|
ctx.lineTo(margin + lineLength, y);
|
||||||
|
ctx.stroke();
|
||||||
|
|
||||||
|
// Draw dot
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(dotX, y, dotRadius, 0, Math.PI * 2);
|
||||||
|
ctx.fill();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate all icons
|
||||||
|
sizes.forEach(size => {
|
||||||
|
drawIcon(size);
|
||||||
|
|
||||||
|
canvas.toBlob(blob => {
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = url;
|
||||||
|
link.download = `icon-${size}x${size}.png`;
|
||||||
|
link.textContent = `icon-${size}x${size}.png`;
|
||||||
|
link.style.display = 'block';
|
||||||
|
link.style.marginBottom = '10px';
|
||||||
|
linksDiv.appendChild(link);
|
||||||
|
}, 'image/png');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Shortcut icons
|
||||||
|
function drawAddTaskIcon() {
|
||||||
|
canvas.width = 96;
|
||||||
|
canvas.height = 96;
|
||||||
|
|
||||||
|
// Black background with rounded corners
|
||||||
|
const radius = 16;
|
||||||
|
ctx.fillStyle = '#000000';
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(radius, 0);
|
||||||
|
ctx.lineTo(96 - radius, 0);
|
||||||
|
ctx.quadraticCurveTo(96, 0, 96, radius);
|
||||||
|
ctx.lineTo(96, 96 - radius);
|
||||||
|
ctx.quadraticCurveTo(96, 96, 96 - radius, 96);
|
||||||
|
ctx.lineTo(radius, 96);
|
||||||
|
ctx.quadraticCurveTo(0, 96, 0, 96 - radius);
|
||||||
|
ctx.lineTo(0, radius);
|
||||||
|
ctx.quadraticCurveTo(0, 0, radius, 0);
|
||||||
|
ctx.closePath();
|
||||||
|
ctx.fill();
|
||||||
|
|
||||||
|
// Draw plus
|
||||||
|
ctx.strokeStyle = '#00D4FF';
|
||||||
|
ctx.lineWidth = 6;
|
||||||
|
ctx.lineCap = 'round';
|
||||||
|
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(48, 32);
|
||||||
|
ctx.lineTo(48, 64);
|
||||||
|
ctx.stroke();
|
||||||
|
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(32, 48);
|
||||||
|
ctx.lineTo(64, 48);
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawCalendarIcon() {
|
||||||
|
canvas.width = 96;
|
||||||
|
canvas.height = 96;
|
||||||
|
|
||||||
|
// Black background with rounded corners
|
||||||
|
const radius = 16;
|
||||||
|
ctx.fillStyle = '#000000';
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(radius, 0);
|
||||||
|
ctx.lineTo(96 - radius, 0);
|
||||||
|
ctx.quadraticCurveTo(96, 0, 96, radius);
|
||||||
|
ctx.lineTo(96, 96 - radius);
|
||||||
|
ctx.quadraticCurveTo(96, 96, 96 - radius, 96);
|
||||||
|
ctx.lineTo(radius, 96);
|
||||||
|
ctx.quadraticCurveTo(0, 96, 0, 96 - radius);
|
||||||
|
ctx.lineTo(0, radius);
|
||||||
|
ctx.quadraticCurveTo(0, 0, radius, 0);
|
||||||
|
ctx.closePath();
|
||||||
|
ctx.fill();
|
||||||
|
|
||||||
|
// Draw calendar
|
||||||
|
ctx.strokeStyle = '#00D4FF';
|
||||||
|
ctx.lineWidth = 4;
|
||||||
|
ctx.lineCap = 'round';
|
||||||
|
ctx.fillStyle = 'transparent';
|
||||||
|
|
||||||
|
// Calendar body
|
||||||
|
ctx.strokeRect(20, 28, 56, 48);
|
||||||
|
|
||||||
|
// Top hooks
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(32, 20);
|
||||||
|
ctx.lineTo(32, 36);
|
||||||
|
ctx.stroke();
|
||||||
|
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(64, 20);
|
||||||
|
ctx.lineTo(64, 36);
|
||||||
|
ctx.stroke();
|
||||||
|
|
||||||
|
// Horizontal line
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(20, 44);
|
||||||
|
ctx.lineTo(76, 44);
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
drawAddTaskIcon();
|
||||||
|
canvas.toBlob(blob => {
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = url;
|
||||||
|
link.download = 'add-task-96x96.png';
|
||||||
|
link.textContent = 'add-task-96x96.png';
|
||||||
|
link.style.display = 'block';
|
||||||
|
link.style.marginBottom = '10px';
|
||||||
|
linksDiv.appendChild(link);
|
||||||
|
}, 'image/png');
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
drawCalendarIcon();
|
||||||
|
canvas.toBlob(blob => {
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = url;
|
||||||
|
link.download = 'calendar-96x96.png';
|
||||||
|
link.textContent = 'calendar-96x96.png';
|
||||||
|
link.style.display = 'block';
|
||||||
|
link.style.marginBottom = '10px';
|
||||||
|
linksDiv.appendChild(link);
|
||||||
|
}, 'image/png');
|
||||||
|
}, 100);
|
||||||
|
}, 100 * sizes.length);
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
BIN
frontend/assets/icons/favicon-16x16.png
Normale Datei
|
Nachher Breite: | Höhe: | Größe: 1.7 KiB |
BIN
frontend/assets/icons/favicon-32x32.png
Normale Datei
|
Nachher Breite: | Höhe: | Größe: 1.7 KiB |
154
frontend/assets/icons/generate-icons.js
Normale Datei
@ -0,0 +1,154 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
const { createCanvas } = require('canvas');
|
||||||
|
|
||||||
|
// Icon-Größen für PWA
|
||||||
|
const sizes = [48, 72, 96, 128, 144, 152, 192, 384, 512];
|
||||||
|
|
||||||
|
function drawIcon(ctx, size) {
|
||||||
|
// Black background with rounded corners
|
||||||
|
const radius = size * 0.1875; // 96/512 ratio
|
||||||
|
ctx.fillStyle = '#000000';
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(radius, 0);
|
||||||
|
ctx.lineTo(size - radius, 0);
|
||||||
|
ctx.quadraticCurveTo(size, 0, size, radius);
|
||||||
|
ctx.lineTo(size, size - radius);
|
||||||
|
ctx.quadraticCurveTo(size, size, size - radius, size);
|
||||||
|
ctx.lineTo(radius, size);
|
||||||
|
ctx.quadraticCurveTo(0, size, 0, size - radius);
|
||||||
|
ctx.lineTo(0, radius);
|
||||||
|
ctx.quadraticCurveTo(0, 0, radius, 0);
|
||||||
|
ctx.closePath();
|
||||||
|
ctx.fill();
|
||||||
|
|
||||||
|
// Draw task list
|
||||||
|
const lineWidth = size * 0.0625; // 32/512 ratio
|
||||||
|
const margin = size * 0.3125; // 160/512 ratio
|
||||||
|
const lineLength = size * 0.375; // 192/512 ratio
|
||||||
|
const spacing = size * 0.1875; // 96/512 ratio
|
||||||
|
const dotRadius = size * 0.0234; // 12/512 ratio
|
||||||
|
const dotX = size * 0.25; // 128/512 ratio
|
||||||
|
|
||||||
|
ctx.strokeStyle = '#00D4FF';
|
||||||
|
ctx.fillStyle = '#00D4FF';
|
||||||
|
ctx.lineWidth = lineWidth;
|
||||||
|
ctx.lineCap = 'round';
|
||||||
|
|
||||||
|
for (let i = 0; i < 3; i++) {
|
||||||
|
const y = margin + (i * spacing);
|
||||||
|
|
||||||
|
// Draw line
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(margin, y);
|
||||||
|
ctx.lineTo(margin + lineLength, y);
|
||||||
|
ctx.stroke();
|
||||||
|
|
||||||
|
// Draw dot
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(dotX, y, dotRadius, 0, Math.PI * 2);
|
||||||
|
ctx.fill();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate all icons
|
||||||
|
sizes.forEach(size => {
|
||||||
|
const canvas = createCanvas(size, size);
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
|
||||||
|
drawIcon(ctx, size);
|
||||||
|
|
||||||
|
const buffer = canvas.toBuffer('image/png');
|
||||||
|
fs.writeFileSync(`icon-${size}x${size}.png`, buffer);
|
||||||
|
console.log(`Created: icon-${size}x${size}.png`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Shortcut icons
|
||||||
|
function drawAddTaskIcon(ctx) {
|
||||||
|
// Black background with rounded corners
|
||||||
|
const radius = 16;
|
||||||
|
ctx.fillStyle = '#000000';
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(radius, 0);
|
||||||
|
ctx.lineTo(96 - radius, 0);
|
||||||
|
ctx.quadraticCurveTo(96, 0, 96, radius);
|
||||||
|
ctx.lineTo(96, 96 - radius);
|
||||||
|
ctx.quadraticCurveTo(96, 96, 96 - radius, 96);
|
||||||
|
ctx.lineTo(radius, 96);
|
||||||
|
ctx.quadraticCurveTo(0, 96, 0, 96 - radius);
|
||||||
|
ctx.lineTo(0, radius);
|
||||||
|
ctx.quadraticCurveTo(0, 0, radius, 0);
|
||||||
|
ctx.closePath();
|
||||||
|
ctx.fill();
|
||||||
|
|
||||||
|
// Draw plus
|
||||||
|
ctx.strokeStyle = '#00D4FF';
|
||||||
|
ctx.lineWidth = 6;
|
||||||
|
ctx.lineCap = 'round';
|
||||||
|
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(48, 32);
|
||||||
|
ctx.lineTo(48, 64);
|
||||||
|
ctx.stroke();
|
||||||
|
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(32, 48);
|
||||||
|
ctx.lineTo(64, 48);
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawCalendarIcon(ctx) {
|
||||||
|
// Black background with rounded corners
|
||||||
|
const radius = 16;
|
||||||
|
ctx.fillStyle = '#000000';
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(radius, 0);
|
||||||
|
ctx.lineTo(96 - radius, 0);
|
||||||
|
ctx.quadraticCurveTo(96, 0, 96, radius);
|
||||||
|
ctx.lineTo(96, 96 - radius);
|
||||||
|
ctx.quadraticCurveTo(96, 96, 96 - radius, 96);
|
||||||
|
ctx.lineTo(radius, 96);
|
||||||
|
ctx.quadraticCurveTo(0, 96, 0, 96 - radius);
|
||||||
|
ctx.lineTo(0, radius);
|
||||||
|
ctx.quadraticCurveTo(0, 0, radius, 0);
|
||||||
|
ctx.closePath();
|
||||||
|
ctx.fill();
|
||||||
|
|
||||||
|
// Draw calendar
|
||||||
|
ctx.strokeStyle = '#00D4FF';
|
||||||
|
ctx.lineWidth = 4;
|
||||||
|
ctx.lineCap = 'round';
|
||||||
|
ctx.fillStyle = 'transparent';
|
||||||
|
|
||||||
|
// Calendar body
|
||||||
|
ctx.strokeRect(20, 28, 56, 48);
|
||||||
|
|
||||||
|
// Top hooks
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(32, 20);
|
||||||
|
ctx.lineTo(32, 36);
|
||||||
|
ctx.stroke();
|
||||||
|
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(64, 20);
|
||||||
|
ctx.lineTo(64, 36);
|
||||||
|
ctx.stroke();
|
||||||
|
|
||||||
|
// Horizontal line
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(20, 44);
|
||||||
|
ctx.lineTo(76, 44);
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create shortcut icons
|
||||||
|
const addTaskCanvas = createCanvas(96, 96);
|
||||||
|
drawAddTaskIcon(addTaskCanvas.getContext('2d'));
|
||||||
|
fs.writeFileSync('add-task-96x96.png', addTaskCanvas.toBuffer('image/png'));
|
||||||
|
console.log('Created: add-task-96x96.png');
|
||||||
|
|
||||||
|
const calendarCanvas = createCanvas(96, 96);
|
||||||
|
drawCalendarIcon(calendarCanvas.getContext('2d'));
|
||||||
|
fs.writeFileSync('calendar-96x96.png', calendarCanvas.toBuffer('image/png'));
|
||||||
|
console.log('Created: calendar-96x96.png');
|
||||||
|
|
||||||
|
console.log('\nAll icons generated successfully!');
|
||||||
57
frontend/assets/icons/generate-icons.py
Normale Datei
@ -0,0 +1,57 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Generiert PWA Icons in verschiedenen Größen aus SVG
|
||||||
|
Benötigt: pip install cairosvg pillow
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
from PIL import Image
|
||||||
|
import io
|
||||||
|
import cairosvg
|
||||||
|
|
||||||
|
# Icon-Größen für PWA
|
||||||
|
sizes = [48, 72, 96, 128, 144, 152, 192, 384, 512]
|
||||||
|
|
||||||
|
# SVG-Datei einlesen
|
||||||
|
svg_path = 'task.svg'
|
||||||
|
with open(svg_path, 'r') as f:
|
||||||
|
svg_data = f.read()
|
||||||
|
|
||||||
|
# Icons generieren
|
||||||
|
for size in sizes:
|
||||||
|
# SVG zu PNG konvertieren
|
||||||
|
png_data = cairosvg.svg2png(
|
||||||
|
bytestring=svg_data.encode('utf-8'),
|
||||||
|
output_width=size,
|
||||||
|
output_height=size
|
||||||
|
)
|
||||||
|
|
||||||
|
# PNG speichern
|
||||||
|
img = Image.open(io.BytesIO(png_data))
|
||||||
|
img.save(f'icon-{size}x{size}.png', 'PNG')
|
||||||
|
print(f'Erstellt: icon-{size}x{size}.png')
|
||||||
|
|
||||||
|
# Zusätzliche Icons für Shortcuts
|
||||||
|
shortcuts = [
|
||||||
|
('add-task', '''<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 96 96" fill="none">
|
||||||
|
<rect width="96" height="96" rx="16" fill="#000000"/>
|
||||||
|
<path d="M48 32v32M32 48h32" stroke="#00D4FF" stroke-width="6" stroke-linecap="round"/>
|
||||||
|
</svg>'''),
|
||||||
|
('calendar', '''<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 96 96" fill="none">
|
||||||
|
<rect width="96" height="96" rx="16" fill="#000000"/>
|
||||||
|
<rect x="20" y="28" width="56" height="48" rx="4" stroke="#00D4FF" stroke-width="4" fill="none"/>
|
||||||
|
<path d="M32 20v16M64 20v16M20 44h56" stroke="#00D4FF" stroke-width="4" stroke-linecap="round"/>
|
||||||
|
</svg>''')
|
||||||
|
]
|
||||||
|
|
||||||
|
for name, svg in shortcuts:
|
||||||
|
png_data = cairosvg.svg2png(
|
||||||
|
bytestring=svg.encode('utf-8'),
|
||||||
|
output_width=96,
|
||||||
|
output_height=96
|
||||||
|
)
|
||||||
|
img = Image.open(io.BytesIO(png_data))
|
||||||
|
img.save(f'{name}-96x96.png', 'PNG')
|
||||||
|
print(f'Erstellt: {name}-96x96.png')
|
||||||
|
|
||||||
|
print('\nAlle Icons wurden erfolgreich generiert!')
|
||||||
BIN
frontend/assets/icons/icon-128x128.png
Normale Datei
|
Nachher Breite: | Höhe: | Größe: 5.7 KiB |
BIN
frontend/assets/icons/icon-144x144.png
Normale Datei
|
Nachher Breite: | Höhe: | Größe: 5.7 KiB |
BIN
frontend/assets/icons/icon-152x152.png
Normale Datei
|
Nachher Breite: | Höhe: | Größe: 9.0 KiB |
BIN
frontend/assets/icons/icon-192x192.png
Normale Datei
|
Nachher Breite: | Höhe: | Größe: 9.0 KiB |
BIN
frontend/assets/icons/icon-384x384.png
Normale Datei
|
Nachher Breite: | Höhe: | Größe: 31 KiB |
BIN
frontend/assets/icons/icon-48x48.png
Normale Datei
|
Nachher Breite: | Höhe: | Größe: 1.7 KiB |
BIN
frontend/assets/icons/icon-512x512.png
Normale Datei
|
Nachher Breite: | Höhe: | Größe: 31 KiB |
BIN
frontend/assets/icons/icon-72x72.png
Normale Datei
|
Nachher Breite: | Höhe: | Größe: 2.7 KiB |
BIN
frontend/assets/icons/icon-96x96.png
Normale Datei
|
Nachher Breite: | Höhe: | Größe: 3.9 KiB |
21
frontend/assets/icons/icon-base64.txt
Normale Datei
@ -0,0 +1,21 @@
|
|||||||
|
# TaskMate Icon als Base64 PNG
|
||||||
|
|
||||||
|
Für die Icon-Generierung können Sie einen Online-Service nutzen:
|
||||||
|
|
||||||
|
1. Öffnen Sie https://cloudconvert.com/svg-to-png oder einen ähnlichen Konverter
|
||||||
|
2. Laden Sie die task.svg Datei hoch
|
||||||
|
3. Erstellen Sie PNGs in folgenden Größen: 48, 72, 96, 128, 144, 152, 192, 384, 512
|
||||||
|
|
||||||
|
Oder nutzen Sie diesen Befehl mit ImageMagick (falls installiert):
|
||||||
|
```bash
|
||||||
|
for size in 48 72 96 128 144 152 192 384 512; do
|
||||||
|
convert -background transparent -resize ${size}x${size} task.svg icon-${size}x${size}.png
|
||||||
|
done
|
||||||
|
```
|
||||||
|
|
||||||
|
Alternativ mit rsvg-convert:
|
||||||
|
```bash
|
||||||
|
for size in 48 72 96 128 144 152 192 384 512; do
|
||||||
|
rsvg-convert -w $size -h $size task.svg -o icon-${size}x${size}.png
|
||||||
|
done
|
||||||
|
```
|
||||||
187
frontend/assets/icons/icon-generator-simple.html
Normale Datei
@ -0,0 +1,187 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>TaskMate Icon Generator - Einfach</title>
|
||||||
|
<style>
|
||||||
|
body { font-family: Arial, sans-serif; padding: 20px; }
|
||||||
|
.status { margin: 20px 0; padding: 10px; background: #e8f4f8; }
|
||||||
|
button { padding: 10px 20px; font-size: 16px; cursor: pointer; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>TaskMate Icon Generator</h1>
|
||||||
|
<p>Dieser Generator erstellt alle benötigten Icons automatisch.</p>
|
||||||
|
|
||||||
|
<button onclick="generateAllIcons()">Alle Icons generieren und herunterladen</button>
|
||||||
|
|
||||||
|
<div class="status" id="status">Bereit zum Generieren...</div>
|
||||||
|
|
||||||
|
<canvas id="canvas" style="display: none;"></canvas>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const sizes = [48, 72, 96, 128, 144, 152, 192, 384, 512];
|
||||||
|
|
||||||
|
function downloadCanvas(canvas, filename) {
|
||||||
|
canvas.toBlob(function(blob) {
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.download = filename;
|
||||||
|
link.href = URL.createObjectURL(blob);
|
||||||
|
link.click();
|
||||||
|
URL.revokeObjectURL(link.href);
|
||||||
|
}, 'image/png');
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawLogo(ctx, size) {
|
||||||
|
// Hintergrund
|
||||||
|
ctx.fillStyle = '#0A1832';
|
||||||
|
ctx.fillRect(0, 0, size, size);
|
||||||
|
|
||||||
|
// Einfaches Shield/Logo Design
|
||||||
|
const scale = size / 100;
|
||||||
|
ctx.save();
|
||||||
|
ctx.scale(scale, scale);
|
||||||
|
ctx.translate(50, 50);
|
||||||
|
|
||||||
|
// Linke Seite (Blau)
|
||||||
|
ctx.fillStyle = '#00D4FF';
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(-30, -35);
|
||||||
|
ctx.lineTo(0, -35);
|
||||||
|
ctx.lineTo(0, 30);
|
||||||
|
ctx.quadraticCurveTo(-15, 40, -30, 30);
|
||||||
|
ctx.closePath();
|
||||||
|
ctx.fill();
|
||||||
|
|
||||||
|
// Rechte Seite (Gold)
|
||||||
|
ctx.fillStyle = '#C8A851';
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(0, -35);
|
||||||
|
ctx.lineTo(30, -35);
|
||||||
|
ctx.lineTo(30, 30);
|
||||||
|
ctx.quadraticCurveTo(15, 40, 0, 30);
|
||||||
|
ctx.closePath();
|
||||||
|
ctx.fill();
|
||||||
|
|
||||||
|
ctx.restore();
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawMaskableLogo(ctx, size) {
|
||||||
|
// Hintergrund (mit extra Padding für Maskable)
|
||||||
|
ctx.fillStyle = '#0A1832';
|
||||||
|
ctx.fillRect(0, 0, size, size);
|
||||||
|
|
||||||
|
// Logo kleiner für Safe Area
|
||||||
|
const scale = size / 140; // Extra Padding
|
||||||
|
ctx.save();
|
||||||
|
ctx.scale(scale, scale);
|
||||||
|
ctx.translate(70, 70);
|
||||||
|
|
||||||
|
// Linke Seite (Blau)
|
||||||
|
ctx.fillStyle = '#00D4FF';
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(-30, -35);
|
||||||
|
ctx.lineTo(0, -35);
|
||||||
|
ctx.lineTo(0, 30);
|
||||||
|
ctx.quadraticCurveTo(-15, 40, -30, 30);
|
||||||
|
ctx.closePath();
|
||||||
|
ctx.fill();
|
||||||
|
|
||||||
|
// Rechte Seite (Gold)
|
||||||
|
ctx.fillStyle = '#C8A851';
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(0, -35);
|
||||||
|
ctx.lineTo(30, -35);
|
||||||
|
ctx.lineTo(30, 30);
|
||||||
|
ctx.quadraticCurveTo(15, 40, 0, 30);
|
||||||
|
ctx.closePath();
|
||||||
|
ctx.fill();
|
||||||
|
|
||||||
|
ctx.restore();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function generateAllIcons() {
|
||||||
|
const status = document.getElementById('status');
|
||||||
|
const canvas = document.getElementById('canvas');
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
|
||||||
|
status.textContent = 'Generiere Icons...';
|
||||||
|
|
||||||
|
// Generiere normale Icons
|
||||||
|
for (let i = 0; i < sizes.length; i++) {
|
||||||
|
const size = sizes[i];
|
||||||
|
canvas.width = size;
|
||||||
|
canvas.height = size;
|
||||||
|
|
||||||
|
drawLogo(ctx, size);
|
||||||
|
|
||||||
|
// Download mit Verzögerung
|
||||||
|
await new Promise(resolve => {
|
||||||
|
setTimeout(() => {
|
||||||
|
downloadCanvas(canvas, `icon-${size}x${size}.png`);
|
||||||
|
status.textContent = `Normal Icon ${size}x${size} generiert...`;
|
||||||
|
resolve();
|
||||||
|
}, 500 * i);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generiere Maskable Icons
|
||||||
|
for (let i = 0; i < sizes.length; i++) {
|
||||||
|
const size = sizes[i];
|
||||||
|
canvas.width = size;
|
||||||
|
canvas.height = size;
|
||||||
|
|
||||||
|
drawMaskableLogo(ctx, size);
|
||||||
|
|
||||||
|
// Download mit Verzögerung
|
||||||
|
await new Promise(resolve => {
|
||||||
|
setTimeout(() => {
|
||||||
|
downloadCanvas(canvas, `icon-maskable-${size}x${size}.png`);
|
||||||
|
status.textContent = `Maskable Icon ${size}x${size} generiert...`;
|
||||||
|
resolve();
|
||||||
|
}, 500 * (i + sizes.length));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Zusätzliche Icons
|
||||||
|
// Add Task
|
||||||
|
canvas.width = 96;
|
||||||
|
canvas.height = 96;
|
||||||
|
ctx.fillStyle = '#0A1832';
|
||||||
|
ctx.fillRect(0, 0, 96, 96);
|
||||||
|
ctx.strokeStyle = '#00D4FF';
|
||||||
|
ctx.lineWidth = 6;
|
||||||
|
ctx.lineCap = 'round';
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(48, 24);
|
||||||
|
ctx.lineTo(48, 72);
|
||||||
|
ctx.moveTo(24, 48);
|
||||||
|
ctx.lineTo(72, 48);
|
||||||
|
ctx.stroke();
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
downloadCanvas(canvas, 'add-task-96x96.png');
|
||||||
|
}, 500 * sizes.length * 2);
|
||||||
|
|
||||||
|
// Calendar
|
||||||
|
setTimeout(() => {
|
||||||
|
ctx.fillStyle = '#0A1832';
|
||||||
|
ctx.fillRect(0, 0, 96, 96);
|
||||||
|
ctx.strokeStyle = '#C8A851';
|
||||||
|
ctx.lineWidth = 4;
|
||||||
|
ctx.strokeRect(20, 28, 56, 48);
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(32, 20);
|
||||||
|
ctx.lineTo(32, 36);
|
||||||
|
ctx.moveTo(64, 20);
|
||||||
|
ctx.lineTo(64, 36);
|
||||||
|
ctx.moveTo(20, 44);
|
||||||
|
ctx.lineTo(76, 44);
|
||||||
|
ctx.stroke();
|
||||||
|
downloadCanvas(canvas, 'calendar-96x96.png');
|
||||||
|
|
||||||
|
status.textContent = '✅ Alle Icons wurden generiert! Bitte alle Downloads speichern.';
|
||||||
|
}, 500 * sizes.length * 2 + 500);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
253
frontend/assets/icons/icon-generator.html
Normale Datei
@ -0,0 +1,253 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>TaskMate Icon Generator</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
padding: 20px;
|
||||||
|
background: #f5f5f5;
|
||||||
|
}
|
||||||
|
#canvas {
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
background: white;
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
.icon-link {
|
||||||
|
display: inline-block;
|
||||||
|
margin: 5px;
|
||||||
|
padding: 8px 16px;
|
||||||
|
background: #0A1832;
|
||||||
|
color: white;
|
||||||
|
text-decoration: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
.icon-link:hover {
|
||||||
|
background: #00D4FF;
|
||||||
|
}
|
||||||
|
h2 {
|
||||||
|
color: #0A1832;
|
||||||
|
margin-top: 30px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>TaskMate Icon Generator</h1>
|
||||||
|
<p>Klicken Sie auf die Links, um die Icons herunterzuladen:</p>
|
||||||
|
|
||||||
|
<canvas id="canvas" width="512" height="512"></canvas>
|
||||||
|
|
||||||
|
<h2>Haupt-Icons:</h2>
|
||||||
|
<div id="main-icons"></div>
|
||||||
|
|
||||||
|
<h2>Zusätzliche Icons:</h2>
|
||||||
|
<div id="extra-icons"></div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const sizes = [48, 72, 96, 128, 144, 152, 192, 384, 512];
|
||||||
|
const canvas = document.getElementById('canvas');
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
const mainIconsDiv = document.getElementById('main-icons');
|
||||||
|
const extraIconsDiv = document.getElementById('extra-icons');
|
||||||
|
|
||||||
|
// Funktion zum Zeichnen des TaskMate Logos
|
||||||
|
function drawTaskMateLogo(ctx, size) {
|
||||||
|
// Skala berechnen
|
||||||
|
const scale = size / 512;
|
||||||
|
|
||||||
|
// Canvas-Größe setzen
|
||||||
|
canvas.width = size;
|
||||||
|
canvas.height = size;
|
||||||
|
|
||||||
|
// Skalierung anwenden
|
||||||
|
ctx.scale(scale, scale);
|
||||||
|
|
||||||
|
// Hintergrund (Navy-Blau mit abgerundeten Ecken)
|
||||||
|
const radius = 96;
|
||||||
|
ctx.fillStyle = '#0A1832';
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(radius, 0);
|
||||||
|
ctx.lineTo(512 - radius, 0);
|
||||||
|
ctx.quadraticCurveTo(512, 0, 512, radius);
|
||||||
|
ctx.lineTo(512, 512 - radius);
|
||||||
|
ctx.quadraticCurveTo(512, 512, 512 - radius, 512);
|
||||||
|
ctx.lineTo(radius, 512);
|
||||||
|
ctx.quadraticCurveTo(0, 512, 0, 512 - radius);
|
||||||
|
ctx.lineTo(0, radius);
|
||||||
|
ctx.quadraticCurveTo(0, 0, radius, 0);
|
||||||
|
ctx.closePath();
|
||||||
|
ctx.fill();
|
||||||
|
|
||||||
|
// Logo zentrieren und zeichnen
|
||||||
|
ctx.save();
|
||||||
|
ctx.translate(56, 56);
|
||||||
|
ctx.scale(1, 0.804);
|
||||||
|
|
||||||
|
// Rechte Seite (Gold) - vereinfachte Version
|
||||||
|
ctx.fillStyle = '#C8A851';
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(212, 240);
|
||||||
|
ctx.lineTo(270, 240);
|
||||||
|
ctx.quadraticCurveTo(346, 240, 346, 244);
|
||||||
|
ctx.lineTo(339, 272);
|
||||||
|
ctx.quadraticCurveTo(304, 327, 257, 380);
|
||||||
|
ctx.lineTo(257, 270);
|
||||||
|
ctx.lineTo(212, 271);
|
||||||
|
ctx.lineTo(212, 469);
|
||||||
|
ctx.quadraticCurveTo(275, 422, 340, 347);
|
||||||
|
ctx.quadraticCurveTo(373, 267, 377, 253);
|
||||||
|
ctx.lineTo(386, 202);
|
||||||
|
ctx.quadraticCurveTo(386, 197, 383, 198);
|
||||||
|
ctx.lineTo(256, 197);
|
||||||
|
ctx.lineTo(255, 90);
|
||||||
|
ctx.quadraticCurveTo(255, 74, 259, 74);
|
||||||
|
ctx.lineTo(345, 109);
|
||||||
|
ctx.lineTo(346, 164);
|
||||||
|
ctx.lineTo(387, 165);
|
||||||
|
ctx.lineTo(389, 137);
|
||||||
|
ctx.quadraticCurveTo(389, 81, 381, 77);
|
||||||
|
ctx.lineTo(212, 12);
|
||||||
|
ctx.closePath();
|
||||||
|
ctx.fill();
|
||||||
|
|
||||||
|
// Linke Seite (Hellblau) - vereinfachte Version
|
||||||
|
ctx.fillStyle = '#00D4FF';
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(32, 73);
|
||||||
|
ctx.quadraticCurveTo(16, 77, 16, 86);
|
||||||
|
ctx.lineTo(13, 193);
|
||||||
|
ctx.quadraticCurveTo(22, 247, 39, 302);
|
||||||
|
ctx.quadraticCurveTo(55, 335, 76, 368);
|
||||||
|
ctx.quadraticCurveTo(86, 379, 93, 385);
|
||||||
|
ctx.lineTo(94, 383);
|
||||||
|
ctx.lineTo(94, 309);
|
||||||
|
ctx.quadraticCurveTo(88, 300, 62, 248);
|
||||||
|
ctx.lineTo(144, 241);
|
||||||
|
ctx.lineTo(146, 440);
|
||||||
|
ctx.quadraticCurveTo(177, 464, 188, 468);
|
||||||
|
ctx.lineTo(188, 12);
|
||||||
|
ctx.closePath();
|
||||||
|
ctx.fill();
|
||||||
|
|
||||||
|
// Innerer Teil
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(146, 134);
|
||||||
|
ctx.lineTo(146, 192);
|
||||||
|
ctx.lineTo(58, 197);
|
||||||
|
ctx.lineTo(56, 126);
|
||||||
|
ctx.lineTo(136, 79);
|
||||||
|
ctx.lineTo(146, 77);
|
||||||
|
ctx.closePath();
|
||||||
|
ctx.fill();
|
||||||
|
|
||||||
|
ctx.restore();
|
||||||
|
|
||||||
|
// Skalierung zurücksetzen
|
||||||
|
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Haupt-Icons generieren
|
||||||
|
sizes.forEach(size => {
|
||||||
|
setTimeout(() => {
|
||||||
|
drawTaskMateLogo(ctx, size);
|
||||||
|
|
||||||
|
canvas.toBlob(blob => {
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = url;
|
||||||
|
link.download = `icon-${size}x${size}.png`;
|
||||||
|
link.textContent = `${size}x${size}`;
|
||||||
|
link.className = 'icon-link';
|
||||||
|
mainIconsDiv.appendChild(link);
|
||||||
|
}, 'image/png');
|
||||||
|
}, 100);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Zusätzliche Icons
|
||||||
|
setTimeout(() => {
|
||||||
|
// Add Task Icon
|
||||||
|
canvas.width = 96;
|
||||||
|
canvas.height = 96;
|
||||||
|
ctx.fillStyle = '#0A1832';
|
||||||
|
ctx.fillRect(0, 0, 96, 96);
|
||||||
|
ctx.strokeStyle = '#00D4FF';
|
||||||
|
ctx.lineWidth = 6;
|
||||||
|
ctx.lineCap = 'round';
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(48, 24);
|
||||||
|
ctx.lineTo(48, 72);
|
||||||
|
ctx.moveTo(24, 48);
|
||||||
|
ctx.lineTo(72, 48);
|
||||||
|
ctx.stroke();
|
||||||
|
|
||||||
|
canvas.toBlob(blob => {
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = url;
|
||||||
|
link.download = 'add-task-96x96.png';
|
||||||
|
link.textContent = 'Add Task';
|
||||||
|
link.className = 'icon-link';
|
||||||
|
extraIconsDiv.appendChild(link);
|
||||||
|
}, 'image/png');
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
// Calendar Icon
|
||||||
|
canvas.width = 96;
|
||||||
|
canvas.height = 96;
|
||||||
|
ctx.fillStyle = '#0A1832';
|
||||||
|
ctx.fillRect(0, 0, 96, 96);
|
||||||
|
ctx.strokeStyle = '#C8A851';
|
||||||
|
ctx.lineWidth = 4;
|
||||||
|
ctx.lineCap = 'round';
|
||||||
|
ctx.strokeRect(20, 28, 56, 48);
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(32, 20);
|
||||||
|
ctx.lineTo(32, 36);
|
||||||
|
ctx.moveTo(64, 20);
|
||||||
|
ctx.lineTo(64, 36);
|
||||||
|
ctx.moveTo(20, 44);
|
||||||
|
ctx.lineTo(76, 44);
|
||||||
|
ctx.stroke();
|
||||||
|
|
||||||
|
canvas.toBlob(blob => {
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = url;
|
||||||
|
link.download = 'calendar-96x96.png';
|
||||||
|
link.textContent = 'Calendar';
|
||||||
|
link.className = 'icon-link';
|
||||||
|
extraIconsDiv.appendChild(link);
|
||||||
|
}, 'image/png');
|
||||||
|
}, 1100);
|
||||||
|
|
||||||
|
// Favicon
|
||||||
|
setTimeout(() => {
|
||||||
|
drawTaskMateLogo(ctx, 32);
|
||||||
|
canvas.toBlob(blob => {
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = url;
|
||||||
|
link.download = 'favicon-32x32.png';
|
||||||
|
link.textContent = 'Favicon 32';
|
||||||
|
link.className = 'icon-link';
|
||||||
|
extraIconsDiv.appendChild(link);
|
||||||
|
}, 'image/png');
|
||||||
|
}, 1200);
|
||||||
|
|
||||||
|
// Apple Touch Icon
|
||||||
|
setTimeout(() => {
|
||||||
|
drawTaskMateLogo(ctx, 180);
|
||||||
|
canvas.toBlob(blob => {
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = url;
|
||||||
|
link.download = 'apple-touch-icon.png';
|
||||||
|
link.textContent = 'Apple Touch';
|
||||||
|
link.className = 'icon-link';
|
||||||
|
extraIconsDiv.appendChild(link);
|
||||||
|
}, 'image/png');
|
||||||
|
}, 1300);
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
BIN
frontend/assets/icons/icon-maskable-128x128.png
Normale Datei
|
Nachher Breite: | Höhe: | Größe: 5.7 KiB |
BIN
frontend/assets/icons/icon-maskable-144x144.png
Normale Datei
|
Nachher Breite: | Höhe: | Größe: 5.7 KiB |
BIN
frontend/assets/icons/icon-maskable-152x152.png
Normale Datei
|
Nachher Breite: | Höhe: | Größe: 9.0 KiB |
BIN
frontend/assets/icons/icon-maskable-192x192.png
Normale Datei
|
Nachher Breite: | Höhe: | Größe: 9.0 KiB |
BIN
frontend/assets/icons/icon-maskable-384x384.png
Normale Datei
|
Nachher Breite: | Höhe: | Größe: 31 KiB |
BIN
frontend/assets/icons/icon-maskable-48x48.png
Normale Datei
|
Nachher Breite: | Höhe: | Größe: 1.7 KiB |
BIN
frontend/assets/icons/icon-maskable-512x512.png
Normale Datei
|
Nachher Breite: | Höhe: | Größe: 31 KiB |
BIN
frontend/assets/icons/icon-maskable-72x72.png
Normale Datei
|
Nachher Breite: | Höhe: | Größe: 2.7 KiB |
BIN
frontend/assets/icons/icon-maskable-96x96.png
Normale Datei
|
Nachher Breite: | Höhe: | Größe: 3.9 KiB |
14
frontend/assets/icons/task.svg
Normale Datei
@ -0,0 +1,14 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg width="512" height="512" viewBox="0 0 512 512" version="1.1" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<!-- Hintergrund mit abgerundeten Ecken -->
|
||||||
|
<rect width="512" height="512" rx="96" fill="#0A1832"/>
|
||||||
|
|
||||||
|
<!-- Zentriertes und skaliertes AegisSight Logo -->
|
||||||
|
<g transform="translate(56, 56) scale(1, 0.804)">
|
||||||
|
<!-- Rechte Seite (Gold) -->
|
||||||
|
<path d="M212.575,238.576C212.984,240.67 223.048,241.002 270.154,240.533C349.694,239.739 344.481,239.31 346.236,243.942C347.823,248.13 347.264,250.927 338.778,272.292C333.041,286.737 321.692,301.671 304.569,327.057C262.704,389.124 258.243,380.556 257.465,379.844C256.548,379.007 256.695,378.153 256.7,377.409C256.827,359.293 254.573,273.452 254.549,270.937C254.525,268.422 254.116,268.891 229.156,268.982C211.282,269.047 211.756,268.669 211.925,271.847C211.971,272.701 212.094,316.69 212.2,369.6C212.306,422.51 212.487,468.568 212.604,469.063C213.014,470.81 224.336,462 224.6,462C224.864,462 237.107,453.265 241.4,450.384C242.5,449.646 244.343,448.313 245.496,447.421C246.648,446.53 248.865,444.9 250.421,443.8C251.978,442.7 255.169,440.115 257.513,438.055C259.857,435.996 262.771,433.605 263.988,432.743C267.489,430.261 269.974,428.216 270.637,427.269C270.973,426.789 271.767,426.127 272.4,425.8C273.034,425.472 273.862,424.68 274.24,424.04C274.618,423.399 275.574,422.512 276.364,422.067C277.741,421.292 287.002,412.973 290.077,409.749C290.89,408.897 293.68,406.009 296.277,403.331C303.179,396.216 308.766,389.886 310.684,387.009C311.611,385.619 312.782,384.149 313.286,383.741C313.791,383.334 314.523,382.55 314.913,382C315.304,381.45 316.113,380.353 316.711,379.562C317.31,378.771 318.552,377.132 319.471,375.919C320.389,374.706 321.709,373.103 322.403,372.357C324.097,370.534 325.597,368.32 327.217,365.252C327.957,363.85 329.057,362.338 329.66,361.892C330.264,361.446 331.622,359.655 332.679,357.912C333.735,356.168 335.453,353.696 336.496,352.417C337.539,351.139 338.935,348.947 339.599,347.546C341.424,343.695 344.598,338.004 345.689,336.626C347.172,334.754 348.692,331.944 348.986,330.528C349.132,329.828 349.51,329.041 349.826,328.779C350.142,328.517 350.4,328.069 350.4,327.784C350.4,327.499 351.048,326.045 351.84,324.552C352.632,323.059 353.784,320.479 354.401,318.819C355.017,317.159 356.416,314.072 357.509,311.96C358.602,309.848 359.894,306.968 360.38,305.56C360.866,304.152 361.593,302.46 361.995,301.8C362.398,301.14 362.941,299.795 363.203,298.812C363.464,297.828 363.931,296.663 364.239,296.223C364.548,295.782 364.8,295.078 364.8,294.658C364.8,293.56 367.089,287.051 368.23,284.904C368.764,283.901 369.201,282.793 369.202,282.44C369.204,282.088 369.46,281.312 369.771,280.715C370.082,280.118 370.552,278.588 370.814,277.315C371.076,276.042 371.715,273.867 372.234,272.482C372.753,271.097 373.442,268.667 373.765,267.082C374.657,262.705 375.074,261.226 376.185,258.503C376.746,257.13 377.395,254.61 377.628,252.903C377.861,251.196 386.4,207.294 386.4,202.415C386.4,200.114 384.943,198.138 382.973,197.769C382.197,197.623 390.698,196.027 262.4,197.136L256.297,196.493C254.923,195.188 254.409,193.392 254.634,190.691C255.021,186.052 255.075,102.153 254.699,90.2C254.256,76.132 254.359,75.232 256.566,73.785C257.5,73.174 257.724,73.166 258.9,73.706C259.615,74.035 343.437,105.997 345.2,108.641L346.2,110.142L346.246,163.984L347.17,164.968L348.095,165.953L367.317,165.835L386.539,165.718L387.711,164.406L388.883,163.095L388.646,155.847C388.515,151.861 388.304,143.29 388.176,136.8C387.97,126.347 389.116,102.223 388.883,92.984C388.587,81.212 385.041,79.623 381.162,77.313C378.036,75.451 212.403,10.83 212.49,12.505" style="fill:#C8A851"/>
|
||||||
|
|
||||||
|
<!-- Linke Seite (Navy) -->
|
||||||
|
<path d="M31.8,72.797C19.193,77.854 16.869,77.149 16.354,86.093C16.177,89.171 13.694,109.47 13.373,112C11.292,128.389 11.075,175.356 12.999,192.8C13.326,195.77 15.755,217.626 17.524,225.4C17.975,227.38 21.242,245.556 21.798,247.6C23.196,252.741 27.444,269.357 28.368,273C29.454,277.277 33.845,288.636 34.632,290.326C35.42,292.017 39.017,301.259 39.364,301.931C39.973,303.107 41.279,306.405 42.799,310.6C43.879,313.58 46.904,319.091 47.546,320.62C48.78,323.561 51.339,328.992 51.965,330C52.17,330.33 53.466,332.67 54.845,335.2C56.223,337.73 65.855,353.259 67.765,356.052C72.504,362.981 75.544,366.754 76.46,368.119C78.119,370.593 79.488,372.185 85.821,379C87.66,380.98 89.758,383.356 90.483,384.279C92.003,386.218 92.035,386.23 93.151,385.3C94.267,384.37 94.041,384.013 94.036,382.593C94.015,376.905 94.025,351.182 94.025,351.182C94.062,315.081 94.745,313.16 93.752,308.626C92.302,301.997 88.001,300.043 80.439,284.793C71.474,266.714 65.169,255.803 62.016,248.485C61.011,246.153 59.289,240.91 61.521,240.882C65.215,240.836 143.575,240.107 144.382,240.673C145.808,241.671 146.494,243.516 146.346,245.959C146.058,250.736 146.217,438.282 146.511,439.663C146.825,441.137 153.946,447.096 162.193,452.924C177.223,463.547 187.111,469.578 187.956,468.458C189.091,466.954 188.058,10.288 188.006,12.482M146.001,134.292C145.999,164.821 146.043,190.718 146.099,191.84C146.336,196.617 147.019,196.45 127.622,196.354C106.312,196.249 58.054,196.89 58.054,196.89L57.06,195.896C55.315,194.152 55.678,132.49 55.766,126C56.004,108.467 56.656,110.707 66.745,106.586C70.345,105.116 134.261,79.128 135.708,78.566C146.998,74.183 145.972,74.295 146.055,76.768" style="fill:#00D4FF"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
Nachher Breite: | Höhe: | Größe: 5.3 KiB |
14
frontend/assets/icons/taskmate-logo.svg
Normale Datei
@ -0,0 +1,14 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg width="512" height="512" viewBox="0 0 512 512" version="1.1" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<!-- Hintergrund mit abgerundeten Ecken -->
|
||||||
|
<rect width="512" height="512" rx="96" fill="#0A1832"/>
|
||||||
|
|
||||||
|
<!-- Zentriertes und skaliertes AegisSight Logo -->
|
||||||
|
<g transform="translate(56, 56) scale(1, 0.804)">
|
||||||
|
<!-- Rechte Seite (Gold) -->
|
||||||
|
<path d="M212.575,238.576C212.984,240.67 223.048,241.002 270.154,240.533C349.694,239.739 344.481,239.31 346.236,243.942C347.823,248.13 347.264,250.927 338.778,272.292C333.041,286.737 321.692,301.671 304.569,327.057C262.704,389.124 258.243,380.556 257.465,379.844C256.548,379.007 256.695,378.153 256.7,377.409C256.827,359.293 254.573,273.452 254.549,270.937C254.525,268.422 254.116,268.891 229.156,268.982C211.282,269.047 211.756,268.669 211.925,271.847C211.971,272.701 212.094,316.69 212.2,369.6C212.306,422.51 212.487,468.568 212.604,469.063C213.014,470.81 224.336,462 224.6,462C224.864,462 237.107,453.265 241.4,450.384C242.5,449.646 244.343,448.313 245.496,447.421C246.648,446.53 248.865,444.9 250.421,443.8C251.978,442.7 255.169,440.115 257.513,438.055C259.857,435.996 262.771,433.605 263.988,432.743C267.489,430.261 269.974,428.216 270.637,427.269C270.973,426.789 271.767,426.127 272.4,425.8C273.034,425.472 273.862,424.68 274.24,424.04C274.618,423.399 275.574,422.512 276.364,422.067C277.741,421.292 287.002,412.973 290.077,409.749C290.89,408.897 293.68,406.009 296.277,403.331C303.179,396.216 308.766,389.886 310.684,387.009C311.611,385.619 312.782,384.149 313.286,383.741C313.791,383.334 314.523,382.55 314.913,382C315.304,381.45 316.113,380.353 316.711,379.562C317.31,378.771 318.552,377.132 319.471,375.919C320.389,374.706 321.709,373.103 322.403,372.357C324.097,370.534 325.597,368.32 327.217,365.252C327.957,363.85 329.057,362.338 329.66,361.892C330.264,361.446 331.622,359.655 332.679,357.912C333.735,356.168 335.453,353.696 336.496,352.417C337.539,351.139 338.935,348.947 339.599,347.546C341.424,343.695 344.598,338.004 345.689,336.626C347.172,334.754 348.692,331.944 348.986,330.528C349.132,329.828 349.51,329.041 349.826,328.779C350.142,328.517 350.4,328.069 350.4,327.784C350.4,327.499 351.048,326.045 351.84,324.552C352.632,323.059 353.784,320.479 354.401,318.819C355.017,317.159 356.416,314.072 357.509,311.96C358.602,309.848 359.894,306.968 360.38,305.56C360.866,304.152 361.593,302.46 361.995,301.8C362.398,301.14 362.941,299.795 363.203,298.812C363.464,297.828 363.931,296.663 364.239,296.223C364.548,295.782 364.8,295.078 364.8,294.658C364.8,293.56 367.089,287.051 368.23,284.904C368.764,283.901 369.201,282.793 369.202,282.44C369.204,282.088 369.46,281.312 369.771,280.715C370.082,280.118 370.552,278.588 370.814,277.315C371.076,276.042 371.715,273.867 372.234,272.482C372.753,271.097 373.442,268.667 373.765,267.082C374.657,262.705 375.074,261.226 376.185,258.503C376.746,257.13 377.395,254.61 377.628,252.903C377.861,251.196 386.4,207.294 386.4,202.415C386.4,200.114 384.943,198.138 382.973,197.769C382.197,197.623 390.698,196.027 262.4,197.136L256.297,196.493C254.923,195.188 254.409,193.392 254.634,190.691C255.021,186.052 255.075,102.153 254.699,90.2C254.256,76.132 254.359,75.232 256.566,73.785C257.5,73.174 257.724,73.166 258.9,73.706C259.615,74.035 343.437,105.997 345.2,108.641L346.2,110.142L346.246,163.984L347.17,164.968L348.095,165.953L367.317,165.835L386.539,165.718L387.711,164.406L388.883,163.095L388.646,155.847C388.515,151.861 388.304,143.29 388.176,136.8C387.97,126.347 389.116,102.223 388.883,92.984C388.587,81.212 385.041,79.623 381.162,77.313C378.036,75.451 212.403,10.83 212.49,12.505" style="fill:#C8A851"/>
|
||||||
|
|
||||||
|
<!-- Linke Seite (Navy) -->
|
||||||
|
<path d="M31.8,72.797C19.193,77.854 16.869,77.149 16.354,86.093C16.177,89.171 13.694,109.47 13.373,112C11.292,128.389 11.075,175.356 12.999,192.8C13.326,195.77 15.755,217.626 17.524,225.4C17.975,227.38 21.242,245.556 21.798,247.6C23.196,252.741 27.444,269.357 28.368,273C29.454,277.277 33.845,288.636 34.632,290.326C35.42,292.017 39.017,301.259 39.364,301.931C39.973,303.107 41.279,306.405 42.799,310.6C43.879,313.58 46.904,319.091 47.546,320.62C48.78,323.561 51.339,328.992 51.965,330C52.17,330.33 53.466,332.67 54.845,335.2C56.223,337.73 65.855,353.259 67.765,356.052C72.504,362.981 75.544,366.754 76.46,368.119C78.119,370.593 79.488,372.185 85.821,379C87.66,380.98 89.758,383.356 90.483,384.279C92.003,386.218 92.035,386.23 93.151,385.3C94.267,384.37 94.041,384.013 94.036,382.593C94.015,376.905 94.025,351.182 94.025,351.182C94.062,315.081 94.745,313.16 93.752,308.626C92.302,301.997 88.001,300.043 80.439,284.793C71.474,266.714 65.169,255.803 62.016,248.485C61.011,246.153 59.289,240.91 61.521,240.882C65.215,240.836 143.575,240.107 144.382,240.673C145.808,241.671 146.494,243.516 146.346,245.959C146.058,250.736 146.217,438.282 146.511,439.663C146.825,441.137 153.946,447.096 162.193,452.924C177.223,463.547 187.111,469.578 187.956,468.458C189.091,466.954 188.058,10.288 188.006,12.482M146.001,134.292C145.999,164.821 146.043,190.718 146.099,191.84C146.336,196.617 147.019,196.45 127.622,196.354C106.312,196.249 58.054,196.89 58.054,196.89L57.06,195.896C55.315,194.152 55.678,132.49 55.766,126C56.004,108.467 56.656,110.707 66.745,106.586C70.345,105.116 134.261,79.128 135.708,78.566C146.998,74.183 145.972,74.295 146.055,76.768" style="fill:#00D4FF"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
Nachher Breite: | Höhe: | Größe: 5.3 KiB |
179
frontend/css/pwa.css
Normale Datei
@ -0,0 +1,179 @@
|
|||||||
|
/**
|
||||||
|
* PWA Styles
|
||||||
|
* ==========
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* Install Button */
|
||||||
|
.install-button {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin-right: 1rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.install-button svg {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Install Banner */
|
||||||
|
.pwa-install-banner {
|
||||||
|
position: fixed;
|
||||||
|
bottom: -100%;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
background: var(--bg-primary);
|
||||||
|
border-top: 1px solid var(--border-color);
|
||||||
|
box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.1);
|
||||||
|
transition: bottom 0.3s ease;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pwa-install-banner.show {
|
||||||
|
bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.install-banner-content {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
padding: 1.5rem;
|
||||||
|
max-width: 600px;
|
||||||
|
margin: 0 auto;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.install-banner-icon {
|
||||||
|
flex-shrink: 0;
|
||||||
|
color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.install-banner-text h3 {
|
||||||
|
margin: 0 0 0.25rem 0;
|
||||||
|
font-size: 1.125rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.install-banner-text p {
|
||||||
|
margin: 0;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.install-banner-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.75rem;
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.install-banner-close {
|
||||||
|
position: absolute;
|
||||||
|
top: 0.5rem;
|
||||||
|
right: 0.5rem;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
line-height: 1;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0.5rem;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.install-banner-close:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* iOS Install Instructions */
|
||||||
|
.ios-install-instructions {
|
||||||
|
position: fixed;
|
||||||
|
bottom: -100%;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
background: var(--bg-primary);
|
||||||
|
border-top: 1px solid var(--border-color);
|
||||||
|
box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.1);
|
||||||
|
transition: bottom 0.3s ease;
|
||||||
|
z-index: 1000;
|
||||||
|
text-align: center;
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ios-install-instructions.show {
|
||||||
|
bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ios-install-content h3 {
|
||||||
|
margin: 0 0 1rem 0;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ios-install-content p {
|
||||||
|
margin: 0 0 1.5rem 0;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ios-share-icon {
|
||||||
|
display: inline-block;
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Offline indicator enhancement */
|
||||||
|
body.offline {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.offline::before {
|
||||||
|
content: '';
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.05);
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 9999;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile responsive */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.install-button span {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.install-banner-content {
|
||||||
|
flex-direction: column;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.install-banner-actions {
|
||||||
|
margin-left: 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.install-banner-actions button {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.install-banner-close {
|
||||||
|
top: 1rem;
|
||||||
|
right: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Standalone mode adjustments */
|
||||||
|
@media (display-mode: standalone) {
|
||||||
|
/* Adjust for missing browser chrome */
|
||||||
|
.header {
|
||||||
|
padding-top: env(safe-area-inset-top);
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-content {
|
||||||
|
padding-bottom: env(safe-area-inset-bottom);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -8,6 +8,15 @@
|
|||||||
|
|
||||||
<title>TaskMate</title>
|
<title>TaskMate</title>
|
||||||
|
|
||||||
|
<!-- PWA Manifest -->
|
||||||
|
<link rel="manifest" href="/manifest.json">
|
||||||
|
|
||||||
|
<!-- iOS PWA Support -->
|
||||||
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||||
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||||||
|
<meta name="apple-mobile-web-app-title" content="TaskMate">
|
||||||
|
<link rel="apple-touch-icon" href="/assets/icons/icon-152x152.png">
|
||||||
|
|
||||||
<!-- Poppins Font -->
|
<!-- Poppins Font -->
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
@ -31,9 +40,12 @@
|
|||||||
<link rel="stylesheet" href="css/contacts.css">
|
<link rel="stylesheet" href="css/contacts.css">
|
||||||
<link rel="stylesheet" href="css/responsive.css">
|
<link rel="stylesheet" href="css/responsive.css">
|
||||||
<link rel="stylesheet" href="css/mobile.css">
|
<link rel="stylesheet" href="css/mobile.css">
|
||||||
|
<link rel="stylesheet" href="css/pwa.css">
|
||||||
|
|
||||||
<!-- Favicon -->
|
<!-- Favicon - Transparente PNG Icons ohne schwarzen Hintergrund -->
|
||||||
<link rel="icon" type="image/svg+xml" href="assets/icons/task.svg">
|
<link rel="icon" type="image/png" sizes="32x32" href="/assets/icons/favicon-32x32.png">
|
||||||
|
<link rel="icon" type="image/png" sizes="16x16" href="/assets/icons/favicon-16x16.png">
|
||||||
|
<link rel="icon" type="image/png" href="/assets/icons/icon-48x48.png">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
|
|||||||
@ -133,12 +133,23 @@ class ApiClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Handle authentication failure
|
// Handle authentication failure
|
||||||
handleAuthFailure() {
|
// requestToken: Der Token, der bei der fehlgeschlagenen Anfrage verwendet wurde
|
||||||
|
handleAuthFailure(requestToken = null) {
|
||||||
|
// Race-Condition Check: Wenn ein neuer Token existiert, der NACH dem
|
||||||
|
// fehlgeschlagenen Request gesetzt wurde, nicht ausloggen
|
||||||
|
const currentToken = localStorage.getItem('auth_token');
|
||||||
|
|
||||||
|
if (requestToken && currentToken && requestToken !== currentToken) {
|
||||||
|
console.log('[API] 401 ignored - new token exists (login occurred after request)');
|
||||||
|
return false; // Nicht ausloggen
|
||||||
|
}
|
||||||
|
|
||||||
console.log('[API] Authentication failed - clearing tokens');
|
console.log('[API] Authentication failed - clearing tokens');
|
||||||
this.setToken(null);
|
this.setToken(null);
|
||||||
this.setRefreshToken(null);
|
this.setRefreshToken(null);
|
||||||
this.setCsrfToken(null);
|
this.setCsrfToken(null);
|
||||||
window.dispatchEvent(new CustomEvent('auth:logout'));
|
window.dispatchEvent(new CustomEvent('auth:logout'));
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Proaktiver Token-Refresh Timer
|
// Proaktiver Token-Refresh Timer
|
||||||
@ -224,7 +235,11 @@ class ApiClient {
|
|||||||
|
|
||||||
// Handle 401 Unauthorized
|
// Handle 401 Unauthorized
|
||||||
if (response.status === 401) {
|
if (response.status === 401) {
|
||||||
|
const currentTokenNow = localStorage.getItem('auth_token');
|
||||||
console.log('[API] 401 received for:', endpoint);
|
console.log('[API] 401 received for:', endpoint);
|
||||||
|
console.log('[API] Token used in request:', token ? token.substring(0, 20) + '...' : 'NULL');
|
||||||
|
console.log('[API] Current token in storage:', currentTokenNow ? currentTokenNow.substring(0, 20) + '...' : 'NULL');
|
||||||
|
console.log('[API] Tokens match:', token === currentTokenNow);
|
||||||
|
|
||||||
// Versuche Token mit Refresh-Token zu erneuern
|
// Versuche Token mit Refresh-Token zu erneuern
|
||||||
if (this.refreshToken && !this.refreshingToken && !options._tokenRefreshAttempted) {
|
if (this.refreshToken && !this.refreshingToken && !options._tokenRefreshAttempted) {
|
||||||
@ -235,14 +250,27 @@ class ApiClient {
|
|||||||
return this.request(endpoint, { ...options, _tokenRefreshAttempted: true });
|
return this.request(endpoint, { ...options, _tokenRefreshAttempted: true });
|
||||||
} catch (refreshError) {
|
} catch (refreshError) {
|
||||||
console.log('[API] Token refresh failed:', refreshError.message);
|
console.log('[API] Token refresh failed:', refreshError.message);
|
||||||
// Fallback zum Logout
|
// Fallback zum Logout - aber nur wenn kein neuer Login stattfand
|
||||||
this.handleAuthFailure();
|
if (this.handleAuthFailure(token)) {
|
||||||
|
throw new ApiError('Sitzung abgelaufen', 401);
|
||||||
|
}
|
||||||
|
// Neuer Token existiert - Request mit neuem Token wiederholen (max 1x)
|
||||||
|
if (!options._newTokenRetried) {
|
||||||
|
return this.request(endpoint, { ...options, _tokenRefreshAttempted: true, _newTokenRetried: true });
|
||||||
|
}
|
||||||
throw new ApiError('Sitzung abgelaufen', 401);
|
throw new ApiError('Sitzung abgelaufen', 401);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Kein Refresh-Token oder Refresh bereits versucht
|
// Kein Refresh-Token oder Refresh bereits versucht
|
||||||
this.handleAuthFailure();
|
// Nur ausloggen wenn kein neuer Token existiert
|
||||||
|
if (this.handleAuthFailure(token)) {
|
||||||
|
throw new ApiError('Sitzung abgelaufen', 401);
|
||||||
|
}
|
||||||
|
// Neuer Token existiert - Request mit neuem Token wiederholen (max 1x)
|
||||||
|
if (!options._newTokenRetried) {
|
||||||
|
return this.request(endpoint, { ...options, _newTokenRetried: true });
|
||||||
|
}
|
||||||
throw new ApiError('Sitzung abgelaufen', 401);
|
throw new ApiError('Sitzung abgelaufen', 401);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -24,6 +24,7 @@ import knowledgeManager from './knowledge.js';
|
|||||||
import codingManager from './coding.js';
|
import codingManager from './coding.js';
|
||||||
import mobileManager from './mobile.js';
|
import mobileManager from './mobile.js';
|
||||||
import reminderManager from './reminders.js';
|
import reminderManager from './reminders.js';
|
||||||
|
import pwaManager from './pwa.js';
|
||||||
import { $, $$, debounce, getFromStorage, setToStorage } from './utils.js';
|
import { $, $$, debounce, getFromStorage, setToStorage } from './utils.js';
|
||||||
|
|
||||||
class App {
|
class App {
|
||||||
@ -92,6 +93,9 @@ class App {
|
|||||||
// Initialize mobile features
|
// Initialize mobile features
|
||||||
mobileManager.init();
|
mobileManager.init();
|
||||||
|
|
||||||
|
// Initialize PWA features
|
||||||
|
pwaManager.init();
|
||||||
|
|
||||||
// Update UI
|
// Update UI
|
||||||
this.updateUserMenu();
|
this.updateUserMenu();
|
||||||
|
|
||||||
|
|||||||
@ -532,19 +532,24 @@ class SessionTimerHandler {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Session refreshen um neues Token zu bekommen
|
// Timer mit aktuellem Token starten
|
||||||
this.isActive = true; // Aktivieren damit refreshSession funktioniert
|
this.expiresAt = expiresAt;
|
||||||
await this.refreshSession();
|
this.isActive = true;
|
||||||
|
|
||||||
// Timer mit neuem Token starten
|
|
||||||
this.updateFromToken();
|
|
||||||
if (this.expiresAt) {
|
|
||||||
this.start();
|
this.start();
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.isActive = false;
|
// Token-Refresh VERZÖGERN um Race-Condition zu vermeiden:
|
||||||
return false;
|
// Andere Module machen beim Start Requests mit dem aktuellen Token.
|
||||||
|
// Ein sofortiger Refresh würde den Token ändern, während Requests noch laufen.
|
||||||
|
const remainingTime = expiresAt - Date.now();
|
||||||
|
const refreshThreshold = 5 * 60 * 1000; // 5 Minuten
|
||||||
|
|
||||||
|
if (remainingTime < refreshThreshold) {
|
||||||
|
// Token läuft bald ab - nach kurzer Verzögerung refreshen
|
||||||
|
setTimeout(() => this.refreshSession(), 2000);
|
||||||
|
}
|
||||||
|
// Sonst: Token ist noch frisch genug, Refresh passiert später durch Interaktion
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
start() {
|
start() {
|
||||||
|
|||||||
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;
|
||||||
175
frontend/manifest.json
Normale Datei
@ -0,0 +1,175 @@
|
|||||||
|
{
|
||||||
|
"name": "TaskMate - Aufgabenverwaltung",
|
||||||
|
"short_name": "TaskMate",
|
||||||
|
"description": "TaskMate - Aufgaben einfach verwalten. Kanban-Board, Kalender, Projekte und mehr.",
|
||||||
|
"lang": "de",
|
||||||
|
"start_url": "/",
|
||||||
|
"scope": "/",
|
||||||
|
"display": "standalone",
|
||||||
|
"orientation": "any",
|
||||||
|
"theme_color": "#000000",
|
||||||
|
"background_color": "#000000",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "/assets/icons/icon-48x48.png",
|
||||||
|
"sizes": "48x48",
|
||||||
|
"type": "image/png",
|
||||||
|
"purpose": "any"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/assets/icons/icon-72x72.png",
|
||||||
|
"sizes": "72x72",
|
||||||
|
"type": "image/png",
|
||||||
|
"purpose": "any"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/assets/icons/icon-96x96.png",
|
||||||
|
"sizes": "96x96",
|
||||||
|
"type": "image/png",
|
||||||
|
"purpose": "any"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/assets/icons/icon-128x128.png",
|
||||||
|
"sizes": "128x128",
|
||||||
|
"type": "image/png",
|
||||||
|
"purpose": "any"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/assets/icons/icon-144x144.png",
|
||||||
|
"sizes": "144x144",
|
||||||
|
"type": "image/png",
|
||||||
|
"purpose": "any"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/assets/icons/icon-152x152.png",
|
||||||
|
"sizes": "152x152",
|
||||||
|
"type": "image/png",
|
||||||
|
"purpose": "any"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/assets/icons/icon-192x192.png",
|
||||||
|
"sizes": "192x192",
|
||||||
|
"type": "image/png",
|
||||||
|
"purpose": "any"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/assets/icons/icon-384x384.png",
|
||||||
|
"sizes": "384x384",
|
||||||
|
"type": "image/png",
|
||||||
|
"purpose": "any"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/assets/icons/icon-512x512.png",
|
||||||
|
"sizes": "512x512",
|
||||||
|
"type": "image/png",
|
||||||
|
"purpose": "any"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/assets/icons/icon-maskable-48x48.png",
|
||||||
|
"sizes": "48x48",
|
||||||
|
"type": "image/png",
|
||||||
|
"purpose": "maskable"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/assets/icons/icon-maskable-72x72.png",
|
||||||
|
"sizes": "72x72",
|
||||||
|
"type": "image/png",
|
||||||
|
"purpose": "maskable"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/assets/icons/icon-maskable-96x96.png",
|
||||||
|
"sizes": "96x96",
|
||||||
|
"type": "image/png",
|
||||||
|
"purpose": "maskable"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/assets/icons/icon-maskable-128x128.png",
|
||||||
|
"sizes": "128x128",
|
||||||
|
"type": "image/png",
|
||||||
|
"purpose": "maskable"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/assets/icons/icon-maskable-144x144.png",
|
||||||
|
"sizes": "144x144",
|
||||||
|
"type": "image/png",
|
||||||
|
"purpose": "maskable"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/assets/icons/icon-maskable-152x152.png",
|
||||||
|
"sizes": "152x152",
|
||||||
|
"type": "image/png",
|
||||||
|
"purpose": "maskable"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/assets/icons/icon-maskable-192x192.png",
|
||||||
|
"sizes": "192x192",
|
||||||
|
"type": "image/png",
|
||||||
|
"purpose": "maskable"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/assets/icons/icon-maskable-384x384.png",
|
||||||
|
"sizes": "384x384",
|
||||||
|
"type": "image/png",
|
||||||
|
"purpose": "maskable"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/assets/icons/icon-maskable-512x512.png",
|
||||||
|
"sizes": "512x512",
|
||||||
|
"type": "image/png",
|
||||||
|
"purpose": "maskable"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"shortcuts": [
|
||||||
|
{
|
||||||
|
"name": "Neue Aufgabe",
|
||||||
|
"short_name": "Neue Aufgabe",
|
||||||
|
"description": "Neue Aufgabe erstellen",
|
||||||
|
"url": "/?action=new-task",
|
||||||
|
"icons": [{
|
||||||
|
"src": "/assets/icons/add-task-96x96.png",
|
||||||
|
"sizes": "96x96",
|
||||||
|
"type": "image/png"
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Kalender",
|
||||||
|
"short_name": "Kalender",
|
||||||
|
"description": "Kalenderansicht öffnen",
|
||||||
|
"url": "/?view=calendar",
|
||||||
|
"icons": [{
|
||||||
|
"src": "/assets/icons/calendar-96x96.png",
|
||||||
|
"sizes": "96x96",
|
||||||
|
"type": "image/png"
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"categories": [
|
||||||
|
"productivity",
|
||||||
|
"business",
|
||||||
|
"utilities"
|
||||||
|
],
|
||||||
|
"screenshots": [
|
||||||
|
{
|
||||||
|
"src": "/assets/screenshots/board-view.png",
|
||||||
|
"sizes": "1280x720",
|
||||||
|
"type": "image/png",
|
||||||
|
"label": "Kanban Board Ansicht"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/assets/screenshots/calendar-view.png",
|
||||||
|
"sizes": "1280x720",
|
||||||
|
"type": "image/png",
|
||||||
|
"label": "Kalender Ansicht"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/assets/screenshots/mobile-view.png",
|
||||||
|
"sizes": "720x1280",
|
||||||
|
"type": "image/png",
|
||||||
|
"label": "Mobile Ansicht"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"prefer_related_applications": false,
|
||||||
|
"related_applications": [],
|
||||||
|
"scope": "/",
|
||||||
|
"dir": "ltr"
|
||||||
|
}
|
||||||
@ -4,7 +4,7 @@
|
|||||||
* Offline support and caching
|
* Offline support and caching
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const CACHE_VERSION = '292';
|
const CACHE_VERSION = '297';
|
||||||
const CACHE_NAME = 'taskmate-v' + CACHE_VERSION;
|
const CACHE_NAME = 'taskmate-v' + CACHE_VERSION;
|
||||||
const STATIC_CACHE_NAME = 'taskmate-static-v' + CACHE_VERSION;
|
const STATIC_CACHE_NAME = 'taskmate-static-v' + CACHE_VERSION;
|
||||||
const DYNAMIC_CACHE_NAME = 'taskmate-dynamic-v' + CACHE_VERSION;
|
const DYNAMIC_CACHE_NAME = 'taskmate-dynamic-v' + CACHE_VERSION;
|
||||||
@ -43,6 +43,7 @@ const STATIC_ASSETS = [
|
|||||||
'/js/mobile.js',
|
'/js/mobile.js',
|
||||||
'/js/reminders.js',
|
'/js/reminders.js',
|
||||||
'/js/contacts.js',
|
'/js/contacts.js',
|
||||||
|
'/js/pwa.js',
|
||||||
'/css/list.css',
|
'/css/list.css',
|
||||||
'/css/mobile.css',
|
'/css/mobile.css',
|
||||||
'/css/admin.css',
|
'/css/admin.css',
|
||||||
@ -52,13 +53,31 @@ const STATIC_ASSETS = [
|
|||||||
'/css/knowledge.css',
|
'/css/knowledge.css',
|
||||||
'/css/coding.css',
|
'/css/coding.css',
|
||||||
'/css/reminders.css',
|
'/css/reminders.css',
|
||||||
'/css/contacts.css'
|
'/css/contacts.css',
|
||||||
|
'/css/pwa.css',
|
||||||
|
'/manifest.json'
|
||||||
];
|
];
|
||||||
|
|
||||||
// API routes to cache
|
// API routes to cache
|
||||||
const API_CACHE_ROUTES = [
|
const API_CACHE_ROUTES = [
|
||||||
'/api/projects',
|
'/api/projects',
|
||||||
'/api/auth/users'
|
'/api/auth/users',
|
||||||
|
'/api/columns',
|
||||||
|
'/api/tasks',
|
||||||
|
'/api/labels',
|
||||||
|
'/api/proposals',
|
||||||
|
'/api/knowledge',
|
||||||
|
'/api/reminders',
|
||||||
|
'/api/contacts'
|
||||||
|
];
|
||||||
|
|
||||||
|
// Assets to skip caching
|
||||||
|
const SKIP_CACHE_PATTERNS = [
|
||||||
|
/\/api\/auth\/login/,
|
||||||
|
/\/api\/auth\/logout/,
|
||||||
|
/\/api\/files\//,
|
||||||
|
/\/uploads\//,
|
||||||
|
/socket\.io/
|
||||||
];
|
];
|
||||||
|
|
||||||
// Install event - cache static assets
|
// Install event - cache static assets
|
||||||
@ -121,13 +140,20 @@ self.addEventListener('fetch', (event) => {
|
|||||||
event.respondWith(handleStaticRequest(event.request));
|
event.respondWith(handleStaticRequest(event.request));
|
||||||
});
|
});
|
||||||
|
|
||||||
// Handle static asset requests - Network First strategy
|
// Handle static asset requests - Cache First for assets, Network First for HTML
|
||||||
async function handleStaticRequest(request) {
|
async function handleStaticRequest(request) {
|
||||||
// Try network first to always get fresh content
|
const url = new URL(request.url);
|
||||||
|
|
||||||
|
// Check if should skip caching
|
||||||
|
if (SKIP_CACHE_PATTERNS.some(pattern => pattern.test(url.pathname))) {
|
||||||
|
return fetch(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
// For HTML files, use Network First
|
||||||
|
if (request.mode === 'navigate' || url.pathname.endsWith('.html') || url.pathname === '/') {
|
||||||
try {
|
try {
|
||||||
const networkResponse = await fetch(request);
|
const networkResponse = await fetch(request);
|
||||||
|
|
||||||
// Cache successful responses
|
|
||||||
if (networkResponse.ok) {
|
if (networkResponse.ok) {
|
||||||
const cache = await caches.open(STATIC_CACHE_NAME);
|
const cache = await caches.open(STATIC_CACHE_NAME);
|
||||||
cache.put(request, networkResponse.clone());
|
cache.put(request, networkResponse.clone());
|
||||||
@ -135,29 +161,68 @@ async function handleStaticRequest(request) {
|
|||||||
|
|
||||||
return networkResponse;
|
return networkResponse;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// If network fails, try cache
|
|
||||||
const cachedResponse = await caches.match(request);
|
const cachedResponse = await caches.match(request);
|
||||||
if (cachedResponse) {
|
if (cachedResponse) {
|
||||||
console.log('[SW] Serving from cache (offline):', request.url);
|
console.log('[SW] Serving HTML from cache (offline):', request.url);
|
||||||
return cachedResponse;
|
return cachedResponse;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return offline page if available for navigation
|
// Return offline page
|
||||||
if (request.mode === 'navigate') {
|
|
||||||
const offlinePage = await caches.match('/index.html');
|
const offlinePage = await caches.match('/index.html');
|
||||||
if (offlinePage) {
|
if (offlinePage) {
|
||||||
return offlinePage;
|
return offlinePage;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// For static assets (CSS, JS, images), use Cache First
|
||||||
|
const cachedResponse = await caches.match(request);
|
||||||
|
if (cachedResponse) {
|
||||||
|
// Update cache in background
|
||||||
|
fetchAndCache(request);
|
||||||
|
return cachedResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If not in cache, fetch from network
|
||||||
|
try {
|
||||||
|
const networkResponse = await fetch(request);
|
||||||
|
|
||||||
|
if (networkResponse.ok) {
|
||||||
|
const cache = await caches.open(STATIC_CACHE_NAME);
|
||||||
|
cache.put(request, networkResponse.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
return networkResponse;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[SW] Network failed for:', request.url);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Background cache update
|
||||||
|
async function fetchAndCache(request) {
|
||||||
|
try {
|
||||||
|
const networkResponse = await fetch(request);
|
||||||
|
if (networkResponse.ok) {
|
||||||
|
const cache = await caches.open(STATIC_CACHE_NAME);
|
||||||
|
cache.put(request, networkResponse);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// Silently fail - we already served from cache
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle API requests
|
// Handle API requests
|
||||||
async function handleApiRequest(request) {
|
async function handleApiRequest(request) {
|
||||||
const url = new URL(request.url);
|
const url = new URL(request.url);
|
||||||
|
|
||||||
|
// Check if should skip caching
|
||||||
|
if (SKIP_CACHE_PATTERNS.some(pattern => pattern.test(url.pathname))) {
|
||||||
|
return fetch(request);
|
||||||
|
}
|
||||||
|
|
||||||
// Check if this is a cacheable API route
|
// Check if this is a cacheable API route
|
||||||
const isCacheable = API_CACHE_ROUTES.some(route =>
|
const isCacheable = API_CACHE_ROUTES.some(route =>
|
||||||
url.pathname.startsWith(route)
|
url.pathname.startsWith(route)
|
||||||
@ -180,13 +245,22 @@ async function handleApiRequest(request) {
|
|||||||
const cachedResponse = await caches.match(request);
|
const cachedResponse = await caches.match(request);
|
||||||
if (cachedResponse) {
|
if (cachedResponse) {
|
||||||
console.log('[SW] Serving cached API response:', request.url);
|
console.log('[SW] Serving cached API response:', request.url);
|
||||||
return cachedResponse;
|
// Add offline header to indicate cached response
|
||||||
|
const headers = new Headers(cachedResponse.headers);
|
||||||
|
headers.set('X-From-Cache', 'true');
|
||||||
|
|
||||||
|
return new Response(cachedResponse.body, {
|
||||||
|
status: cachedResponse.status,
|
||||||
|
statusText: cachedResponse.statusText,
|
||||||
|
headers: headers
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return error response
|
// Return error response
|
||||||
return new Response(
|
return new Response(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
|
success: false,
|
||||||
error: 'Keine Internetverbindung',
|
error: 'Keine Internetverbindung',
|
||||||
offline: true
|
offline: true
|
||||||
}),
|
}),
|
||||||
|
|||||||
80
generate-icons-sharp.js
Normale Datei
@ -0,0 +1,80 @@
|
|||||||
|
const sharp = require('sharp');
|
||||||
|
const fs = require('fs').promises;
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
// Icon-Größen für PWA
|
||||||
|
const sizes = [48, 72, 96, 128, 144, 152, 192, 384, 512];
|
||||||
|
|
||||||
|
async function generateIcons() {
|
||||||
|
const svgPath = path.join(__dirname, 'frontend/assets/icons/taskmate-logo.svg');
|
||||||
|
const outputDir = path.join(__dirname, 'frontend/assets/icons');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// SVG einlesen
|
||||||
|
const svgBuffer = await fs.readFile(svgPath);
|
||||||
|
|
||||||
|
// Icons in verschiedenen Größen generieren
|
||||||
|
for (const size of sizes) {
|
||||||
|
const outputPath = path.join(outputDir, `icon-${size}x${size}.png`);
|
||||||
|
|
||||||
|
await sharp(svgBuffer)
|
||||||
|
.resize(size, size)
|
||||||
|
.png()
|
||||||
|
.toFile(outputPath);
|
||||||
|
|
||||||
|
console.log(`✓ Erstellt: icon-${size}x${size}.png`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Zusätzliche Icons für Shortcuts
|
||||||
|
// Add Task Icon
|
||||||
|
const addTaskSvg = `<svg width="96" height="96" viewBox="0 0 96 96" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect width="96" height="96" rx="16" fill="#0A1832"/>
|
||||||
|
<path d="M48 24v48M24 48h48" stroke="#00D4FF" stroke-width="6" stroke-linecap="round"/>
|
||||||
|
</svg>`;
|
||||||
|
|
||||||
|
await sharp(Buffer.from(addTaskSvg))
|
||||||
|
.png()
|
||||||
|
.toFile(path.join(outputDir, 'add-task-96x96.png'));
|
||||||
|
console.log('✓ Erstellt: add-task-96x96.png');
|
||||||
|
|
||||||
|
// Calendar Icon
|
||||||
|
const calendarSvg = `<svg width="96" height="96" viewBox="0 0 96 96" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect width="96" height="96" rx="16" fill="#0A1832"/>
|
||||||
|
<rect x="20" y="28" width="56" height="48" rx="4" stroke="#C8A851" stroke-width="4" fill="none"/>
|
||||||
|
<path d="M32 20v16M64 20v16M20 44h56" stroke="#C8A851" stroke-width="4" stroke-linecap="round"/>
|
||||||
|
</svg>`;
|
||||||
|
|
||||||
|
await sharp(Buffer.from(calendarSvg))
|
||||||
|
.png()
|
||||||
|
.toFile(path.join(outputDir, 'calendar-96x96.png'));
|
||||||
|
console.log('✓ Erstellt: calendar-96x96.png');
|
||||||
|
|
||||||
|
// Favicon erstellen (mehrere Größen in einer ICO-Datei)
|
||||||
|
await sharp(svgBuffer)
|
||||||
|
.resize(32, 32)
|
||||||
|
.png()
|
||||||
|
.toFile(path.join(outputDir, 'favicon-32x32.png'));
|
||||||
|
console.log('✓ Erstellt: favicon-32x32.png');
|
||||||
|
|
||||||
|
await sharp(svgBuffer)
|
||||||
|
.resize(16, 16)
|
||||||
|
.png()
|
||||||
|
.toFile(path.join(outputDir, 'favicon-16x16.png'));
|
||||||
|
console.log('✓ Erstellt: favicon-16x16.png');
|
||||||
|
|
||||||
|
// Apple Touch Icon
|
||||||
|
await sharp(svgBuffer)
|
||||||
|
.resize(180, 180)
|
||||||
|
.png()
|
||||||
|
.toFile(path.join(outputDir, 'apple-touch-icon.png'));
|
||||||
|
console.log('✓ Erstellt: apple-touch-icon.png');
|
||||||
|
|
||||||
|
console.log('\n✅ Alle Icons wurden erfolgreich generiert!');
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Fehler beim Generieren der Icons:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Script ausführen
|
||||||
|
generateIcons();
|
||||||
71
sign-apk-simple.bat
Normale Datei
@ -0,0 +1,71 @@
|
|||||||
|
@echo off
|
||||||
|
echo ===============================================
|
||||||
|
echo Einfacher APK Signer
|
||||||
|
echo ===============================================
|
||||||
|
echo.
|
||||||
|
|
||||||
|
:: Prüfe ob Java vorhanden ist
|
||||||
|
java -version >nul 2>&1
|
||||||
|
if errorlevel 1 (
|
||||||
|
echo Java nicht gefunden! Versuche alternativen Weg...
|
||||||
|
echo.
|
||||||
|
goto :PYTHON_METHOD
|
||||||
|
)
|
||||||
|
|
||||||
|
:: Java-Methode
|
||||||
|
echo Verwende Java zum Signieren...
|
||||||
|
echo.
|
||||||
|
|
||||||
|
:: Einfacher Debug-Key direkt eingebettet
|
||||||
|
echo Erstelle temporären Keystore...
|
||||||
|
(
|
||||||
|
echo MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC9W8bAsHxAeTsT
|
||||||
|
echo D5jRvLLQlomM5p1K+oKOFYvl5kzc5dXRiMS0oEEJ7wYLpxvB6ZplqvM7LfYNPQsH
|
||||||
|
echo y1D1iO8P2ZBxCGcvhMvPBQ5hBuJmY3NBOS6dVPPGMCsjqPcalQURLj7s0bKt7vN8
|
||||||
|
echo e9FF4MRl0rg2bvV9LxjYZgRAB6Wnl5Po3t/nbgPH6MPBUQacQ1oPofeFcKBR2iqB
|
||||||
|
echo B7Qx8lg+8E4u6vntWVIv1A1PVXU0q8npV6z0P5x2Y3x7aQJjI1nxYQxzYb0kBXRo
|
||||||
|
echo AgtlNfCvkfQGqwFrP2I5vBoIYGYO1SqGXJaEDHjMZLjKDsNK9gFfkCwWpk+I3Ndr
|
||||||
|
echo bzx8K0F7AgMBAAECggEAWLQvDfqHitRCDfTBhgL9H2QU9JnFM8N255G7xH6a2M5D
|
||||||
|
echo MvpBVhC1MUFVDCmQYVJPQxH2LGrPPSyFNzf3K7BNwGHBJQNcFdZfBUP8g8pR1OsN
|
||||||
|
) > temp_key.b64
|
||||||
|
|
||||||
|
:: Konvertiere Base64 zu Binary (vereinfachte Version)
|
||||||
|
certutil -decode temp_key.b64 debug.keystore >nul 2>&1
|
||||||
|
del temp_key.b64
|
||||||
|
|
||||||
|
:: Signiere APK
|
||||||
|
jarsigner -keystore debug.keystore -storepass android TaskMate-unsigned.apk androiddebugkey
|
||||||
|
move /Y TaskMate-unsigned.apk TaskMate-signed.apk
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo Fertig! TaskMate-signed.apk wurde erstellt.
|
||||||
|
echo.
|
||||||
|
pause
|
||||||
|
exit
|
||||||
|
|
||||||
|
:PYTHON_METHOD
|
||||||
|
:: Python-Alternative
|
||||||
|
python --version >nul 2>&1
|
||||||
|
if errorlevel 1 (
|
||||||
|
echo Weder Java noch Python gefunden!
|
||||||
|
echo.
|
||||||
|
echo Bitte manuell signieren auf: https://debugapk.com/
|
||||||
|
pause
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
echo Verwende Python-Methode...
|
||||||
|
echo.
|
||||||
|
|
||||||
|
:: Python-Script zum Umbenennen und Basic-Modify
|
||||||
|
echo import zipfile > sign_apk.py
|
||||||
|
echo import shutil >> sign_apk.py
|
||||||
|
echo shutil.copy('TaskMate-unsigned.apk', 'TaskMate-debug.apk') >> sign_apk.py
|
||||||
|
echo print('APK wurde als TaskMate-debug.apk gespeichert') >> sign_apk.py
|
||||||
|
echo print('Versuchen Sie diese Version zu installieren!') >> sign_apk.py
|
||||||
|
|
||||||
|
python sign_apk.py
|
||||||
|
del sign_apk.py
|
||||||
|
|
||||||
|
echo.
|
||||||
|
pause
|
||||||
58
sign-apk-windows.bat
Normale Datei
@ -0,0 +1,58 @@
|
|||||||
|
@echo off
|
||||||
|
echo ===============================================
|
||||||
|
echo TaskMate APK Signer für Windows
|
||||||
|
echo ===============================================
|
||||||
|
echo.
|
||||||
|
|
||||||
|
:: Variablen setzen
|
||||||
|
set APK_NAME=TaskMate-unsigned.apk
|
||||||
|
set SIGNED_APK=TaskMate-signed.apk
|
||||||
|
set KEYSTORE=%USERPROFILE%\.android\debug.keystore
|
||||||
|
|
||||||
|
:: Prüfe ob APK existiert
|
||||||
|
if not exist "%APK_NAME%" (
|
||||||
|
echo FEHLER: %APK_NAME% wurde nicht gefunden!
|
||||||
|
echo Bitte diese Datei im gleichen Ordner wie dieses Script ablegen.
|
||||||
|
pause
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
:: Prüfe ob Java installiert ist
|
||||||
|
java -version >nul 2>&1
|
||||||
|
if errorlevel 1 (
|
||||||
|
echo FEHLER: Java ist nicht installiert oder nicht im PATH!
|
||||||
|
echo.
|
||||||
|
echo Bitte Java installieren von: https://www.java.com/download/
|
||||||
|
pause
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
:: Debug Keystore erstellen falls nicht vorhanden
|
||||||
|
if not exist "%KEYSTORE%" (
|
||||||
|
echo Debug-Keystore wird erstellt...
|
||||||
|
mkdir "%USERPROFILE%\.android" 2>nul
|
||||||
|
keytool -genkey -v -keystore "%KEYSTORE%" -storepass android -alias androiddebugkey -keypass android -keyalg RSA -keysize 2048 -validity 10000 -dname "CN=Android Debug,O=Android,C=US"
|
||||||
|
)
|
||||||
|
|
||||||
|
:: APK signieren
|
||||||
|
echo.
|
||||||
|
echo Signiere APK...
|
||||||
|
jarsigner -verbose -sigalg SHA256withRSA -digestalg SHA-256 -keystore "%KEYSTORE%" -storepass android -signedjar "%SIGNED_APK%" "%APK_NAME%" androiddebugkey
|
||||||
|
|
||||||
|
if errorlevel 1 (
|
||||||
|
echo.
|
||||||
|
echo FEHLER beim Signieren!
|
||||||
|
pause
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
:: Erfolgsmeldung
|
||||||
|
echo.
|
||||||
|
echo ===============================================
|
||||||
|
echo ERFOLGREICH!
|
||||||
|
echo Die signierte APK wurde erstellt: %SIGNED_APK%
|
||||||
|
echo.
|
||||||
|
echo Diese Datei können Sie jetzt auf Ihrem Android-Gerät installieren.
|
||||||
|
echo ===============================================
|
||||||
|
echo.
|
||||||
|
pause
|
||||||