Luigit
repositories / dotfiles

dotfiles

bugabingas dorkfiles

owned by admin

quickshell/nuguland/ClockPanel.qml

Raw
import 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 : ""
        }
    }
}