Logo für Webseiten-Tab implementiert

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

Datei anzeigen

@ -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äre Datei nicht angezeigt.

Nachher

Breite:  |  Höhe:  |  Größe: 3.9 KiB

Binäre Datei nicht angezeigt.

Nachher

Breite:  |  Höhe:  |  Größe: 5.7 KiB

Binäre Datei nicht angezeigt.

Nachher

Breite:  |  Höhe:  |  Größe: 9.0 KiB

Binäre Datei nicht angezeigt.

Nachher

Breite:  |  Höhe:  |  Größe: 1.7 KiB

Binäre Datei nicht angezeigt.

Nachher

Breite:  |  Höhe:  |  Größe: 31 KiB

Binäre Datei nicht angezeigt.

Nachher

Breite:  |  Höhe:  |  Größe: 2.7 KiB

Binäre Datei nicht angezeigt.

Nachher

Breite:  |  Höhe:  |  Größe: 3.9 KiB

Binäre Datei nicht angezeigt.

Nachher

Breite:  |  Höhe:  |  Größe: 3.9 KiB

Datei anzeigen

@ -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äre Datei nicht angezeigt.

Nachher

Breite:  |  Höhe:  |  Größe: 1.7 KiB

Binäre Datei nicht angezeigt.

Nachher

Breite:  |  Höhe:  |  Größe: 1.7 KiB

Datei anzeigen

@ -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!');

Datei anzeigen

@ -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äre Datei nicht angezeigt.

Nachher

Breite:  |  Höhe:  |  Größe: 5.7 KiB

Binäre Datei nicht angezeigt.

Nachher

Breite:  |  Höhe:  |  Größe: 5.7 KiB

Binäre Datei nicht angezeigt.

Nachher

Breite:  |  Höhe:  |  Größe: 9.0 KiB

Binäre Datei nicht angezeigt.

Nachher

Breite:  |  Höhe:  |  Größe: 9.0 KiB

Binäre Datei nicht angezeigt.

Nachher

Breite:  |  Höhe:  |  Größe: 31 KiB

Binäre Datei nicht angezeigt.

Nachher

Breite:  |  Höhe:  |  Größe: 1.7 KiB

Binäre Datei nicht angezeigt.

Nachher

Breite:  |  Höhe:  |  Größe: 31 KiB

Binäre Datei nicht angezeigt.

Nachher

Breite:  |  Höhe:  |  Größe: 2.7 KiB

Binäre Datei nicht angezeigt.

Nachher

Breite:  |  Höhe:  |  Größe: 3.9 KiB

Datei anzeigen

@ -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
```

Datei anzeigen

@ -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>

Datei anzeigen

@ -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äre Datei nicht angezeigt.

Nachher

Breite:  |  Höhe:  |  Größe: 5.7 KiB

Binäre Datei nicht angezeigt.

Nachher

Breite:  |  Höhe:  |  Größe: 5.7 KiB

Binäre Datei nicht angezeigt.

Nachher

Breite:  |  Höhe:  |  Größe: 9.0 KiB

Binäre Datei nicht angezeigt.

Nachher

Breite:  |  Höhe:  |  Größe: 9.0 KiB

Binäre Datei nicht angezeigt.

Nachher

Breite:  |  Höhe:  |  Größe: 31 KiB

Binäre Datei nicht angezeigt.

Nachher

Breite:  |  Höhe:  |  Größe: 1.7 KiB

Binäre Datei nicht angezeigt.

Nachher

Breite:  |  Höhe:  |  Größe: 31 KiB

Binäre Datei nicht angezeigt.

Nachher

Breite:  |  Höhe:  |  Größe: 2.7 KiB

Binäre Datei nicht angezeigt.

Nachher

Breite:  |  Höhe:  |  Größe: 3.9 KiB

Datei anzeigen

@ -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

Datei anzeigen

@ -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
Datei anzeigen

@ -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);
}
}

Datei anzeigen

@ -8,6 +8,15 @@
<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 -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<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/responsive.css">
<link rel="stylesheet" href="css/mobile.css">
<link rel="stylesheet" href="css/pwa.css">
<!-- Favicon -->
<link rel="icon" type="image/svg+xml" href="assets/icons/task.svg">
<!-- Favicon - Transparente PNG Icons ohne schwarzen Hintergrund -->
<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>
<body>

Datei anzeigen

@ -133,12 +133,23 @@ class ApiClient {
}
// 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');
this.setToken(null);
this.setRefreshToken(null);
this.setCsrfToken(null);
window.dispatchEvent(new CustomEvent('auth:logout'));
return true;
}
// Proaktiver Token-Refresh Timer
@ -224,8 +235,12 @@ class ApiClient {
// Handle 401 Unauthorized
if (response.status === 401) {
const currentTokenNow = localStorage.getItem('auth_token');
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
if (this.refreshToken && !this.refreshingToken && !options._tokenRefreshAttempted) {
console.log('[API] Attempting token refresh...');
@ -235,14 +250,27 @@ class ApiClient {
return this.request(endpoint, { ...options, _tokenRefreshAttempted: true });
} catch (refreshError) {
console.log('[API] Token refresh failed:', refreshError.message);
// Fallback zum Logout
this.handleAuthFailure();
// Fallback zum Logout - aber nur wenn kein neuer Login stattfand
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);
}
}
// 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);
}

Datei anzeigen

@ -24,6 +24,7 @@ import knowledgeManager from './knowledge.js';
import codingManager from './coding.js';
import mobileManager from './mobile.js';
import reminderManager from './reminders.js';
import pwaManager from './pwa.js';
import { $, $$, debounce, getFromStorage, setToStorage } from './utils.js';
class App {
@ -91,6 +92,9 @@ class App {
// Initialize mobile features
mobileManager.init();
// Initialize PWA features
pwaManager.init();
// Update UI
this.updateUserMenu();

Datei anzeigen

@ -532,19 +532,24 @@ class SessionTimerHandler {
return false;
}
// Session refreshen um neues Token zu bekommen
this.isActive = true; // Aktivieren damit refreshSession funktioniert
await this.refreshSession();
// Timer mit aktuellem Token starten
this.expiresAt = expiresAt;
this.isActive = true;
this.start();
// Timer mit neuem Token starten
this.updateFromToken();
if (this.expiresAt) {
this.start();
return true;
// Token-Refresh VERZÖGERN um Race-Condition zu vermeiden:
// 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
this.isActive = false;
return false;
return true;
}
start() {

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

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

175
frontend/manifest.json Normale Datei
Datei anzeigen

@ -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"
}

Datei anzeigen

@ -4,7 +4,7 @@
* Offline support and caching
*/
const CACHE_VERSION = '292';
const CACHE_VERSION = '297';
const CACHE_NAME = 'taskmate-v' + CACHE_VERSION;
const STATIC_CACHE_NAME = 'taskmate-static-v' + CACHE_VERSION;
const DYNAMIC_CACHE_NAME = 'taskmate-dynamic-v' + CACHE_VERSION;
@ -43,6 +43,7 @@ const STATIC_ASSETS = [
'/js/mobile.js',
'/js/reminders.js',
'/js/contacts.js',
'/js/pwa.js',
'/css/list.css',
'/css/mobile.css',
'/css/admin.css',
@ -52,13 +53,31 @@ const STATIC_ASSETS = [
'/css/knowledge.css',
'/css/coding.css',
'/css/reminders.css',
'/css/contacts.css'
'/css/contacts.css',
'/css/pwa.css',
'/manifest.json'
];
// API routes to cache
const API_CACHE_ROUTES = [
'/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
@ -121,42 +140,88 @@ self.addEventListener('fetch', (event) => {
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) {
// Try network first to always get fresh content
try {
const networkResponse = await fetch(request);
// Cache successful responses
if (networkResponse.ok) {
const cache = await caches.open(STATIC_CACHE_NAME);
cache.put(request, networkResponse.clone());
}
return networkResponse;
} catch (error) {
// If network fails, try cache
const cachedResponse = await caches.match(request);
if (cachedResponse) {
console.log('[SW] Serving from cache (offline):', request.url);
return cachedResponse;
}
// Return offline page if available for navigation
if (request.mode === 'navigate') {
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 {
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) {
const cachedResponse = await caches.match(request);
if (cachedResponse) {
console.log('[SW] Serving HTML from cache (offline):', request.url);
return cachedResponse;
}
// Return offline page
const offlinePage = await caches.match('/index.html');
if (offlinePage) {
return offlinePage;
}
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
async function handleApiRequest(request) {
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
const isCacheable = API_CACHE_ROUTES.some(route =>
@ -180,13 +245,22 @@ async function handleApiRequest(request) {
const cachedResponse = await caches.match(request);
if (cachedResponse) {
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 new Response(
JSON.stringify({
success: false,
error: 'Keine Internetverbindung',
offline: true
}),