Feature: Calendars are now exportable shareable so Third Party Calendar services can read them, also added recoccurence to calendar, and all day along with sperating the time fields and click in a box to add calendar event is possible

This commit is contained in:
johnnyq
2026-07-30 02:18:30 -04:00
parent 3c8f812a16
commit f2d5ac4a29
15 changed files with 1233 additions and 62 deletions

View File

@@ -62,22 +62,56 @@
</div>
</div>
<div class="form-group">
<label>Start / End <strong class="text-danger">*</strong></label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-calendar-check"></i></span>
</div>
<input type="datetime-local" class="form-control" id="event_add_start" name="start" required onblur="updateIncrementEndTime()">
<div class="custom-control custom-switch">
<input type="checkbox" class="custom-control-input event-all-day-toggle" id="event_add_all_day" name="all_day" value="1" checked>
<label class="custom-control-label" for="event_add_all_day">All day</label>
</div>
</div>
<div class="form-group">
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-calendar"></i></span>
<div class="form-row">
<div class="form-group col-md-6">
<label>Date from <strong class="text-danger">*</strong></label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-calendar-check"></i></span>
</div>
<input type="date" class="form-control event-start-date" id="event_add_start_date" name="start_date" required>
</div>
</div>
<div class="form-group col-md-6">
<label>Date to <strong class="text-danger">*</strong></label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-calendar"></i></span>
</div>
<input type="date" class="form-control" id="event_add_end_date" name="end_date" required>
</div>
</div>
</div>
<!-- Hidden while All day is on. The toggle handler in app.js also
adds and removes required, because a hidden required field
blocks submission with an unfocusable-element error. -->
<div class="form-row d-none" id="event_add_time_fields">
<div class="form-group col-md-6">
<label>Time from <strong class="text-danger">*</strong></label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-clock"></i></span>
</div>
<input type="time" class="form-control event-start-time" id="event_add_start_time" name="start_time">
</div>
</div>
<div class="form-group col-md-6">
<label>Time to <strong class="text-danger">*</strong></label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-clock"></i></span>
</div>
<input type="time" class="form-control" id="event_add_end_time" name="end_time">
</div>
<input type="datetime-local" class="form-control" id="event_add_end" name="end" required>
</div>
</div>
@@ -87,7 +121,7 @@
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-recycle"></i></span>
</div>
<select class="form-control select2" name="repeat" disabled>
<select class="form-control select2" name="repeat">
<option value="">Never</option>
<option>Day</option>
<option>Week</option>

View File

@@ -13,6 +13,18 @@ $event_location = escapeHtml($row['event_location']);
$event_start = escapeHtml($row['event_start']);
$event_end = escapeHtml($row['event_end']);
$event_repeat = escapeHtml($row['event_repeat']);
$event_all_day = intval($row['event_all_day'] ?? 0);
// Split the stored datetimes into the four fields the form now uses. An empty
// event_end previously fed strtotime('') and rendered as 1970 - fall back to the
// start instead.
$event_start_ts = strtotime($event_start) ?: time();
$event_end_ts = !empty($row['event_end']) ? (strtotime($event_end) ?: $event_start_ts) : $event_start_ts;
$event_start_date = date('Y-m-d', $event_start_ts);
$event_start_time = date('H:i', $event_start_ts);
$event_end_date = date('Y-m-d', $event_end_ts);
$event_end_time = date('H:i', $event_end_ts);
$calendar_id = intval($row['calendar_id']);
$calendar_name = escapeHtml($row['calendar_name']);
$calendar_color = escapeHtml($row['calendar_color']);
@@ -89,22 +101,61 @@ ob_start();
</div>
</div>
<?php if (!empty($event_repeat)) { ?>
<div class="alert alert-info">
<i class="fas fa-fw fa-redo mr-2"></i>
This event repeats <strong>every <?= strtolower($event_repeat) ?></strong>.
Saving or deleting affects <strong>every occurrence</strong> &mdash; a single
occurrence cannot be moved or cancelled on its own.
</div>
<?php } ?>
<div class="form-group">
<label>Start / End <strong class="text-danger">*</strong></label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-calendar-check"></i></span>
</div>
<input type="datetime-local" class="form-control" name="start" value="<?= date('Y-m-d\TH:i:s', strtotime($event_start)) ?>" required>
<div class="custom-control custom-switch">
<input type="checkbox" class="custom-control-input event-all-day-toggle" id="event_edit_all_day" name="all_day" value="1" <?php if ($event_all_day) { echo "checked"; } ?>>
<label class="custom-control-label" for="event_edit_all_day">All day</label>
</div>
</div>
<div class="form-group">
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-calendar-day"></i></span>
<div class="form-row">
<div class="form-group col-md-6">
<label>Date from <strong class="text-danger">*</strong></label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-calendar-check"></i></span>
</div>
<input type="date" class="form-control event-start-date" id="event_edit_start_date" name="start_date" value="<?= $event_start_date ?>" required>
</div>
</div>
<div class="form-group col-md-6">
<label>Date to <strong class="text-danger">*</strong></label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-calendar-day"></i></span>
</div>
<input type="date" class="form-control" id="event_edit_end_date" name="end_date" value="<?= $event_end_date ?>" required>
</div>
</div>
</div>
<div class="form-row<?= $event_all_day ? ' d-none' : '' ?>" id="event_edit_time_fields">
<div class="form-group col-md-6">
<label>Time from <strong class="text-danger">*</strong></label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-clock"></i></span>
</div>
<input type="time" class="form-control event-start-time" id="event_edit_start_time" name="start_time" value="<?= $event_start_time ?>"<?= $event_all_day ? '' : ' required' ?>>
</div>
</div>
<div class="form-group col-md-6">
<label>Time to <strong class="text-danger">*</strong></label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-clock"></i></span>
</div>
<input type="time" class="form-control" id="event_edit_end_time" name="end_time" value="<?= $event_end_time ?>"<?= $event_all_day ? '' : ' required' ?>>
</div>
<input type="datetime-local" class="form-control" name="end" value="<?= date('Y-m-d\TH:i:s', strtotime($event_end)) ?>"required>
</div>
</div>
@@ -114,7 +165,7 @@ ob_start();
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-fw fa-recycle"></i></span>
</div>
<select class="form-control select2" name="repeat" disabled>
<select class="form-control select2" name="repeat">
<option <?php if (empty($event_repeat)) { echo "selected"; } ?> value="">Never</option>
<option <?php if ($event_repeat == "Day") { echo "selected"; } ?>>Day</option>
<option <?php if ($event_repeat == "Week") { echo "selected"; } ?>>Week</option>
@@ -190,7 +241,7 @@ ob_start();
</div>
<div class="modal-footer">
<a class="btn btn-default text-danger mr-auto" href="post.php?delete_event=<?= $event_id ?>&csrf_token=<?= $_SESSION['csrf_token'] ?>"><i class="fa fa-calendar-times mr-2"></i>Delete</a>
<a class="btn btn-default text-danger mr-auto confirm-link" href="post.php?delete_event=<?= $event_id ?>&csrf_token=<?= $_SESSION['csrf_token'] ?>"><i class="fa fa-calendar-times mr-2"></i><?= empty($event_repeat) ? 'Delete' : 'Delete series' ?></a>
<button type="submit" name="edit_event" class="btn btn-primary text-bold"><i class="fa fa-check mr-2"></i>Save</button>
<button type="button" class="btn btn-light" data-dismiss="modal"><i class="fa fa-times mr-2"></i>Cancel</button>
</div>

View File

@@ -0,0 +1,189 @@
<?php
require_once '../../../includes/modal_header.php';
$calendar_id = intval($_GET['id']);
$sql = mysqli_query($mysqli, "SELECT * FROM calendars WHERE calendar_id = $calendar_id LIMIT 1");
$row = mysqli_fetch_assoc($sql);
$calendar_name = escapeHtml($row['calendar_name']);
$calendar_color = escapeHtml($row['calendar_color']);
$calendar_feed_key = escapeHtml($row['calendar_feed_key']);
$calendar_feed_busy_only = intval($row['calendar_feed_busy_only']);
$calendar_feed_created_at = escapeHtml($row['calendar_feed_created_at']);
$calendar_feed_accessed_at = escapeHtml($row['calendar_feed_accessed_at']);
// $config_base_url lives in config.php (written at setup from HTTP_HOST, or from
// the --base_url argument to setup_cli.php) and is expected to be a bare host.
// Tolerate a scheme having been saved into it rather than emitting https://https://
$base_url = rtrim(preg_replace('#^[a-z]+://#i', '', (string) $config_base_url), '/');
$feed_path = "/guest/guest_calendar_feed.php?key=$calendar_feed_key";
$feed_url_https = "https://$base_url$feed_path";
$feed_url_webcal = "webcal://$base_url$feed_path";
// Generate the HTML form content using output buffering.
ob_start();
?>
<div class="modal-header bg-dark">
<h5 class="modal-title"><i class="fas fa-fw fa-share-alt mr-2"></i>Share <?= $calendar_name ?></h5>
<button type="button" class="close text-white" data-dismiss="modal">
<span>&times;</span>
</button>
</div>
<?php if (empty($base_url)) { ?>
<div class="modal-body">
<div class="alert alert-danger mb-0">
<i class="fas fa-fw fa-exclamation-triangle mr-2"></i>
<strong>$config_base_url is not set in config.php.</strong>
Without it there is no hostname to build a subscription link from. Set it
to the hostname this install is reached on and reopen this dialog.
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-light" data-dismiss="modal">
<i class="fas fa-fw fa-times mr-2"></i>Close
</button>
</div>
<?php } elseif (empty($calendar_feed_key)) { ?>
<form action="post.php" method="post" autocomplete="off">
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
<input type="hidden" name="calendar_id" value="<?= $calendar_id ?>">
<div class="modal-body">
<p>
Publishing <strong><?= $calendar_name ?></strong> creates a secret link that
Google Calendar, Nextcloud, Apple Calendar or Thunderbird can subscribe to.
</p>
<div class="alert alert-warning">
<i class="fas fa-fw fa-exclamation-triangle mr-2"></i>
Anyone with the link can read this calendar without logging in. Share it
like a password, and revoke it here if it gets out.
</div>
<div class="form-group">
<div class="custom-control custom-switch">
<input type="checkbox" class="custom-control-input" id="feedBusyOnly" name="busy_only" value="1">
<label class="custom-control-label" for="feedBusyOnly">
Busy only &mdash; publish time blocks without titles, descriptions or locations
</label>
</div>
</div>
</div>
<div class="modal-footer">
<button type="submit" name="share_calendar" class="btn btn-primary">
<i class="fas fa-fw fa-link mr-2"></i>Create link
</button>
<button type="button" class="btn btn-light" data-dismiss="modal">
<i class="fas fa-fw fa-times mr-2"></i>Cancel
</button>
</div>
</form>
<?php } else { ?>
<form action="post.php" method="post" autocomplete="off">
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
<input type="hidden" name="calendar_id" value="<?= $calendar_id ?>">
<div class="modal-body">
<div class="form-group">
<label>Subscription link</label>
<div class="input-group">
<input type="text" class="form-control" value="<?= $feed_url_https ?>" readonly onclick="this.select();">
<div class="input-group-append">
<button class="btn btn-secondary clipboardjs" type="button" data-clipboard-text="<?= $feed_url_https ?>" title="Copy link">
<i class="far fa-fw fa-copy"></i>
</button>
</div>
</div>
<small class="form-text text-muted">
Paste this into <strong>Google Calendar</strong> &rarr; Other calendars &rarr; From URL,
or <strong>Nextcloud Calendar</strong> &rarr; New calendar &rarr; New subscription.
</small>
</div>
<div class="form-group">
<label>Or open directly in a desktop calendar app</label>
<div class="input-group">
<input type="text" class="form-control" value="<?= $feed_url_webcal ?>" readonly onclick="this.select();">
<div class="input-group-append">
<button class="btn btn-secondary clipboardjs" type="button" data-clipboard-text="<?= $feed_url_webcal ?>" title="Copy link">
<i class="far fa-fw fa-copy"></i>
</button>
<a class="btn btn-secondary" href="<?= $feed_url_webcal ?>" title="Open">
<i class="fas fa-fw fa-external-link-alt"></i>
</a>
</div>
</div>
</div>
<hr>
<div class="form-group">
<div class="custom-control custom-switch">
<input type="checkbox" class="custom-control-input" id="feedBusyOnly" name="busy_only" value="1" <?php if ($calendar_feed_busy_only == 1) { echo "checked"; } ?>>
<label class="custom-control-label" for="feedBusyOnly">
Busy only &mdash; publish time blocks without titles, descriptions or locations
</label>
</div>
</div>
<dl class="row mb-0 text-muted">
<dt class="col-5">Link created</dt>
<dd class="col-7"><?= $calendar_feed_created_at ?: 'Unknown' ?></dd>
<dt class="col-5">Last fetched</dt>
<dd class="col-7"><?= $calendar_feed_accessed_at ?: 'Never' ?></dd>
</dl>
<div class="alert alert-secondary mt-3 mb-0">
<i class="fas fa-fw fa-info-circle mr-2"></i>
Subscriptions are read-only, and clients decide how often to refresh.
Google refreshes on its own schedule (often 12&ndash;24 hours) and cannot be
forced. Nextcloud defaults to once a week unless
<code>calendarSubscriptionRefreshRate</code> is lowered, and refuses
subscriptions pointing at a private IP address.
</div>
</div>
<div class="modal-footer justify-content-between">
<div>
<button type="submit" name="share_calendar" class="btn btn-primary">
<i class="fas fa-fw fa-check mr-2"></i>Save
</button>
<button type="button" class="btn btn-light" data-dismiss="modal">
<i class="fas fa-fw fa-times mr-2"></i>Close
</button>
</div>
<div class="dropdown dropup">
<button class="btn btn-light" type="button" data-toggle="dropdown">
<i class="fas fa-fw fa-ellipsis-v"></i>
</button>
<div class="dropdown-menu dropdown-menu-right">
<a class="dropdown-item confirm-link" href="post.php?regenerate_calendar_feed=<?= $calendar_id ?>&csrf_token=<?= $_SESSION['csrf_token'] ?>">
<i class="fas fa-fw fa-sync mr-2"></i>Regenerate link
</a>
<div class="dropdown-divider"></div>
<a class="dropdown-item text-danger text-bold confirm-link" href="post.php?unshare_calendar=<?= $calendar_id ?>&csrf_token=<?= $_SESSION['csrf_token'] ?>">
<i class="fas fa-fw fa-unlink mr-2"></i>Stop sharing
</a>
</div>
</div>
</div>
</form>
<?php } ?>
<?php
require_once '../../../includes/modal_footer.php';