Calendar V3

This commit is contained in:
2026-03-11 13:15:20 +01:00
parent a4815fc672
commit 94215fdd65
13 changed files with 190 additions and 61 deletions

View File

@@ -174,6 +174,7 @@ Key settings:
- **Username**: CalDAV authentication username - **Username**: CalDAV authentication username
- **Password**: CalDAV authentication password - **Password**: CalDAV authentication password
- **Color**: CSS color for calendar widget indicators - **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. 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. 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. - 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 - Calendar output is marked as non-cacheable to keep missing/existing link styling and
current-day highlighting up to date. current-day highlighting up to date.
- Each day cell shows colored corner indicators for calendar slots that have events on that day. - Each day cell can show colored corner triangles for slots that have events on that day.
Indicator positions (clockwise): general = top-left, maintenance = top-right, Indicator placement is configured per slot via the `Display` setting.
slot3 = bottom-right, slot4 = bottom-left.
Indicator colors are taken from the slot's configured color. Indicator colors are taken from the slot's configured color.
### 0.4) Virtual chronological day pages ### 0.4) Virtual chronological day pages

View File

@@ -168,14 +168,16 @@ class action_plugin_luxtools extends ActionPlugin
$slots = CalendarSlot::loadEnabled($this); $slots = CalendarSlot::loadEnabled($this);
$indicators = CalendarService::monthIndicators($slots, $year, $month); $indicators = CalendarService::monthIndicators($slots, $year, $month);
$slotColors = []; $slotColors = [];
$slotDisplays = [];
foreach ($slots as $slot) { foreach ($slots as $slot) {
$color = $slot->getColor(); $color = $slot->getColor();
if ($color !== '') { if ($color !== '') {
$slotColors[$slot->getKey()] = $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 === '') { if ($html === '') {
http_status(500); http_status(500);
echo 'Calendar rendering failed'; echo 'Calendar rendering failed';
@@ -741,7 +743,7 @@ class action_plugin_luxtools extends ActionPlugin
$remoteError = ''; $remoteError = '';
if ($maintenanceSlot->hasRemoteSource()) { if ($maintenanceSlot->hasRemoteSource()) {
try { try {
$caldavOk = CalDavClient::updateEventStatus( $caldavResult = CalDavClient::updateEventStatus(
$maintenanceSlot->getCaldavUrl(), $maintenanceSlot->getCaldavUrl(),
$maintenanceSlot->getUsername(), $maintenanceSlot->getUsername(),
$maintenanceSlot->getPassword(), $maintenanceSlot->getPassword(),
@@ -750,13 +752,13 @@ class action_plugin_luxtools extends ActionPlugin
$newStatus, $newStatus,
$dateIso $dateIso
); );
if (!$caldavOk) { if ($caldavResult !== '') {
$remoteOk = false; $remoteOk = false;
$remoteError = $this->getLang('maintenance_remote_write_failed'); $remoteError = $this->getLang('maintenance_remote_write_failed') . ': ' . $caldavResult;
} }
} catch (Throwable $e) { } catch (Throwable $e) {
$remoteOk = false; $remoteOk = false;
$remoteError = $this->getLang('maintenance_remote_write_failed'); $remoteError = $this->getLang('maintenance_remote_write_failed') . ': ' . $e->getMessage();
} }
} }

View File

@@ -37,21 +37,25 @@ class admin_plugin_luxtools_main extends DokuWiki_Admin_Plugin
'calendar_general_username', 'calendar_general_username',
'calendar_general_password', 'calendar_general_password',
'calendar_general_color', 'calendar_general_color',
'calendar_general_display',
'calendar_maintenance_file', 'calendar_maintenance_file',
'calendar_maintenance_caldav_url', 'calendar_maintenance_caldav_url',
'calendar_maintenance_username', 'calendar_maintenance_username',
'calendar_maintenance_password', 'calendar_maintenance_password',
'calendar_maintenance_color', 'calendar_maintenance_color',
'calendar_maintenance_display',
'calendar_slot3_file', 'calendar_slot3_file',
'calendar_slot3_caldav_url', 'calendar_slot3_caldav_url',
'calendar_slot3_username', 'calendar_slot3_username',
'calendar_slot3_password', 'calendar_slot3_password',
'calendar_slot3_color', 'calendar_slot3_color',
'calendar_slot3_display',
'calendar_slot4_file', 'calendar_slot4_file',
'calendar_slot4_caldav_url', 'calendar_slot4_caldav_url',
'calendar_slot4_username', 'calendar_slot4_username',
'calendar_slot4_password', 'calendar_slot4_password',
'calendar_slot4_color', 'calendar_slot4_color',
'calendar_slot4_display',
'pagelink_search_depth', '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 . '_username'] = trim($INPUT->str('calendar_' . $slot . '_username'));
$newConf['calendar_' . $slot . '_password'] = trim($INPUT->str('calendar_' . $slot . '_password')); $newConf['calendar_' . $slot . '_password'] = trim($INPUT->str('calendar_' . $slot . '_password'));
$newConf['calendar_' . $slot . '_color'] = trim($INPUT->str('calendar_' . $slot . '_color')); $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'); $depth = (int)$INPUT->int('pagelink_search_depth');
@@ -274,6 +279,13 @@ class admin_plugin_luxtools_main extends DokuWiki_Admin_Plugin
'slot3' => 'Slot 3', 'slot3' => 'Slot 3',
'slot4' => 'Slot 4', '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) { foreach ($this->calendarSlotKeys as $slot) {
echo '<h2>' . hsc($this->getLang('calendar_slot_heading') . ': ' . $slotLabels[$slot]) . '</h2>'; echo '<h2>' . hsc($this->getLang('calendar_slot_heading') . ': ' . $slotLabels[$slot]) . '</h2>';
@@ -298,6 +310,18 @@ class admin_plugin_luxtools_main extends DokuWiki_Admin_Plugin
echo '<label class="block"><span>' . hsc($this->getLang('calendar_slot_color')) . '</span> '; echo '<label class="block"><span>' . hsc($this->getLang('calendar_slot_color')) . '</span> ';
echo '<input type="color" name="' . hsc($prefix . 'color') . '" value="' . hsc((string)$this->getConf($prefix . 'color') ?: '#999999') . '" />'; echo '<input type="color" name="' . hsc($prefix . 'color') . '" value="' . hsc((string)$this->getConf($prefix . 'color') ?: '#999999') . '" />';
echo '</label><br />'; echo '</label><br />';
$currentDisplay = (string)$this->getConf($prefix . 'display');
if ($currentDisplay === '') $currentDisplay = 'none';
echo '<label class="block"><span>' . hsc($this->getLang('calendar_slot_display')) . '</span> ';
echo '<select name="' . hsc($prefix . 'display') . '" class="edit">';
foreach ($displayOptions as $value => $label) {
if ($label === '') $label = $value;
$selected = ($currentDisplay === $value) ? ' selected="selected"' : '';
echo '<option value="' . hsc($value) . '"' . $selected . '>' . hsc($label) . '</option>';
}
echo '</select>';
echo '</label><br />';
} }
// CalDAV Sync button (outside the save form, separate action) // CalDAV Sync button (outside the save form, separate action)

View File

@@ -38,30 +38,34 @@ $conf['open_service_url'] = 'http://127.0.0.1:8765';
$conf['image_base_path'] = ''; $conf['image_base_path'] = '';
// Calendar slot configuration (4 slots: general, maintenance, slot3, slot4) // 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_file'] = '';
$conf['calendar_general_caldav_url'] = ''; $conf['calendar_general_caldav_url'] = '';
$conf['calendar_general_username'] = ''; $conf['calendar_general_username'] = '';
$conf['calendar_general_password'] = ''; $conf['calendar_general_password'] = '';
$conf['calendar_general_color'] = '#4a90d9'; $conf['calendar_general_color'] = '#4a90d9';
$conf['calendar_general_display'] = 'none';
$conf['calendar_maintenance_file'] = ''; $conf['calendar_maintenance_file'] = '';
$conf['calendar_maintenance_caldav_url'] = ''; $conf['calendar_maintenance_caldav_url'] = '';
$conf['calendar_maintenance_username'] = ''; $conf['calendar_maintenance_username'] = '';
$conf['calendar_maintenance_password'] = ''; $conf['calendar_maintenance_password'] = '';
$conf['calendar_maintenance_color'] = '#e67e22'; $conf['calendar_maintenance_color'] = '#e67e22';
$conf['calendar_maintenance_display'] = 'none';
$conf['calendar_slot3_file'] = ''; $conf['calendar_slot3_file'] = '';
$conf['calendar_slot3_caldav_url'] = ''; $conf['calendar_slot3_caldav_url'] = '';
$conf['calendar_slot3_username'] = ''; $conf['calendar_slot3_username'] = '';
$conf['calendar_slot3_password'] = ''; $conf['calendar_slot3_password'] = '';
$conf['calendar_slot3_color'] = '#27ae60'; $conf['calendar_slot3_color'] = '#27ae60';
$conf['calendar_slot3_display'] = 'none';
$conf['calendar_slot4_file'] = ''; $conf['calendar_slot4_file'] = '';
$conf['calendar_slot4_caldav_url'] = ''; $conf['calendar_slot4_caldav_url'] = '';
$conf['calendar_slot4_username'] = ''; $conf['calendar_slot4_username'] = '';
$conf['calendar_slot4_password'] = ''; $conf['calendar_slot4_password'] = '';
$conf['calendar_slot4_color'] = '#8e44ad'; $conf['calendar_slot4_color'] = '#8e44ad';
$conf['calendar_slot4_display'] = 'none';
// Maximum depth when searching for .pagelink files under allowed roots. // Maximum depth when searching for .pagelink files under allowed roots.
$conf['pagelink_search_depth'] = 3; $conf['pagelink_search_depth'] = 3;

View File

@@ -66,12 +66,12 @@
html += '<div class="luxtools-event-popup-field">'; html += '<div class="luxtools-event-popup-field">';
if (allDay) { if (allDay) {
html += '<strong>Date:</strong> ' + formatDate(start); html += '<strong>Date:</strong> ' + formatDate(start);
if (end) { if (end && !isSameMoment(start, end)) {
html += ' &ndash; ' + formatDate(end); html += ' &ndash; ' + formatDate(end);
} }
} else { } else {
html += '<strong>Time:</strong> ' + formatDateTime(start); html += '<strong>Time:</strong> ' + formatDateTime(start);
if (end) { if (end && !isSameMoment(start, end)) {
html += ' &ndash; ' + formatDateTime(end); html += ' &ndash; ' + formatDateTime(end);
} }
} }
@@ -114,14 +114,23 @@
if (!isoStr) return ''; if (!isoStr) return '';
var d = new Date(isoStr); var d = new Date(isoStr);
if (isNaN(d.getTime())) return isoStr; if (isNaN(d.getTime())) return isoStr;
return d.toLocaleDateString(); return pad2(d.getDate()) + '.' + pad2(d.getMonth() + 1) + '.' + d.getFullYear();
} }
function formatDateTime(isoStr) { function formatDateTime(isoStr) {
if (!isoStr) return ''; if (!isoStr) return '';
var d = new Date(isoStr); var d = new Date(isoStr);
if (isNaN(d.getTime())) return 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) { function escapeHtml(text) {
@@ -198,11 +207,9 @@
// Visual feedback: mark item as done or revert // Visual feedback: mark item as done or revert
if (action === 'complete') { if (action === 'complete') {
item.classList.add('luxtools-task-completed'); item.classList.add('luxtools-task-completed');
// Fade out and remove after a short delay button.textContent = 'Reopen';
item.style.opacity = '0.5'; button.setAttribute('data-action', 'reopen');
setTimeout(function () { button.disabled = false;
item.style.display = 'none';
}, 1000);
} else { } else {
item.classList.remove('luxtools-task-completed'); item.classList.remove('luxtools-task-completed');
item.style.opacity = '1'; item.style.opacity = '1';

View File

@@ -86,9 +86,10 @@
var formatter; var formatter;
try { try {
formatter = new Intl.DateTimeFormat(undefined, { formatter = new Intl.DateTimeFormat('de-DE', {
hour: '2-digit', hour: '2-digit',
minute: '2-digit' minute: '2-digit',
hour12: false
}); });
} catch (e) { } catch (e) {
formatter = null; formatter = null;

View File

@@ -70,6 +70,12 @@ $lang["calendar_slot_caldav_url"] = "CalDAV-URL";
$lang["calendar_slot_username"] = "Benutzername"; $lang["calendar_slot_username"] = "Benutzername";
$lang["calendar_slot_password"] = "Passwort"; $lang["calendar_slot_password"] = "Passwort";
$lang["calendar_slot_color"] = "Anzeigefarbe"; $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"] = $lang["pagelink_search_depth"] =
"Maximale Verzeichnisebene für .pagelink-Suche (0 = nur Root)."; "Maximale Verzeichnisebene für .pagelink-Suche (0 = nur Root).";

View File

@@ -70,6 +70,12 @@ $lang["calendar_slot_caldav_url"] = "CalDAV URL";
$lang["calendar_slot_username"] = "Username"; $lang["calendar_slot_username"] = "Username";
$lang["calendar_slot_password"] = "Password"; $lang["calendar_slot_password"] = "Password";
$lang["calendar_slot_color"] = "Display color"; $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"] = $lang["pagelink_search_depth"] =
"Maximum directory depth for .pagelink search (0 = only root)."; "Maximum directory depth for .pagelink search (0 = only root).";

View File

@@ -20,6 +20,9 @@ class CalDavClient
/** @var int HTTP timeout in seconds */ /** @var int HTTP timeout in seconds */
protected const TIMEOUT = 30; 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. * 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 $recurrenceId Recurrence ID (empty for non-recurring)
* @param string $newStatus New status value (e.g. COMPLETED, TODO) * @param string $newStatus New status value (e.g. COMPLETED, TODO)
* @param string $dateIso Occurrence date YYYY-MM-DD * @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( public static function updateEventStatus(
string $caldavUrl, string $caldavUrl,
@@ -43,13 +46,17 @@ class CalDavClient
string $recurrenceId, string $recurrenceId,
string $newStatus, string $newStatus,
string $dateIso string $dateIso
): bool { ): string {
if ($caldavUrl === '' || $uid === '') return false; if ($caldavUrl === '' || $uid === '') return 'Missing CalDAV URL or UID';
try { try {
// Find the calendar object href for this UID via REPORT // Find the calendar object href for this UID via REPORT
$objectInfo = self::findObjectByUid($caldavUrl, $username, $password, $uid); $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']; $objectHref = $objectInfo['href'];
$etag = $objectInfo['etag']; $etag = $objectInfo['etag'];
@@ -57,19 +64,35 @@ class CalDavClient
// Parse and update the status // Parse and update the status
$calendar = Reader::read($calendarData, Reader::OPTION_FORGIVING); $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( $updated = IcsWriter::applyStatusUpdateToCalendar(
$calendar, $uid, $recurrenceId, $newStatus, $dateIso $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(); $newData = $calendar->serialize();
// PUT the updated object back with If-Match for conflict detection // 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) { } 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 $password
* @param string $data ICS data to write * @param string $data ICS data to write
* @param string $etag ETag for If-Match header (empty to skip) * @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( protected static function putCalendarObject(
string $href, string $href,
@@ -251,7 +274,7 @@ class CalDavClient
string $password, string $password,
string $data, string $data,
string $etag string $etag
): bool { ): string {
$headers = [ $headers = [
'Content-Type: text/calendar; charset=utf-8', 'Content-Type: text/calendar; charset=utf-8',
]; ];
@@ -260,9 +283,10 @@ class CalDavClient
} }
$response = self::request('PUT', $href, $username, $password, $data, $headers); $response = self::request('PUT', $href, $username, $password, $data, $headers);
// PUT returns null body but we check by HTTP status via the request method if ($response === null) {
// A successful PUT returns 2xx return self::$lastRequestError ?: 'CalDAV PUT failed (unknown error)';
return $response !== null; }
return '';
} }
/** /**
@@ -293,8 +317,6 @@ class CalDavClient
$etags = $resp->xpath('.//d:getetag'); $etags = $resp->xpath('.//d:getetag');
$etag = ($etags && count($etags) > 0) ? trim((string)$etags[0]) : ''; $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'); $caldata = $resp->xpath('.//cal:calendar-data');
$data = ($caldata && count($caldata) > 0) ? trim((string)$caldata[0]) : ''; $data = ($caldata && count($caldata) > 0) ? trim((string)$caldata[0]) : '';
@@ -392,10 +414,18 @@ class CalDavClient
string $body = '', string $body = '',
array $headers = [] array $headers = []
): ?string { ): ?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(); $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_URL, $url);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method); curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
@@ -423,9 +453,13 @@ class CalDavClient
// Capture HTTP status code // Capture HTTP status code
$responseBody = curl_exec($ch); $responseBody = curl_exec($ch);
$httpCode = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE); $httpCode = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
$curlError = curl_error($ch);
curl_close($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 // Accept 2xx and 207 (multistatus) responses
if ($httpCode >= 200 && $httpCode < 300) { if ($httpCode >= 200 && $httpCode < 300) {
@@ -435,6 +469,7 @@ class CalDavClient
return $responseBody; return $responseBody;
} }
self::$lastRequestError = "CalDAV $method failed: HTTP $httpCode";
return null; return null;
} }
} }

View File

@@ -6,13 +6,17 @@ namespace dokuwiki\plugin\luxtools;
* Represents one calendar slot configuration. * Represents one calendar slot configuration.
* *
* Each slot has a stable key, a human-readable label, local/remote source * 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 class CalendarSlot
{ {
/** @var string[] Ordered list of all supported slot keys */ /** @var string[] Ordered list of all supported slot keys */
public const SLOT_KEYS = ['general', 'maintenance', 'slot3', 'slot4']; 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<string,string> Human-readable labels for slot keys */ /** @var array<string,string> Human-readable labels for slot keys */
public const SLOT_LABELS = [ public const SLOT_LABELS = [
'general' => 'General', 'general' => 'General',
@@ -42,6 +46,9 @@ class CalendarSlot
/** @var string CSS color for widget indicators */ /** @var string CSS color for widget indicators */
protected $color; protected $color;
/** @var string Widget indicator display position */
protected $display;
/** /**
* @param string $key * @param string $key
* @param string $file * @param string $file
@@ -49,6 +56,7 @@ class CalendarSlot
* @param string $username * @param string $username
* @param string $password * @param string $password
* @param string $color * @param string $color
* @param string $display
*/ */
public function __construct( public function __construct(
string $key, string $key,
@@ -56,7 +64,8 @@ class CalendarSlot
string $caldavUrl = '', string $caldavUrl = '',
string $username = '', string $username = '',
string $password = '', string $password = '',
string $color = '' string $color = '',
string $display = 'none'
) { ) {
$this->key = $key; $this->key = $key;
$this->label = self::SLOT_LABELS[$key] ?? $key; $this->label = self::SLOT_LABELS[$key] ?? $key;
@@ -65,6 +74,7 @@ class CalendarSlot
$this->username = trim($username); $this->username = trim($username);
$this->password = trim($password); $this->password = trim($password);
$this->color = trim($color); $this->color = trim($color);
$this->display = self::normalizeIndicatorDisplay($display);
} }
public function getKey(): string public function getKey(): string
@@ -102,6 +112,16 @@ class CalendarSlot
return $this->color; 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. * 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 . '_caldav_url'),
(string)$plugin->getConf('calendar_' . $key . '_username'), (string)$plugin->getConf('calendar_' . $key . '_username'),
(string)$plugin->getConf('calendar_' . $key . '_password'), (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; return $slots;
@@ -168,4 +189,14 @@ class CalendarSlot
return $slot->isEnabled(); 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;
}
} }

View File

@@ -15,6 +15,7 @@ class ChronologicalCalendarWidget
* @param string $baseNs * @param string $baseNs
* @param array<string,string[]> $indicators date => [slotKey, ...] from CalendarService::monthIndicators() * @param array<string,string[]> $indicators date => [slotKey, ...] from CalendarService::monthIndicators()
* @param array<string,string> $slotColors slotKey => CSS color * @param array<string,string> $slotColors slotKey => CSS color
* @param array<string,string> $slotDisplays slotKey => configured indicator position
* @return string * @return string
*/ */
public static function render( public static function render(
@@ -22,7 +23,8 @@ class ChronologicalCalendarWidget
int $month, int $month,
string $baseNs = 'chronological', string $baseNs = 'chronological',
array $indicators = [], array $indicators = [],
array $slotColors = [] array $slotColors = [],
array $slotDisplays = []
): string { ): string {
if (!self::isValidMonth($year, $month)) return ''; if (!self::isValidMonth($year, $month)) return '';
@@ -120,13 +122,19 @@ class ChronologicalCalendarWidget
// Render slot indicators if any // Render slot indicators if any
$dayIndicators = $indicators[$date] ?? []; $dayIndicators = $indicators[$date] ?? [];
if ($dayIndicators !== []) { if ($dayIndicators !== []) {
$html .= '<div class="luxtools-calendar-indicators">'; $indicatorHtml = '';
foreach ($dayIndicators as $slotKey) { foreach ($dayIndicators as $slotKey) {
$display = $slotDisplays[$slotKey] ?? 'none';
if ($display === 'none') continue;
$color = $slotColors[$slotKey] ?? ''; $color = $slotColors[$slotKey] ?? '';
$style = ($color !== '') ? ' style="background-color:' . hsc($color) . '"' : ''; $style = ($color !== '') ? ' style="background-color:' . hsc($color) . '"' : '';
$html .= '<span class="luxtools-calendar-indicator luxtools-indicator-' . hsc($slotKey) . '"' . $style . '></span>'; $indicatorHtml .= '<span class="luxtools-calendar-indicator luxtools-indicator-' . hsc($display) . '"' . $style . '></span>';
}
if ($indicatorHtml !== '') {
$html .= '<div class="luxtools-calendar-indicators">' . $indicatorHtml . '</div>';
} }
$html .= '</div>';
} }
if ($dayId !== null && function_exists('html_wikilink')) { if ($dayId !== null && function_exists('html_wikilink')) {

View File

@@ -668,29 +668,32 @@ div.luxtools-calendar td.luxtools-calendar-day {
.luxtools-calendar-indicator { .luxtools-calendar-indicator {
position: absolute; position: absolute;
width: 6px; width: 10px;
height: 6px; height: 10px;
border-radius: 50%;
} }
.luxtools-indicator-general { .luxtools-indicator-top-left {
top: 2px; top: 0;
left: 2px; left: 0;
clip-path: polygon(0 0, 100% 0, 0 100%);
} }
.luxtools-indicator-maintenance { .luxtools-indicator-top-right {
top: 2px; top: 0;
right: 2px; right: 0;
clip-path: polygon(0 0, 100% 0, 100% 100%);
} }
.luxtools-indicator-slot3 { .luxtools-indicator-bottom-right {
bottom: 2px; bottom: 0;
right: 2px; right: 0;
clip-path: polygon(100% 0, 100% 100%, 0 100%);
} }
.luxtools-indicator-slot4 { .luxtools-indicator-bottom-left {
bottom: 2px; bottom: 0;
left: 2px; left: 0;
clip-path: polygon(0 0, 0 100%, 100% 100%);
} }

View File

@@ -91,14 +91,16 @@ class syntax_plugin_luxtools_calendar extends SyntaxPlugin
$slots = CalendarSlot::loadEnabled($this); $slots = CalendarSlot::loadEnabled($this);
$indicators = CalendarService::monthIndicators($slots, $year, $month); $indicators = CalendarService::monthIndicators($slots, $year, $month);
$slotColors = []; $slotColors = [];
$slotDisplays = [];
foreach ($slots as $slot) { foreach ($slots as $slot) {
$color = $slot->getColor(); $color = $slot->getColor();
if ($color !== '') { if ($color !== '') {
$slotColors[$slot->getKey()] = $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; return true;
} }