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:
@@ -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, "&").replace(/</g, "<").replace(/>/g, ">");
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user