1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-06-20 01:55:20 -04:00

calendar(dank): Add support for DankCalendar backend

- Add keyboard navigation to overview
- Add edit events to overview
- Add create events to overview
- Add setting for auto/khal/dankcalendar backend selection
This commit is contained in:
bbedward
2026-06-15 14:02:35 -04:00
parent 1df7e478df
commit 59998e9fd2
14 changed files with 1906 additions and 380 deletions
@@ -0,0 +1,311 @@
import QtQuick
import qs.Common
import qs.Services
import qs.Widgets
Item {
id: root
LayoutMirroring.enabled: I18n.isRtl
LayoutMirroring.childrenInherit: true
property var eventData: null
property bool canEdit: false
signal editRequested
signal deleteRequested
signal closeRequested
readonly property bool _descriptionIsHtml: /<[a-z][^>]*>/i.test((eventData && eventData.description) || "")
function _styleAnchors(html) {
return html.replace(/<a\s([^>]*)>/gi, (m, attrs) => {
const cleaned = attrs.replace(/style="[^"]*"/gi, "");
return "<a style=\"text-decoration:none; color:" + Theme.primary + ";\" " + cleaned + ">";
});
}
function _inlineMarkdown(line) {
let out = line.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
out = out.replace(/\\([\\`*_{}[\]()#+\-.!~>])/g, "$1");
out = out.replace(/(?:https?:\/\/|www\.)[^\s<>)\]]*[^\s<>)\].,;:!?"']/g, (m, offset, s) => {
const prev = offset > 0 ? s[offset - 1] : "";
if (prev === "(" || prev === "[" || prev === "\"" || prev === "'")
return m;
const href = m.startsWith("www.") ? "https://" + m : m;
return "<a href=\"" + href + "\">" + m + "</a>";
});
out = out.replace(/\[([^\]]+)\]\(([^()\s]+)\)/g, "<a href=\"$2\">$1</a>");
out = out.replace(/\*\*([^*]+)\*\*/g, "<b>$1</b>");
out = out.replace(/(^|[^*])\*([^*\s][^*]*)\*/g, "$1<i>$2</i>");
return out;
}
// Descriptions arrive as HTML (Google) or markdown/plain text; both render
// as RichText so links become clickable anchors recolored to the theme.
function _descriptionRichText() {
const raw = ((eventData && eventData.description) || "").trim();
if (raw === "")
return "";
if (_descriptionIsHtml)
return _styleAnchors(raw);
const parts = [];
let list = "";
const closeList = () => {
if (list === "")
return;
parts.push("</" + list + ">");
list = "";
};
const lines = raw.split("\n");
for (let i = 0; i < lines.length; i++) {
const ul = lines[i].match(/^\s*[-*+]\s+(.+)$/);
const ol = lines[i].match(/^\s*\d+[.)]\s+(.+)$/);
if (ul || ol) {
const tag = ul ? "ul" : "ol";
if (list !== tag) {
closeList();
parts.push("<" + tag + ">");
list = tag;
}
parts.push("<li>" + _inlineMarkdown((ul || ol)[1]) + "</li>");
continue;
}
closeList();
parts.push(_inlineMarkdown(lines[i]) + "<br/>");
}
closeList();
return _styleAnchors(parts.join("").replace(/<br\/>$/, ""));
}
function _timeText() {
if (!eventData)
return "";
const dateStr = Qt.formatDate(eventData.start, "ddd, MMM d");
if (eventData.allDay)
return I18n.tr("All day") + " · " + dateStr;
const fmt = SettingsData.use24HourClock ? "HH:mm" : "h:mm AP";
const startStr = Qt.formatTime(eventData.start, fmt);
if (eventData.start.getTime() === eventData.end.getTime())
return dateStr + " · " + startStr;
return dateStr + " · " + startStr + " " + Qt.formatTime(eventData.end, fmt);
}
Rectangle {
anchors.fill: parent
radius: Theme.cornerRadius
color: Qt.rgba(0, 0, 0, 0.45)
MouseArea {
anchors.fill: parent
onClicked: root.closeRequested()
}
}
Rectangle {
anchors.centerIn: parent
width: Math.min(parent.width - Theme.spacingL * 2, 380)
height: Math.min(parent.height - Theme.spacingM * 2, body.implicitHeight + Theme.spacingL * 2)
radius: Theme.cornerRadius
color: Theme.surfaceContainerHigh
border.color: Theme.outlineMedium
border.width: 1
clip: true
MouseArea {
anchors.fill: parent
}
DankActionButton {
id: closeButton
anchors.top: parent.top
anchors.right: parent.right
anchors.margins: Theme.spacingXS
circular: false
iconName: "close"
iconSize: 16
z: 1
onClicked: root.closeRequested()
}
DankFlickable {
anchors.fill: parent
anchors.margins: Theme.spacingL
anchors.topMargin: Theme.spacingL
contentWidth: width
contentHeight: body.implicitHeight
clip: true
Column {
id: body
width: parent.width
spacing: Theme.spacingS
Row {
width: parent.width
spacing: Theme.spacingS
Rectangle {
width: 4
height: titleText.implicitHeight
radius: 2
anchors.top: parent.top
color: (root.eventData && root.eventData.color) ? root.eventData.color : Theme.primary
}
StyledText {
id: titleText
width: parent.width - 4 - Theme.spacingS - closeButton.width
text: root.eventData ? root.eventData.title : ""
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
horizontalAlignment: Text.AlignLeft
wrapMode: Text.Wrap
maximumLineCount: 3
elide: Text.ElideRight
}
}
StyledText {
width: parent.width
text: root._timeText()
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.Wrap
}
Row {
width: parent.width
spacing: Theme.spacingXS
visible: root.eventData && root.eventData.calendar
DankIcon {
name: "calendar_month"
size: 14
color: Theme.surfaceVariantText
anchors.top: parent.top
anchors.topMargin: 2
}
StyledText {
width: parent.width - 14 - Theme.spacingXS
text: {
if (!root.eventData)
return "";
const acc = root.eventData.account || "";
return root.eventData.calendar + (acc ? " · " + acc : "");
}
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.Wrap
maximumLineCount: 2
elide: Text.ElideRight
}
}
Row {
width: parent.width
spacing: Theme.spacingXS
visible: root.eventData && root.eventData.location
DankIcon {
name: "place"
size: 14
color: Theme.surfaceVariantText
anchors.top: parent.top
anchors.topMargin: 2
}
StyledText {
width: parent.width - 14 - Theme.spacingXS
text: root.eventData ? root.eventData.location : ""
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.Wrap
maximumLineCount: 2
elide: Text.ElideRight
}
}
Row {
width: parent.width
spacing: Theme.spacingXS
visible: root.eventData && root.eventData.url
DankIcon {
name: "link"
size: 14
color: Theme.primary
anchors.top: parent.top
anchors.topMargin: 2
}
StyledText {
width: parent.width - 14 - Theme.spacingXS
text: root.eventData ? root.eventData.url : ""
font.pixelSize: Theme.fontSizeSmall
color: Theme.primary
wrapMode: Text.WrapAnywhere
maximumLineCount: 2
elide: Text.ElideRight
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
if (root.eventData && root.eventData.url)
Qt.openUrlExternally(root.eventData.url);
}
}
}
}
StyledText {
id: descriptionText
width: parent.width
text: root._descriptionRichText()
visible: root.eventData && root.eventData.description
textFormat: Text.RichText
linkColor: Theme.primary
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
horizontalAlignment: Text.AlignLeft
wrapMode: Text.Wrap
onLinkActivated: link => Qt.openUrlExternally(link)
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.NoButton
cursorShape: descriptionText.hoveredLink !== "" ? Qt.PointingHandCursor : Qt.ArrowCursor
}
}
Row {
width: parent.width
spacing: Theme.spacingS
visible: root.canEdit
topPadding: Theme.spacingXS
DankButton {
text: I18n.tr("Edit")
iconName: "edit"
buttonHeight: 32
onClicked: root.editRequested()
}
DankButton {
text: I18n.tr("Delete")
iconName: "delete"
buttonHeight: 32
backgroundColor: Theme.withAlpha(Theme.error, 0.15)
textColor: Theme.error
onClicked: root.deleteRequested()
}
}
}
}
}
}
@@ -0,0 +1,350 @@
import QtQuick
import qs.Common
import qs.Services
import qs.Widgets
Item {
id: root
LayoutMirroring.enabled: I18n.isRtl
LayoutMirroring.childrenInherit: true
property var eventData: null
property date initialDate: new Date()
signal saved
signal closeRequested
property string fTitle: ""
property bool fAllDay: false
property date fDate: initialDate
property string fStart: "10:00"
property string fEnd: "11:00"
property string fLocation: ""
property string fDescription: ""
property string fCalendarId: ""
property int fReminder: -1
property string errorText: ""
property bool saving: false
readonly property var _cals: CalendarService.writableCalendars()
readonly property var _remLabels: [I18n.tr("No reminder"), I18n.tr("At start"), I18n.tr("5 min before"), I18n.tr("10 min before"), I18n.tr("15 min before"), I18n.tr("30 min before"), I18n.tr("1 hour before"), I18n.tr("1 day before")]
readonly property var _remMins: [-1, 0, 5, 10, 15, 30, 60, 1440]
function _parseTime(value) {
const m = value.trim().match(/^(\d{1,2}):(\d{2})$/);
if (!m)
return null;
const h = parseInt(m[1]);
const min = parseInt(m[2]);
if (h > 23 || min > 59)
return null;
return {
"h": h,
"m": min
};
}
function _isoFromDateTime(dateObj, h, m) {
const d = new Date(dateObj);
d.setHours(h, m, 0, 0);
return d.toISOString();
}
function _allDayIso(dateObj, dayOffset) {
return new Date(Date.UTC(dateObj.getFullYear(), dateObj.getMonth(), dateObj.getDate() + dayOffset)).toISOString();
}
function _calendarName(id) {
for (let i = 0; i < _cals.length; i++) {
if (_cals[i].id === id)
return _cals[i].name;
}
return _cals.length > 0 ? _cals[0].name : "";
}
function save() {
const title = fTitle.trim();
if (!title) {
errorText = I18n.tr("Title is required");
return;
}
let calId = fCalendarId;
if (!calId) {
const def = CalendarService.defaultCalendar();
calId = def ? def.id : "";
}
if (!calId) {
errorText = I18n.tr("No writable calendar available");
return;
}
let startIso, endIso;
if (fAllDay) {
startIso = _allDayIso(fDate, 0);
endIso = _allDayIso(fDate, 1);
} else {
const s = _parseTime(fStart);
const e = _parseTime(fEnd);
if (!s || !e) {
errorText = I18n.tr("Use HH:MM time format");
return;
}
startIso = _isoFromDateTime(fDate, s.h, s.m);
endIso = _isoFromDateTime(fDate, e.h, e.m);
if (new Date(endIso).getTime() <= new Date(startIso).getTime()) {
errorText = I18n.tr("End must be after start");
return;
}
}
const fields = {
"calendarId": calId,
"summary": title,
"description": fDescription,
"location": fLocation,
"start": startIso,
"end": endIso,
"allDay": fAllDay,
"reminders": fReminder >= 0 ? [
{
"method": "popup",
"minutes": fReminder
}
] : []
};
saving = true;
errorText = "";
const cb = response => {
saving = false;
if (response.error) {
errorText = response.error;
return;
}
root.saved();
};
if (eventData && eventData.id)
CalendarService.updateEvent(eventData.id, fields, cb);
else
CalendarService.createEvent(fields, cb);
}
Component.onCompleted: {
if (!eventData) {
fCalendarId = CalendarService.defaultCalendar() ? CalendarService.defaultCalendar().id : "";
return;
}
fTitle = eventData.title || "";
fAllDay = !!eventData.allDay;
fDate = eventData.start;
const fmt = "HH:mm";
fStart = Qt.formatTime(eventData.start, fmt);
fEnd = Qt.formatTime(eventData.end, fmt);
fLocation = eventData.location || "";
fDescription = eventData.description || "";
fCalendarId = eventData.calendarId || "";
if (eventData.reminders && eventData.reminders.length > 0)
fReminder = eventData.reminders[0].minutes;
}
Rectangle {
anchors.fill: parent
radius: Theme.cornerRadius
color: Qt.rgba(0, 0, 0, 0.45)
MouseArea {
anchors.fill: parent
onClicked: root.closeRequested()
}
}
Rectangle {
anchors.centerIn: parent
width: Math.min(parent.width - Theme.spacingL * 2, 400)
height: Math.min(parent.height - Theme.spacingM, 300)
radius: Theme.cornerRadius
color: Theme.surfaceContainerHigh
border.color: Theme.outlineMedium
border.width: 1
MouseArea {
anchors.fill: parent
}
DankFlickable {
anchors.fill: parent
anchors.margins: Theme.spacingM
contentWidth: width
contentHeight: form.implicitHeight
clip: true
Column {
id: form
width: parent.width
spacing: Theme.spacingS
StyledText {
width: parent.width
text: root.eventData ? I18n.tr("Edit event") : I18n.tr("New event")
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.surfaceText
horizontalAlignment: Text.AlignLeft
}
DankTextField {
width: parent.width
labelText: I18n.tr("Title")
leftIconName: "title"
leftIconSize: Theme.iconSize - 6
placeholderText: I18n.tr("Event title")
text: root.fTitle
onTextChanged: root.fTitle = text
}
DankToggle {
width: parent.width
text: I18n.tr("All day")
checked: root.fAllDay
onToggled: checked => root.fAllDay = checked
}
Row {
width: parent.width
spacing: Theme.spacingXS
DankActionButton {
circular: false
iconName: "chevron_left"
iconSize: 16
onClicked: {
let d = new Date(root.fDate);
d.setDate(d.getDate() - 1);
root.fDate = d;
}
}
StyledText {
width: parent.width - 72
text: Qt.formatDate(root.fDate, "ddd, MMM d yyyy")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
height: 32
}
DankActionButton {
circular: false
iconName: "chevron_right"
iconSize: 16
onClicked: {
let d = new Date(root.fDate);
d.setDate(d.getDate() + 1);
root.fDate = d;
}
}
}
Row {
width: parent.width
spacing: Theme.spacingS
visible: !root.fAllDay
DankTextField {
width: (parent.width - Theme.spacingS) / 2
labelText: I18n.tr("Start")
leftIconName: "schedule"
leftIconSize: Theme.iconSize - 6
placeholderText: "HH:MM"
text: root.fStart
onTextChanged: root.fStart = text
}
DankTextField {
width: (parent.width - Theme.spacingS) / 2
labelText: I18n.tr("End")
placeholderText: "HH:MM"
text: root.fEnd
onTextChanged: root.fEnd = text
}
}
DankDropdown {
width: parent.width
text: I18n.tr("Calendar")
options: root._cals.map(c => c.name)
currentValue: root._calendarName(root.fCalendarId)
onValueChanged: value => {
for (let i = 0; i < root._cals.length; i++) {
if (root._cals[i].name === value) {
root.fCalendarId = root._cals[i].id;
return;
}
}
}
}
DankDropdown {
width: parent.width
text: I18n.tr("Reminder")
options: root._remLabels
currentValue: root._remLabels[Math.max(0, root._remMins.indexOf(root.fReminder))]
onValueChanged: value => {
const idx = root._remLabels.indexOf(value);
if (idx >= 0)
root.fReminder = root._remMins[idx];
}
}
DankTextField {
width: parent.width
labelText: I18n.tr("Location")
leftIconName: "place"
leftIconSize: Theme.iconSize - 6
placeholderText: I18n.tr("Add location")
text: root.fLocation
onTextChanged: root.fLocation = text
}
DankTextField {
width: parent.width
labelText: I18n.tr("Notes")
leftIconName: "notes"
leftIconSize: Theme.iconSize - 6
placeholderText: I18n.tr("Add notes")
text: root.fDescription
onTextChanged: root.fDescription = text
}
StyledText {
width: parent.width
text: root.errorText
visible: root.errorText !== ""
font.pixelSize: Theme.fontSizeSmall
color: Theme.error
wrapMode: Text.WordWrap
}
Row {
width: parent.width
spacing: Theme.spacingS
DankButton {
text: root.saving ? I18n.tr("Saving…") : I18n.tr("Save")
iconName: "check"
buttonHeight: 32
backgroundColor: Theme.primary
textColor: Theme.primaryText
enabled: !root.saving
onClicked: root.save()
}
DankButton {
text: I18n.tr("Cancel")
buttonHeight: 32
onClicked: root.closeRequested()
}
}
}
}
}
}
@@ -8,14 +8,21 @@ Rectangle {
id: root
readonly property var log: Log.scoped("CalendarOverviewCard")
LayoutMirroring.enabled: I18n.isRtl
LayoutMirroring.childrenInherit: true
implicitWidth: SettingsData.showWeekNumber ? 736 : 700
property bool showEventDetails: false
property date selectedDate: systemClock.date
property var selectedDateEvents: []
property bool hasEvents: selectedDateEvents && selectedDateEvents.length > 0
property var detailEvent: null
property bool showEditor: false
property var editorEvent: null
signal closeDash
signal navFocusRequested
function weekStartQt() {
if (SettingsData.firstDayOfWeek >= 7 || SettingsData.firstDayOfWeek < 0) {
@@ -79,7 +86,7 @@ Rectangle {
}
function updateSelectedDateEvents() {
if (CalendarService && CalendarService.khalAvailable) {
if (CalendarService && CalendarService.calendarAvailable) {
const events = CalendarService.getEventsForDate(selectedDate);
selectedDateEvents = events;
} else {
@@ -88,7 +95,7 @@ Rectangle {
}
function loadEventsForMonth() {
if (!CalendarService || !CalendarService.khalAvailable) {
if (!CalendarService || !CalendarService.calendarAvailable) {
return;
}
@@ -104,11 +111,83 @@ Rectangle {
CalendarService.loadEvents(startDate, endDate);
}
function goToToday() {
const now = systemClock.date;
calendarGrid.selectedDate = now;
calendarGrid.displayDate = now;
root.selectedDate = now;
loadEventsForMonth();
}
function moveSelection(days) {
let d = new Date(calendarGrid.selectedDate);
d.setDate(d.getDate() + days);
calendarGrid.selectedDate = d;
root.selectedDate = d;
if (d.getMonth() !== calendarGrid.displayDate.getMonth() || d.getFullYear() !== calendarGrid.displayDate.getFullYear()) {
calendarGrid.displayDate = d;
loadEventsForMonth();
}
}
function shiftMonth(delta) {
let d = new Date(calendarGrid.displayDate);
d.setMonth(d.getMonth() + delta);
calendarGrid.displayDate = d;
loadEventsForMonth();
}
function handleKeyEvent(event) {
if (showEventDetails) {
if (event.key === Qt.Key_Escape) {
showEventDetails = false;
return true;
}
return false;
}
switch (event.key) {
case Qt.Key_Left:
case Qt.Key_H:
moveSelection(I18n.isRtl ? 1 : -1);
return true;
case Qt.Key_Right:
case Qt.Key_L:
moveSelection(I18n.isRtl ? -1 : 1);
return true;
case Qt.Key_Up:
case Qt.Key_K:
moveSelection(-7);
return true;
case Qt.Key_Down:
case Qt.Key_J:
moveSelection(7);
return true;
case Qt.Key_PageUp:
shiftMonth(-1);
return true;
case Qt.Key_PageDown:
shiftMonth(1);
return true;
case Qt.Key_T:
goToToday();
return true;
case Qt.Key_Return:
case Qt.Key_Enter:
case Qt.Key_Space:
root.selectedDate = calendarGrid.selectedDate;
showEventDetails = true;
return true;
}
return false;
}
onSelectedDateChanged: updateSelectedDateEvents()
onShowEventDetailsChanged: {
if (showEventDetails) {
taskInput.forceActiveFocus();
} else {
navFocusRequested();
}
}
@@ -122,8 +201,8 @@ Rectangle {
updateSelectedDateEvents();
}
function onKhalAvailableChanged() {
if (CalendarService && CalendarService.khalAvailable) {
function onCalendarAvailableChanged() {
if (CalendarService && CalendarService.calendarAvailable) {
loadEventsForMonth();
}
updateSelectedDateEvents();
@@ -143,6 +222,55 @@ Rectangle {
anchors.margins: Theme.spacingM
spacing: Theme.spacingS
Rectangle {
id: dankWarning
width: parent.width
visible: CalendarService && CalendarService.dankNeedsLaunch
height: visible ? Math.max(28, warningRow.implicitHeight) + Theme.spacingS : 0
radius: Theme.cornerRadius
color: Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.12)
border.color: Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.35)
border.width: 1
Row {
id: warningRow
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: Theme.spacingS
anchors.rightMargin: Theme.spacingS
spacing: Theme.spacingS
DankIcon {
name: "warning"
size: 16
color: Theme.warning
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
width: parent.width - 16 - Theme.spacingS - (launchButton.visible ? launchButton.width + Theme.spacingS : 0)
anchors.verticalCenter: parent.verticalCenter
text: (CalendarService && CalendarService.dankBinaryExists) ? I18n.tr("DankCalendar isn't running") : I18n.tr("DankCalendar isn't installed")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
horizontalAlignment: Text.AlignLeft
wrapMode: Text.Wrap
}
DankButton {
id: launchButton
anchors.verticalCenter: parent.verticalCenter
visible: CalendarService && CalendarService.dankBinaryExists
text: I18n.tr("Launch")
buttonHeight: 26
backgroundColor: Theme.primary
textColor: Theme.primaryText
onClicked: CalendarService.launchDankCalendar()
}
}
}
Item {
width: parent.width
height: 40
@@ -173,11 +301,40 @@ Rectangle {
}
}
Rectangle {
width: 32
height: 32
anchors.verticalCenter: parent.verticalCenter
anchors.right: parent.right
anchors.rightMargin: Theme.spacingS
radius: Theme.cornerRadius
visible: CalendarService && CalendarService.canCreateEvents
color: addEventArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
DankIcon {
anchors.centerIn: parent
name: "event"
size: 16
color: Theme.primary
}
MouseArea {
id: addEventArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
root.editorEvent = null;
root.showEditor = true;
}
}
}
StyledText {
anchors.left: parent.left
anchors.right: parent.right
anchors.leftMargin: 32 + Theme.spacingS * 2
anchors.rightMargin: Theme.spacingS
anchors.rightMargin: (CalendarService && CalendarService.canCreateEvents) ? 32 + Theme.spacingS * 2 : Theme.spacingS
height: 40
anchors.verticalCenter: parent.verticalCenter
text: {
@@ -229,7 +386,7 @@ Rectangle {
}
StyledText {
width: parent.width - 56
width: parent.width - 84
height: 28
text: calendarGrid.displayDate.toLocaleDateString(I18n.locale(), "MMMM yyyy")
font.pixelSize: Theme.fontSizeMedium
@@ -239,6 +396,28 @@ Rectangle {
verticalAlignment: Text.AlignVCenter
}
Rectangle {
width: 28
height: 28
radius: Theme.cornerRadius
color: todayArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
DankIcon {
anchors.centerIn: parent
name: "today"
size: 14
color: Theme.primary
}
MouseArea {
id: todayArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: root.goToToday()
}
}
Rectangle {
width: 28
height: 28
@@ -388,6 +567,8 @@ Rectangle {
height: width
color: isToday ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : dayArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : "transparent"
radius: Theme.cornerRadius
border.color: (isSelected && !isToday) ? Theme.primary : "transparent"
border.width: (isSelected && !isToday) ? 1 : 0
StyledText {
anchors.centerIn: parent
@@ -397,21 +578,31 @@ Rectangle {
font.weight: isToday ? Font.Medium : Font.Normal
}
Rectangle {
Row {
anchors.bottom: parent.bottom
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottomMargin: 4
width: 12
height: 2
radius: Theme.cornerRadius
visible: CalendarService && CalendarService.khalAvailable && CalendarService.hasEventsForDate(dayDate)
color: isToday ? Qt.lighter(Theme.primary, 1.3) : Theme.primary
opacity: isToday ? 0.9 : 0.7
anchors.bottomMargin: 3
spacing: 2
visible: CalendarService && CalendarService.calendarAvailable && CalendarService.hasEventsForDate(dayDate)
Behavior on opacity {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
Repeater {
model: {
const evs = CalendarService.getEventsForDate(dayDate);
const seen = [];
for (let i = 0; i < evs.length && seen.length < 3; i++) {
const c = (evs[i].color && evs[i].color.length) ? evs[i].color : "primary";
if (seen.indexOf(c) === -1)
seen.push(c);
}
return seen;
}
Rectangle {
width: 5
height: 5
radius: 2.5
color: modelData === "primary" ? (isToday ? Qt.lighter(Theme.primary, 1.3) : Theme.primary) : modelData
opacity: isToday ? 0.95 : 0.8
}
}
}
@@ -423,6 +614,7 @@ Rectangle {
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
calendarGrid.selectedDate = dayDate;
root.selectedDate = dayDate;
root.showEventDetails = true;
}
@@ -622,7 +814,15 @@ Rectangle {
}
}
color: isDragging ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.15) : (eventMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.06) : Theme.nestedSurface)
readonly property bool isTask: modelData && modelData.id && modelData.id.startsWith("task_")
readonly property color accentColor: {
if (isTask)
return modelData.completed ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.4) : Theme.primary;
return (modelData && modelData.color && modelData.color.length) ? modelData.color : Theme.primary;
}
readonly property color surfaceColor: isDragging ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.15) : (eventMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.06) : Theme.nestedSurface)
color: surfaceColor
border.color: isDragging ? Theme.primary : (eventMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.15) : Theme.outlineMedium)
border.width: (isDragging || eventMouseArea.containsMouse) ? 1 : Theme.layerOutlineWidth
@@ -660,15 +860,22 @@ Rectangle {
}
}
Rectangle {
width: 3
height: parent.height - 6
Item {
id: accentClip
width: 4
clip: true
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.leftMargin: 3
anchors.verticalCenter: parent.verticalCenter
radius: Theme.cornerRadius
color: (modelData && modelData.id && modelData.id.startsWith("task_")) ? (modelData.completed ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.4) : Theme.primary) : Theme.primary
opacity: 0.8
Rectangle {
width: taskItem.width
height: taskItem.height
radius: taskItem.radius
color: taskItem.accentColor
anchors.top: parent.top
anchors.left: parent.left
}
}
// Drag Handle
@@ -767,6 +974,7 @@ Rectangle {
font.pixelSize: Theme.fontSizeSmall
color: (modelData && modelData.id && modelData.id.startsWith("task_") && modelData.completed) ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.5) : Theme.surfaceText
font.weight: Font.Medium
horizontalAlignment: Text.AlignLeft
elide: Text.ElideRight
maximumLineCount: 1
}
@@ -774,21 +982,24 @@ Rectangle {
StyledText {
width: parent.width
text: {
if (!modelData || modelData.allDay) {
return I18n.tr("All day", "calendar task with no specific time");
} else if (modelData.start && modelData.end) {
if (!modelData)
return "";
const cal = (modelData.calendar && modelData.calendar.length) ? " · " + modelData.calendar : "";
if (modelData.allDay)
return I18n.tr("All day", "calendar task with no specific time") + cal;
if (modelData.start && modelData.end) {
const timeFormat = SettingsData.use24HourClock ? "HH:mm" : "h:mm AP";
const startTime = Qt.formatTime(modelData.start, timeFormat);
if (modelData.start.toDateString() !== modelData.end.toDateString() || modelData.start.getTime() !== modelData.end.getTime()) {
return startTime + " " + Qt.formatTime(modelData.end, timeFormat);
}
return startTime;
if (modelData.start.toDateString() !== modelData.end.toDateString() || modelData.start.getTime() !== modelData.end.getTime())
return startTime + " " + Qt.formatTime(modelData.end, timeFormat) + cal;
return startTime + cal;
}
return "";
}
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
font.weight: Font.Normal
horizontalAlignment: Text.AlignLeft
visible: text !== "" && modelData && modelData.id && !modelData.id.startsWith("task_")
}
}
@@ -824,8 +1035,9 @@ Rectangle {
taskItem.isEditing = false;
}
Keys.onEscapePressed: {
Keys.onEscapePressed: event => {
taskItem.isEditing = false;
event.accepted = true;
}
}
}
@@ -838,18 +1050,15 @@ Rectangle {
anchors.leftMargin: (modelData && modelData.id && modelData.id.startsWith("task_")) ? 32 : 6
anchors.rightMargin: (modelData && modelData.id && modelData.id.startsWith("task_")) ? 64 : 0
hoverEnabled: true
cursorShape: (modelData && (modelData.url || (modelData.id && modelData.id.startsWith("task_")))) ? Qt.PointingHandCursor : Qt.ArrowCursor
enabled: modelData && (modelData.url !== "" || (modelData.id && modelData.id.startsWith("task_"))) && !taskItem.isEditing
cursorShape: modelData ? Qt.PointingHandCursor : Qt.ArrowCursor
enabled: modelData && !taskItem.isEditing
onClicked: {
if (modelData && modelData.id && modelData.id.startsWith("task_")) {
CalendarService.toggleTask(modelData.id);
} else if (modelData && modelData.url && modelData.url !== "") {
if (Qt.openUrlExternally(modelData.url) === false) {
log.warn("Failed to open URL: " + modelData.url);
} else {
root.closeDash();
}
return;
}
if (modelData)
root.detailEvent = modelData;
}
}
@@ -953,7 +1162,7 @@ Rectangle {
Text {
text: I18n.tr("Add a task...", "placeholder in the new-task input field")
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.4)
visible: !taskInput.text && !taskInput.activeFocus
visible: taskInput.text.length === 0
font.pixelSize: Theme.fontSizeSmall
anchors.verticalCenter: parent.verticalCenter
}
@@ -965,6 +1174,52 @@ Rectangle {
text = "";
}
}
Keys.onEscapePressed: event => {
root.showEventDetails = false;
event.accepted = true;
}
}
}
}
Loader {
anchors.fill: parent
z: 1000
active: root.detailEvent !== null
sourceComponent: CalendarEventDetail {
eventData: root.detailEvent
canEdit: CalendarService && CalendarService.canCreateEvents && root.detailEvent && !root.detailEvent.readOnly && !(root.detailEvent.id && root.detailEvent.id.startsWith("task_"))
onCloseRequested: root.detailEvent = null
onEditRequested: {
root.editorEvent = root.detailEvent;
root.detailEvent = null;
root.showEditor = true;
}
onDeleteRequested: {
if (root.detailEvent && root.detailEvent.id)
CalendarService.deleteEvent(root.detailEvent.id, null);
root.detailEvent = null;
}
}
}
Loader {
anchors.fill: parent
z: 1000
active: root.showEditor
sourceComponent: CalendarEventEditor {
eventData: root.editorEvent
initialDate: root.selectedDate
onCloseRequested: {
root.showEditor = false;
root.editorEvent = null;
}
onSaved: {
root.showEditor = false;
root.editorEvent = null;
}
}
}