mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-10-14 15:24:00 -04:00
860 lines
31 KiB
QML
860 lines
31 KiB
QML
pragma ComponentBehavior: Bound
|
||
|
||
import QtQuick
|
||
import Quickshell
|
||
import qs.Common
|
||
import qs.Services
|
||
import qs.DCommon.Widgets
|
||
import qs.Widgets
|
||
import qs.Modules.DDash
|
||
import "../../../Common/DateOnly.js" as DateOnly
|
||
|
||
Card {
|
||
id: root
|
||
|
||
property bool live: Window.window?.visible ?? false
|
||
property bool showEventDetails: false
|
||
property date selectedDate: systemClock.date
|
||
property var selectedDateEvents: []
|
||
readonly property bool hasEvents: selectedDateEvents && selectedDateEvents.length > 0
|
||
readonly property bool canCreate: CalendarService.canCreateEvents
|
||
|
||
property int eventsRevision: 0
|
||
|
||
signal navFocusRequested
|
||
signal detailRequested(var eventData)
|
||
signal editorRequested(var eventData, var initialDate)
|
||
|
||
focusTarget: root
|
||
blocksTabNavigation: showEventDetails
|
||
activeFocusOnTab: interactive
|
||
|
||
entryId: "calendar"
|
||
pad: Theme.spacingS
|
||
|
||
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(dateObj) {
|
||
const weekStartDay = startOfWeek(DateOnly.fromDate(dateObj));
|
||
const isoWeeks = weekStartJs() === 1;
|
||
const yearTarget = weekStartDay.addDays(isoWeeks ? 3 : 6);
|
||
const week1Start = startOfWeek(DateOnly.of(yearTarget.year, 0, isoWeeks ? 4 : 1));
|
||
const diffDays = week1Start.daysUntil(weekStartDay);
|
||
return Math.floor(diffDays / 7) + 1;
|
||
}
|
||
|
||
function eventColorsFor(date) {
|
||
if (!CalendarService.calendarAvailable || !CalendarService.hasEventsForDate(date))
|
||
return [];
|
||
return CalendarService.getEventsForDate(date).map(event => event.color?.length ? event.color : Theme.primary);
|
||
}
|
||
|
||
function updateSelectedDateEvents() {
|
||
const events = CalendarService.calendarAvailable ? CalendarService.getEventsForDate(selectedDate) : [];
|
||
if (JSON.stringify(events) === JSON.stringify(selectedDateEvents))
|
||
return;
|
||
selectedDateEvents = events;
|
||
}
|
||
|
||
function loadEventsForMonth() {
|
||
if (!CalendarService.calendarAvailable)
|
||
return;
|
||
const year = calendarGrid.displayDate.getFullYear();
|
||
const month = calendarGrid.displayDate.getMonth();
|
||
const startDate = startOfWeek(DateOnly.firstOfMonth(year, month)).addDays(-7);
|
||
const endDate = endOfWeek(DateOnly.lastOfMonth(year, month)).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 selectDay(dayDate) {
|
||
calendarGrid.selectedDate = dayDate;
|
||
root.selectedDate = dayDate;
|
||
}
|
||
|
||
function moveSelection(days) {
|
||
const moved = DateOnly.fromDate(calendarGrid.selectedDate).addDays(days);
|
||
const d = moved.toDate();
|
||
selectDay(d);
|
||
if (moved.month === calendarGrid.displayDate.getMonth() && moved.year === calendarGrid.displayDate.getFullYear())
|
||
return;
|
||
calendarGrid.displayDate = d;
|
||
loadEventsForMonth();
|
||
}
|
||
|
||
function shiftMonth(delta) {
|
||
calendarGrid.displayDate = DateOnly.fromDate(calendarGrid.displayDate).addMonths(delta).toDate();
|
||
loadEventsForMonth();
|
||
}
|
||
|
||
function openEditor(eventData) {
|
||
root.editorRequested(eventData, root.selectedDate);
|
||
}
|
||
|
||
function handleKeyEvent(event) {
|
||
if (!interactive)
|
||
return false;
|
||
if (showEventDetails) {
|
||
if (event.key !== Qt.Key_Escape)
|
||
return false;
|
||
showEventDetails = false;
|
||
return true;
|
||
}
|
||
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();
|
||
return;
|
||
}
|
||
navFocusRequested();
|
||
}
|
||
|
||
Component.onCompleted: {
|
||
loadEventsForMonth();
|
||
updateSelectedDateEvents();
|
||
}
|
||
|
||
Connections {
|
||
target: CalendarService
|
||
|
||
function onEventsByDateChanged() {
|
||
root.eventsRevision++;
|
||
root.updateSelectedDateEvents();
|
||
}
|
||
|
||
function onCalendarAvailableChanged() {
|
||
if (CalendarService.calendarAvailable)
|
||
root.loadEventsForMonth();
|
||
root.updateSelectedDateEvents();
|
||
}
|
||
}
|
||
|
||
Rectangle {
|
||
id: dankWarning
|
||
|
||
readonly property bool showWarning: CalendarService.dankNeedsLaunch ?? false
|
||
|
||
anchors.left: parent.left
|
||
anchors.right: parent.right
|
||
anchors.top: parent.top
|
||
visible: showWarning
|
||
height: showWarning ? Math.max(Theme.buttonHeightXS, warningRow.implicitHeight) + Theme.spacingXS * 2 : 0
|
||
radius: Theme.cornerRadiusS
|
||
color: Theme.foregroundColor(Theme.chipSurface, Theme.isFloatingWindow(root))
|
||
|
||
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
|
||
|
||
DIcon {
|
||
name: "warning"
|
||
size: Theme.iconSizeSmall
|
||
color: Theme.warning
|
||
anchors.verticalCenter: parent.verticalCenter
|
||
}
|
||
|
||
StyledText {
|
||
width: parent.width - Theme.iconSizeSmall - Theme.spacingS - (launchButton.visible ? launchButton.width + Theme.spacingS : 0)
|
||
anchors.verticalCenter: parent.verticalCenter
|
||
text: CalendarService.dankBinaryExists ? I18n.tr("DankCalendar isn't running") : I18n.tr("DankCalendar isn't installed")
|
||
font.pixelSize: Theme.fontSizeSmall
|
||
color: Theme.surfaceText
|
||
wrapMode: Text.Wrap
|
||
}
|
||
|
||
DButton {
|
||
id: launchButton
|
||
anchors.verticalCenter: parent.verticalCenter
|
||
visible: CalendarService.dankBinaryExists
|
||
text: I18n.tr("Launch")
|
||
buttonHeight: Theme.buttonHeightXS
|
||
backgroundColor: Theme.primary
|
||
textColor: Theme.onPrimary
|
||
onClicked: CalendarService.launchDankCalendar()
|
||
}
|
||
}
|
||
}
|
||
|
||
Item {
|
||
id: header
|
||
|
||
anchors.left: parent.left
|
||
anchors.right: parent.right
|
||
anchors.top: dankWarning.bottom
|
||
anchors.topMargin: dankWarning.visible ? Theme.spacingS : 0
|
||
height: DashMetrics.monthNavSize
|
||
|
||
Item {
|
||
anchors.fill: parent
|
||
visible: !root.showEventDetails
|
||
|
||
NavButton {
|
||
anchors.left: parent.left
|
||
iconName: I18n.isRtl ? "chevron_right" : "chevron_left"
|
||
iconColor: Theme.onSurfaceVariant
|
||
Accessible.name: I18n.tr("Previous")
|
||
onClicked: root.shiftMonth(-1)
|
||
}
|
||
|
||
StyledText {
|
||
anchors.centerIn: parent
|
||
width: parent.width - DashMetrics.monthNavSize * 4
|
||
text: calendarGrid.displayDate.toLocaleDateString(I18n.locale(), "MMMM yyyy")
|
||
font.pixelSize: Theme.fontSizeLarge
|
||
font.weight: Theme.fontWeightMedium
|
||
color: Theme.surfaceText
|
||
horizontalAlignment: Text.AlignHCenter
|
||
elide: Text.ElideRight
|
||
}
|
||
|
||
Row {
|
||
anchors.right: parent.right
|
||
|
||
NavButton {
|
||
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.toDateString() === now.toDateString();
|
||
}
|
||
|
||
iconName: "today"
|
||
enabled: !isToday
|
||
Accessible.name: I18n.tr("Today")
|
||
onClicked: root.goToToday()
|
||
}
|
||
|
||
NavButton {
|
||
iconName: I18n.isRtl ? "chevron_left" : "chevron_right"
|
||
iconColor: Theme.onSurfaceVariant
|
||
Accessible.name: I18n.tr("Next")
|
||
onClicked: root.shiftMonth(1)
|
||
}
|
||
}
|
||
}
|
||
|
||
Item {
|
||
anchors.fill: parent
|
||
visible: root.showEventDetails
|
||
|
||
NavButton {
|
||
anchors.left: parent.left
|
||
iconName: I18n.isRtl ? "arrow_forward" : "arrow_back"
|
||
onClicked: root.showEventDetails = false
|
||
}
|
||
|
||
StyledText {
|
||
anchors.centerIn: parent
|
||
width: parent.width - DashMetrics.monthNavSize * 2 - Theme.spacingS * 2
|
||
text: {
|
||
const dateStr = Qt.formatDate(root.selectedDate, "MMM d");
|
||
if (!root.hasEvents)
|
||
return dateStr;
|
||
const count = root.selectedDateEvents.length;
|
||
const eventCount = count === 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(count);
|
||
return dateStr + " • " + eventCount;
|
||
}
|
||
font.pixelSize: Theme.fontSizeMedium
|
||
font.weight: Theme.fontWeightMedium
|
||
color: Theme.surfaceText
|
||
horizontalAlignment: Text.AlignHCenter
|
||
elide: Text.ElideRight
|
||
}
|
||
|
||
NavButton {
|
||
anchors.right: parent.right
|
||
iconName: "add"
|
||
visible: root.canCreate
|
||
Accessible.name: I18n.tr("New event")
|
||
onClicked: root.openEditor(null)
|
||
}
|
||
}
|
||
}
|
||
|
||
Item {
|
||
id: body
|
||
|
||
anchors.left: parent.left
|
||
anchors.right: parent.right
|
||
anchors.top: header.bottom
|
||
anchors.topMargin: Theme.spacingS
|
||
anchors.bottom: parent.bottom
|
||
|
||
DMonthGrid {
|
||
id: calendarGrid
|
||
|
||
anchors.fill: parent
|
||
visible: !root.showEventDetails
|
||
displayDate: systemClock.date
|
||
selectedDate: systemClock.date
|
||
today: systemClock.date
|
||
firstDayOfWeek: root.weekStartJs()
|
||
dayNames: {
|
||
const days = [];
|
||
const qtFirst = root.weekStartQt();
|
||
for (let i = 0; i < 7; ++i)
|
||
days.push(I18n.locale().dayName(((qtFirst - 1 + i) % 7) + 1, Locale.ShortFormat));
|
||
return days;
|
||
}
|
||
showWeekNumbers: SettingsData.showWeekNumber
|
||
highlightWeekends: root.options.weekends === true
|
||
weekColumnWidth: DashMetrics.weekColumnWidth
|
||
weekNumberFor: date => root.getWeekNumber(date)
|
||
dotColorsFor: date => root.eventColorsFor(date)
|
||
revision: root.eventsRevision
|
||
interactive: root.interactive
|
||
onDayClicked: date => {
|
||
root.forceActiveFocus(Qt.MouseFocusReason);
|
||
root.selectDay(date);
|
||
root.showEventDetails = true;
|
||
}
|
||
}
|
||
|
||
Column {
|
||
anchors.fill: parent
|
||
visible: root.showEventDetails
|
||
spacing: Theme.spacingS
|
||
|
||
DFlickable {
|
||
id: flickableArea
|
||
showScrollBar: false
|
||
width: parent.width
|
||
height: parent.height - taskInput.height - parent.spacing
|
||
clip: true
|
||
contentWidth: width
|
||
contentHeight: listViewContainer.height
|
||
interactive: listViewContainer.draggedItem === null
|
||
|
||
Item {
|
||
id: listViewContainer
|
||
|
||
property var draggedItem: null
|
||
property bool orderChanged: false
|
||
|
||
width: parent.width
|
||
height: 0
|
||
|
||
function sortedItems() {
|
||
const items = [];
|
||
for (let i = 0; i < repeater.count; i++) {
|
||
const item = repeater.itemAt(i);
|
||
if (item)
|
||
items.push(item);
|
||
}
|
||
items.sort((a, b) => a.visualIndex - b.visualIndex);
|
||
return items;
|
||
}
|
||
|
||
function resetAndLayout() {
|
||
for (let i = 0; i < repeater.count; i++) {
|
||
const item = repeater.itemAt(i);
|
||
if (!item)
|
||
continue;
|
||
item.visualIndex = i;
|
||
item.isDragging = false;
|
||
item.isEditing = false;
|
||
}
|
||
updateLayout();
|
||
}
|
||
|
||
function updateLayout() {
|
||
const items = sortedItems();
|
||
let currentY = 0;
|
||
for (let i = 0; i < items.length; i++) {
|
||
if (!items[i].isDragging)
|
||
items[i].y = currentY;
|
||
currentY += items[i].height + Theme.groupedListGap;
|
||
}
|
||
listViewContainer.height = Math.max(0, currentY - Theme.groupedListGap);
|
||
}
|
||
|
||
function checkAndReorder(dragged) {
|
||
const items = sortedItems();
|
||
let swapped = false;
|
||
|
||
const targetY = index => {
|
||
let y = 0;
|
||
for (let i = 0; i < index; i++)
|
||
y += items[i].height + Theme.groupedListGap;
|
||
return y;
|
||
};
|
||
|
||
for (; ; ) {
|
||
const draggedIdx = items.indexOf(dragged);
|
||
if (draggedIdx < 0)
|
||
break;
|
||
let neighbour = null;
|
||
let neighbourIdx = -1;
|
||
if (draggedIdx > 0 && dragged.y < targetY(draggedIdx - 1) + items[draggedIdx - 1].height / 2)
|
||
neighbourIdx = draggedIdx - 1;
|
||
else if (draggedIdx < items.length - 1 && dragged.y + dragged.height > targetY(draggedIdx + 1) + items[draggedIdx + 1].height / 2)
|
||
neighbourIdx = draggedIdx + 1;
|
||
if (neighbourIdx < 0)
|
||
break;
|
||
neighbour = items[neighbourIdx];
|
||
const temp = dragged.visualIndex;
|
||
dragged.visualIndex = neighbour.visualIndex;
|
||
neighbour.visualIndex = temp;
|
||
items[draggedIdx] = neighbour;
|
||
items[neighbourIdx] = dragged;
|
||
listViewContainer.orderChanged = true;
|
||
swapped = true;
|
||
}
|
||
|
||
if (swapped)
|
||
updateLayout();
|
||
}
|
||
|
||
function saveNewOrder() {
|
||
if (!orderChanged)
|
||
return;
|
||
const orderedIds = sortedItems().map(item => item.taskId).filter(id => id.startsWith("task_")).map(id => id.replace("task_", ""));
|
||
if (orderedIds.length > 0)
|
||
CalendarService.reorderTasksForDate(root.selectedDate, orderedIds);
|
||
orderChanged = false;
|
||
}
|
||
|
||
Repeater {
|
||
id: repeater
|
||
model: root.selectedDateEvents
|
||
|
||
onModelChanged: Qt.callLater(listViewContainer.resetAndLayout)
|
||
|
||
delegate: EventRow {}
|
||
}
|
||
}
|
||
}
|
||
|
||
DTextField {
|
||
id: taskInput
|
||
width: parent.width
|
||
height: DashMetrics.taskInputHeight
|
||
cornerRadius: Theme.fullRadius(width, height)
|
||
font.pixelSize: Theme.fontSizeSmall
|
||
leftIconName: "add_task"
|
||
leftIconSize: Theme.iconSizeSmall
|
||
placeholderText: I18n.tr("Add a task...", "placeholder in the new-task input field")
|
||
hidePlaceholderOnFocus: false
|
||
keyForwardTargets: [taskKeyHandler]
|
||
|
||
onAccepted: {
|
||
const txt = text.trim();
|
||
if (txt === "")
|
||
return;
|
||
CalendarService.addTaskForDate(root.selectedDate, txt);
|
||
text = "";
|
||
}
|
||
|
||
Item {
|
||
id: taskKeyHandler
|
||
|
||
Keys.onEscapePressed: event => {
|
||
root.showEventDetails = false;
|
||
event.accepted = true;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
component NavButton: DActionButton {
|
||
buttonSize: DashMetrics.monthNavSize
|
||
iconSize: DashMetrics.monthNavIconSize
|
||
iconColor: Theme.primary
|
||
}
|
||
|
||
component EventRow: Rectangle {
|
||
id: taskItem
|
||
|
||
required property int index
|
||
required property var modelData
|
||
|
||
property int visualIndex: index
|
||
property bool isDragging: false
|
||
property bool isEditing: false
|
||
|
||
readonly property string taskId: modelData?.id ?? ""
|
||
readonly property bool isLocalTask: taskId.startsWith("task_")
|
||
readonly property bool isDankTask: taskId.startsWith("vtodo_")
|
||
readonly property bool isTask: isLocalTask || isDankTask
|
||
readonly property bool completed: isTask && !!modelData?.completed
|
||
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 color baseAccent: (isLocalTask || !modelData?.color?.length) ? Theme.primary : modelData.color
|
||
readonly property color accentColor: completed ? Theme.withAlpha(baseAccent, Theme.stateLayerDrag) : baseAccent
|
||
readonly property real leadingWidth: Theme.spacingM + DashMetrics.eventAccentWidth + Theme.spacingM + (isLocalTask ? DashMetrics.eventActionSize + Theme.spacingXS : 0) + (isTask ? DashMetrics.eventActionSize + Theme.spacingXS : 0)
|
||
readonly property real trailingWidth: {
|
||
if (canModify)
|
||
return DashMetrics.eventActionSize * 2 + Theme.spacingXS + Theme.spacingS * 2;
|
||
if (quickUrl !== "")
|
||
return DashMetrics.eventActionSize + Theme.spacingS * 2;
|
||
return Theme.spacingS;
|
||
}
|
||
|
||
width: parent ? parent.width : 0
|
||
height: Math.max(DashMetrics.eventRowMinHeight, eventContent.implicitHeight + Theme.spacingS * 2)
|
||
radius: Theme.cornerRadiusS
|
||
color: isDragging ? Theme.withAlpha(Theme.primary, Theme.stateLayerDrag) : Theme.foregroundColor(Theme.chipSurface, Theme.isFloatingWindow(root))
|
||
z: isDragging ? 100 : visualIndex
|
||
activeFocusOnTab: !!modelData && !isEditing && root.interactive
|
||
|
||
function activate() {
|
||
if (isTask && canModify) {
|
||
CalendarService.toggleTask(taskId);
|
||
return;
|
||
}
|
||
root.detailRequested(modelData);
|
||
}
|
||
|
||
Keys.onPressed: event => {
|
||
if (!activeFocusOnTab)
|
||
return;
|
||
switch (event.key) {
|
||
case Qt.Key_Space:
|
||
case Qt.Key_Return:
|
||
case Qt.Key_Enter:
|
||
activate();
|
||
event.accepted = true;
|
||
break;
|
||
}
|
||
}
|
||
|
||
StateLayer {
|
||
cornerRadius: taskItem.radius
|
||
stateColor: taskItem.accentColor
|
||
disabled: !taskItem.modelData || taskItem.isEditing || !root.interactive
|
||
onClicked: taskItem.activate()
|
||
}
|
||
|
||
FocusRing {}
|
||
|
||
onIndexChanged: visualIndex = index
|
||
|
||
onYChanged: {
|
||
if (isDragging)
|
||
listViewContainer.checkAndReorder(taskItem);
|
||
}
|
||
|
||
Behavior on y {
|
||
enabled: !taskItem.isDragging && DashMetrics.animationsEnabled
|
||
NumberAnimation {
|
||
duration: Theme.expressiveDurations.expressiveFastSpatial
|
||
easing.type: Easing.BezierSpline
|
||
easing.bezierCurve: Theme.expressiveCurves.standard
|
||
}
|
||
}
|
||
|
||
Component.onCompleted: {
|
||
visualIndex = index;
|
||
listViewContainer.updateLayout();
|
||
}
|
||
|
||
onHeightChanged: listViewContainer.updateLayout()
|
||
|
||
onIsEditingChanged: {
|
||
if (!isEditing)
|
||
return;
|
||
editInput.forceActiveFocus();
|
||
editInput.selectAll();
|
||
}
|
||
|
||
function commitEdit() {
|
||
const txt = editInput.text.trim();
|
||
if (txt !== "" && taskItem.taskId !== "")
|
||
CalendarService.editTask(taskItem.taskId, txt);
|
||
taskItem.isEditing = false;
|
||
}
|
||
|
||
Rectangle {
|
||
anchors.left: parent.left
|
||
anchors.leftMargin: Theme.spacingM
|
||
anchors.verticalCenter: parent.verticalCenter
|
||
width: DashMetrics.eventAccentWidth
|
||
height: parent.height - Theme.spacingM * 2
|
||
radius: Theme.fullRadius(width, height)
|
||
color: taskItem.accentColor
|
||
}
|
||
|
||
DIcon {
|
||
id: dragHandle
|
||
anchors.left: parent.left
|
||
anchors.leftMargin: Theme.spacingM + DashMetrics.eventAccentWidth + Theme.spacingM
|
||
anchors.verticalCenter: parent.verticalCenter
|
||
name: "drag_indicator"
|
||
size: DashMetrics.eventActionIconSize
|
||
color: dragMouseArea.containsMouse ? Theme.primary : Theme.onSurfaceVariant
|
||
visible: taskItem.isLocalTask && !taskItem.isEditing
|
||
|
||
MouseArea {
|
||
id: dragMouseArea
|
||
anchors.fill: parent
|
||
anchors.margins: -Theme.spacingS
|
||
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;
|
||
}
|
||
|
||
onReleased: {
|
||
taskItem.isDragging = false;
|
||
listViewContainer.draggedItem = null;
|
||
if (listViewContainer.orderChanged)
|
||
listViewContainer.saveNewOrder();
|
||
else
|
||
listViewContainer.updateLayout();
|
||
}
|
||
|
||
onCanceled: {
|
||
taskItem.isDragging = false;
|
||
listViewContainer.draggedItem = null;
|
||
listViewContainer.resetAndLayout();
|
||
}
|
||
}
|
||
}
|
||
|
||
DActionButton {
|
||
anchors.left: parent.left
|
||
anchors.leftMargin: Theme.spacingM + DashMetrics.eventAccentWidth + Theme.spacingM + (taskItem.isLocalTask && !taskItem.isEditing ? DashMetrics.eventActionSize + Theme.spacingXS : 0)
|
||
anchors.verticalCenter: parent.verticalCenter
|
||
buttonSize: DashMetrics.eventActionSize
|
||
iconSize: DashMetrics.eventActionIconSize
|
||
iconName: taskItem.completed ? "check_box" : "check_box_outline_blank"
|
||
Accessible.name: taskItem.completed ? I18n.tr("Mark incomplete") : I18n.tr("Mark complete")
|
||
iconColor: taskItem.completed ? Theme.primary : Theme.onSurfaceVariant
|
||
visible: taskItem.isTask
|
||
enabled: taskItem.canModify && !taskItem.isEditing
|
||
onClicked: CalendarService.toggleTask(taskItem.taskId)
|
||
}
|
||
|
||
Column {
|
||
id: eventContent
|
||
|
||
anchors.left: parent.left
|
||
anchors.right: parent.right
|
||
anchors.verticalCenter: parent.verticalCenter
|
||
anchors.leftMargin: taskItem.leadingWidth
|
||
anchors.rightMargin: taskItem.trailingWidth
|
||
spacing: Theme.spacingXXS
|
||
visible: !taskItem.isEditing
|
||
|
||
StyledText {
|
||
width: parent.width
|
||
text: taskItem.modelData?.title ?? ""
|
||
font.pixelSize: Theme.fontSizeMedium
|
||
font.weight: Theme.fontWeightMedium
|
||
color: taskItem.completed ? Theme.onSurfaceVariant : Theme.surfaceText
|
||
elide: Text.ElideRight
|
||
maximumLineCount: 1
|
||
}
|
||
|
||
StyledText {
|
||
width: parent.width
|
||
text: {
|
||
const data = taskItem.modelData;
|
||
if (!data)
|
||
return "";
|
||
const cal = data.calendar?.length ? " · " + data.calendar : "";
|
||
if (data.allDay)
|
||
return I18n.tr("All day", "calendar task with no specific time") + cal;
|
||
if (!data.start || !data.end)
|
||
return "";
|
||
const timeFormat = SettingsData.use24HourClock ? "HH:mm" : "h:mm AP";
|
||
const startTime = Qt.formatTime(data.start, timeFormat);
|
||
if (data.start.getTime() !== data.end.getTime())
|
||
return startTime + " – " + Qt.formatTime(data.end, timeFormat) + cal;
|
||
return startTime + cal;
|
||
}
|
||
font.pixelSize: Theme.fontSizeSmall
|
||
color: Theme.onSurfaceVariant
|
||
visible: text !== "" && !taskItem.isLocalTask
|
||
}
|
||
}
|
||
|
||
DTextField {
|
||
id: editInput
|
||
anchors.left: parent.left
|
||
anchors.right: parent.right
|
||
anchors.leftMargin: taskItem.leadingWidth
|
||
anchors.rightMargin: taskItem.trailingWidth
|
||
anchors.verticalCenter: parent.verticalCenter
|
||
height: DashMetrics.eventActionSize
|
||
visible: taskItem.isEditing
|
||
font.pixelSize: Theme.fontSizeSmall
|
||
backgroundColor: "transparent"
|
||
borderWidth: 0
|
||
focusedBorderWidth: 0
|
||
topPadding: 0
|
||
bottomPadding: 0
|
||
text: taskItem.modelData?.title ?? ""
|
||
keyForwardTargets: [editKeyHandler]
|
||
onAccepted: taskItem.commitEdit()
|
||
|
||
Item {
|
||
id: editKeyHandler
|
||
|
||
Keys.onEscapePressed: event => {
|
||
taskItem.isEditing = false;
|
||
event.accepted = true;
|
||
}
|
||
}
|
||
}
|
||
|
||
DActionButton {
|
||
anchors.right: parent.right
|
||
anchors.rightMargin: Theme.spacingS
|
||
anchors.verticalCenter: parent.verticalCenter
|
||
buttonSize: DashMetrics.eventActionSize
|
||
iconSize: DashMetrics.eventActionIconSize
|
||
iconName: taskItem.modelData?.meetingUrl ? "videocam" : "link"
|
||
iconColor: Theme.primary
|
||
visible: taskItem.quickUrl !== "" && !taskItem.canModify
|
||
tooltipText: taskItem.quickUrl
|
||
tooltipSide: "left"
|
||
onClicked: Qt.openUrlExternally(taskItem.quickUrl)
|
||
}
|
||
|
||
DActionButton {
|
||
id: deleteButton
|
||
anchors.right: parent.right
|
||
anchors.rightMargin: Theme.spacingS
|
||
anchors.verticalCenter: parent.verticalCenter
|
||
buttonSize: DashMetrics.eventActionSize
|
||
iconSize: DashMetrics.eventActionIconSize
|
||
iconName: taskItem.isEditing ? "close" : "delete"
|
||
Accessible.name: taskItem.isEditing ? I18n.tr("Cancel") : I18n.tr("Delete")
|
||
iconColor: taskItem.isEditing ? Theme.onSurfaceVariant : Theme.error
|
||
visible: taskItem.canModify
|
||
onClicked: {
|
||
if (taskItem.isEditing) {
|
||
taskItem.isEditing = false;
|
||
return;
|
||
}
|
||
if (taskItem.taskId !== "")
|
||
CalendarService.removeTask(taskItem.taskId);
|
||
}
|
||
}
|
||
|
||
DActionButton {
|
||
anchors.right: deleteButton.left
|
||
anchors.rightMargin: Theme.spacingXS
|
||
anchors.verticalCenter: parent.verticalCenter
|
||
buttonSize: DashMetrics.eventActionSize
|
||
iconSize: DashMetrics.eventActionIconSize
|
||
iconName: taskItem.isEditing ? "check" : "edit"
|
||
Accessible.name: taskItem.isEditing ? I18n.tr("Save") : I18n.tr("Edit")
|
||
iconColor: taskItem.isEditing ? Theme.primary : Theme.onSurfaceVariant
|
||
visible: taskItem.canModify
|
||
onClicked: {
|
||
if (taskItem.isEditing) {
|
||
taskItem.commitEdit();
|
||
return;
|
||
}
|
||
taskItem.isEditing = true;
|
||
}
|
||
}
|
||
}
|
||
|
||
SystemClock {
|
||
id: systemClock
|
||
enabled: root.live
|
||
precision: SystemClock.Hours
|
||
}
|
||
|
||
Connections {
|
||
target: SessionService
|
||
|
||
function onSessionResumed() {
|
||
if (!root.live)
|
||
return;
|
||
systemClock.enabled = false;
|
||
systemClock.enabled = true;
|
||
}
|
||
}
|
||
}
|