Bound the notification app list, and give Focus a real editor

Claude-Session: https://claude.ai/code/session_01Ms2FbjQy31TVf3CEvQhGM8
This commit is contained in:
Gabriel Brown
2026-08-24 14:25:26 -04:00
parent 07db1068f1
commit d5b6e62515
21 changed files with 1405 additions and 453 deletions
@@ -56,6 +56,11 @@ Rectangle {
// Critical notifications get a red edge rather than a color wash, so the
// text contrast never changes.
//
// Read through Notifs.effectiveUrgency rather than off the notification, so
// an application the rules treat as critical is marked here too -- and one
// demoted to low is not. The bell, the popup timeout and the Do Not Disturb
// breakthrough all ask the same question the same way.
Rectangle {
anchors.left: parent.left
anchors.top: parent.top
@@ -65,7 +70,7 @@ Rectangle {
radius: 1.5
border.width: 0
color: Theme.urgent
visible: root.notification.urgency === NotificationUrgency.Critical
visible: Notifs.effectiveUrgency(root.notification) === NotificationUrgency.Critical
}
readonly property var defaultAction: {
@@ -123,13 +123,13 @@ SettingsPage {
detail: {
const mode = FocusModes.modes.find(entry => entry.id === "gaming");
if (!mode)
return "Handled by a focus mode on Notifications & Focus.";
return "Handled by a focus mode on the Focus tab.";
return mode.enabled === true
? "Handled by the Gaming focus mode, which is on."
: "The Gaming focus mode is off, so notifications are not silenced.";
}
action: "Open Focus"
onTriggered: ShellState.settingsPage = "notifications"
onTriggered: ShellState.openSettings("focus")
}
ToggleRow {
@@ -1,62 +1,219 @@
// The notifications tab: what interrupts, how long it stays, and which
// applications are allowed to do it.
//
// The application list used to be every application that had ever notified,
// rendered in full, forever. On a machine that has been running for a year that
// is a wall nobody reads. It is now bounded: recent senders and applications
// whose rule has been changed sit up top, and everything else waits behind a
// searchable expander. Rows unfold in place into the rule itself.
//
// Focus modes moved to their own tab. What stays here is the one line saying a
// mode is currently quieting things, because that is the question this page is
// opened to answer.
import QtQuick
import qs.config
import qs.services
import qs.modules.clipboard
SettingsPage {
id: root
// Only one application is expanded at a time; the point is a card you
// can read, not twenty open at once.
// One application is unfolded at a time; the point is a card you can read,
// not twenty open at once.
property string expandedApp: ""
property bool allAppsOpen: false
// Which focus mode is open for editing. One at a time, like the app rules.
property string expandedMode: ""
readonly property int weekMs: 7 * 24 * 60 * 60 * 1000
// Which mode's exception list is open. Separate from expandedMode so the
// list does not unfold every time a mode is opened to change a switch.
property string expandedAllow: ""
function toggleAllowed(mode: var, appId: string, allowed: bool): void {
const current = Array.isArray(mode.allow) ? mode.allow.map(String) : [];
const at = current.indexOf(appId);
if (allowed && at < 0)
current.push(appId);
else if (!allowed && at >= 0)
current.splice(at, 1);
FocusModes.update(String(mode.id), { allow: current });
// Every rule write leaves through here, so a change made in a row goes down
// exactly the same path as one made anywhere else.
function editRule(appId: string, patch: var): void {
Notifs.setAppRule(appId, patch);
}
// Schedule edits go through the mode's trigger list rather than replacing
// it, so a mode that also has a game or fullscreen trigger keeps it.
function reschedule(mode: var, changes: var): void {
FocusModes.update(String(mode.id), {
triggers: (mode.triggers ?? []).map(trigger =>
trigger.kind === "schedule" ? Object.assign({}, trigger, changes) : trigger)
});
function forgetRule(appId: string): void {
if (root.expandedApp === appId)
root.expandedApp = "";
Notifs.forgetApp(appId);
}
function toggleDay(mode: var, day: int): void {
const schedule = (mode.triggers ?? []).find(trigger => trigger.kind === "schedule");
// A rule that differs from the defaults in any field. This is what earns an
// application a place above the fold even when it has not notified lately.
function customized(rule: var): bool {
return rule.enabled === false
|| rule.sound === false
|| String(rule.display ?? "banners") !== "banners"
|| String(rule.urgency ?? "auto") !== "auto";
}
// Relative rather than a timestamp: "three days ago" is the question being
// asked, and a clock time from last March answers a different one.
function lastSeenText(lastSeenMs: var): string {
const at = Number(lastSeenMs ?? 0);
if (!at)
return "";
const days = Math.floor((Date.now() - at) / 86400000);
if (days <= 0)
return "Today";
if (days === 1)
return "Yesterday";
if (days < 7)
return days + " days ago";
return Qt.formatDateTime(new Date(at), "d MMM");
}
// Spelled the same way the Focus tab spells it, so the two pages describe
// one schedule in one voice.
function daysText(days: var): string {
const list = Array.isArray(days) ? days.map(Number).slice().sort((a, b) => a - b) : [];
if (list.length === 0)
return "no days";
if (list.length === 7)
return "every day";
if (list.join(",") === "1,2,3,4,5")
return "weekdays";
if (list.join(",") === "0,6")
return "weekends";
const names = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
return list.map(day => names[day]).join(", ");
}
// Quiet hours are the Sleep mode's schedule. Rather than inventing a second
// place to set them, the row states what Sleep is actually doing and hands
// over to the tab that owns it.
function quietHoursDetail(): string {
const sleep = FocusModes.modes.find(mode => String(mode.id) === "sleep");
if (!sleep)
return "There is no Sleep focus mode, so nothing is scheduled";
const schedule = (sleep.triggers ?? []).find(trigger => trigger.kind === "schedule");
if (!schedule)
return;
const days = Array.isArray(schedule.days) ? schedule.days.slice() : [];
const at = days.indexOf(day);
if (at >= 0)
days.splice(at, 1);
else
days.push(day);
days.sort((a, b) => a - b);
root.reschedule(mode, { days: days });
return "The Sleep focus mode has no schedule, so quiet hours are not scheduled";
const when = String(schedule.start ?? "") + " to " + String(schedule.end ?? "")
+ ", " + root.daysText(schedule.days);
return sleep.enabled === true
? "Scheduled through the Sleep focus mode — " + when
: "The Sleep focus mode is off; it would run " + when;
}
title: "Notifications & Focus"
readonly property var recentApps: {
const now = Date.now();
return Notifs.applications
.filter(app => Number(app.lastSeenMs ?? 0) > 0
&& now - Number(app.lastSeenMs) < root.weekMs)
.sort((a, b) => Number(b.lastSeenMs) - Number(a.lastSeenMs));
}
// Sections are partitioned by id rather than by object identity: these are
// plain objects rebuilt by a binding, and an application appearing in two
// sections at once is the failure that would cause.
readonly property var recentIds: root.recentApps.map(app => String(app.id))
readonly property var customizedApps: Notifs.applications.filter(app =>
root.recentIds.indexOf(String(app.id)) < 0 && root.customized(app.rule))
readonly property var customizedIds: root.customizedApps.map(app => String(app.id))
// Everything not already on screen. Notifs.applications arrives sorted by
// name, so this stays alphabetical for free.
readonly property var otherApps: Notifs.applications.filter(app =>
root.recentIds.indexOf(String(app.id)) < 0
&& root.customizedIds.indexOf(String(app.id)) < 0)
// With nothing typed the expander holds what is not shown above. A typed
// query searches the whole universe instead -- a search box that cannot
// find an application you can see is worse than a duplicated row.
readonly property var expanderApps: {
const needle = appSearch.text.trim().toLowerCase();
if (needle === "")
return root.otherApps;
return Notifs.applications.filter(app =>
(String(app.name) + " " + String(app.id)).toLowerCase().indexOf(needle) >= 0);
}
readonly property string allowSummary: {
const allow = FocusModes.allowedApps;
if (allow.length === 0)
return "nothing may interrupt";
const names = allow.map(id => {
const app = Notifs.applications.find(entry => String(entry.id) === id);
return app ? String(app.name) : id;
});
if (names.length <= 2)
return names.join(" and ") + " may interrupt";
return names.slice(0, 2).join(", ") + " and " + (names.length - 2)
+ " more may interrupt";
}
title: "Notifications"
lede: "Control interruptions without losing useful history."
// Why the machine is quiet, at the top, where the question is asked.
Rectangle {
id: activeBanner
width: parent.width
visible: FocusModes.activeMode !== null
height: visible ? Math.max(58, bannerCopy.implicitHeight + 24) : 0
radius: Theme.cardRadius + 1
color: Theme.alpha(Theme.ok, 0.08)
border.width: 1
border.color: Theme.alpha(Theme.ok, 0.3)
Column {
id: bannerCopy
anchors.left: parent.left
anchors.leftMargin: 15
anchors.right: bannerButton.left
anchors.rightMargin: 14
anchors.verticalCenter: parent.verticalCenter
spacing: 3
Text {
width: parent.width
text: FocusModes.activeMode?.silence === true
? FocusModes.activeName + " is quieting notifications"
: FocusModes.activeName + " is on"
color: Theme.fg
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSize
font.weight: Font.DemiBold
elide: Text.ElideRight
}
Text {
width: parent.width
text: {
const why = "Because " + FocusModes.activeReason;
return FocusModes.activeMode?.silence === true
? why + " · " + root.allowSummary
: why + " · notifications are unaffected";
}
color: Theme.fgDim
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeSmall
wrapMode: Text.WordWrap
}
}
SettingsButton {
id: bannerButton
anchors.right: parent.right
anchors.rightMargin: 14
anchors.verticalCenter: parent.verticalCenter
text: "Open Focus"
onClicked: ShellState.openSettings("focus")
}
}
SettingsCard {
title: "Notifications"
title: "Quiet"
SettingRow {
label: "Do Not Disturb"
detail: "Keep notifications in the center but suppress banners"
detail: "Banners stop; everything still lands in the notification center"
controlWidth: 48
SettingsToggle {
@@ -67,37 +224,43 @@ SettingsPage {
}
}
TextRow {
label: "Notification history"
detail: "Live notifications retained by the shell"
value: Notifs.history.length === 1 ? "1 item" : `${Notifs.history.length} items`
}
ToggleRow { setting: "criticalBreaksThrough" }
ActionRow {
label: "Clear notification history"
detail: "Dismiss every item currently in the notification center"
label: "Quiet hours"
detail: root.quietHoursDetail()
action: "Open Focus"
divider: false
action: "Clear all"
enabled: Notifs.history.length > 0
onTriggered: Notifs.dismissAll()
onTriggered: ShellState.openSettings("focus")
}
}
SettingsCard {
title: "Banner behavior"
title: "Banners & history"
SliderRow { setting: "notificationTimeoutMs" }
SliderRow {
setting: "notificationTimeoutCriticalMs"
zeroLabel: "Never"
}
SliderRow { setting: "maxVisibleToasts" }
SliderRow { setting: "notificationHistoryLimit" }
SliderRow { setting: "maxVisibleToasts"; divider: false }
ActionRow {
label: "Notification history"
detail: Notifs.history.length === 1
? "1 kept now · the oldest are dropped past the limit"
: `${Notifs.history.length} kept now · the oldest are dropped past the limit`
action: "Clear"
enabled: Notifs.history.length > 0
divider: false
onTriggered: Notifs.dismissAll()
}
}
SettingsCard {
title: "Application rules"
subtitle: "Apps appear here after they send a notification."
title: "Applications"
subtitle: "Apps appear after their first notification. Recent senders and apps you have customized stay up top; the rest wait in All apps."
TextRow {
visible: Notifs.applications.length === 0
@@ -106,332 +269,155 @@ SettingsPage {
divider: false
}
Repeater {
model: Notifs.applications
Item {
width: parent.width
height: root.recentApps.length > 0 ? 28 : 0
visible: height > 0
Column {
id: appEntry
Text {
anchors.left: parent.left
anchors.bottom: parent.bottom
anchors.bottomMargin: 4
text: "Recent"
color: Theme.fgMuted
font.family: Theme.fontFamily
font.pixelSize: Math.max(9, Theme.fontSizeSmall - 1)
font.weight: Font.Bold
font.capitalization: Font.AllUppercase
font.letterSpacing: 0.8
}
}
Repeater {
model: root.recentApps
delegate: NotificationAppRow {
id: recentRow
required property var modelData
readonly property var app: appEntry.modelData
readonly property var rule: Notifs.appRule(appEntry.app.id)
width: parent.width
SettingRow {
width: parent.width
label: appEntry.app.name
detail: appEntry.rule.enabled ? String(appEntry.app.id) : "Notifications off"
divider: true
controlWidth: 48
SettingsToggle {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
checked: appEntry.rule.enabled
onToggled: value => Notifs.setAppRule(appEntry.app.id, { enabled: value })
}
}
appId: String(recentRow.modelData.id)
appName: String(recentRow.modelData.name)
iconName: String(recentRow.modelData.icon ?? "")
rule: Notifs.appRule(recentRow.modelData.id)
lastSeen: root.lastSeenText(recentRow.modelData.lastSeenMs)
expanded: root.expandedApp === String(recentRow.modelData.id)
onActivated: root.expandedApp = recentRow.expanded
? ""
: String(recentRow.modelData.id)
onEdited: patch => root.editRule(String(recentRow.modelData.id), patch)
onForgotten: root.forgetRule(String(recentRow.modelData.id))
}
}
}
SettingsCard {
title: "Focus modes"
subtitle: FocusModes.active
? FocusModes.activeName + " is on because " + FocusModes.activeReason + "."
: "What quiets this machine, and what turns it on. The first mode whose condition is true wins, so order is priority."
Item {
width: parent.width
height: root.customizedApps.length > 0 ? 28 : 0
visible: height > 0
Text {
anchors.left: parent.left
anchors.bottom: parent.bottom
anchors.bottomMargin: 4
text: "Customized"
color: Theme.fgMuted
font.family: Theme.fontFamily
font.pixelSize: Math.max(9, Theme.fontSizeSmall - 1)
font.weight: Font.Bold
font.capitalization: Font.AllUppercase
font.letterSpacing: 0.8
}
}
Repeater {
model: FocusModes.modes
model: root.customizedApps
delegate: Column {
id: modeEntry
delegate: NotificationAppRow {
id: customRow
required property var modelData
readonly property var mode: modeEntry.modelData
readonly property string modeId: String(modeEntry.mode.id ?? "")
readonly property bool open: root.expandedMode === modeEntry.modeId
readonly property bool running: FocusModes.activeMode?.id === modeEntry.modeId
readonly property var schedule: (modeEntry.mode.triggers ?? [])
.find(trigger => trigger.kind === "schedule") ?? null
width: parent.width
appId: String(customRow.modelData.id)
appName: String(customRow.modelData.name)
iconName: String(customRow.modelData.icon ?? "")
rule: Notifs.appRule(customRow.modelData.id)
lastSeen: root.lastSeenText(customRow.modelData.lastSeenMs)
expanded: root.expandedApp === String(customRow.modelData.id)
onActivated: root.expandedApp = customRow.expanded
? ""
: String(customRow.modelData.id)
onEdited: patch => root.editRule(String(customRow.modelData.id), patch)
onForgotten: root.forgetRule(String(customRow.modelData.id))
}
}
SettingRow {
visible: root.otherApps.length > 0
label: `All apps (${root.otherApps.length})`
detail: "The rest of what has notified, alphabetically — search matches a name or an id"
activatable: true
divider: !root.allAppsOpen
controlWidth: 40
onActivated: root.allAppsOpen = !root.allAppsOpen
Text {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
text: root.allAppsOpen ? "▴" : "▾"
color: Theme.fgMuted
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeSmall
}
}
Item {
width: parent.width
height: root.allAppsOpen ? 42 : 0
visible: root.allAppsOpen
SearchField {
id: appSearch
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
placeholder: "Search apps"
}
}
Repeater {
model: root.allAppsOpen ? root.expanderApps : []
delegate: NotificationAppRow {
id: expanderRow
required property var modelData
required property int index
width: parent.width
SettingRow {
width: parent.width
label: String(modeEntry.mode.name ?? "")
detail: modeEntry.running
? "On now — " + FocusModes.activeReason
: FocusModes.summary(modeEntry.mode)
activatable: true
divider: !modeEntry.open
controlWidth: 92
onActivated: root.expandedMode = modeEntry.open ? "" : modeEntry.modeId
Row {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
spacing: 11
SettingsToggle {
anchors.verticalCenter: parent.verticalCenter
checked: modeEntry.mode.enabled === true
onToggled: value => FocusModes.setEnabled(modeEntry.modeId, value)
}
Text {
anchors.verticalCenter: parent.verticalCenter
text: modeEntry.open ? "\u25B4" : "\u25BE"
color: Theme.fgMuted
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeSmall
}
}
}
SettingRow {
width: parent.width
visible: modeEntry.open
label: "Silence notifications"
detail: "Banners are held until the mode ends"
controlWidth: 48
SettingsToggle {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
checked: modeEntry.mode.silence === true
onToggled: value => FocusModes.update(modeEntry.modeId, { silence: value })
}
}
SettingRow {
width: parent.width
visible: modeEntry.open
label: "Keep the screen awake"
detail: "Caffeine, for as long as the mode is on"
controlWidth: 48
divider: modeEntry.schedule !== null
SettingsToggle {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
checked: modeEntry.mode.keepAwake === true
onToggled: value => FocusModes.update(modeEntry.modeId, { keepAwake: value })
}
}
// Only modes that actually have a schedule get the schedule
// controls; a game trigger has no times to set.
TextFieldRow {
width: parent.width
visible: modeEntry.open && modeEntry.schedule !== null
label: "From"
detail: "24-hour, such as 23:30"
text: String(modeEntry.schedule?.start ?? "")
placeholder: "23:30"
onAccepted: value => root.reschedule(modeEntry.mode, { start: value })
}
TextFieldRow {
width: parent.width
visible: modeEntry.open && modeEntry.schedule !== null
label: "Until"
detail: "A time earlier than the start means the window crosses midnight"
text: String(modeEntry.schedule?.end ?? "")
placeholder: "07:00"
onAccepted: value => root.reschedule(modeEntry.mode, { end: value })
}
SettingRow {
width: parent.width
visible: modeEntry.open && modeEntry.mode.silence === true
label: "May interrupt"
detail: (modeEntry.mode.allow ?? []).length === 0
? "Nothing gets through while this mode is on"
: "Everything else is held until the mode ends"
activatable: true
controlWidth: 150
onActivated: root.expandedAllow =
root.expandedAllow === modeEntry.modeId ? "" : modeEntry.modeId
Row {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
spacing: 9
Text {
anchors.verticalCenter: parent.verticalCenter
text: (modeEntry.mode.allow ?? []).length === 0
? "Nothing"
: (modeEntry.mode.allow ?? []).length + " apps"
color: Theme.fgDim
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSize
}
Text {
anchors.verticalCenter: parent.verticalCenter
text: root.expandedAllow === modeEntry.modeId ? "\u25B4" : "\u25BE"
color: Theme.fgMuted
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeSmall
}
}
}
// Drawn from the applications that have actually sent a
// notification, which is the same list the rules above use --
// an exception for something that never notifies is not a
// choice worth offering.
Repeater {
model: (modeEntry.open && root.expandedAllow === modeEntry.modeId)
? Notifs.applications
: []
delegate: SettingRow {
required property var modelData
width: parent.width
label: String(modelData.name ?? "")
detail: String(modelData.id ?? "")
controlWidth: 48
SettingsToggle {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
checked: (modeEntry.mode.allow ?? []).indexOf(String(modelData.id)) >= 0
onToggled: value => root.toggleAllowed(modeEntry.mode, String(modelData.id), value)
}
}
}
TextRow {
width: parent.width
visible: modeEntry.open && root.expandedAllow === modeEntry.modeId
&& Notifs.applications.length === 0
label: "No applications yet"
detail: "They appear here once they have sent a notification."
value: ""
}
SettingRow {
width: parent.width
visible: modeEntry.open && modeEntry.schedule !== null
label: "On these days"
detail: "A window that crosses midnight belongs to the day it starts on"
controlWidth: 250
divider: false
Row {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
spacing: 5
Repeater {
model: [
{ value: 1, label: "M" }, { value: 2, label: "T" },
{ value: 3, label: "W" }, { value: 4, label: "T" },
{ value: 5, label: "F" }, { value: 6, label: "S" },
{ value: 0, label: "S" }
]
delegate: Rectangle {
id: dayPill
required property var modelData
readonly property bool on:
(modeEntry.schedule?.days ?? []).indexOf(dayPill.modelData.value) >= 0
width: 30
height: 28
radius: 8
color: dayPill.on ? Theme.alpha(Theme.accent, 0.22)
: Theme.alpha(Theme.fg, 0.06)
border.width: dayPill.on ? 1 : 0
border.color: Theme.alpha(Theme.accent, 0.5)
Text {
anchors.centerIn: parent
text: String(dayPill.modelData.label)
color: dayPill.on ? Theme.fg : Theme.fgDim
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeSmall
font.weight: dayPill.on ? Font.DemiBold : Font.Medium
}
HoverHandler { cursorShape: Qt.PointingHandCursor }
TapHandler {
onTapped: root.toggleDay(modeEntry.mode, dayPill.modelData.value)
}
}
}
}
}
}
}
}
SettingsCard {
title: "Focus sessions"
subtitle: "A focus session binds quiet mode and Caffeine to the current workspace."
SettingRow {
label: "Keep the screen awake"
detail: Caffeine.enabled
? "The display will not blank or lock while this is on"
: "Idle timings on Power & Lock apply normally"
controlWidth: 48
SettingsToggle {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
checked: Caffeine.enabled
onToggled: value => Caffeine.enabled = value
appId: String(expanderRow.modelData.id)
appName: String(expanderRow.modelData.name)
iconName: String(expanderRow.modelData.icon ?? "")
rule: Notifs.appRule(expanderRow.modelData.id)
lastSeen: root.lastSeenText(expanderRow.modelData.lastSeenMs)
expanded: root.expandedApp === String(expanderRow.modelData.id)
divider: expanderRow.index < root.expanderApps.length - 1
onActivated: root.expandedApp = expanderRow.expanded
? ""
: String(expanderRow.modelData.id)
onEdited: patch => root.editRule(String(expanderRow.modelData.id), patch)
onForgotten: root.forgetRule(String(expanderRow.modelData.id))
}
}
SettingRow {
id: durationRow
label: "Default duration"
detail: "Used by Super+Shift+F and Quick Settings"
controlWidth: 264
Row {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
spacing: 6
Repeater {
model: [25, 45, 60, 90]
SettingsButton {
required property int modelData
text: `${modelData}m`
tone: DesktopPreferences.get("focusDurationMinutes") === modelData ? "accent" : "normal"
onClicked: SystemSettings.commitPreference("focusDurationMinutes", modelData)
}
}
}
}
SettingRow {
label: FocusSession.active ? `Active on ${FocusSession.workspaceLabel}` : "No active focus session"
detail: FocusSession.active ? `${FocusSession.remainingText} remaining` : "Start one without leaving Settings"
TextRow {
visible: root.allAppsOpen && root.expanderApps.length === 0
label: appSearch.text.trim() === "" ? "Nothing else remembered" : "No matching applications"
detail: appSearch.text.trim() === ""
? "Every application that has notified is already listed above."
: "Search matches an application's name or its id."
divider: false
controlWidth: 120
SettingsButton {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
text: FocusSession.active ? "Show controls" : "Start focus"
tone: FocusSession.active ? "normal" : "accent"
onClicked: FocusSession.reveal()
}
}
}
}
@@ -175,6 +175,7 @@ Rectangle {
case "sound": return soundPage;
case "gaming": return gamingPage;
case "notifications": return notificationsPage;
case "focus": return focusPage;
case "screen-intelligence": return screenIntelligencePage;
case "shortcuts": return shortcutsPage;
case "mouse": return mousePage;
@@ -260,6 +261,7 @@ Rectangle {
Component { id: soundPage; SoundPage {} }
Component { id: gamingPage; GamingPage {} }
Component { id: notificationsPage; NotificationsPage {} }
Component { id: focusPage; FocusPage {} }
Component { id: screenIntelligencePage; ScreenIntelligencePage {} }
Component { id: shortcutsPage; ShortcutsPage {} }
Component { id: mousePage; MousePage {} }
@@ -30,12 +30,6 @@ SettingsPage {
ToggleRow { setting: "windowSwallow"; divider: false }
}
SettingsCard {
title: "Focus"
SliderRow { setting: "focusDurationMinutes"; divider: false }
}
// Saved here, not created here. A layout is worth recording at the moment
// you have it right, so saving is a launcher command; this is where the
// ones you kept are reviewed and the ones you did not are removed.
@@ -22,6 +22,10 @@ SyncPage 1.0 SyncPage.qml
DisplaysPage 1.0 DisplaysPage.qml
HomePage 1.0 HomePage.qml
NotificationsPage 1.0 NotificationsPage.qml
NotificationAppRow 1.0 NotificationAppRow.qml
FocusPage 1.0 FocusPage.qml
FocusModeRow 1.0 FocusModeRow.qml
FocusAllowChips 1.0 FocusAllowChips.qml
PasswordRow 1.0 PasswordRow.qml
PrintersPage 1.0 PrintersPage.qml
ScreenIntelligencePage 1.0 ScreenIntelligencePage.qml