Files
quickdots/polkit/Polkit.qml

544 lines
23 KiB
QML

import QtQuick
import Quickshell
import Quickshell.Wayland
import Quickshell.Services.Polkit
import "../config" as Cfg
import "../components"
Item {
id: root
readonly property var t: Cfg.Config.theme
readonly property var cfg: Cfg.Config
property bool interactionAvailable: false
property bool windowShouldExist: false
PolkitAgent {
id: agent
onAuthenticationRequestStarted: {
root.windowShouldExist = true
root.interactionAvailable = true
resetInput()
}
}
readonly property var flow: agent.flow ?? null
Connections {
target: flow
function onAuthenticationFailed() {
root.interactionAvailable = true
resetInput()
}
}
function submitAuth() {
if (!root.interactionAvailable) return
if (flow?.isResponseRequired) {
root.interactionAvailable = false
flow.submit(windowLoader.item?.passwordText ?? "")
windowLoader.item?.clearPassword()
}
}
function resetInput() {
windowLoader.item?.resetInput()
}
Loader {
id: windowLoader
active: root.windowShouldExist
sourceComponent: Component {
PanelWindow {
id: overlay
property bool exiting: false
property bool authFailed: false
property var displayFlow: null
function captureFlowSnapshot(flow) {
if (!flow) return null
return {
message: flow.message,
actionId: flow.actionId,
supplementaryMessage: flow.supplementaryMessage,
supplementaryIsError: flow.supplementaryIsError,
identities: flow.identities?.slice() ?? [],
selectedIdentity: flow.selectedIdentity,
isResponseRequired: flow.isResponseRequired,
inputPrompt: flow.inputPrompt,
responseVisible: flow.responseVisible
}
}
readonly property string passwordText: passwordInput.text
function clearPassword() { passwordInput.clear() }
function resetInput() {
passwordInput.clear()
passwordInput.forceActiveFocus()
}
visible: true
anchors { top: true; bottom: true; left: true; right: true }
color: "transparent"
WlrLayershell.layer: WlrLayer.Overlay
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive
WlrLayershell.namespace: "main-shell-polkit"
Timer {
id: exitDebounce
interval: 250
onTriggered: {
if (!agent.isActive && !overlay.exiting) {
overlay.exiting = true
exitAnim.start()
}
}
}
function cancelAndClose() {
if (overlay.exiting) return
overlay.exiting = true
exitAnim.start()
root.flow?.cancelAuthenticationRequest()
}
Rectangle {
anchors.fill: parent
color: t.polkitOverlayBg
}
component DialogButton: HoverButton {
property string label
property int labelWeight: Font.Normal
property color labelColor
signal tapped()
width: (parent.width - 12) / 2
height: 44
radius: 10
border.width: 1
onClicked: tapped()
Text {
anchors.centerIn: parent
text: label
color: labelColor
font.pixelSize: 13
font.weight: labelWeight
font.letterSpacing: 0.2
}
}
Rectangle {
id: card
property real shakeX: 0
x: Math.round((parent.width - width) / 2) + shakeX
y: Math.round((parent.height - height) / 2)
width: 440
height: content.implicitHeight + 64
radius: cfg.popupRadius
color: t.polkitCardBg
border.color: t.polkitCardBorder
border.width: 1
opacity: 0
scale: 0.86
transform: Scale {
yScale: 1
origin.y: card.height / 2
}
Component.onCompleted: {
overlay.displayFlow = agent.flow
enterAnim.start()
}
ParallelAnimation {
id: enterAnim
NumberAnimation {
target: card; property: "opacity"
from: 0; to: 1
duration: 260; easing.type: Easing.OutCubic
}
NumberAnimation {
target: card; property: "scale"
from: 0.86; to: 1.0
duration: 360; easing.type: Easing.OutBack; easing.overshoot: 0.6
}
NumberAnimation {
target: card.transform; property: "yScale"
from: 0; to: 1
duration: 360; easing.type: Easing.OutBack; easing.overshoot: 0.6
}
}
ParallelAnimation {
id: exitAnim
NumberAnimation {
target: card; property: "opacity"
from: 1; to: 0
duration: 200; easing.type: Easing.InCubic
}
NumberAnimation {
target: card; property: "scale"
from: 1.0; to: 0.88
duration: 260; easing.type: Easing.InBack
}
NumberAnimation {
target: card.transform; property: "yScale"
from: 1; to: 0
duration: 260; easing.type: Easing.InBack
}
onFinished: {
overlay.exiting = false
root.windowShouldExist = false
}
}
Connections {
target: agent
function onIsActiveChanged() {
if (agent.isActive) {
overlay.authFailed = false
overlay.displayFlow = agent.flow
exitDebounce.stop()
if (overlay.exiting) {
exitAnim.stop()
overlay.exiting = false
}
card.opacity = 0
card.scale = 0.86
card.transform.yScale = 0
enterAnim.restart()
} else {
if (overlay.displayFlow)
overlay.displayFlow = captureFlowSnapshot(overlay.displayFlow)
exitDebounce.restart()
}
}
}
Connections {
target: flow
function onAuthenticationFailed() {
overlay.authFailed = true
shakeAnim.restart()
}
}
SequentialAnimation {
id: shakeAnim
NumberAnimation { target: card; property: "shakeX"; to: 12; duration: 45; easing.type: Easing.InOutQuad }
NumberAnimation { target: card; property: "shakeX"; to: -10; duration: 45 }
NumberAnimation { target: card; property: "shakeX"; to: 8; duration: 45 }
NumberAnimation { target: card; property: "shakeX"; to: -5; duration: 45 }
NumberAnimation { target: card; property: "shakeX"; to: 3; duration: 45 }
NumberAnimation { target: card; property: "shakeX"; to: 0; duration: 45 }
}
Column {
id: content
anchors {
top: parent.top
left: parent.left
right: parent.right
topMargin: 32
leftMargin: 32
rightMargin: 32
}
spacing: 16
Item {
width: parent.width
height: 72
Rectangle {
anchors.centerIn: parent
width: 66
height: 66
radius: 33
color: t.polkitIconBg
border.color: t.polkitIconBorder
border.width: 1
Text {
anchors.centerIn: parent
text: "󰌆"
font.pixelSize: 28
color: t.polkitIconColor
Component.onCompleted: {
if (implicitWidth < 4) text = "🔒"
}
}
}
}
Text {
width: parent.width
horizontalAlignment: Text.AlignHCenter
text: "Authentication Required"
color: t.polkitTitle
font.pixelSize: 17
font.weight: Font.DemiBold
font.letterSpacing: 0.3
}
Text {
visible: !!displayFlow?.message
width: parent.width
wrapMode: Text.WordWrap
horizontalAlignment: Text.AlignHCenter
text: displayFlow?.message ?? ""
color: t.polkitMessage
font.pixelSize: 13
lineHeight: 1.50
}
Item {
visible: !!displayFlow?.actionId
width: parent.width
height: pillBg.height
Rectangle {
id: pillBg
anchors.horizontalCenter: parent.horizontalCenter
width: Math.min(pillText.implicitWidth + 22, parent.width)
height: pillText.implicitHeight + 10
radius: 6
color: t.polkitPillBg
Text {
id: pillText
anchors.centerIn: parent
width: parent.width - 22
elide: Text.ElideMiddle
text: displayFlow?.actionId ?? ""
color: t.polkitPillText
font.pixelSize: 11
font.family: "monospace"
}
}
}
Rectangle {
width: parent.width
height: 1
color: t.polkitDivider
}
Rectangle {
visible: overlay.authFailed || !!displayFlow?.supplementaryMessage
width: parent.width
height: suppRow.implicitHeight + 20
radius: 10
color: overlay.authFailed || displayFlow?.supplementaryIsError ? t.polkitSuppErrBg : t.polkitSuppOkBg
border.color: overlay.authFailed || displayFlow?.supplementaryIsError ? t.polkitSuppErrBorder : t.polkitSuppOkBorder
border.width: 1
Row {
id: suppRow
anchors {
top: parent.top; topMargin: 10
bottom: parent.bottom; bottomMargin: 10
left: parent.left; leftMargin: 14
right: parent.right; rightMargin: 14
}
spacing: 9
Text {
anchors.top: parent.top
text: "✕"
font.pixelSize: 12
font.weight: Font.Medium
lineHeight: 1.8
color: t.polkitSuppErrText
}
Text {
width: parent.width - 22
wrapMode: Text.WordWrap
text: overlay.authFailed ? "Authentication failed, please try again." : (displayFlow?.supplementaryMessage ?? "")
font.pixelSize: 12
lineHeight: 1.45
color: overlay.authFailed || displayFlow?.supplementaryIsError ? t.polkitSuppErrText : t.polkitSuppOkText
}
}
}
Text {
visible: true
width: parent.width
text: displayFlow?.inputPrompt ?? "Password"
color: t.polkitInputPrompt
font.pixelSize: 12
}
Rectangle {
visible: true
width: parent.width
height: 46
radius: 10
color: t.polkitFieldBg
border.color: passwordInput.activeFocus ? t.polkitFieldBorderFocus : t.polkitFieldBorder
border.width: passwordInput.activeFocus ? 1.5 : 1
Behavior on border.color { ColorAnimation { duration: 120 } }
Behavior on border.width { NumberAnimation { duration: 120 } }
TextInput {
id: passwordInput
enabled: root.interactionAvailable
anchors {
left: parent.left; leftMargin: 16
right: eyeBtn.left; rightMargin: 8
verticalCenter: parent.verticalCenter
}
property bool showPassword: false
echoMode: (displayFlow?.responseVisible || showPassword)
? TextInput.Normal : TextInput.Password
color: t.polkitInputText
selectionColor: t.polkitInputSelection
selectedTextColor: t.polkitInputSelectedText
font.pixelSize: 14
passwordCharacter: "•"
clip: true
Keys.onReturnPressed: root.submitAuth()
Keys.onEnterPressed: root.submitAuth()
Keys.onEscapePressed: overlay.cancelAndClose()
}
Text {
id: eyeBtn
anchors {
right: parent.right; rightMargin: 14
verticalCenter: parent.verticalCenter
}
text: passwordInput.showPassword ? "🙈" : "👁"
font.pixelSize: 20
color: eyeHover.hovered ? t.polkitEyeIconHover : t.polkitEyeIcon
Behavior on color { ColorAnimation { duration: 120 } }
HoverHandler { id: eyeHover }
MouseArea {
anchors.fill: parent
anchors.margins: -8
cursorShape: Qt.PointingHandCursor
onClicked: passwordInput.showPassword = !passwordInput.showPassword
}
}
}
Item { width: 1; height: 2 }
Row {
width: parent.width
spacing: 12
layoutDirection: Qt.RightToLeft
DialogButton {
label: "Authenticate"
labelWeight: Font.Medium
labelColor: t.polkitAuthText
idleColor: t.polkitAuthBg
hoverColor: t.polkitAuthBgHover
borderIdleColor: t.polkitAuthBg
borderHoverColor: t.polkitAuthBgHover
animDuration: 200
onTapped: root.submitAuth()
}
DialogButton {
label: "Cancel"
labelColor: t.polkitCancelText
idleColor: t.polkitCancelBg
hoverColor: t.polkitCancelBgHover
borderIdleColor: t.polkitCancelBorder
borderHoverColor: t.polkitCancelBgHover
animDuration: 200
onTapped: overlay.cancelAndClose()
}
}
}
Rectangle {
anchors.fill: parent
radius: cfg.popupRadius
color: Qt.rgba(
t.polkitCardBg.r,
t.polkitCardBg.g,
t.polkitCardBg.b, 0.78)
opacity: root.interactionAvailable ? 0 : 1
visible: opacity > 0
Behavior on opacity { NumberAnimation { duration: 180; easing.type: Easing.InOutSine } }
Canvas {
id: spinnerCanvas
anchors.centerIn: parent
width: 40
height: 40
property real angle: 0
NumberAnimation on angle {
running: !root.interactionAvailable
from: 0
to: 360
duration: 700
loops: Animation.Infinite
}
onAngleChanged: requestPaint()
onPaint: {
const ctx = getContext("2d")
ctx.clearRect(0, 0, width, height)
const cx = width / 2, cy = height / 2
const r = width / 2 - 3
ctx.strokeStyle = Qt.rgba(
t.polkitTitle.r, t.polkitTitle.g, t.polkitTitle.b, 0.12)
ctx.lineWidth = 3.5
ctx.beginPath()
ctx.arc(cx, cy, r, 0, 2 * Math.PI)
ctx.stroke()
const start = (angle - 90) * Math.PI / 180
ctx.strokeStyle = Qt.rgba(
t.polkitTitle.r, t.polkitTitle.g, t.polkitTitle.b, 0.88)
ctx.lineCap = "round"
ctx.beginPath()
ctx.arc(cx, cy, r, start, start + 1.3 * Math.PI)
ctx.stroke()
}
}
}
}
}
}
}
}