Files

1146 lines
47 KiB
QML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import QtQuick
import Quickshell
import qs.Common
import qs.Services
import qs.Widgets
import "../../../Common/DateOnly.js" as DateOnly
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) {
return Qt.locale().firstDayOfWeek;
}
return SettingsData.firstDayOfWeek;
}
function weekStartJs() {
return weekStartQt() % 7;
}
function startOfWeek(dateOnly) {
const diff = (dateOnly.dayOfWeek() - weekStartJs() + 7) % 7;
return dateOnly.addDays(-diff);
}
function endOfWeek(dateOnly) {
const add = (weekStartJs() + 6 - dateOnly.dayOfWeek() + 7) % 7;
return dateOnly.addDays(add);
}
function getWeekNumber(dateOnly) {
const weekStartDay = startOfWeek(dateOnly);
let week1Start;
if (weekStartJs() === 1) {
// ISO 8601 Standard, week start on Monday
// A week belongs to the year its Thursday falls in
// So we have to get the yearTarget from weekStartDay instead of dateOnly
const yearTarget = weekStartDay.addDays(3); // Monday + 3 = Thursday
// Week 1 is the week containing Jan 4th
week1Start = startOfWeek(DateOnly.of(yearTarget.year, 0, 4));
} else {
// Traditional / US Standard, week start on Sunday
// A week belongs to the year its Sunday falls in
const yearTarget = weekStartDay.addDays(6); // Monday + 6 = Sunday
// Week 1 is the week containing Jan 1st
week1Start = startOfWeek(DateOnly.of(yearTarget.year, 0, 1));
}
const diffDays = week1Start.daysUntil(weekStartDay);
return Math.floor(diffDays / 7) + 1;
}
function updateSelectedDateEvents() {
const events = (CalendarService && CalendarService.calendarAvailable) ? CalendarService.getEventsForDate(selectedDate) : [];
if (JSON.stringify(events) === JSON.stringify(selectedDateEvents))
return;
selectedDateEvents = events;
}
function loadEventsForMonth() {
if (!CalendarService || !CalendarService.calendarAvailable) {
return;
}
const year = calendarGrid.displayDate.getFullYear();
const month = calendarGrid.displayDate.getMonth();
const firstOfMonth = DateOnly.firstOfMonth(year, month);
const lastOfMonth = DateOnly.lastOfMonth(year, month);
const startDate = startOfWeek(firstOfMonth).addDays(-7);
const endDate = endOfWeek(lastOfMonth).addDays(7);
CalendarService.loadEvents(startDate.toDate(), endDate.toDate());
}
function goToToday() {
const now = systemClock.date;
calendarGrid.selectedDate = now;
calendarGrid.displayDate = now;
root.selectedDate = now;
loadEventsForMonth();
}
function moveSelection(days) {
const moved = DateOnly.fromDate(calendarGrid.selectedDate).addDays(days);
const d = moved.toDate();
calendarGrid.selectedDate = d;
root.selectedDate = d;
if (moved.month !== calendarGrid.displayDate.getMonth() || moved.year !== 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();
}
}
Component.onCompleted: {
loadEventsForMonth();
updateSelectedDateEvents();
}
Connections {
function onEventsByDateChanged() {
updateSelectedDateEvents();
}
function onCalendarAvailableChanged() {
if (CalendarService && CalendarService.calendarAvailable) {
loadEventsForMonth();
}
updateSelectedDateEvents();
}
target: CalendarService
enabled: CalendarService !== null
}
radius: Theme.cornerRadius
color: Theme.nestedSurface
border.color: Theme.outlineMedium
border.width: 1
Column {
id: cardColumn
anchors.fill: parent
anchors.margins: Theme.spacingM
spacing: Theme.spacingS
readonly property real warningInset: dankWarning.visible ? dankWarning.height + spacing : 0
Rectangle {
id: dankWarning
width: parent.width
readonly property bool showWarning: CalendarService?.dankNeedsLaunch ?? false
visible: showWarning
height: showWarning ? Math.max(28, warningRow.implicitHeight) + Theme.spacingS : 0
radius: Theme.cornerRadius
color: Theme.warningHover
border.color: Theme.withAlpha(Theme.warning, 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
visible: showEventDetails
DankActionButton {
buttonSize: 32
iconSize: 14
iconName: "arrow_back"
iconColor: Theme.primary
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
onClicked: root.showEventDetails = false
}
DankActionButton {
buttonSize: 32
iconSize: 16
iconName: "event"
iconColor: Theme.primary
anchors.verticalCenter: parent.verticalCenter
anchors.right: parent.right
anchors.rightMargin: Theme.spacingS
visible: CalendarService && CalendarService.canCreateEvents
onClicked: {
root.editorEvent = null;
root.showEditor = true;
}
}
StyledText {
anchors.left: parent.left
anchors.right: parent.right
anchors.leftMargin: 32 + Theme.spacingS * 2
anchors.rightMargin: (CalendarService && CalendarService.canCreateEvents) ? 32 + Theme.spacingS * 2 : Theme.spacingS
height: 40
anchors.verticalCenter: parent.verticalCenter
text: {
const dateStr = Qt.formatDate(selectedDate, "MMM d");
if (selectedDateEvents && selectedDateEvents.length > 0) {
const eventCount = selectedDateEvents.length === 1 ? I18n.tr("1 task", "task count next to a date") : I18n.tr("%1 tasks", "task count next to a date, %1 is the number of tasks").arg(selectedDateEvents.length);
return dateStr + " • " + eventCount;
}
return dateStr;
}
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Medium
verticalAlignment: Text.AlignVCenter
elide: Text.ElideRight
}
}
Row {
width: parent.width
height: 28
visible: !showEventDetails
DankActionButton {
buttonSize: 28
iconSize: 14
iconName: "chevron_left"
iconColor: Theme.primary
onClicked: root.shiftMonth(-1)
}
StyledText {
width: parent.width - 84
height: 28
text: calendarGrid.displayDate.toLocaleDateString(I18n.locale(), "MMMM yyyy")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Medium
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
DankActionButton {
readonly property bool isToday: {
const now = systemClock.date;
const disp = calendarGrid.displayDate;
const sel = calendarGrid.selectedDate;
return disp.getFullYear() === now.getFullYear() && disp.getMonth() === now.getMonth() && sel.getFullYear() === now.getFullYear() && sel.getMonth() === now.getMonth() && sel.getDate() === now.getDate();
}
buttonSize: 28
iconSize: 14
iconName: "today"
iconColor: enabled ? Theme.primary : Theme.surfaceTextMedium
enabled: !isToday
opacity: enabled ? 1 : 0.38
tooltipText: I18n.tr("Today")
Behavior on opacity {
NumberAnimation {
duration: Theme.shortDuration
}
}
onClicked: root.goToToday()
}
DankActionButton {
buttonSize: 28
iconSize: 14
iconName: "chevron_right"
iconColor: Theme.primary
onClicked: root.shiftMonth(1)
}
}
Row {
width: parent.width
height: parent.height - 28 - Theme.spacingS - cardColumn.warningInset
visible: !showEventDetails
spacing: SettingsData.showWeekNumber ? Theme.spacingS : 0
Column {
id: weekNumberColumn
visible: SettingsData.showWeekNumber
width: SettingsData.showWeekNumber ? 28 : 0
height: parent.height
spacing: Theme.spacingS
Item {
width: parent.width
height: 18
}
Grid {
width: parent.width
height: parent.height - 18 - Theme.spacingS
columns: 1
rows: 6
Repeater {
model: 6
Rectangle {
width: parent.width
height: parent.height / 6
color: "transparent"
StyledText {
anchors.centerIn: parent
text: {
const rowDate = calendarGrid.firstDay.addDays(index * 7);
return root.getWeekNumber(rowDate);
}
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceTextSecondary
font.weight: Font.Medium
}
}
}
}
}
Column {
width: SettingsData.showWeekNumber ? (parent.width - weekNumberColumn.width - parent.spacing) : parent.width
height: parent.height
spacing: Theme.spacingS
Row {
width: parent.width
height: 18
Repeater {
model: {
const days = [];
const qtFirst = weekStartQt();
for (let i = 0; i < 7; ++i) {
const qtDay = ((qtFirst - 1 + i) % 7) + 1;
days.push(I18n.locale().dayName(qtDay, Locale.ShortFormat));
}
return days;
}
Rectangle {
width: parent.width / 7
height: 18
color: "transparent"
StyledText {
anchors.centerIn: parent
text: modelData
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceTextSecondary
font.weight: Font.Medium
}
}
}
}
Grid {
id: calendarGrid
width: parent.width
height: parent.height - 18 - Theme.spacingS
columns: 7
rows: 6
property date displayDate: systemClock.date
property date selectedDate: systemClock.date
readonly property var firstDay: {
const firstOfMonth = DateOnly.firstOfMonth(displayDate.getFullYear(), displayDate.getMonth());
return startOfWeek(firstOfMonth);
}
Repeater {
model: 42
Rectangle {
readonly property var dayDate: calendarGrid.firstDay.addDays(index)
readonly property bool isCurrentMonth: dayDate.month === calendarGrid.displayDate.getMonth()
readonly property bool isToday: dayDate.equals(DateOnly.fromDate(systemClock.date))
readonly property bool isSelected: dayDate.equals(DateOnly.fromDate(calendarGrid.selectedDate))
width: parent.width / 7
height: parent.height / 6
color: "transparent"
Rectangle {
anchors.centerIn: parent
width: Math.min(parent.width - 4, parent.height - 4, 32)
height: width
color: isToday ? Theme.primaryHover : dayArea.containsMouse ? Theme.primaryHoverLight : Theme.withAlpha(Theme.primaryHoverLight, 0)
radius: Theme.cornerRadius
border.color: (isSelected && !isToday) ? Theme.primary : Theme.withAlpha(Theme.primary, 0)
border.width: (isSelected && !isToday) ? 1 : 0
StyledText {
anchors.centerIn: parent
text: dayDate.day
font.pixelSize: Theme.fontSizeSmall
color: isToday ? Theme.primary : isCurrentMonth ? Theme.surfaceText : Theme.surfaceVariantText
font.weight: isToday ? Font.Medium : Font.Normal
}
Row {
anchors.bottom: parent.bottom
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottomMargin: 3
spacing: Theme.spacingXXS
visible: CalendarService && CalendarService.calendarAvailable && CalendarService.hasEventsForDate(dayDate.toDate())
Repeater {
model: {
const evs = CalendarService.getEventsForDate(dayDate.toDate());
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
}
}
}
}
MouseArea {
id: dayArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
calendarGrid.selectedDate = dayDate.toDate();
root.selectedDate = dayDate.toDate();
root.showEventDetails = true;
}
}
}
}
}
}
}
Flickable {
id: flickableArea
width: parent.width - Theme.spacingS * 2
height: parent.height - (showEventDetails ? 40 + 42 : 28 + 18) - Theme.spacingS - cardColumn.warningInset
anchors.horizontalCenter: parent.horizontalCenter
visible: showEventDetails
clip: true
contentWidth: width
contentHeight: listViewContainer.height
interactive: listViewContainer.draggedItem === null
Item {
id: listViewContainer
width: parent.width
height: 100
property var draggedItem: null
property bool orderChanged: false
function resetAndLayout() {
for (let i = 0; i < repeater.count; i++) {
let item = repeater.itemAt(i);
if (item) {
item.visualIndex = i;
item.isDragging = false;
item.isEditing = false;
}
}
updateLayout();
}
function updateLayout() {
let items = [];
for (let i = 0; i < repeater.count; i++) {
let item = repeater.itemAt(i);
if (item) {
items.push(item);
}
}
items.sort((a, b) => a.visualIndex - b.visualIndex);
let currentY = 0;
for (let i = 0; i < items.length; i++) {
let item = items[i];
if (item && !item.isDragging) {
item.y = currentY;
}
if (item) {
currentY += item.height + Theme.spacingXS;
}
}
listViewContainer.height = Math.max(0, currentY - Theme.spacingXS);
}
function checkAndReorder(dragged) {
let items = [];
for (let i = 0; i < repeater.count; i++) {
let item = repeater.itemAt(i);
if (item) {
items.push(item);
}
}
items.sort((a, b) => a.visualIndex - b.visualIndex);
let swapped = false;
// Helper to get target Y position without animation offsets
function getTargetY(index) {
let y = 0;
for (let i = 0; i < index; i++) {
y += items[i].height + Theme.spacingXS;
}
return y;
}
while (true) {
let draggedIdx = items.indexOf(dragged);
if (draggedIdx === -1)
break;
let didSwap = false;
// Check item above
if (draggedIdx > 0) {
let above = items[draggedIdx - 1];
let targetYAbove = getTargetY(draggedIdx - 1);
if (above && dragged.y < (targetYAbove + above.height / 2)) {
// Swap visualIndex
let temp = dragged.visualIndex;
dragged.visualIndex = above.visualIndex;
above.visualIndex = temp;
// Swap in local array
items[draggedIdx] = above;
items[draggedIdx - 1] = dragged;
listViewContainer.orderChanged = true;
swapped = true;
didSwap = true;
}
}
// Check item below
if (!didSwap && draggedIdx < items.length - 1) {
let below = items[draggedIdx + 1];
let targetYBelow = getTargetY(draggedIdx + 1);
if (below && (dragged.y + dragged.height) > (targetYBelow + below.height / 2)) {
// Swap visualIndex
let temp = dragged.visualIndex;
dragged.visualIndex = below.visualIndex;
below.visualIndex = temp;
// Swap in local array
items[draggedIdx] = below;
items[draggedIdx + 1] = dragged;
listViewContainer.orderChanged = true;
swapped = true;
didSwap = true;
}
}
if (!didSwap) {
break;
}
}
if (swapped) {
updateLayout();
}
}
function saveNewOrder() {
if (!orderChanged)
return;
let items = [];
for (let i = 0; i < repeater.count; i++) {
let item = repeater.itemAt(i);
if (item) {
items.push(item);
}
}
items.sort((a, b) => a.visualIndex - b.visualIndex);
let orderedIds = [];
for (let i = 0; i < items.length; i++) {
let tid = items[i].taskId;
if (tid && tid.startsWith("task_")) {
orderedIds.push(tid.replace("task_", ""));
}
}
if (orderedIds.length > 0) {
CalendarService.reorderTasksForDate(root.selectedDate, orderedIds);
}
orderChanged = false;
}
Repeater {
id: repeater
model: selectedDateEvents
onModelChanged: {
Qt.callLater(listViewContainer.resetAndLayout);
}
delegate: Rectangle {
id: taskItem
width: parent ? parent.width : 0
height: isEditing ? 34 : (eventContent.implicitHeight + Theme.spacingS)
radius: Theme.cornerRadius
property int modelIndex: index
property int visualIndex: index
property string taskId: (modelData && modelData.id) ? modelData.id : ""
property bool isDragging: false
property bool isEditing: false
property real dragMouseOffsetY: 0
onModelIndexChanged: {
visualIndex = modelIndex;
}
onYChanged: {
if (isDragging) {
listViewContainer.checkAndReorder(taskItem);
}
}
readonly property bool isLocalTask: taskId.startsWith("task_")
readonly property bool isDankTask: taskId.startsWith("vtodo_")
readonly property bool isTask: isLocalTask || isDankTask
readonly property bool canModify: isLocalTask || (isDankTask && modelData && !modelData.readOnly)
readonly property string quickUrl: {
if (isTask || !modelData)
return "";
if (modelData.meetingUrl)
return modelData.meetingUrl;
const loc = (modelData.location || "").trim();
if (/^https?:\/\/\S+$/i.test(loc))
return loc;
if (/^www\.\S+$/i.test(loc))
return "https://" + loc;
return modelData.url || "";
}
readonly property string quickUrlLabel: {
const u = quickUrl.replace(/^https?:\/\//i, "");
if (u.length <= 40)
return u;
return u.slice(0, 39) + "…";
}
readonly property color baseAccent: {
if (isLocalTask || !modelData || !modelData.color || !modelData.color.length)
return Theme.primary;
return modelData.color;
}
readonly property color accentColor: (isTask && modelData && modelData.completed) ? Theme.withAlpha(baseAccent, 0.4) : baseAccent
readonly property color surfaceColor: isDragging ? Theme.primaryPressed : (eventMouseArea.containsMouse ? Theme.primaryBackground : Theme.nestedSurface)
color: surfaceColor
border.color: isDragging ? Theme.primary : (eventMouseArea.containsMouse ? Theme.primaryPressed : Theme.outlineMedium)
border.width: (isDragging || eventMouseArea.containsMouse) ? 1 : Theme.layerOutlineWidth
scale: isDragging ? 1.02 : 1.0
z: isDragging ? 100 : visualIndex
Behavior on scale {
NumberAnimation {
duration: 100
}
}
Behavior on y {
id: yBehavior
enabled: !taskItem.isDragging
NumberAnimation {
duration: 150
easing.type: Easing.OutQuad
}
}
Component.onCompleted: {
visualIndex = index;
listViewContainer.updateLayout();
}
onHeightChanged: {
listViewContainer.updateLayout();
}
onIsEditingChanged: {
if (isEditing) {
editInput.forceActiveFocus();
editInput.selectAll();
}
}
Item {
id: accentClip
width: 4
clip: true
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.left: parent.left
Rectangle {
width: taskItem.width
height: taskItem.height
radius: taskItem.radius
color: taskItem.accentColor
anchors.top: parent.top
anchors.left: parent.left
}
}
// Drag Handle
Rectangle {
id: dragHandle
width: 24
height: 24
anchors.left: parent.left
anchors.leftMargin: 8
anchors.verticalCenter: parent.verticalCenter
radius: Theme.cornerRadius
color: "transparent"
visible: taskItem.isLocalTask && !taskItem.isEditing
DankIcon {
anchors.centerIn: parent
name: "drag_indicator"
size: 14
color: dragMouseArea.containsMouse ? Theme.primary : Theme.surfaceTextMedium
}
MouseArea {
id: dragMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.SizeAllCursor
preventStealing: true
drag.target: taskItem
drag.axis: Drag.YAxis
drag.minimumY: 0
drag.maximumY: listViewContainer.height - taskItem.height
onPressed: {
taskItem.isDragging = true;
listViewContainer.orderChanged = false;
listViewContainer.draggedItem = taskItem;
}
onPositionChanged: {
// Handled natively by MouseArea.drag
}
onReleased: {
taskItem.isDragging = false;
listViewContainer.draggedItem = null;
if (listViewContainer.orderChanged) {
listViewContainer.saveNewOrder();
} else {
listViewContainer.updateLayout();
}
}
onCanceled: {
taskItem.isDragging = false;
listViewContainer.draggedItem = null;
listViewContainer.resetAndLayout();
}
}
}
Column {
id: eventContent
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: taskItem.isLocalTask ? 60 : taskItem.isDankTask ? 36 : (Theme.spacingS + 6)
anchors.rightMargin: taskItem.canModify ? 64 : (taskItem.quickUrl !== "" ? 32 + Theme.spacingS : Theme.spacingXS)
spacing: Theme.spacingXXS
visible: !taskItem.isEditing
StyledText {
width: parent.width
text: modelData ? modelData.title : ""
font.pixelSize: Theme.fontSizeSmall
color: (taskItem.isTask && modelData && modelData.completed) ? Theme.surfaceTextSecondary : Theme.surfaceText
font.weight: Font.Medium
horizontalAlignment: Text.AlignLeft
elide: Text.ElideRight
maximumLineCount: 1
}
StyledText {
width: parent.width
text: {
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) + cal;
return startTime + cal;
}
return "";
}
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceTextMedium
font.weight: Font.Normal
horizontalAlignment: Text.AlignLeft
visible: text !== "" && !taskItem.isLocalTask
}
}
DankTextField {
id: editInput
anchors.left: parent.left
anchors.right: parent.right
anchors.leftMargin: 36
anchors.rightMargin: 64
anchors.verticalCenter: parent.verticalCenter
height: 28
visible: taskItem.isEditing
font.pixelSize: Theme.fontSizeSmall
backgroundColor: "transparent"
borderWidth: 0
focusedBorderWidth: 0
topPadding: 0
bottomPadding: 0
text: modelData ? modelData.title : ""
keyForwardTargets: [editKeyHandler]
onAccepted: {
let txt = text.trim();
if (txt !== "" && modelData && modelData.id) {
CalendarService.editTask(modelData.id, txt);
}
taskItem.isEditing = false;
}
Item {
id: editKeyHandler
Keys.onEscapePressed: event => {
taskItem.isEditing = false;
event.accepted = true;
}
}
}
// Main body MouseArea (declared before the delete/edit buttons so they sit on top)
MouseArea {
id: eventMouseArea
anchors.fill: parent
anchors.leftMargin: taskItem.isLocalTask ? 32 : 6
anchors.rightMargin: taskItem.canModify ? 64 : 0
hoverEnabled: true
cursorShape: modelData ? Qt.PointingHandCursor : Qt.ArrowCursor
enabled: modelData && !taskItem.isEditing
onClicked: {
if (!modelData)
return;
if (taskItem.isTask && taskItem.canModify) {
CalendarService.toggleTask(modelData.id);
return;
}
root.detailEvent = modelData;
}
}
DankActionButton {
buttonSize: 24
iconSize: 14
iconName: (modelData && modelData.meetingUrl) ? "videocam" : "link"
iconColor: Theme.primary
anchors.right: parent.right
anchors.rightMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
visible: taskItem.quickUrl !== ""
tooltipText: taskItem.quickUrlLabel
tooltipSide: "left"
onClicked: Qt.openUrlExternally(taskItem.quickUrl)
}
DankActionButton {
buttonSize: 24
iconSize: 16
iconName: (modelData && modelData.completed) ? "check_box" : "check_box_outline_blank"
iconColor: (modelData && modelData.completed) ? Theme.primary : Theme.surfaceText
anchors.left: parent.left
anchors.leftMargin: taskItem.isLocalTask ? (taskItem.isEditing ? 8 : 32) : 8
anchors.verticalCenter: parent.verticalCenter
visible: taskItem.isTask
enabled: taskItem.canModify && !taskItem.isEditing
onClicked: CalendarService.toggleTask(modelData.id)
}
DankActionButton {
id: deleteButton
buttonSize: 24
iconSize: 14
iconName: taskItem.isEditing ? "close" : "delete"
iconColor: taskItem.isEditing ? Theme.surfaceText : Theme.error
anchors.right: parent.right
anchors.rightMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
visible: taskItem.canModify
onClicked: {
if (taskItem.isEditing) {
taskItem.isEditing = false;
return;
}
if (modelData && modelData.id)
CalendarService.removeTask(modelData.id);
}
}
DankActionButton {
buttonSize: 24
iconSize: 14
iconName: taskItem.isEditing ? "check" : "edit"
iconColor: taskItem.isEditing ? Theme.primary : Theme.surfaceText
anchors.right: deleteButton.left
anchors.rightMargin: Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter
visible: taskItem.canModify
onClicked: {
if (!taskItem.isEditing) {
taskItem.isEditing = true;
return;
}
let txt = editInput.text.trim();
if (txt !== "" && modelData && modelData.id)
CalendarService.editTask(modelData.id, txt);
taskItem.isEditing = false;
}
}
}
}
}
}
DankTextField {
id: taskInput
width: parent.width - Theme.spacingS * 2
height: 34
anchors.horizontalCenter: parent.horizontalCenter
visible: showEventDetails
font.pixelSize: Theme.fontSizeSmall
backgroundColor: Theme.nestedSurface
normalBorderColor: Theme.outlineMedium
placeholderText: I18n.tr("Add a task...", "placeholder in the new-task input field")
hidePlaceholderOnFocus: false
keyForwardTargets: [taskKeyHandler]
onAccepted: {
let txt = text.trim();
if (txt !== "") {
CalendarService.addTaskForDate(root.selectedDate, txt);
text = "";
}
}
Item {
id: taskKeyHandler
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;
}
}
}
SystemClock {
id: systemClock
precision: SystemClock.Hours
}
Connections {
target: SessionService
function onSessionResumed() {
systemClock.enabled = false;
systemClock.enabled = true;
}
}
}