Add the four components the Focus commit referenced but forgot to ship
Claude-Session: https://claude.ai/code/session_01Ms2FbjQy31TVf3CEvQhGM8
This commit is contained in:
@@ -0,0 +1,297 @@
|
||||
// One application's notification rule: a line you can read, unfolding in place
|
||||
// into the four controls that make up the rule.
|
||||
//
|
||||
// The head is hand-built rather than a SettingRow because it needs two things
|
||||
// SettingRow has no room for -- the application's own icon, and a subtitle
|
||||
// whose second half is coloured when the rule is no longer the default, so a
|
||||
// muted or redirected application is obvious without opening it.
|
||||
//
|
||||
// Every write leaves through `edited`/`forgotten`. The page owns the service
|
||||
// calls, so a rule changed here goes down exactly the same path as one changed
|
||||
// anywhere else.
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Widgets
|
||||
import qs.config
|
||||
|
||||
Column {
|
||||
id: root
|
||||
|
||||
required property string appId
|
||||
required property string appName
|
||||
|
||||
// Notifs.appRule(appId), passed in as a binding so a rule changed elsewhere
|
||||
// still reaches this row.
|
||||
required property var rule
|
||||
|
||||
// Relative last-seen ("Today", "3 days ago"), or "" when nothing has been
|
||||
// recorded for this application yet.
|
||||
property string lastSeen: ""
|
||||
|
||||
// The live-resolved icon name from Notifs.applications. Falls back to the
|
||||
// one cached on the rule, which is what a no-longer-installed application
|
||||
// still has.
|
||||
property string iconName: ""
|
||||
|
||||
property bool expanded: false
|
||||
property bool divider: true
|
||||
|
||||
signal activated
|
||||
signal edited(var patch)
|
||||
signal forgotten
|
||||
|
||||
// What is different from the defaults, in the page's own words. Empty when
|
||||
// the rule is untouched, which is what puts the raw id in the subtitle
|
||||
// instead -- an honest identity beats an empty half-line.
|
||||
readonly property string stateSummary: {
|
||||
const parts = [];
|
||||
if (root.rule.enabled === false)
|
||||
parts.push("Notifications off");
|
||||
if (root.rule.display === "history")
|
||||
parts.push("History only");
|
||||
if (root.rule.sound === false)
|
||||
parts.push("sound off");
|
||||
if (root.rule.urgency === "low")
|
||||
parts.push("treat as low");
|
||||
else if (root.rule.urgency === "critical")
|
||||
parts.push("treat as critical");
|
||||
return parts.join(" · ");
|
||||
}
|
||||
|
||||
// Nothing resolvable leaves this empty, and the letter tile below shows
|
||||
// through instead of a broken image.
|
||||
readonly property string iconSource: {
|
||||
const name = root.iconName !== "" ? root.iconName : String(root.rule.icon ?? "");
|
||||
return name === "" ? "" : Quickshell.iconPath(name, true);
|
||||
}
|
||||
|
||||
// A stable colour per application, so the tile is a recognisable shape
|
||||
// rather than a different colour every time the list re-sorts.
|
||||
readonly property color tileColor: {
|
||||
const palette = [Theme.accent, Theme.teal, Theme.magenta, Theme.cyan,
|
||||
Theme.green, Theme.orange, Theme.pink];
|
||||
let hash = 0;
|
||||
for (let index = 0; index < root.appId.length; index++)
|
||||
hash = (hash * 31 + root.appId.charCodeAt(index)) % 9973;
|
||||
return palette[hash % palette.length];
|
||||
}
|
||||
|
||||
width: parent ? parent.width : 620
|
||||
spacing: 0
|
||||
|
||||
Item {
|
||||
id: head
|
||||
|
||||
width: parent.width
|
||||
implicitHeight: Math.max(56, copy.implicitHeight + 20)
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
anchors.bottomMargin: 1
|
||||
radius: 9
|
||||
z: -1
|
||||
visible: headHover.hovered
|
||||
color: Theme.alpha(Theme.fg, 0.05)
|
||||
border.width: 0
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: tile
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: 30
|
||||
height: 30
|
||||
radius: 8
|
||||
color: root.iconSource === "" ? root.tileColor : "transparent"
|
||||
border.width: 0
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
visible: root.iconSource === ""
|
||||
text: root.appName.length > 0 ? root.appName.charAt(0).toUpperCase() : "?"
|
||||
color: Theme.bgDark
|
||||
font.family: Theme.fontFamily
|
||||
font.pixelSize: Theme.fontSize
|
||||
font.weight: Font.Bold
|
||||
}
|
||||
|
||||
IconImage {
|
||||
anchors.centerIn: parent
|
||||
implicitSize: 24
|
||||
asynchronous: true
|
||||
visible: root.iconSource !== ""
|
||||
source: root.iconSource
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
id: copy
|
||||
|
||||
anchors.left: tile.right
|
||||
anchors.leftMargin: 12
|
||||
anchors.right: controls.left
|
||||
anchors.rightMargin: 16
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: 2
|
||||
|
||||
Text {
|
||||
width: parent.width
|
||||
text: root.appName
|
||||
color: Theme.fg
|
||||
font.family: Theme.fontFamily
|
||||
font.pixelSize: Theme.fontSize
|
||||
font.weight: Font.Medium
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: 0
|
||||
|
||||
Text {
|
||||
id: seenLabel
|
||||
visible: root.lastSeen !== ""
|
||||
width: visible ? implicitWidth : 0
|
||||
text: root.lastSeen + " · "
|
||||
color: Theme.fgDim
|
||||
font.family: Theme.fontFamily
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
}
|
||||
|
||||
Text {
|
||||
width: Math.max(0, parent.width - seenLabel.width)
|
||||
text: root.stateSummary === "" ? root.appId : root.stateSummary
|
||||
color: root.stateSummary === "" ? Theme.fgDim : Theme.warn
|
||||
font.family: Theme.fontFamily
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
id: controls
|
||||
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: 11
|
||||
|
||||
SettingsToggle {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
checked: root.rule.enabled !== false
|
||||
onToggled: value => root.edited({ enabled: value })
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: root.expanded ? "▴" : "▾"
|
||||
color: Theme.fgMuted
|
||||
font.family: Theme.fontFamily
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.left: copy.left
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
height: 1
|
||||
visible: root.divider && !root.expanded
|
||||
color: Theme.alpha(Theme.fg, 0.065)
|
||||
}
|
||||
|
||||
HoverHandler {
|
||||
id: headHover
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
}
|
||||
|
||||
// The whole row is the expand target except the trailing controls: a tap
|
||||
// meant for the switch must not also unfold the row underneath it.
|
||||
TapHandler {
|
||||
onTapped: eventPoint => {
|
||||
if (eventPoint.position.x >= controls.x)
|
||||
return;
|
||||
root.activated();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Indented under the head, so the controls read as belonging to the
|
||||
// application above them rather than to the card.
|
||||
Column {
|
||||
id: body
|
||||
|
||||
x: 42
|
||||
width: Math.max(0, parent.width - 42)
|
||||
visible: root.expanded
|
||||
|
||||
SettingRow {
|
||||
width: parent.width
|
||||
label: "Play sound"
|
||||
detail: "The notification chime, for this application only"
|
||||
controlWidth: 48
|
||||
|
||||
SettingsToggle {
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
checked: root.rule.sound !== false
|
||||
onToggled: value => root.edited({ sound: value })
|
||||
}
|
||||
}
|
||||
|
||||
OptionPickerRow {
|
||||
width: parent.width
|
||||
label: "Show as"
|
||||
options: [
|
||||
{
|
||||
value: "banners",
|
||||
label: "Banners & history",
|
||||
detail: "A banner on arrival, kept in the notification center afterwards"
|
||||
},
|
||||
{
|
||||
value: "history",
|
||||
label: "History only",
|
||||
detail: "Filed in the notification center with no banner and no sound"
|
||||
}
|
||||
]
|
||||
current: String(root.rule.display ?? "banners")
|
||||
onPicked: value => root.edited({ display: value })
|
||||
}
|
||||
|
||||
OptionPickerRow {
|
||||
width: parent.width
|
||||
label: "Urgency"
|
||||
detail: "Override what the app claims its notifications are"
|
||||
options: [
|
||||
{
|
||||
value: "auto",
|
||||
label: "App decides",
|
||||
detail: "Whatever urgency the notification arrives with"
|
||||
},
|
||||
{
|
||||
value: "low",
|
||||
label: "Treat as low",
|
||||
detail: "Never chimes, and never breaks through Do Not Disturb"
|
||||
},
|
||||
{
|
||||
value: "critical",
|
||||
label: "Treat as critical",
|
||||
detail: "Marked urgent, and kept up for the critical duration"
|
||||
}
|
||||
]
|
||||
current: String(root.rule.urgency ?? "auto")
|
||||
onPicked: value => root.edited({ urgency: value })
|
||||
}
|
||||
|
||||
ActionRow {
|
||||
width: parent.width
|
||||
label: "Forget this app"
|
||||
detail: "Remove its rule; it returns on its next notification"
|
||||
action: "Forget"
|
||||
divider: false
|
||||
onTriggered: root.forgotten()
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user