Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Better Calendar Widget Data #4969

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions lib/Controller/ViewController.php
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ public function getCalendarDotSvg(string $color = "#0082c9"): FileDisplayRespons
if (preg_match('/^([0-9a-f]{3}|[0-9a-f]{6})$/i', $color)) {
$validColor = '#' . $color;
}
$svg = '<svg height="32" width="32" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg"><rect width="100%" height="100%" fill="' . $validColor . '"/></svg>';
$svg = '<?xml version="1.0" encoding="UTF-8"?><svg height="32" width="32" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg"><rect width="32" height="32" fill="' . $validColor . '"/></svg>';
$folderName = implode('_', [
'calendar',
$this->userId
Expand All @@ -188,9 +188,11 @@ public function getCalendarDotSvg(string $color = "#0082c9"): FileDisplayRespons
} catch (NotFoundException $e) {
$folder = $this->appData->newFolder($folderName);
}
$file = $folder->newFile($color . '.svg', $svg);
$filename = $color . '.svg';
$file = $folder->fileExists($filename) ? $folder->getFile($filename) : $folder->newFile($filename, $svg);
$response = new FileDisplayResponse($file);
$response->cacheFor(24 * 3600); // 1 day
$response->setHeaders(['Content-Type' => 'image/svg+xml']);
return $response;
}
}
143 changes: 122 additions & 21 deletions lib/Dashboard/CalendarWidget.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
use DateTimeImmutable;
use OCA\Calendar\AppInfo\Application;
use OCA\Calendar\Service\JSDataService;
use OCA\DAV\CalDAV\CalendarImpl;
miaulalala marked this conversation as resolved.
Show resolved Hide resolved
use OCP\AppFramework\Services\IInitialState;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\Calendar\IManager;
Expand All @@ -40,10 +41,17 @@
use OCP\Dashboard\Model\WidgetButton;
use OCP\Dashboard\Model\WidgetItem;
use OCP\Dashboard\Model\WidgetOptions;
use OCP\IConfig;
use OCP\IDateTimeFormatter;
use OCP\IL10N;
use OCP\IURLGenerator;
use OCP\IUserManager;
use OCP\Util;
use Sabre\VObject\Component\VEvent;
use Sabre\VObject\Component\VTimeZone;
use Sabre\VObject\Parameter;
use Sabre\VObject\Property\VCard\Date;
use Sabre\Xml\Reader;

class CalendarWidget implements IAPIWidget, IButtonWidget, IIconWidget, IOptionWidget {
private IL10N $l10n;
Expand All @@ -53,6 +61,7 @@ class CalendarWidget implements IAPIWidget, IButtonWidget, IIconWidget, IOptionW
private IURLGenerator $urlGenerator;
private IManager $calendarManager;
private ITimeFactory $timeFactory;
private IConfig $config;

/**
* CalendarWidget constructor.
Expand All @@ -70,14 +79,16 @@ public function __construct(IL10N $l10n,
IDateTimeFormatter $dateTimeFormatter,
IURLGenerator $urlGenerator,
IManager $calendarManager,
ITimeFactory $timeFactory) {
ITimeFactory $timeFactory,
IConfig $config) {
$this->l10n = $l10n;
$this->initialStateService = $initialStateService;
$this->dataService = $dataService;
$this->dateTimeFormatter = $dateTimeFormatter;
$this->urlGenerator = $urlGenerator;
$this->calendarManager = $calendarManager;
$this->timeFactory = $timeFactory;
$this->config = $config;
}

/**
Expand Down Expand Up @@ -143,34 +154,96 @@ public function load(): void {
* @param int $limit Max 14 items is the default
*/
public function getItems(string $userId, ?string $since = null, int $limit = 7): array {
// This is hw JS does it:
// const start = dateFactory()
// const end = dateFactory()
// end.setDate(end.getDate() + 14)
// const startOfToday = moment(start).startOf('day').toDate()
// get all vevents in this time range
// if "show tasks" is enabled, get all todos in the time range
// sort events by time
// filter events by COMPLETED and CANCELLED
// filter out all events that are before the start of the day:
// decorate the items with url / task url, colour, etc

$calendars = $this->calendarManager->getCalendarsForPrincipal('principals/users/' . $userId);
$count = count($calendars);
if ($count === 0) {
return [];
}
$dateTime = (new DateTimeImmutable())->setTimestamp($this->timeFactory->getTime());
$inTwoWeeks = $dateTime->add(new DateInterval('P14D'));
$options = [
'timerange' => [
'start' => $dateTime,
'end' => $inTwoWeeks,
]
];

$widgetItems = [];
foreach ($calendars as $calendar) {
$searchResult = $calendar->search('', [], $options, $limit);
foreach ($searchResult as $calendarEvent) {
/** @var DateTimeImmutable $startDate */
$startDate = $calendarEvent['objects'][0]['DTSTART'][0];
$widget = new WidgetItem(
$calendarEvent['objects'][0]['SUMMARY'][0] ?? 'New Event',
$this->dateTimeFormatter->formatTimeSpan(DateTime::createFromImmutable($startDate)),
$this->urlGenerator->getAbsoluteURL($this->urlGenerator->linkToRoute('calendar.view.index', ['objectId' => $calendarEvent['uid']])),
$this->urlGenerator->getAbsoluteURL($this->urlGenerator->linkToRoute('calendar.view.getCalendarDotSvg', ['color' => $calendar->getDisplayColor() ?? '#0082c9'])), // default NC blue fallback
(string) $startDate->getTimestamp(),
);
$widgetItems[] = $widget;
$timezone = null;
if($calendar instanceof CalendarImpl) {
$tz = $calendar->getCalendarTimezoneString() ?? 'UTC';
$timezone = new \DateTimeZone($tz);
}
// make sure to include all day events
$startTimeWithTimezoneMidnight = $this->timeFactory->getDateTime('today', $timezone);
$startTimeWithTimezoneNow = $this->timeFactory->getDateTime('now', $timezone);
$endDate = clone $startTimeWithTimezoneMidnight;
$endDate->modify('+15 days');
$options = [
'timerange' => [
'start' => $startTimeWithTimezoneMidnight,
'end' => $endDate,
],
'types' => [
'VEVENT'
],
'sort_asc' => [
'firstoccurence'
]
];
if($this->config->getUserValue($userId, Application::APP_ID, 'showTasks') === 'yes') {
$options['types'][] = 'VTODO';
}
$searchResults = $calendar->search('', [], $options, $limit);
foreach ($searchResults as $calendarEvent) {
$dtstart = DateTime::createFromImmutable($calendarEvent['objects'][0]['DTSTART'][0]);
if($calendarEvent['type'] === 'VEVENT') {
if($calendarEvent['objects'][0]['STATUS'][0] === 'CANCELLED') {
continue;
}
$timestring = $this->createVeventString($calendarEvent);
if($timestring === null) {
continue;
}
$widgetItems[] = new WidgetItem(
$calendarEvent['objects'][0]['SUMMARY'][0] ?? 'Untitled Event',
$timestring,
$this->urlGenerator->getAbsoluteURL($this->urlGenerator->linkToRoute('calendar.view.index', ['objectId' => $calendarEvent['uid']])),
$this->urlGenerator->getAbsoluteURL($this->urlGenerator->linkToRoute('calendar.view.getCalendarDotSvg', ['color' => $calendar->getDisplayColor() ?? '#0082c9'])), // default NC blue fallback
(string) $dtstart->getTimestamp(),
);
}
}
// if($this->config->getUserValue($userId, Application::APP_ID, 'showTasks') === 'yes') {
// $vTodoOptions = [
// 'types' => [
// 'VTODO'
// ],
// 'sort_desc' => [
// 'id'
// ]
// ];
// $vTodoSearchResults = $calendar->search('', [], $vTodoOptions, $limit);
// foreach($vTodoSearchResults as $vTodo) {
// if($vTodo['objects'][0]['STATUS'][0] === 'COMPLETED') {
// continue;
// }
// $timestring = $this->createVTodoString($vTodo);
// $widget = new WidgetItem(
// $vTodo['objects'][0]['SUMMARY'][0] ?? 'Untitled Task',
// $timestring,
// $this->urlGenerator->getAbsoluteURL($this->urlGenerator->linkToRoute('calendar.view.index', ['objectId' => $calendarEvent['uid']])),
// $this->urlGenerator->getAbsoluteURL($this->urlGenerator->linkToRoute('calendar.view.getCalendarDotSvg', ['color' => $calendar->getDisplayColor() ?? '#0082c9'])), // default NC blue fallback
// (string) $dtstart->getTimestamp(),
// );
// $widgetItems[] = $widget;
// }
// }
}
return $widgetItems;
}
Expand All @@ -196,4 +269,32 @@ public function getWidgetButtons(string $userId): array {
public function getWidgetOptions(): WidgetOptions {
return new WidgetOptions(true);
}

private function createVeventString(array $calendarEvent) {
$dtstart = DateTime::createFromImmutable($calendarEvent['objects'][0]['DTSTART'][0]);
if(isset($calendarEvent['objects'][0]['STATUS']) && $calendarEvent['objects'][0]['STATUS'][0] === 'CANCELLED') {
return null;
}
if (isset($calendarEvent['objects'][0]['DTEND'])) {
/** @var Property\ICalendar\DateTime $dtend */
$dtend = DateTime::createFromImmutable($calendarEvent['objects'][0]['DTEND'][0]);
} elseif(isset($calendarEvent['objects'][0]['DURATION'])) {
$dtend = clone $dtstart;
$dtend = $dtend->add(new DateInterval($calendarEvent['objects'][0]['DURATION'][0]));
} else {
$dtend = clone $dtstart;
}

// End is in the past, skipping
if($dtend->getTimestamp() < $this->timeFactory->getTime()) {
return null;
}

// all day (and longer) events
if($dtstart->diff($dtend)->days >= 1) {
return $this->dateTimeFormatter->formatDate($dtstart);
}

return $this->dateTimeFormatter->formatDateTime($dtstart, 'short') . ' - ' . $this->dateTimeFormatter->formatTime($dtend, 'short');
}
}
86 changes: 18 additions & 68 deletions src/views/Dashboard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -34,16 +34,13 @@
</template>
</EmptyContent>
<DashboardWidgetItem v-else
:main-text="item.mainText"
:sub-text="item.subText"
:target-url="item.targetUrl">
:main-text="item.title ?? ''"
:sub-text="item.subtitle"
:target-url="item.link">
<template #avatar>
<div v-if="item.componentName === 'VEVENT'"
class="calendar-dot"
:style="{'background-color': item.calendarColor}"
:title="item.calendarDisplayName" />
<IconCheckbox v-else
:fill-color="item.calendarColor" />
<div class="calendar-dot">
<img :src="item.iconUrl">
</div>
</template>
</DashboardWidgetItem>
</template>
Expand All @@ -64,20 +61,18 @@
</template>

<script>
import axios from '@nextcloud/axios'
import { DashboardWidget, DashboardWidgetItem } from '@nextcloud/vue-dashboard'
import EmptyContent from '@nextcloud/vue/dist/Components/NcEmptyContent.js'
import EmptyCalendar from 'vue-material-design-icons/CalendarBlankOutline.vue'
import IconCheck from 'vue-material-design-icons/Check.vue'
import IconCheckbox from 'vue-material-design-icons/CheckboxBlankOutline.vue'
import { loadState } from '@nextcloud/initial-state'
import moment from '@nextcloud/moment'
import NcButton from '@nextcloud/vue/dist/Components/NcButton.js'
import { imagePath, generateUrl } from '@nextcloud/router'
import { initializeClientForUserView } from '../services/caldavService.js'
import { imagePath, generateUrl, generateOcsUrl } from '@nextcloud/router'
import { dateFactory } from '../utils/date.js'
import pLimit from 'p-limit'
import { eventSourceFunction } from '../fullcalendar/eventSources/eventSourceFunction.js'
import loadMomentLocalization from '../utils/moment.js'
import { DateTimeValue } from '@nextcloud/calendar-js'
import { mapGetters } from 'vuex'

Expand All @@ -94,6 +89,7 @@ export default {
},
data() {
return {
apiItems: null,
events: null,
locale: 'en',
imagePath: imagePath('calendar', 'illustrations/calendar'),
Expand All @@ -111,19 +107,20 @@ export default {
* @return {Array}
*/
items() {
if (!Array.isArray(this.events) || this.events.length === 0) {
if (!Array.isArray(this.apiItems) || this.apiItems.length === 0) {
return []
}

const firstEvent = this.events[0]
const firstEvent = this.apiItems[0]
const endOfToday = moment(this.now).endOf('day')
if (endOfToday.isBefore(firstEvent.startDate)) {
debugger;
if (endOfToday.isBefore(moment.unix(firstEvent.sinceId))) {
return [{
isEmptyItem: true,
}].concat(this.events.slice(0, 4))
}].concat(this.apiItems.slice(0, 4))
}

return this.events
return this.apiItems
},
/**
* Redirects to the new event route
Expand All @@ -142,15 +139,7 @@ export default {
* Initialize the widget
*/
async initialize() {
const start = dateFactory()
const end = dateFactory()
end.setDate(end.getDate() + 14)

const startOfToday = moment(start).startOf('day').toDate()

await this.initializeEnvironment()
const expandedEvents = await this.fetchExpandedEvents(start, end)
this.events = await this.formatEvents(expandedEvents, startOfToday)
this.loading = false
},
/**
Expand All @@ -160,23 +149,8 @@ export default {
* @return {Promise<void>}
*/
async initializeEnvironment() {
await initializeClientForUserView()
await this.$store.dispatch('fetchCurrentUserPrincipal')
await this.$store.dispatch('loadCollections')

const {
show_tasks: showTasks,
timezone,
} = loadState('calendar', 'dashboard_data')
const locale = await loadMomentLocalization()

this.$store.commit('loadSettingsFromServer', {
timezone,
showTasks,
})
this.$store.commit('setMomentLocale', {
locale,
})
const response = await axios.get(generateOcsUrl('apps/dashboard/api/v1/widget-items?format=json&widgets[]=calendar'))
this.apiItems = response.data.ocs.data.calendar
},
/**
* Fetch events
Expand Down Expand Up @@ -209,31 +183,6 @@ export default {
const expandedEvents = await Promise.all(fetchEventPromises)
return expandedEvents.flat()
},
/**
* @param {object[]} expandedEvents Array of fullcalendar events
* @param {Date} filterBefore filter events that start before date
* @return {object[]}
*/
formatEvents(expandedEvents, filterBefore) {
return expandedEvents
.sort((a, b) => a.start.getTime() - b.start.getTime())
.filter(event => !event.classNames.includes('fc-event-nc-task-completed'))
.filter(event => !event.classNames.includes('fc-event-nc-cancelled'))
.filter(event => filterBefore.getTime() <= event.start.getTime())
.slice(0, 7)
.map((event) => ({
isEmptyItem: false,
componentName: event.extendedProps.objectType,
targetUrl: event.extendedProps.objectType === 'VEVENT'
? this.getCalendarAppUrl(event)
: this.getTasksAppUrl(event),
subText: this.formatSubtext(event),
mainText: event.title,
startDate: event.start,
calendarColor: this.$store.state.calendars.calendarsById[event.extendedProps.calendarId].color,
calendarDisplayName: this.$store.state.calendars.calendarsById[event.extendedProps.calendarId].displayname,
}))
},
/**
* @param {object} event The full-calendar formatted event
* @return {string}
Expand Down Expand Up @@ -300,6 +249,7 @@ export default {
width: 1rem;
margin-top: 0.2rem;
border-radius: 50%;
overflow: hidden;
}

#calendar-widget-empty-content {
Expand Down