Luigit
repositories / dotfiles

dotfiles

bugabingas dorkfiles

owned by admin

quickshell/nuguland/NokoPanel.qml

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

Column {
    id: root

    required property Theme theme
    readonly property Flickable scrollView: historyFlick
    required property Noko panelState
    property int waveTick: 0
    property int copiedIndex: -1

    function roleColor(role) {
        if (!theme)
            return "transparent";
        if (role === "warning")
            return theme.warning;
        if (role === "error")
            return theme.error;
        return theme.ui.accent;
    }

    function assetOk(name) {
        const a = panelState && panelState.assets ? panelState.assets : ({});
        if (name === "vad")
            return !!(a.vad && a.vad.exists);
        if (name === "model")
            return !!(a.parakeet && a.parakeet.complete);
        if (name === "runtime")
            return !!(a.runtime && a.runtime.exists);
        return false;
    }

    function toolOk(command) {
        const deps = panelState && panelState.doctor && panelState.doctor.systemDependencies ? panelState.doctor.systemDependencies : [];
        for (let i = 0; i < deps.length; i++)
            if (deps[i].command === command)
                return !!deps[i].exists;
        return false;
    }

    function friendlyProblem() {
        if (!panelState)
            return "local speech status unknown";
        if (panelState.listening)
            return "capturing locally until toggled off";
        if (panelState.transcribing)
            return "transcribing locally";
        if (panelState.installing)
            return "downloading local runtime and speech models";
        if (panelState.lastError.length === 0)
            return "local speech ready";
        return "setup required before recording";
    }

    function setupActionLabel() {
        if (!panelState)
            return "install setup";
        if (panelState.installing)
            return `${panelState.installLabel || "downloading"} ${Math.round(panelState.installProgress * 100)}%`;
        if (panelState.ready && panelState.updateAvailable)
            return "apply update";
        return panelState.ready ? "" : "install setup";
    }

    function showSetupAction() {
        return !!panelState && (panelState.installing || !panelState.ready || panelState.updateAvailable);
    }

    function updateAccent() {
        if (!theme)
            return "white";
        if (!panelState || panelState.updateUnknown || panelState.updateAvailable)
            return theme.warning;
        if (panelState.checkingUpdates)
            return theme.ui.accent;
        return panelState.updateChecked ? theme.ui.accent : theme.ui.minor;
    }

    function waveLevel(index) {
        if (!panelState || (!panelState.listening && !panelState.transcribing))
            return 0.2 + (index % 3) * 0.08;
        const phase = (waveTick + index * 5) / (panelState.transcribing ? 7 : 4);
        return 0.24 + Math.abs(Math.sin(phase)) * (panelState.transcribing ? 0.42 : 0.7);
    }

    function updateLine() {
        if (!panelState)
            return "updates not checked";
        if (panelState.checkingUpdates)
            return "checking updates";
        if (panelState.updateUnknown)
            return "update check failed";
        if (!panelState.updateChecked)
            return "updates not checked";
        return panelState.updateAvailable ? "update available" : "up to date";
    }

    width: parent ? parent.width : 0
    height: parent ? parent.height : implicitHeight
    spacing: 14
    Component.onCompleted: if (panelState)
        panelState.refresh()

    Timer {
        interval: 90
        repeat: true
        running: root.panelState && (root.panelState.listening || root.panelState.transcribing)
        onTriggered: root.waveTick++
    }

    Timer {
        id: copyFeedbackTimer

        interval: 1200
        onTriggered: root.copiedIndex = -1
    }

    PopupCard {
        theme: root.theme
        padding: 0
        attention: root.panelState && root.panelState.role === "warning"
        destructive: root.panelState && root.panelState.role === "error"
        width: parent.width
        height: 76

        Column {
            anchors.fill: parent
            anchors.leftMargin: 14
            anchors.rightMargin: root.showSetupAction() ? 242 : 14
            anchors.topMargin: 13
            anchors.bottomMargin: 12
            spacing: 7

            Row {
                width: parent.width
                spacing: 8

                NuguText {
                    theme: root.theme
                    variant: "title"
                    text: "noko"
                    color: root.theme ? root.theme.content.normal : "white"
                }

                NuguText {
                    theme: root.theme
                    variant: "meta"
                    text: root.panelState ? root.panelState.status : "unknown"
                    color: root.theme ? root.roleColor(root.panelState ? root.panelState.role : "warning") : "white"
                }
            }

            NuguText {
                theme: root.theme
                width: parent.width
                variant: "meta"
                text: root.friendlyProblem()
                color: root.theme ? root.theme.ui.minor : "white"
            }
        }

        NokoButton {
            visible: root.showSetupAction()
            anchors.right: parent.right
            anchors.rightMargin: 14
            anchors.verticalCenter: parent.verticalCenter
            theme: root.theme
            label: root.setupActionLabel()
            kind: "primary"
            buttonWidth: 210
            onClicked: if (root.panelState) {
                if (root.panelState.ready)
                    root.panelState.update();
                else
                    root.panelState.install();
            }
        }
    }

    Column {
        width: parent.width
        spacing: 7

        Row {
            width: parent.width
            height: 24
            spacing: 8

            NuguText {
                id: setupTitle

                anchors.verticalCenter: parent.verticalCenter
                theme: root.theme
                variant: "title"
                font.pixelSize: 13
                text: "setup"
                color: root.theme ? root.theme.ui.accent : "white"
            }

            Item {
                width: Math.max(1, parent.width - setupTitle.implicitWidth - updateBadge.width - parent.spacing * 2)
                height: 1
            }

            PopupBadge {
                id: updateBadge

                anchors.verticalCenter: parent.verticalCenter
                theme: root.theme
                width: 136
                text: root.updateLine()
                icon: "●"
                role: root.panelState && (root.panelState.updateUnknown || root.panelState.updateAvailable) ? "warning" : "active"
            }
        }

        PopupCard {
            theme: root.theme
            padding: 0
            width: parent.width
            height: setupFlow.implicitHeight + 18 + (root.panelState && root.panelState.installing ? 25 : 0)

            Column {
                anchors.fill: parent
                anchors.margins: 9
                spacing: 6

                Flow {
                    id: setupFlow

                    width: parent.width
                    spacing: 14

                    NokoAssetRow {
                        theme: root.theme
                        label: "voice detector"
                        ok: root.assetOk("vad")
                    }
                    NokoAssetRow {
                        theme: root.theme
                        label: "speech model"
                        ok: root.assetOk("model")
                    }
                    NokoAssetRow {
                        theme: root.theme
                        label: "local runtime"
                        ok: root.assetOk("runtime")
                    }
                    NokoAssetRow {
                        theme: root.theme
                        label: "mic capture"
                        ok: root.toolOk("pw-record")
                    }
                    NokoAssetRow {
                        theme: root.theme
                        label: "text insertion"
                        ok: root.toolOk("wtype")
                    }
                }

                Rectangle {
                    visible: root.panelState && root.panelState.installing
                    width: parent.width
                    height: 6
                    radius: 3
                    color: root.theme ? root.theme.withAlpha(root.theme.ui.minor, 0.22) : "transparent"

                    Rectangle {
                        width: parent.width * Math.max(0, Math.min(1, root.panelState ? root.panelState.installProgress : 0))
                        height: parent.height
                        radius: parent.radius
                        color: root.theme ? root.theme.ui.accent : "white"
                    }
                }

                NuguText {
                    visible: root.panelState && root.panelState.installing
                    theme: root.theme
                    variant: "meta"
                    font.pixelSize: 12
                    text: root.panelState ? root.panelState.installLabel : "downloading"
                    color: root.theme ? root.theme.ui.minor : "white"
                }
            }
        }
    }

    Column {
        width: parent.width
        spacing: 7

        NuguText {
            theme: root.theme
            variant: "title"
            font.pixelSize: 13
            text: "capture"
            color: root.theme ? root.theme.ui.accent : "white"
        }

        PopupCard {
            theme: root.theme
            padding: 0
            active: root.panelState && root.panelState.listening
            width: parent.width
            height: 58

            Row {
                anchors.centerIn: parent
                spacing: 5
                height: 36

                Repeater {
                    model: 31

                    Rectangle {
                        required property int index

                        anchors.verticalCenter: parent.verticalCenter
                        width: 4
                        height: Math.max(5, parent.height * root.waveLevel(index))
                        radius: 2
                        color: root.theme ? (root.panelState && root.panelState.listening ? root.theme.ui.accent : root.theme.ui.minor) : "white"
                        opacity: root.panelState && root.panelState.ready ? 0.86 : 0.3

                        Behavior on height {
                            NumberAnimation {
                                duration: root.theme && root.theme.lowPowerMode ? 0 : 120
                                easing.type: Easing.OutCubic
                            }
                        }
                    }
                }
            }
        }

        NokoButton {
            theme: root.theme
            label: root.panelState && root.panelState.listening ? "stop capture" : (root.panelState && root.panelState.transcribing ? "transcribing…" : "start capture")
            kind: root.panelState && root.panelState.listening ? "primary" : "ghost"
            enabled: root.panelState && root.panelState.ready && !root.panelState.transcribing
            buttonWidth: parent.width
            onClicked: if (root.panelState)
                root.panelState.trigger()
        }
    }

    PopupCard {
        theme: root.theme
        padding: 0
        width: parent.width
        height: Math.max(150, root.height - y)

        Column {
            anchors.fill: parent
            anchors.margins: 12
            spacing: 7

            NuguText {
                theme: root.theme
                variant: "title"
                font.pixelSize: 13
                text: "last transcripts"
                color: root.theme ? root.theme.ui.accent : "white"
            }

            Item {
                width: parent.width
                height: Math.max(1, parent.height - y)
                clip: true

                Flickable {
                    id: historyFlick

                    anchors.fill: parent
                    anchors.rightMargin: contentHeight > height ? 8 : 0
                    clip: true
                    contentWidth: width
                    contentHeight: historyColumn.implicitHeight
                    boundsBehavior: Flickable.StopAtBounds
                    interactive: contentHeight > height

                    Column {
                        id: historyColumn
                        width: historyFlick.width
                        spacing: 7

                        Repeater {
                            model: root.panelState ? root.panelState.historyEntries : []

                            delegate: PopupCard {
                                id: transcriptEntry

                                required property int index
                                required property var modelData
                                readonly property string transcript: String(modelData.transcript || "")
                                readonly property bool copied: root.copiedIndex === index

                                theme: root.theme
                                padding: 0
                                width: historyColumn.width
                                height: entryColumn.implicitHeight + 10

                                Column {
                                    id: entryColumn

                                    anchors.left: parent.left
                                    anchors.right: copyButton.left
                                    anchors.top: parent.top
                                    anchors.margins: 8
                                    spacing: 3

                                    NuguText {
                                        theme: root.theme
                                        width: parent.width
                                        wrapMode: Text.WordWrap
                                        text: transcriptEntry.transcript
                                        color: root.theme ? root.theme.content.normal : "white"
                                    }

                                    NuguText {
                                        theme: root.theme
                                        variant: "meta"
                                        width: parent.width
                                        text: transcriptEntry.modelData.duration !== undefined ? `${transcriptEntry.modelData.duration}s` : "local history"
                                        color: root.theme ? root.theme.ui.minor : "white"
                                    }
                                }

                                PopupButton {
                                    id: copyButton

                                    anchors.right: parent.right
                                    anchors.top: parent.top
                                    anchors.margins: 8
                                    theme: root.theme
                                    width: 58
                                    height: 26
                                    text: transcriptEntry.copied ? "copied" : "copy"
                                    checked: transcriptEntry.copied
                                    onControlFocusedChanged: if (controlFocused)
                                        Util.revealIn(copyButton, historyFlick)
                                    onClicked: if (transcriptEntry.transcript.length > 0) {
                                        Quickshell.execDetached(["wl-copy", transcriptEntry.transcript]);
                                        root.copiedIndex = transcriptEntry.index;
                                        copyFeedbackTimer.restart();
                                    }
                                }
                            }
                        }

                        NuguText {
                            visible: !root.panelState || root.panelState.historyEntries.length === 0
                            theme: root.theme
                            width: parent.width
                            text: "no captures yet"
                            color: root.theme ? root.theme.content.normal : "white"
                        }
                    }
                }

                Rectangle {
                    visible: historyFlick.contentHeight > historyFlick.height
                    anchors.top: parent.top
                    anchors.bottom: parent.bottom
                    anchors.right: parent.right
                    width: 3
                    radius: 2
                    color: root.theme ? root.theme.withAlpha(root.theme.ui.minor, 0.14) : "transparent"

                    Rectangle {
                        width: parent.width
                        height: Math.max(18, parent.height * historyFlick["visibleArea"]["heightRatio"])
                        y: Math.max(0, Math.min(parent.height - height, parent.height * historyFlick["visibleArea"]["yPosition"]))
                        radius: parent.radius
                        color: root.theme ? root.theme.withAlpha(root.theme.ui.accent, 0.55) : "white"
                    }
                }
            }
        }
    }
}