Luigit
repositories / dotfiles

dotfiles

bugabingas dorkfiles

owned by admin

quickshell/nuguland/ClipboardPanel.qml

Raw
pragma ComponentBehavior: Bound
import QtQuick
import "Util.js" as Util

FocusScope {
    id: root

    required property Theme theme
    required property Clipboard clipboardService
    property var sessionState: ({
            "query": "",
            "selectedId": "",
            "scrollY": 0
        })
    readonly property string query: String(sessionState.query || "")
    property bool stateReady: false
    readonly property var panelState: clipboardService ? Util.parseJson(clipboardService.search(query), {
        "rows": [],
        "privateMode": false,
        "lastAction": {}
    }) : ({
            "rows": [],
            "privateMode": false,
            "lastAction": {}
        })
    readonly property var rows: panelState.rows || []
    readonly property int selectedIndex: selectionIndex()
    readonly property var selectedRow: selectedIndex >= 0 ? rows[selectedIndex] : ({})
    readonly property int rowHeight: 56
    readonly property int rowSpacing: 6
    readonly property int rowStride: rowHeight + rowSpacing
    signal pasteRequested(string id)

    function updateState(values) {
        Object.assign(sessionState, values);
        // Notify bindings when the owner supplies a plain JS object.
        sessionStateChanged();
    }

    function selectionIndex() {
        const index = rows.findIndex(row => String(row.id) === String(sessionState.selectedId));
        return index >= 0 ? index : (rows.length > 0 ? 0 : -1);
    }

    function reconcileSelection() {
        const id = selectedIndex >= 0 ? String(rows[selectedIndex].id) : "";
        if (sessionState.selectedId !== id)
            updateState({
                "selectedId": id
            });
        if (stateReady)
            ensureSelectedVisible();
    }

    function search(text) {
        updateState({
            "query": String(text)
        });
    }

    function select(index) {
        if (rows.length === 0)
            return;
        const i = Math.max(0, Math.min(rows.length - 1, Number(index)));
        updateState({
            "selectedId": String(rows[i].id)
        });
        ensureSelectedVisible();
    }

    function rememberScroll() {
        if (stateReady && sessionState.scrollY !== list.contentY)
            updateState({
                "scrollY": list.contentY
            });
    }

    function ensureSelectedVisible() {
        if (selectedIndex < 0)
            return;
        const top = selectedIndex * rowStride;
        const bottom = top + rowHeight;
        if (top < list.contentY)
            updateState({
                "scrollY": top
            });
        else if (bottom > list.contentY + list.height)
            updateState({
                "scrollY": bottom - list.height
            });
    }

    function copy(id) {
        if (clipboardService)
            clipboardService.copy(id);
    }

    function paste(id) {
        if (clipboardService && rows.some(row => String(row.id) === String(id)))
            pasteRequested(String(id));
    }

    function togglePin(id) {
        if (clipboardService)
            clipboardService.togglePin(id);
    }

    function deleteRow(id) {
        if (clipboardService)
            clipboardService.deleteRow(id);
    }

    function setPrivateMode(enabled) {
        if (clipboardService)
            clipboardService.setPrivateMode(enabled);
    }

    width: parent ? parent.width : 0
    height: parent ? parent.height : implicitHeight
    implicitHeight: 510
    focus: true
    onRowsChanged: selectionTimer.restart()

    Timer {
        id: selectionTimer
        interval: 0
        running: true
        onTriggered: {
            root.reconcileSelection();
            if (!root.stateReady) {
                root.stateReady = true;
                searchInput.input.forceActiveFocus();
            }
        }
    }

    Keys.onUpPressed: event => {
        event.accepted = true;
        select(selectedIndex - 1);
    }
    Keys.onDownPressed: event => {
        event.accepted = true;
        select(selectedIndex + 1);
    }
    Keys.onReturnPressed: event => {
        event.accepted = true;
        if (!event.isAutoRepeat)
            paste(selectedRow.id);
    }
    Keys.onEnterPressed: event => {
        event.accepted = true;
        if (!event.isAutoRepeat)
            paste(selectedRow.id);
    }

    Shortcut {
        sequences: ["Return", "Enter"]
        enabled: searchInput.input.activeFocus
        autoRepeat: false
        onActivated: root.paste(root.selectedRow.id)
    }

    StableListModel {
        id: clipboardRows
        rows: root.rows
    }

    Row {
        id: searchRow

        anchors.left: parent.left
        anchors.right: parent.right
        anchors.top: parent.top
        spacing: 8
        height: 38

        PopupTextField {
            id: searchInput
            objectName: "searchField"
            theme: root.theme
            width: searchRow.width - privateButton.width - searchRow.spacing
            height: searchRow.height
            icon: ""
            placeholderText: "search clipboard"
            text: root.query
            onEdited: text => root.search(text)
        }

        PopupButton {
            id: privateButton
            objectName: "privateModeChip"
            theme: root.theme
            width: 38
            height: searchRow.height
            icon: root.panelState.privateMode ? "" : ""
            text: root.panelState.privateMode ? "private" : "normal"
            showText: false
            role: "ghost"
            accessibleName: "private clipboard mode"
            tooltip: root.panelState.privateMode ? "resume clipboard capture" : "pause clipboard capture"
            checked: root.panelState.privateMode
            enabled: !!root.clipboardService
            onClicked: root.setPrivateMode(!root.panelState.privateMode)
        }
    }

    Item {
        objectName: "clipboardHistoryPanel"
        anchors.left: parent.left
        anchors.right: parent.right
        anchors.top: searchRow.bottom
        anchors.bottom: pasteFeedback.top
        anchors.topMargin: 10
        anchors.bottomMargin: 10

        Flickable {
            id: list
            objectName: "clipboardResultList"

            anchors.fill: parent
            contentHeight: rowColumn.implicitHeight
            contentY: Math.max(0, Math.min(Number(root.sessionState.scrollY || 0), contentHeight - height))
            onContentYChanged: root.rememberScroll()
            clip: true
            boundsBehavior: Flickable.StopAtBounds

            Column {
                id: rowColumn

                width: list.width
                spacing: root.rowSpacing

                Repeater {
                    model: clipboardRows

                    delegate: PopupCard {
                        id: clipRow

                        required property int index
                        required property var rowData
                        readonly property var modelData: rowData
                        readonly property bool isSelected: index === root.selectedIndex

                        objectName: "clipRow"
                        theme: root.theme
                        padding: 0
                        active: isSelected
                        hoverable: true
                        externallyPressed: rowArea.pressed
                        activeFocusOnTab: isSelected
                        Accessible.role: Accessible.ListItem
                        Accessible.name: String(modelData.title || "clipboard item")
                        Accessible.selected: isSelected
                        onActiveFocusChanged: if (activeFocus)
                            root.select(index)
                        width: rowColumn.width
                        height: root.rowHeight

                        MouseArea {
                            id: rowArea

                            anchors.fill: parent
                            cursorShape: Qt.PointingHandCursor
                            onClicked: {
                                root.select(clipRow.index);
                                clipRow.forceActiveFocus();
                            }
                            onDoubleClicked: root.paste(clipRow.modelData.id)
                        }

                        Item {
                            id: leadingSlot

                            anchors.left: parent.left
                            anchors.leftMargin: 9
                            anchors.verticalCenter: parent.verticalCenter
                            width: 36
                            height: 36

                            Image {
                                id: thumb

                                anchors.fill: parent
                                visible: clipRow.modelData.isImage && thumb.status !== Image.Error
                                source: clipRow.modelData.isImage ? "file://" + encodeURI(clipRow.modelData.imagePath || "") : ""
                                sourceSize.width: 36
                                sourceSize.height: 36
                                fillMode: Image.PreserveAspectCrop
                                asynchronous: true
                                clip: true
                                smooth: true
                            }

                            Rectangle {
                                id: swatch

                                anchors.fill: parent
                                visible: !clipRow.modelData.isImage && !!clipRow.modelData.swatch
                                radius: 9
                                color: clipRow.modelData.swatch ? clipRow.modelData.swatch : "transparent"
                                border.width: 1
                                border.color: root.theme.withAlpha(root.theme.ui.normal, 0.25)
                            }

                            Text {
                                anchors.centerIn: parent
                                visible: !swatch.visible && !thumb.visible
                                text: clipRow.modelData.icon || ""
                                color: root.theme.ui.accent
                                font.family: "Symbols Nerd Font Mono"
                                font.pixelSize: 18
                            }
                        }

                        Column {
                            anchors.left: leadingSlot.right
                            anchors.right: pinBadge.left
                            anchors.verticalCenter: parent.verticalCenter
                            anchors.leftMargin: 11
                            anchors.rightMargin: 8
                            spacing: 3

                            NuguText {
                                theme: root.theme
                                selected: clipRow.isSelected
                                text: clipRow.modelData.title
                                width: parent.width
                                elide: Text.ElideRight
                            }

                            NuguText {
                                theme: root.theme
                                variant: "meta"
                                selected: clipRow.isSelected
                                text: clipRow.modelData.kind + " · " + clipRow.modelData.age
                                width: parent.width
                                elide: Text.ElideRight
                            }
                        }

                        Text {
                            id: pinBadge

                            anchors.right: parent.right
                            anchors.rightMargin: 12
                            anchors.verticalCenter: parent.verticalCenter
                            width: 16
                            text: clipRow.modelData.pinned ? "" : ""
                            Accessible.name: clipRow.modelData.pinned ? "pinned" : ""
                            color: root.theme.ui.focus
                            font.family: "Symbols Nerd Font Mono"
                            font.pixelSize: 12
                        }
                    }
                }
            }
        }

        NuguText {
            anchors.centerIn: parent
            width: parent.width
            horizontalAlignment: Text.AlignHCenter
            theme: root.theme
            variant: "meta"
            visible: root.rows.length === 0
            text: !root.clipboardService ? "clipboard unavailable" : (root.query.length > 0 ? "no matches" : "clipboard is empty")
        }
    }

    NuguText {
        id: pasteFeedback
        anchors.left: parent.left
        anchors.right: parent.right
        anchors.bottom: actionRail.top
        anchors.bottomMargin: text.length > 0 ? 8 : 0
        theme: root.theme
        text: root.panelState.lastAction.error || ""
        color: root.theme.error
        height: text.length > 0 ? implicitHeight : 0
        wrapMode: Text.Wrap
    }

    Row {
        id: actionRail
        objectName: "clipboardActionRail"

        anchors.left: parent.left
        anchors.bottom: parent.bottom
        height: 38
        spacing: 8
        enabled: root.selectedIndex >= 0 && !!root.clipboardService

        PopupButton {
            objectName: "clipboardPaste"
            theme: root.theme
            height: actionRail.height
            text: "paste"
            icon: ""
            role: "primary"
            tooltip: "paste selected item into the previous window"
            enabled: !root.panelState.lastAction.pending
            onClicked: root.paste(root.selectedRow.id)
        }

        PopupButton {
            objectName: "clipboardCopy"
            theme: root.theme
            width: 38
            height: actionRail.height
            icon: ""
            showText: false
            accessibleName: "copy selected item"
            tooltip: "copy selected item to the clipboard"
            onClicked: root.copy(root.selectedRow.id)
        }

        PopupButton {
            objectName: "clipboardPin"
            theme: root.theme
            width: 38
            height: actionRail.height
            icon: ""
            showText: false
            checked: !!root.selectedRow.pinned
            accessibleName: root.selectedRow.pinned ? "unpin selected item" : "pin selected item"
            tooltip: root.selectedRow.pinned ? "unpin selected item" : "keep selected item at the top"
            onClicked: root.togglePin(root.selectedRow.id)
        }

        PopupButton {
            objectName: "clipboardDelete"
            theme: root.theme
            width: 38
            height: actionRail.height
            icon: ""
            showText: false
            role: "destructive"
            accessibleName: "delete selected item"
            tooltip: "remove selected item from clipboard history"
            onClicked: root.deleteRow(root.selectedRow.id)
        }
    }
}