544 lines
23 KiB
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()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|