collab attachments, multi-day events, calendar improvements

- Collab buckets sync attachments alongside cards (images/PDFs/audio).
  New /api/collab-attachments endpoint on relay, encrypted at rest,
  bucket-scoped for members.
- CalDAV multi-day events via **Bis:** body field. Calendar renders
  event on every day in range with day counter (1/6, 2/6, ...).
- Calendar preserves month across close/reopen (only jumps to today
  on first open).
- Calendar title: centered nav group with [MM-DD] Mon Y [KW-KW] —
  dimmed brackets, fixed width so nothing wiggles.
- Editor: scratchpad uses custom confirm, autobackup-draft dialog
  replaced with custom styled dialog.
- Scanlines theme feature removed (performance issue).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
thandClaude Opus 4.6 committed 2026-04-14 18:22:02 +02:00
1 parent 002562b1d3
commit ca47052768
11 files changed
+254 -64

No files matched your search

+36 -4
View File
@@ -171,6 +171,12 @@ class CalDavSync
$time = $zm[1];
$timeEnd = $zm[2] ?? '';
}
// Multi-day: **Bis:** 2026-04-20 or **Bis:** 2026-04-20 18:00
$endDate = ''; $endTime = '';
if (preg_match('/\*\*Bis:\*\*\s*(\d{4}-\d{2}-\d{2})(?:\s+(\d{1,2}:\d{2}))?/i', $body, $bm)) {
$endDate = $bm[1];
$endTime = $bm[2] ?? '';
}
$location = '';
if (preg_match('/\*\*Ort:\*\*\s*(.+)/i', $body, $lm)) $location = trim($lm[1]);
$description = '';
@@ -178,9 +184,19 @@ class CalDavSync
// Build DTSTART/DTEND
$dateCompact = str_replace('-', '', $date);
$endDateCompact = $endDate ? str_replace('-', '', $endDate) : '';
if ($time) {
$dtstart = $dateCompact . 'T' . str_replace(':', '', $time) . '00';
if ($timeEnd) {
if ($endDate) {
// Multi-day with time — use end date + end time (or start time + 1h)
$useEndTime = $endTime ?: $time;
if (!$endTime) {
$ts = strtotime($endDate . ' ' . $useEndTime);
$dtend = date('Ymd\THis', $ts + 3600);
} else {
$dtend = $endDateCompact . 'T' . str_replace(':', '', $endTime) . '00';
}
} elseif ($timeEnd) {
$dtend = $dateCompact . 'T' . str_replace(':', '', $timeEnd) . '00';
} else {
// Default: +1 hour
@@ -189,8 +205,13 @@ class CalDavSync
}
} else {
$dtstart = $dateCompact;
// All-day: DTEND = next day
$dtend = date('Ymd', strtotime($date . ' +1 day'));
if ($endDate) {
// Multi-day all-day: DTEND is exclusive, so +1 day after end
$dtend = date('Ymd', strtotime($endDate . ' +1 day'));
} else {
// Single-day all-day: DTEND = next day
$dtend = date('Ymd', strtotime($date . ' +1 day'));
}
}
$uid = $card['caldav_uid'] ?: 'card-' . $card['id'] . '@hyprcrd';
@@ -224,12 +245,22 @@ class CalDavSync
$time = '';
$timeEnd = '';
$endDate = '';
if (preg_match('/T(\d{2})(\d{2})/', $dt, $hm)) {
$time = "{$hm[1]}:{$hm[2]}";
}
if (!empty($vevent['dtend']) && preg_match('/T(\d{2})(\d{2})/', $vevent['dtend'], $hm2)) {
$timeEnd = "{$hm2[1]}:{$hm2[2]}";
}
// Detect multi-day event: dtend's date > dtstart's date
if (!empty($vevent['dtend']) && preg_match('/^(\d{4})(\d{2})(\d{2})/', $vevent['dtend'], $edm)) {
$endIso = "{$edm[1]}-{$edm[2]}-{$edm[3]}";
// For all-day events, dtend is exclusive — subtract one day
if (!$time && !empty($vevent['dtend']) && strpos($vevent['dtend'], 'T') === false) {
$endIso = date('Y-m-d', strtotime($endIso . ' -1 day'));
}
if ($endIso > $date) $endDate = $endIso;
}
$summary = $vevent['summary'] ?? 'Termin';
$title = $summary ? "{$date} — {$summary}" : $date;
@@ -244,7 +275,8 @@ class CalDavSync
}
$zeitLine = $time ? ($timeEnd ? "{$time}-{$timeEnd}" : $time) : '';
$body = "**Zeit:** {$zeitLine}\n**Ort:** " . ($vevent['location'] ?? '') . "\n**Teilnehmer:** \n\n---\n\n**Notizen:**\n" . ($vevent['description'] ?? '') . "\n\n{$tag}";
$bisLine = $endDate ? ("\n**Bis:** " . $endDate . ($timeEnd && $endDate ? ' ' . $timeEnd : '')) : '';
$body = "**Zeit:** {$zeitLine}{$bisLine}\n**Ort:** " . ($vevent['location'] ?? '') . "\n**Teilnehmer:** \n\n---\n\n**Notizen:**\n" . ($vevent['description'] ?? '') . "\n\n{$tag}";
// Color: birthdays pink, others orange
$defaultColor = $isBirthdayCal ? '#fb7185' : '#fb923c';