lock: notifications card + NotifyEvents fan-out seam, battery fraction fix
This commit is contained in:
parent
5083954c38
commit
90077bb1ea
8 changed files with 207 additions and 7 deletions
|
|
@ -40,6 +40,7 @@ services/SessionEvents.qml souveraine/services/SessionEvents.qml
|
|||
services/SessiondBridge.qml souveraine/services/SessiondBridge.qml
|
||||
services/StepUpAuth.qml souveraine/services/StepUpAuth.qml
|
||||
services/SessionAudit.qml souveraine/services/SessionAudit.qml
|
||||
services/NotifyEvents.qml souveraine/services/NotifyEvents.qml
|
||||
modules/ii/sidebarLeft/SidebarLeft.qml souveraine/modules/ii/sidebarLeft/SidebarLeft.qml
|
||||
modules/ii/sidebarLeft/AiChat.qml souveraine/modules/ii/sidebarLeft/AiChat.qml
|
||||
modules/ii/sidebarRight/SidebarRight.qml souveraine/modules/ii/sidebarRight/SidebarRight.qml
|
||||
|
|
@ -75,6 +76,7 @@ modules/ii/lock/Lock.qml souveraine/modules/ii/lock/Lock.qml
|
|||
modules/ii/lock/TouchLockSurface.qml souveraine/modules/ii/lock/TouchLockSurface.qml
|
||||
modules/souveraine/lock/LockMediaCard.qml souveraine/modules/souveraine/lock/LockMediaCard.qml
|
||||
modules/souveraine/lock/LockAgentCard.qml souveraine/modules/souveraine/lock/LockAgentCard.qml
|
||||
modules/souveraine/lock/LockNotifyCard.qml souveraine/modules/souveraine/lock/LockNotifyCard.qml
|
||||
modules/souveraine/lock/LockSurfaceHost.qml souveraine/modules/souveraine/lock/LockSurfaceHost.qml
|
||||
modules/souveraine/lock/qmldir souveraine/modules/souveraine/lock/qmldir
|
||||
modules/souveraine/navigation/SystemGestureRail.qml souveraine/modules/souveraine/navigation/SystemGestureRail.qml
|
||||
|
|
|
|||
|
|
@ -469,6 +469,10 @@ Singleton {
|
|||
property bool showMediaControls: true
|
||||
property bool mediaMetadataAmbient: false
|
||||
property bool showBattery: true
|
||||
// App identity + count are ambient; the summary line is
|
||||
// personal until promoted. Bodies never render on lock.
|
||||
property bool showNotifications: true
|
||||
property bool notificationContentAmbient: false
|
||||
}
|
||||
property JsonObject idle: JsonObject {
|
||||
// Native idle-notify remains opt-in until verified on the
|
||||
|
|
|
|||
|
|
@ -227,6 +227,27 @@ ContentPage {
|
|||
checked: Config.options.lock.content.showBattery
|
||||
onCheckedChanged: Config.options.lock.content.showBattery = checked
|
||||
}
|
||||
|
||||
ConfigSwitch {
|
||||
buttonIcon: "notifications"
|
||||
text: Translation.tr("Show notifications while locked")
|
||||
checked: Config.options.lock.content.showNotifications
|
||||
onCheckedChanged: Config.options.lock.content.showNotifications = checked
|
||||
StyledToolTip {
|
||||
text: Translation.tr("Shows which apps had notifications arrive and how many. Content stays private unless enabled below.")
|
||||
}
|
||||
}
|
||||
|
||||
ConfigSwitch {
|
||||
buttonIcon: "visibility"
|
||||
text: Translation.tr("Show notification summaries")
|
||||
checked: Config.options.lock.content.notificationContentAmbient
|
||||
enabled: Config.options.lock.content.showNotifications
|
||||
onCheckedChanged: Config.options.lock.content.notificationContentAmbient = checked
|
||||
StyledToolTip {
|
||||
text: Translation.tr("Treats the notification title line as ambient. Bodies never show on the lock screen.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ContentSection {
|
||||
|
|
|
|||
116
surfaces/quickshell/modules/souveraine/lock/LockNotifyCard.qml
Normal file
116
surfaces/quickshell/modules/souveraine/lock/LockNotifyCard.qml
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
// Ambient "notifications arrived while locked" card. Same posture as
|
||||
// LockAgentCard: the lock surface announces that something landed, the
|
||||
// content waits for unlock. App identity and count are ambient; the summary
|
||||
// line is personal and renders only when promoted
|
||||
// (lock.content.notificationContentAmbient). Bodies never render here.
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import qs
|
||||
import qs.services
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
// Newest-first, only what arrived during this lock. Transient
|
||||
// notifications are excluded — they asked not to persist.
|
||||
property var entries: []
|
||||
|
||||
implicitHeight: card.visible ? card.implicitHeight : 0
|
||||
visible: LockContentPolicy.notificationsVisible && entries.length > 0
|
||||
|
||||
Connections {
|
||||
target: NotifyEvents
|
||||
function onLanded(evt) {
|
||||
if (!GlobalStates.screenLocked || evt.isTransient) return;
|
||||
root.entries = [evt, ...root.entries].slice(0, 16);
|
||||
}
|
||||
}
|
||||
Connections {
|
||||
target: GlobalStates
|
||||
// Fresh slate each lock cycle; once unlocked the shell's own
|
||||
// surfaces own notification history.
|
||||
function onScreenLockedChanged() { root.entries = []; }
|
||||
}
|
||||
|
||||
// Grouped by app, newest app first: [{ app, count, latest }]
|
||||
readonly property var groups: {
|
||||
const byApp = {};
|
||||
const order = [];
|
||||
for (const e of entries) {
|
||||
const key = e.app || qsTr("App");
|
||||
if (!byApp[key]) {
|
||||
byApp[key] = { app: key, count: 0, latest: e };
|
||||
order.push(key);
|
||||
}
|
||||
byApp[key].count++;
|
||||
}
|
||||
return order.map(k => byApp[k]);
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: card
|
||||
anchors.fill: parent
|
||||
visible: root.entries.length > 0
|
||||
radius: 16
|
||||
color: "#22000000"
|
||||
border.color: "#33ffffff"
|
||||
border.width: 1
|
||||
implicitHeight: layout.implicitHeight + 24
|
||||
|
||||
ColumnLayout {
|
||||
id: layout
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.margins: 12
|
||||
spacing: 4
|
||||
|
||||
Text {
|
||||
Layout.fillWidth: true
|
||||
text: qsTr("Arrived while locked")
|
||||
color: "#ccffffff"
|
||||
font.pixelSize: 12
|
||||
font.weight: Font.DemiBold
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: root.groups.slice(0, 4)
|
||||
delegate: ColumnLayout {
|
||||
required property var modelData
|
||||
Layout.fillWidth: true
|
||||
spacing: 1
|
||||
|
||||
Text {
|
||||
Layout.fillWidth: true
|
||||
text: modelData.count > 1
|
||||
? modelData.app + " · " + modelData.count
|
||||
: modelData.app
|
||||
color: "#e6ffffff"
|
||||
font.pixelSize: 14
|
||||
elide: Text.ElideRight
|
||||
maximumLineCount: 1
|
||||
}
|
||||
|
||||
Text {
|
||||
Layout.fillWidth: true
|
||||
visible: LockContentPolicy.notificationContentVisible
|
||||
&& (modelData.latest.summary ?? "").length > 0
|
||||
text: "• " + modelData.latest.summary
|
||||
color: "#b3ffffff"
|
||||
font.pixelSize: 13
|
||||
elide: Text.ElideRight
|
||||
maximumLineCount: 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
visible: root.groups.length > 4
|
||||
text: qsTr("+%1 more").arg(root.groups.length - 4)
|
||||
color: "#99ffffff"
|
||||
font.pixelSize: 12
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -56,19 +56,22 @@ Item {
|
|||
visible: LockContentPolicy.batteryVisible && UPower.displayDevice?.isPresent
|
||||
text: {
|
||||
const dev = UPower.displayDevice;
|
||||
const pct = Math.round(dev?.percentage ?? 0);
|
||||
// UPowerDevice.percentage is a 0–1 fraction (TouchLockSurface
|
||||
// multiplies too) — rounding it raw is what rendered the
|
||||
// eternal "Charging 0%" here.
|
||||
const pct = Math.round((dev?.percentage ?? 0) * 100);
|
||||
// State is authoritative — don't infer "Charging" from
|
||||
// onBattery alone. onBattery is false for fully-charged,
|
||||
// pending-charge, and unknown, which is what made the line
|
||||
// read "Charging N%" forever on a topped-off pack. The
|
||||
// recent driver change also leaves energy-full unreliable,
|
||||
// so a stale percentage can read ~0%; never call a
|
||||
// fully-charged pack "1%".
|
||||
// pending-charge, and unknown, which would read "Charging N%"
|
||||
// forever on a topped-off pack.
|
||||
switch (dev?.state ?? UPowerDeviceState.Unknown) {
|
||||
case UPowerDeviceState.FullyCharged:
|
||||
return "Full " + pct + "%";
|
||||
case UPowerDeviceState.Charging:
|
||||
return "Charging " + pct + "%";
|
||||
// Right after a driver rebind the pack can briefly
|
||||
// report 0% while charging. Drop the number rather
|
||||
// than lie.
|
||||
return pct > 0 ? "Charging " + pct + "%" : "Charging";
|
||||
case UPowerDeviceState.Discharging:
|
||||
return "Battery " + pct + "%";
|
||||
default:
|
||||
|
|
@ -92,5 +95,11 @@ Item {
|
|||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
LockNotifyCard {
|
||||
Layout.topMargin: root.compact ? 6 : 10
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,3 +2,4 @@ module qs.modules.souveraine.lock
|
|||
LockMediaCard 1.0 LockMediaCard.qml
|
||||
LockSurfaceHost 1.0 LockSurfaceHost.qml
|
||||
LockAgentCard 1.0 LockAgentCard.qml
|
||||
LockNotifyCard 1.0 LockNotifyCard.qml
|
||||
|
|
|
|||
|
|
@ -24,4 +24,8 @@ Singleton {
|
|||
readonly property bool mediaMetadataVisible: root.allowsOnLock(
|
||||
root.personal, Config.options.lock.content.mediaMetadataAmbient)
|
||||
readonly property bool batteryVisible: Config.options.lock.content.showBattery
|
||||
|
||||
readonly property bool notificationsVisible: Config.options.lock.content.showNotifications
|
||||
readonly property bool notificationContentVisible: root.allowsOnLock(
|
||||
root.personal, Config.options.lock.content.notificationContentAmbient)
|
||||
}
|
||||
|
|
|
|||
43
surfaces/quickshell/services/NotifyEvents.qml
Normal file
43
surfaces/quickshell/services/NotifyEvents.qml
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
// Notification event fan-out. The freedesktop server itself lives in ii's
|
||||
// Notifications.qml (it owns org.freedesktop.Notifications on the bus); this
|
||||
// singleton is the Souveraine-side seam everything else attaches to when a
|
||||
// notification lands: the lock glance card today, crash-report surfacing
|
||||
// (TASK-05) and agent message events (queue 5a/b) later.
|
||||
//
|
||||
// Deliberately NOT here: any decision about what the agent may see. That is
|
||||
// capability-gate territory (queue 5b) — this file is only the wire. Events
|
||||
// are plain objects, not Notif wrappers, so consumers can't reach actions or
|
||||
// dismissal through this path.
|
||||
pragma Singleton
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
// Newest-first ring of recent events: { app, summary, body, urgency,
|
||||
// isTransient, time }. Session-scoped; ii already persists the full
|
||||
// notification list to disk, this ring is for reactive consumers.
|
||||
property var recent: []
|
||||
readonly property int capacity: 32
|
||||
|
||||
// Fired once per incoming notification, after `recent` is updated.
|
||||
signal landed(var evt)
|
||||
|
||||
Connections {
|
||||
target: Notifications
|
||||
function onNotify(notif) {
|
||||
const evt = {
|
||||
app: notif.appName,
|
||||
summary: notif.summary,
|
||||
body: notif.body,
|
||||
urgency: notif.urgency,
|
||||
isTransient: notif.isTransient,
|
||||
time: notif.time,
|
||||
};
|
||||
root.recent = [evt, ...root.recent].slice(0, root.capacity);
|
||||
root.landed(evt);
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue