diff --git a/README.md b/README.md index c4896a4..450cd70 100644 --- a/README.md +++ b/README.md @@ -174,6 +174,7 @@ Key settings: - **Username**: CalDAV authentication username - **Password**: CalDAV authentication password - **Color**: CSS color for calendar widget indicators + - **Display**: Where to show that slot's calendar indicator (`None`, `Top Left`, `Top Right`, `Bottom Left`, `Bottom Right`) A slot is enabled if it has a local file path or a CalDAV URL configured. The old `calendar_ics_files` setting has been replaced by the `general` slot's file path. @@ -300,9 +301,8 @@ Notes: - Month switches fetch server-rendered widget HTML via AJAX and replace only the widget node. - Calendar output is marked as non-cacheable to keep missing/existing link styling and current-day highlighting up to date. -- Each day cell shows colored corner indicators for calendar slots that have events on that day. - Indicator positions (clockwise): general = top-left, maintenance = top-right, - slot3 = bottom-right, slot4 = bottom-left. +- Each day cell can show colored corner triangles for slots that have events on that day. + Indicator placement is configured per slot via the `Display` setting. Indicator colors are taken from the slot's configured color. ### 0.4) Virtual chronological day pages diff --git a/action.php b/action.php index a0d7c4c..5c7d462 100644 --- a/action.php +++ b/action.php @@ -168,14 +168,16 @@ class action_plugin_luxtools extends ActionPlugin $slots = CalendarSlot::loadEnabled($this); $indicators = CalendarService::monthIndicators($slots, $year, $month); $slotColors = []; + $slotDisplays = []; foreach ($slots as $slot) { $color = $slot->getColor(); if ($color !== '') { $slotColors[$slot->getKey()] = $color; } + $slotDisplays[$slot->getKey()] = $slot->getDisplay(); } - $html = ChronologicalCalendarWidget::render($year, $month, $baseNs, $indicators, $slotColors); + $html = ChronologicalCalendarWidget::render($year, $month, $baseNs, $indicators, $slotColors, $slotDisplays); if ($html === '') { http_status(500); echo 'Calendar rendering failed'; @@ -741,7 +743,7 @@ class action_plugin_luxtools extends ActionPlugin $remoteError = ''; if ($maintenanceSlot->hasRemoteSource()) { try { - $caldavOk = CalDavClient::updateEventStatus( + $caldavResult = CalDavClient::updateEventStatus( $maintenanceSlot->getCaldavUrl(), $maintenanceSlot->getUsername(), $maintenanceSlot->getPassword(), @@ -750,13 +752,13 @@ class action_plugin_luxtools extends ActionPlugin $newStatus, $dateIso ); - if (!$caldavOk) { + if ($caldavResult !== '') { $remoteOk = false; - $remoteError = $this->getLang('maintenance_remote_write_failed'); + $remoteError = $this->getLang('maintenance_remote_write_failed') . ': ' . $caldavResult; } } catch (Throwable $e) { $remoteOk = false; - $remoteError = $this->getLang('maintenance_remote_write_failed'); + $remoteError = $this->getLang('maintenance_remote_write_failed') . ': ' . $e->getMessage(); } } diff --git a/admin/main.php b/admin/main.php index 695b88c..5b8a0a3 100644 --- a/admin/main.php +++ b/admin/main.php @@ -37,21 +37,25 @@ class admin_plugin_luxtools_main extends DokuWiki_Admin_Plugin 'calendar_general_username', 'calendar_general_password', 'calendar_general_color', + 'calendar_general_display', 'calendar_maintenance_file', 'calendar_maintenance_caldav_url', 'calendar_maintenance_username', 'calendar_maintenance_password', 'calendar_maintenance_color', + 'calendar_maintenance_display', 'calendar_slot3_file', 'calendar_slot3_caldav_url', 'calendar_slot3_username', 'calendar_slot3_password', 'calendar_slot3_color', + 'calendar_slot3_display', 'calendar_slot4_file', 'calendar_slot4_caldav_url', 'calendar_slot4_username', 'calendar_slot4_password', 'calendar_slot4_color', + 'calendar_slot4_display', 'pagelink_search_depth', ]; @@ -119,6 +123,7 @@ class admin_plugin_luxtools_main extends DokuWiki_Admin_Plugin $newConf['calendar_' . $slot . '_username'] = trim($INPUT->str('calendar_' . $slot . '_username')); $newConf['calendar_' . $slot . '_password'] = trim($INPUT->str('calendar_' . $slot . '_password')); $newConf['calendar_' . $slot . '_color'] = trim($INPUT->str('calendar_' . $slot . '_color')); + $newConf['calendar_' . $slot . '_display'] = trim($INPUT->str('calendar_' . $slot . '_display')); } $depth = (int)$INPUT->int('pagelink_search_depth'); @@ -274,6 +279,13 @@ class admin_plugin_luxtools_main extends DokuWiki_Admin_Plugin 'slot3' => 'Slot 3', 'slot4' => 'Slot 4', ]; + $displayOptions = [ + 'none' => (string)$this->getLang('calendar_slot_display_none'), + 'top-left' => (string)$this->getLang('calendar_slot_display_top_left'), + 'top-right' => (string)$this->getLang('calendar_slot_display_top_right'), + 'bottom-left' => (string)$this->getLang('calendar_slot_display_bottom_left'), + 'bottom-right' => (string)$this->getLang('calendar_slot_display_bottom_right'), + ]; foreach ($this->calendarSlotKeys as $slot) { echo '

' . hsc($this->getLang('calendar_slot_heading') . ': ' . $slotLabels[$slot]) . '

'; @@ -298,6 +310,18 @@ class admin_plugin_luxtools_main extends DokuWiki_Admin_Plugin echo '
'; + + $currentDisplay = (string)$this->getConf($prefix . 'display'); + if ($currentDisplay === '') $currentDisplay = 'none'; + echo '
'; } // CalDAV Sync button (outside the save form, separate action) diff --git a/conf/default.php b/conf/default.php index 28a3509..600aa32 100644 --- a/conf/default.php +++ b/conf/default.php @@ -38,30 +38,34 @@ $conf['open_service_url'] = 'http://127.0.0.1:8765'; $conf['image_base_path'] = ''; // Calendar slot configuration (4 slots: general, maintenance, slot3, slot4) -// Each slot has: file, caldav_url, username, password, color +// Each slot has: file, caldav_url, username, password, color, display $conf['calendar_general_file'] = ''; $conf['calendar_general_caldav_url'] = ''; $conf['calendar_general_username'] = ''; $conf['calendar_general_password'] = ''; $conf['calendar_general_color'] = '#4a90d9'; +$conf['calendar_general_display'] = 'none'; $conf['calendar_maintenance_file'] = ''; $conf['calendar_maintenance_caldav_url'] = ''; $conf['calendar_maintenance_username'] = ''; $conf['calendar_maintenance_password'] = ''; $conf['calendar_maintenance_color'] = '#e67e22'; +$conf['calendar_maintenance_display'] = 'none'; $conf['calendar_slot3_file'] = ''; $conf['calendar_slot3_caldav_url'] = ''; $conf['calendar_slot3_username'] = ''; $conf['calendar_slot3_password'] = ''; $conf['calendar_slot3_color'] = '#27ae60'; +$conf['calendar_slot3_display'] = 'none'; $conf['calendar_slot4_file'] = ''; $conf['calendar_slot4_caldav_url'] = ''; $conf['calendar_slot4_username'] = ''; $conf['calendar_slot4_password'] = ''; $conf['calendar_slot4_color'] = '#8e44ad'; +$conf['calendar_slot4_display'] = 'none'; // Maximum depth when searching for .pagelink files under allowed roots. $conf['pagelink_search_depth'] = 3; diff --git a/js/event-popup.js b/js/event-popup.js index 2044c3f..194258c 100644 --- a/js/event-popup.js +++ b/js/event-popup.js @@ -66,12 +66,12 @@ html += '
'; if (allDay) { html += 'Date: ' + formatDate(start); - if (end) { + if (end && !isSameMoment(start, end)) { html += ' – ' + formatDate(end); } } else { html += 'Time: ' + formatDateTime(start); - if (end) { + if (end && !isSameMoment(start, end)) { html += ' – ' + formatDateTime(end); } } @@ -114,14 +114,23 @@ if (!isoStr) return ''; var d = new Date(isoStr); if (isNaN(d.getTime())) return isoStr; - return d.toLocaleDateString(); + return pad2(d.getDate()) + '.' + pad2(d.getMonth() + 1) + '.' + d.getFullYear(); } function formatDateTime(isoStr) { if (!isoStr) return ''; var d = new Date(isoStr); if (isNaN(d.getTime())) return isoStr; - return d.toLocaleDateString() + ' ' + d.toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' }); + return formatDate(isoStr) + ' ' + pad2(d.getHours()) + ':' + pad2(d.getMinutes()); + } + + function isSameMoment(left, right) { + if (!left || !right) return false; + return left === right; + } + + function pad2(value) { + return String(value).padStart(2, '0'); } function escapeHtml(text) { @@ -198,11 +207,9 @@ // Visual feedback: mark item as done or revert if (action === 'complete') { item.classList.add('luxtools-task-completed'); - // Fade out and remove after a short delay - item.style.opacity = '0.5'; - setTimeout(function () { - item.style.display = 'none'; - }, 1000); + button.textContent = 'Reopen'; + button.setAttribute('data-action', 'reopen'); + button.disabled = false; } else { item.classList.remove('luxtools-task-completed'); item.style.opacity = '1'; diff --git a/js/main.js b/js/main.js index 8ee78c5..710ea2d 100644 --- a/js/main.js +++ b/js/main.js @@ -86,9 +86,10 @@ var formatter; try { - formatter = new Intl.DateTimeFormat(undefined, { + formatter = new Intl.DateTimeFormat('de-DE', { hour: '2-digit', - minute: '2-digit' + minute: '2-digit', + hour12: false }); } catch (e) { formatter = null; diff --git a/lang/de/lang.php b/lang/de/lang.php index cffa425..c241ec4 100644 --- a/lang/de/lang.php +++ b/lang/de/lang.php @@ -70,6 +70,12 @@ $lang["calendar_slot_caldav_url"] = "CalDAV-URL"; $lang["calendar_slot_username"] = "Benutzername"; $lang["calendar_slot_password"] = "Passwort"; $lang["calendar_slot_color"] = "Anzeigefarbe"; +$lang["calendar_slot_display"] = "Anzeige"; +$lang["calendar_slot_display_none"] = "Keine"; +$lang["calendar_slot_display_top_left"] = "Oben links"; +$lang["calendar_slot_display_top_right"] = "Oben rechts"; +$lang["calendar_slot_display_bottom_left"] = "Unten links"; +$lang["calendar_slot_display_bottom_right"] = "Unten rechts"; $lang["pagelink_search_depth"] = "Maximale Verzeichnisebene für .pagelink-Suche (0 = nur Root)."; diff --git a/lang/en/lang.php b/lang/en/lang.php index 887e620..e53c896 100644 --- a/lang/en/lang.php +++ b/lang/en/lang.php @@ -70,6 +70,12 @@ $lang["calendar_slot_caldav_url"] = "CalDAV URL"; $lang["calendar_slot_username"] = "Username"; $lang["calendar_slot_password"] = "Password"; $lang["calendar_slot_color"] = "Display color"; +$lang["calendar_slot_display"] = "Display"; +$lang["calendar_slot_display_none"] = "None"; +$lang["calendar_slot_display_top_left"] = "Top Left"; +$lang["calendar_slot_display_top_right"] = "Top Right"; +$lang["calendar_slot_display_bottom_left"] = "Bottom Left"; +$lang["calendar_slot_display_bottom_right"] = "Bottom Right"; $lang["pagelink_search_depth"] = "Maximum directory depth for .pagelink search (0 = only root)."; diff --git a/src/CalDavClient.php b/src/CalDavClient.php index 501a0bf..f05edad 100644 --- a/src/CalDavClient.php +++ b/src/CalDavClient.php @@ -20,6 +20,9 @@ class CalDavClient /** @var int HTTP timeout in seconds */ protected const TIMEOUT = 30; + /** @var string Last request error message for diagnostics */ + protected static string $lastRequestError = ''; + /** * Update the STATUS of a specific event or task on the remote CalDAV server. * @@ -33,7 +36,7 @@ class CalDavClient * @param string $recurrenceId Recurrence ID (empty for non-recurring) * @param string $newStatus New status value (e.g. COMPLETED, TODO) * @param string $dateIso Occurrence date YYYY-MM-DD - * @return bool True if the remote update succeeded + * @return string Empty string on success, error message on failure */ public static function updateEventStatus( string $caldavUrl, @@ -43,13 +46,17 @@ class CalDavClient string $recurrenceId, string $newStatus, string $dateIso - ): bool { - if ($caldavUrl === '' || $uid === '') return false; + ): string { + if ($caldavUrl === '' || $uid === '') return 'Missing CalDAV URL or UID'; try { // Find the calendar object href for this UID via REPORT $objectInfo = self::findObjectByUid($caldavUrl, $username, $password, $uid); - if ($objectInfo === null) return false; + if ($objectInfo === null) { + $msg = "CalDAV: Could not find object with UID '$uid' on server"; + dbglog($msg); + return $msg; + } $objectHref = $objectInfo['href']; $etag = $objectInfo['etag']; @@ -57,19 +64,35 @@ class CalDavClient // Parse and update the status $calendar = Reader::read($calendarData, Reader::OPTION_FORGIVING); - if (!($calendar instanceof VCalendar)) return false; + if (!($calendar instanceof VCalendar)) { + $msg = "CalDAV: Failed to parse calendar data for UID '$uid'"; + dbglog($msg); + return $msg; + } $updated = IcsWriter::applyStatusUpdateToCalendar( $calendar, $uid, $recurrenceId, $newStatus, $dateIso ); - if (!$updated) return false; + if (!$updated) { + $msg = "CalDAV: applyStatusUpdateToCalendar failed for UID '$uid'"; + dbglog($msg); + return $msg; + } $newData = $calendar->serialize(); // PUT the updated object back with If-Match for conflict detection - return self::putCalendarObject($objectHref, $username, $password, $newData, $etag); + $putError = self::putCalendarObject($objectHref, $username, $password, $newData, $etag); + if ($putError !== '') { + dbglog($putError); + return $putError; + } + + return ''; } catch (Throwable $e) { - return false; + $msg = 'CalDAV: Exception during updateEventStatus: ' . $e->getMessage(); + dbglog($msg); + return $msg; } } @@ -243,7 +266,7 @@ class CalDavClient * @param string $password * @param string $data ICS data to write * @param string $etag ETag for If-Match header (empty to skip) - * @return bool + * @return string Empty string on success, error message on failure */ protected static function putCalendarObject( string $href, @@ -251,7 +274,7 @@ class CalDavClient string $password, string $data, string $etag - ): bool { + ): string { $headers = [ 'Content-Type: text/calendar; charset=utf-8', ]; @@ -260,9 +283,10 @@ class CalDavClient } $response = self::request('PUT', $href, $username, $password, $data, $headers); - // PUT returns null body but we check by HTTP status via the request method - // A successful PUT returns 2xx - return $response !== null; + if ($response === null) { + return self::$lastRequestError ?: 'CalDAV PUT failed (unknown error)'; + } + return ''; } /** @@ -293,8 +317,6 @@ class CalDavClient $etags = $resp->xpath('.//d:getetag'); $etag = ($etags && count($etags) > 0) ? trim((string)$etags[0]) : ''; - // Strip surrounding quotes from etag if present - $etag = trim($etag, '"'); $caldata = $resp->xpath('.//cal:calendar-data'); $data = ($caldata && count($caldata) > 0) ? trim((string)$caldata[0]) : ''; @@ -392,10 +414,18 @@ class CalDavClient string $body = '', array $headers = [] ): ?string { - if (!function_exists('curl_init')) return null; + self::$lastRequestError = ''; + + if (!function_exists('curl_init')) { + self::$lastRequestError = 'CalDAV: curl extension not available'; + return null; + } $ch = curl_init(); - if ($ch === false) return null; + if ($ch === false) { + self::$lastRequestError = 'CalDAV: curl_init() failed'; + return null; + } curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method); @@ -423,9 +453,13 @@ class CalDavClient // Capture HTTP status code $responseBody = curl_exec($ch); $httpCode = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE); + $curlError = curl_error($ch); curl_close($ch); - if (!is_string($responseBody)) return null; + if (!is_string($responseBody)) { + self::$lastRequestError = "CalDAV $method failed: curl error: $curlError"; + return null; + } // Accept 2xx and 207 (multistatus) responses if ($httpCode >= 200 && $httpCode < 300) { @@ -435,6 +469,7 @@ class CalDavClient return $responseBody; } + self::$lastRequestError = "CalDAV $method failed: HTTP $httpCode"; return null; } } diff --git a/src/CalendarSlot.php b/src/CalendarSlot.php index 10965a1..20bb2d4 100644 --- a/src/CalendarSlot.php +++ b/src/CalendarSlot.php @@ -6,13 +6,17 @@ namespace dokuwiki\plugin\luxtools; * Represents one calendar slot configuration. * * Each slot has a stable key, a human-readable label, local/remote source - * configuration, a display color, and a derived enabled state. + * configuration, a display color, an optional widget indicator position, + * and a derived enabled state. */ class CalendarSlot { /** @var string[] Ordered list of all supported slot keys */ public const SLOT_KEYS = ['general', 'maintenance', 'slot3', 'slot4']; + /** @var string[] Allowed widget indicator display positions */ + public const INDICATOR_DISPLAYS = ['none', 'top-left', 'top-right', 'bottom-left', 'bottom-right']; + /** @var array Human-readable labels for slot keys */ public const SLOT_LABELS = [ 'general' => 'General', @@ -42,6 +46,9 @@ class CalendarSlot /** @var string CSS color for widget indicators */ protected $color; + /** @var string Widget indicator display position */ + protected $display; + /** * @param string $key * @param string $file @@ -49,6 +56,7 @@ class CalendarSlot * @param string $username * @param string $password * @param string $color + * @param string $display */ public function __construct( string $key, @@ -56,7 +64,8 @@ class CalendarSlot string $caldavUrl = '', string $username = '', string $password = '', - string $color = '' + string $color = '', + string $display = 'none' ) { $this->key = $key; $this->label = self::SLOT_LABELS[$key] ?? $key; @@ -65,6 +74,7 @@ class CalendarSlot $this->username = trim($username); $this->password = trim($password); $this->color = trim($color); + $this->display = self::normalizeIndicatorDisplay($display); } public function getKey(): string @@ -102,6 +112,16 @@ class CalendarSlot return $this->color; } + public function getDisplay(): string + { + return $this->display; + } + + public function shouldDisplayIndicator(): bool + { + return $this->display !== 'none'; + } + /** * A slot is enabled if it has a local file path or a CalDAV URL. * @@ -149,7 +169,8 @@ class CalendarSlot (string)$plugin->getConf('calendar_' . $key . '_caldav_url'), (string)$plugin->getConf('calendar_' . $key . '_username'), (string)$plugin->getConf('calendar_' . $key . '_password'), - (string)$plugin->getConf('calendar_' . $key . '_color') + (string)$plugin->getConf('calendar_' . $key . '_color'), + (string)$plugin->getConf('calendar_' . $key . '_display') ); } return $slots; @@ -168,4 +189,14 @@ class CalendarSlot return $slot->isEnabled(); }); } + + protected static function normalizeIndicatorDisplay(string $display): string + { + $display = strtolower(trim($display)); + $display = str_replace(['_', ' '], '-', $display); + if (!in_array($display, self::INDICATOR_DISPLAYS, true)) { + return 'none'; + } + return $display; + } } diff --git a/src/ChronologicalCalendarWidget.php b/src/ChronologicalCalendarWidget.php index 66e3bc6..77e7ef7 100644 --- a/src/ChronologicalCalendarWidget.php +++ b/src/ChronologicalCalendarWidget.php @@ -15,6 +15,7 @@ class ChronologicalCalendarWidget * @param string $baseNs * @param array $indicators date => [slotKey, ...] from CalendarService::monthIndicators() * @param array $slotColors slotKey => CSS color + * @param array $slotDisplays slotKey => configured indicator position * @return string */ public static function render( @@ -22,7 +23,8 @@ class ChronologicalCalendarWidget int $month, string $baseNs = 'chronological', array $indicators = [], - array $slotColors = [] + array $slotColors = [], + array $slotDisplays = [] ): string { if (!self::isValidMonth($year, $month)) return ''; @@ -120,13 +122,19 @@ class ChronologicalCalendarWidget // Render slot indicators if any $dayIndicators = $indicators[$date] ?? []; if ($dayIndicators !== []) { - $html .= '
'; + $indicatorHtml = ''; foreach ($dayIndicators as $slotKey) { + $display = $slotDisplays[$slotKey] ?? 'none'; + if ($display === 'none') continue; + $color = $slotColors[$slotKey] ?? ''; $style = ($color !== '') ? ' style="background-color:' . hsc($color) . '"' : ''; - $html .= ''; + $indicatorHtml .= ''; + } + + if ($indicatorHtml !== '') { + $html .= '
' . $indicatorHtml . '
'; } - $html .= '
'; } if ($dayId !== null && function_exists('html_wikilink')) { diff --git a/style.css b/style.css index 6270400..7bff741 100644 --- a/style.css +++ b/style.css @@ -668,29 +668,32 @@ div.luxtools-calendar td.luxtools-calendar-day { .luxtools-calendar-indicator { position: absolute; - width: 6px; - height: 6px; - border-radius: 50%; + width: 10px; + height: 10px; } -.luxtools-indicator-general { - top: 2px; - left: 2px; +.luxtools-indicator-top-left { + top: 0; + left: 0; + clip-path: polygon(0 0, 100% 0, 0 100%); } -.luxtools-indicator-maintenance { - top: 2px; - right: 2px; +.luxtools-indicator-top-right { + top: 0; + right: 0; + clip-path: polygon(0 0, 100% 0, 100% 100%); } -.luxtools-indicator-slot3 { - bottom: 2px; - right: 2px; +.luxtools-indicator-bottom-right { + bottom: 0; + right: 0; + clip-path: polygon(100% 0, 100% 100%, 0 100%); } -.luxtools-indicator-slot4 { - bottom: 2px; - left: 2px; +.luxtools-indicator-bottom-left { + bottom: 0; + left: 0; + clip-path: polygon(0 0, 0 100%, 100% 100%); } diff --git a/syntax/calendar.php b/syntax/calendar.php index a06f822..0cc2505 100644 --- a/syntax/calendar.php +++ b/syntax/calendar.php @@ -91,14 +91,16 @@ class syntax_plugin_luxtools_calendar extends SyntaxPlugin $slots = CalendarSlot::loadEnabled($this); $indicators = CalendarService::monthIndicators($slots, $year, $month); $slotColors = []; + $slotDisplays = []; foreach ($slots as $slot) { $color = $slot->getColor(); if ($color !== '') { $slotColors[$slot->getKey()] = $color; } + $slotDisplays[$slot->getKey()] = $slot->getDisplay(); } - $renderer->doc .= ChronologicalCalendarWidget::render($year, $month, $baseNs, $indicators, $slotColors); + $renderer->doc .= ChronologicalCalendarWidget::render($year, $month, $baseNs, $indicators, $slotColors, $slotDisplays); return true; }