repositories / dotfiles
dotfiles
bugabingas dorkfiles
owned by admin
quickshell/nuguland/ClockPanel.qml
Rawimport QtQuick
import "Util.js" as Util
import "caldav.js" as Ical
Column {
id: root
required property Theme theme
readonly property Flickable scrollView: agendaFlick
required property Time timeService
required property CalDav caldavService
property var panelState: root.timeService ? Util.parseJson(root.timeService.stateJson(), {}) : ({})
property date localNow: new Date()
property var calendar: root.timeService ? Util.parseJson(root.timeService.calendar(localNow.getFullYear(), localNow.getMonth() + 1, localNow.getDate()), {
"weeks": []
}) : ({
"weeks": []
})
property var days: calendar.weeks ? [].concat.apply([], calendar.weeks) : []
readonly property bool setupMode: root.caldavService && root.caldavService.status === "needs-setup"
// grid sizing: big square day cells, grid centered in the popout
readonly property int gridSpacing: 6
readonly property int cellW: 48
readonly property int cellH: 48
readonly property real pitchX: cellW + gridSpacing
readonly property real pitchY: cellH + gridSpacing
readonly property real gridW: cellW * 7 + gridSpacing * 6
// iso "YYYY-MM-DD" -> array of single-day markers {color, allDay}.
// Multi-day events (coveredDates>1) are drawn as bands, not markers.
readonly property var caldavEvents: root.caldavService ? root.caldavService.events : []
readonly property var eventsByDate: {
const map = {};
const evs = caldavEvents || [];
for (let i = 0; i < evs.length; i++) {
const ev = evs[i];
const dates = Ical.coveredDates(ev);
if (dates.length !== 1)
continue;
const iso = dates[0];
(map[iso] || (map[iso] = [])).push({
"color": ev.color,
"allDay": ev.allDay || ev.kind === "task"
});
}
// all-day pills before timed dots within each day
for (const k in map)
map[k].sort(function (a, b) {
return (b.allDay ? 1 : 0) - (a.allDay ? 1 : 0);
});
return map;
}
// multi-day events as week-split band segments (Ical.buildSegments)
readonly property var bandLayout: {
const gs = root.caldavService ? root.caldavService.gridStartDate() : null;
if (!gs)
return {
"segments": [],
"laneCount": 0
};
return Ical.buildSegments(caldavEvents || [], gs);
}
readonly property string todayIso: {
const d = root.localNow;
const month = d.getMonth() + 1;
const day = d.getDate();
return d.getFullYear() + "-" + (month < 10 ? "0" : "") + month + "-" + (day < 10 ? "0" : "") + day;
}
// today's events + tasks, sorted: all-day/untimed first, then by start time
readonly property var todayItems: {
const evs = caldavEvents || [];
const out = [];
for (let i = 0; i < evs.length; i++) {
if (Ical.coveredDates(evs[i]).indexOf(todayIso) >= 0)
out.push(evs[i]);
}
out.sort(function (a, b) {
const ta = (a.allDay || a.startMs == null) ? -1 : a.startMs;
const tb = (b.allDay || b.startMs == null) ? -1 : b.startMs;
return ta - tb;
});
return out;
}
spacing: 8
Component.onCompleted: if (root.caldavService)
root.caldavService.refresh()
NuguText {
width: parent ? parent.width : 0
horizontalAlignment: Text.AlignHCenter
theme: root.theme
variant: "title"
text: root.panelState.clock ? root.panelState.clock.statusDisplay : "--:--:--"
}
NuguText {
width: parent ? parent.width : 0
horizontalAlignment: Text.AlignHCenter
theme: root.theme
variant: "meta"
text: (root.panelState.date ? root.panelState.date.line : "calendar") + (root.caldavService && root.caldavService.stale ? " ·" : "")
}
// centered calendar block: weekday header + month grid + bands
Item {
visible: !root.setupMode
width: parent ? parent.width : 0
height: calStack.height
Column {
id: calStack
anchors.horizontalCenter: parent.horizontalCenter
spacing: root.gridSpacing
// single-letter weekday header, Monday-first; weekend dimmed
Row {
spacing: root.gridSpacing
Repeater {
model: ["M", "D", "M", "D", "F", "S", "S"]
Item {
width: root.cellW
height: hdr.height
NuguText {
id: hdr
anchors.horizontalCenter: parent.horizontalCenter
theme: root.theme
variant: "meta"
text: modelData
opacity: index >= 5 ? 0.4 : 0.7
}
}
}
}
Item {
width: monthGrid.width
height: monthGrid.height
Grid {
id: monthGrid
objectName: "calendar-grid"
columns: 7
spacing: root.gridSpacing
Repeater {
model: root.days
Item {
id: cell
readonly property var markers: root.eventsByDate[modelData.iso] || []
width: root.cellW
height: root.cellH
// today: filled accent circle behind the number
Rectangle {
visible: !!modelData.today
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
anchors.verticalCenterOffset: -5
width: 34
height: 34
radius: 17
color: root.theme ? root.theme.ui.accent : "transparent"
}
NuguText {
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
anchors.verticalCenterOffset: -5
theme: root.theme
variant: "body"
text: modelData.day ? String(modelData.day) : ""
color: modelData.today && root.theme ? root.theme.content.backdrop : (root.theme ? root.theme.content.normal : "white")
opacity: modelData.inMonth ? 1 : 0.3
}
// marker strip: all-day pills + timed dots, max 3 then "+"
Row {
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.bottom
anchors.bottomMargin: 7
spacing: 3
Repeater {
model: Math.min(cell.markers.length, 3)
Rectangle {
readonly property var m: cell.markers[index]
readonly property bool overflow: index === 2 && cell.markers.length > 3
width: overflow ? 5 : (m.allDay ? 8 : 4)
height: 3
radius: 1.5
color: overflow ? (root.theme ? root.theme.content.minor : "gray") : m.color
}
}
}
}
}
}
// multi-day bands sit behind day numbers and dots.
// pitch derives from the live cell size; bands sit near the
// cell bottom, stacking upward per lane.
Item {
anchors.fill: monthGrid
z: -1
Repeater {
model: root.bandLayout.segments
Rectangle {
readonly property var seg: modelData
x: seg.colStart * root.pitchX
y: seg.row * root.pitchY + root.cellH - 17 - seg.lane * 9
width: (seg.colEnd - seg.colStart + 1) * root.pitchX - root.gridSpacing
height: 6
color: Qt.alpha(seg.color, 0.9)
topLeftRadius: seg.roundLeft ? 3 : 0
bottomLeftRadius: seg.roundLeft ? 3 : 0
topRightRadius: seg.roundRight ? 3 : 0
bottomRightRadius: seg.roundRight ? 3 : 0
}
}
}
}
}
}
// today's agenda: events + tasks, ○ for open tasks, ✓ for done; centered
Item {
id: agendaFrame
visible: !root.setupMode
width: parent ? parent.width : 0
height: Math.max(70, parent ? parent.height - y : agendaCol.height)
clip: true
Flickable {
id: agendaFlick
anchors.fill: parent
contentWidth: root.gridW
contentHeight: agendaCol.height
interactive: contentHeight > height
boundsBehavior: Flickable.StopAtBounds
Column {
id: agendaCol
x: Math.round((agendaFrame.width - width) / 2)
width: root.gridW
spacing: 4
Repeater {
model: root.todayItems
Row {
spacing: 6
Rectangle {
anchors.verticalCenter: parent.verticalCenter
width: 6
height: 6
radius: 3
color: modelData.color
}
NuguText {
theme: root.theme
variant: "body"
text: {
const ev = modelData;
const mark = ev.kind === "task" ? (ev.done ? "✓ " : "○ ") : "";
let t = "";
if (!ev.allDay && ev.startMs != null) {
const d = new Date(ev.startMs);
const p = function (n) {
return (n < 10 ? "0" : "") + n;
};
t = p(d.getHours()) + ":" + p(d.getMinutes()) + " ";
}
return t + mark + (ev.summary || "(ohne Titel)");
}
}
}
}
NuguText {
visible: root.todayItems.length === 0
theme: root.theme
variant: "meta"
text: "keine termine"
}
}
}
Rectangle {
visible: agendaFlick.interactive
anchors.right: parent.right
anchors.rightMargin: 2
y: agendaFlick.contentY / Math.max(1, agendaFlick.contentHeight) * parent.height
width: 3
height: Math.max(18, parent.height * Math.min(1, parent.height / Math.max(parent.height, agendaFlick.contentHeight)))
radius: 2
color: root.theme ? root.theme.withAlpha(root.theme.ui.accent, 0.55) : "white"
}
}
// setup form: shown when the service has no usable config yet.
// Keystrokes need the popout to hold keyboard focus; the IPC
// saveSetup path is the verified entry point.
Column {
id: setupForm
visible: root.setupMode
width: parent ? parent.width : 0
spacing: 8
// grab keyboard focus into the first field once the card has
// finished claiming the surface focus (callLater wins the race)
onVisibleChanged: if (visible)
Qt.callLater(urlField.field.forceActiveFocus)
Component.onCompleted: if (visible)
Qt.callLater(urlField.field.forceActiveFocus)
NuguText {
theme: root.theme
variant: "meta"
text: "caldav einrichten"
}
SetupField {
id: urlField
theme: root.theme
placeholder: "https://host/remote.php/dav"
text: root.caldavService ? root.caldavService.setupUrlDraft : ""
onEdited: text => {
if (root.caldavService)
root.caldavService.setupUrlDraft = text;
}
}
SetupField {
id: userField
theme: root.theme
placeholder: "benutzername"
text: root.caldavService ? root.caldavService.setupUsernameDraft : ""
onEdited: text => {
if (root.caldavService)
root.caldavService.setupUsernameDraft = text;
}
}
SetupField {
id: pwField
theme: root.theme
placeholder: "app-passwort"
secret: true
text: root.caldavService ? root.caldavService.setupPasswordDraft : ""
onEdited: text => {
if (root.caldavService)
root.caldavService.setupPasswordDraft = text;
}
}
PopupButton {
theme: root.theme
width: parent.width
height: 30
text: "speichern"
role: "primary"
onClicked: if (root.caldavService)
root.caldavService.saveSetup(urlField.text, userField.text, pwField.text)
}
NuguText {
visible: root.caldavService && root.caldavService.secretError.length > 0
theme: root.theme
variant: "meta"
color: root.theme ? root.theme.error : "red"
text: root.caldavService ? root.caldavService.secretError : ""
}
}
}