// Home — the page you open to do something, not to read a report. // // The Overview tab of the Home category. Quick actions sit under the greeting // as tiles; findings follow; then two glance cards into the sibling tabs (My // Home, Phone), the next calendar event, and weather. The old Focus and // containers cards retired into the tiles and the System category; the // reclaim-space prompt is gone on purpose — stopped development containers // are not clutter, and prompting their deletion was a racket. import QtQuick import qs.config import qs.services SettingsPage { id: root readonly property int openedHour: new Date().getHours() readonly property string greeting: openedHour < 12 ? "Good morning" : (openedHour < 18 ? "Good afternoon" : "Good evening") // Findings, in the order they deserve attention. Each names the page that // can actually resolve it, because a home page that reports a problem it // cannot help with is just an alarm. readonly property var findings: { const found = []; const exposed = Firewall.exposedDataStores ?? []; if (exposed.length > 0) { found.push({ label: exposed.length === 1 ? "A database is reachable from your network" : "Databases are reachable from your network", detail: exposed.map(entry => String(entry.name)).join(" and ") + ", published on every interface", page: "firewall", action: "Review" }); } if (Updates.securityCount > 0) { found.push({ label: Updates.securityCount + " update" + (Updates.securityCount === 1 ? "" : "s") + " carry a security advisory", detail: "Worth installing before the rest", page: "updates", action: "Open" }); } if (Updates.rebootNeeded) { found.push({ label: "A newer kernel is installed than the one running", detail: "Restart to use it", page: "updates", action: "Open" }); } if (Updates.total > 0) { found.push({ label: "Install " + Updates.total + " update" + (Updates.total === 1 ? "" : "s"), detail: "Packages, applications and firmware, from wherever each comes from", page: "updates", action: "Open" }); } if (Health.summary?.errors > 0 || Health.summary?.warnings > 0) { const count = Number(Health.summary?.errors ?? 0) + Number(Health.summary?.warnings ?? 0); found.push({ label: count + " health check" + (count === 1 ? "" : "s") + " need attention", detail: "Something the desktop owns is not in its expected state", page: "services", action: "Open" }); } return found; } // One line of everything that is fine, so the quiet case still says // something rather than showing an empty page. readonly property string reassurance: { const parts = []; if (Health.checks.length > 0) parts.push(Health.checks.length + " health checks pass"); if (Disks.rootFilesystem) parts.push(Disks.formatBytes(Number(Disks.rootFilesystem.availBytes ?? 0)) + " free"); const newest = Snapshots.configs.length > 0 ? (Snapshots.configs[0].snapshots ?? [])[0] : null; if (newest) parts.push("snapshots ran at " + root.clockOf(String(newest.date ?? ""))); return parts.join(" · "); } // snapper prints a full date; only the time is wanted in a summary line. function clockOf(stamp: string): string { const match = /(\d{1,2}:\d{2})(?::\d{2})?\s*(AM|PM)?/i.exec(stamp); if (!match) return stamp; return match[2] ? match[1] + " " + match[2].toUpperCase() : match[1]; } // Greets whoever is signed in, from the same accountsservice record the // lock screen shows. This once hardcoded the author's first name, which // made the first screen of Settings wrong for every other human. readonly property string greetingName: { if (!UserAccounts.me) return ""; const full = UserAccounts.displayName(UserAccounts.me); return full.split(" ")[0] || ""; } // What the glance cards say about the sibling tabs. readonly property var lightsOn: HomeAssistant.catalog.filter(entity => entity.active === true) readonly property string myHomeGlance: { if (HomeAssistant.lastError === "not-configured") return "Set up Home Assistant"; if (root.lightsOn.length === 0) return "No lights are on"; const names = root.lightsOn.slice(0, 2).map(entity => String(entity.name ?? entity.sourceName)).join(", "); return root.lightsOn.length + (root.lightsOn.length === 1 ? " light on · " : " lights on · ") + names; } readonly property string phoneGlance: { if (!KdeConnect.preferredPhone) return "No paired phone"; const parts = [String(KdeConnect.preferredPhone.name)]; if (KdeConnect.phoneBattery) parts.push(KdeConnect.phoneBattery.charge + "%"); parts.push(KdeConnect.phoneReachable ? "nearby" : "away"); return parts.join(" · "); } // The next timed calendar event, worded for a card. readonly property string nextEventWhen: { const event = CalendarAgenda.nextEvent; if (!event) return ""; const start = new Date(Number(event.start) * 1000); const clock = start.toLocaleTimeString(Qt.locale(), Locale.ShortFormat); const today = new Date(); const sameDay = start.getFullYear() === today.getFullYear() && start.getMonth() === today.getMonth() && start.getDate() === today.getDate(); return sameDay ? clock + " today" : clock + " " + start.toLocaleDateString(Qt.locale(), "dddd"); } title: root.greetingName !== "" ? `${root.greeting}, ${root.greetingName}` : root.greeting lede: Weather.available ? Math.round(Weather.temperature) + Weather.unitSuffix + " and " + Weather.description.toLowerCase() + " in " + Settings.weatherLocation : "Your desktop is configured and ready." // These scans also feed the System category's tab gating (Containers, // Snapshots), so they fire when Settings opens even though this page no // longer renders their cards. Component.onCompleted: { if (!Firewall.scanned) Firewall.refresh(); if (!Containers.scanned) Containers.refresh(); if (!Snapshots.scanned) Snapshots.refresh(); if (!Disks.scanned) Disks.refresh(); } // ── Quick actions ─────────────────────────────────────────────────────── Grid { id: quick width: parent.width columns: width >= 640 ? 5 : 2 columnSpacing: 10 rowSpacing: 10 readonly property real tileWidth: (width - (columns - 1) * columnSpacing) / columns HomeQuickTile { width: quick.tileWidth glyph: "\u{F051F}" label: FocusSession.active ? "End focus" : "Focus session" caption: FocusSession.active ? FocusSession.remainingText + " left · tap to end" : Settings.focusDurationMinutes + " min · Super+Shift+F" onActivated: FocusSession.active ? FocusSession.end(false) : FocusSession.startDefault() } HomeQuickTile { width: quick.tileWidth glyph: "\u{F009A}" glyphColor: Notifs.doNotDisturb ? Theme.warn : Theme.accent label: "Do Not Disturb" interactive: !FocusModes.active && !FocusSession.active caption: { if (FocusModes.active) return "Held by the " + FocusModes.activeName + " mode"; if (FocusSession.active) return "Held by the focus session"; return Notifs.doNotDisturb ? "On — tap to allow banners" : "Off — tap to hold banners"; } onActivated: Notifs.doNotDisturb = !Notifs.doNotDisturb } HomeQuickTile { width: quick.tileWidth glyph: "\u{F0493}" glyphColor: Health.status === "error" ? Theme.danger : (Health.status === "warning" ? Theme.warn : Theme.ok) label: "Health" caption: { if (Health.checks.length === 0) return "Checking the desktop"; const count = Number(Health.summary?.errors ?? 0) + Number(Health.summary?.warnings ?? 0); return count > 0 ? count + " need attention" : Health.checks.length + " checks pass"; } onActivated: ShellState.openSettings("services") } HomeQuickTile { width: quick.tileWidth glyph: "\u{F0954}" label: "Snapshots" caption: { const newest = Snapshots.configs.length > 0 ? (Snapshots.configs[0].snapshots ?? [])[0] : null; if (newest) return "Ran at " + root.clockOf(String(newest.date ?? "")); return Snapshots.configs.length > 0 ? "Configured" : "Not protected"; } onActivated: ShellState.openSettings("snapshots") } HomeQuickTile { width: quick.tileWidth glyph: "\u{F02CA}" label: "Storage" caption: Disks.rootFilesystem ? Disks.formatBytes(Number(Disks.rootFilesystem.availBytes ?? 0)) + " free" : "Reading the drive" onActivated: ShellState.openSettings("storage") } } // ── This machine ──────────────────────────────────────────────────────── SettingsCard { title: "This machine" subtitle: root.reassurance TextRow { visible: root.findings.length === 0 label: "Nothing needs your attention" detail: "Anything worth acting on would appear here" value: "" divider: false } Repeater { model: root.findings delegate: SettingRow { required property var modelData required property int index width: parent.width label: String(modelData.label ?? "") detail: String(modelData.detail ?? "") controlWidth: 110 divider: index < root.findings.length - 1 SettingsButton { anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter text: String(modelData.action ?? "Open") onClicked: ShellState.settingsPage = String(modelData.page) } } } } // ── Glances into the sibling tabs ─────────────────────────────────────── Grid { id: glances width: parent.width columns: width >= 720 ? 2 : 1 columnSpacing: 16 rowSpacing: 16 readonly property real cardWidth: columns === 2 ? (width - columnSpacing) / 2 : width Repeater { model: [ { glyph: "\u{F0335}", title: "My Home", sub: root.myHomeGlance, page: "my-home" }, { glyph: "\u{F011C}", title: "Phone", sub: root.phoneGlance, page: "phone" } ] delegate: Rectangle { id: glance required property var modelData width: glances.cardWidth height: 74 radius: 14 color: glanceArea.containsMouse ? Theme.alpha(Theme.bgPanel, 0.95) : Theme.alpha(Theme.bgPanel, 0.72) border.width: 1 border.color: Theme.alpha(Theme.fg, 0.07) Text { id: glanceGlyph anchors.left: parent.left anchors.leftMargin: 16 anchors.verticalCenter: parent.verticalCenter text: String(glance.modelData.glyph) color: Theme.accent font.family: Theme.fontMono font.pixelSize: 19 } Column { anchors.left: glanceGlyph.right anchors.leftMargin: 14 anchors.right: chevron.left anchors.rightMargin: 10 anchors.verticalCenter: parent.verticalCenter spacing: 3 Text { text: String(glance.modelData.title) color: Theme.fg font.family: Theme.fontFamily font.pixelSize: Theme.fontSizeLarge font.weight: Font.DemiBold } Text { width: parent.width text: String(glance.modelData.sub) color: Theme.fgDim font.family: Theme.fontFamily font.pixelSize: Theme.fontSizeSmall elide: Text.ElideRight } } Text { id: chevron anchors.right: parent.right anchors.rightMargin: 15 anchors.verticalCenter: parent.verticalCenter text: "›" color: Theme.fgMuted font.family: Theme.fontFamily font.pixelSize: 18 } MouseArea { id: glanceArea anchors.fill: parent hoverEnabled: true cursorShape: Qt.PointingHandCursor onClicked: ShellState.openSettings(String(glance.modelData.page)) } Accessible.role: Accessible.Button Accessible.name: "Open " + String(glance.modelData.title) } } } // ── Today ─────────────────────────────────────────────────────────────── SettingsCard { visible: CalendarAgenda.available && CalendarAgenda.nextEvent !== null title: "Today" SettingRow { label: CalendarAgenda.nextEvent ? String(CalendarAgenda.nextEvent.summary) + " — " + root.nextEventWhen : "" detail: "Next on your calendar" divider: false controlWidth: 110 SettingsButton { anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter text: String(CalendarAgenda.nextEvent?.joinUrl ?? "").startsWith("https://") ? "Join" : "Open" onClicked: { const event = CalendarAgenda.nextEvent; if (!event) return; if (String(event.joinUrl ?? "").startsWith("https://")) CalendarAgenda.join(event); else CalendarAgenda.openEvent(event); } } } } // ── Weather ───────────────────────────────────────────────────────────── SettingsCard { title: "Weather" subtitle: "Shown above and in the date menu." // The location is set roughly once a year, so the search that changes it // is behind a press rather than permanently open. PickerRow { id: locationPicker label: "Location" detail: "Only the search term is sent; the name is a label kept on this machine" value: Settings.weatherLocation !== "" ? Settings.weatherLocation : "Not set — choose one to see weather" LocationPicker { width: parent.width onPicked: locationPicker.collapse() } } ChoiceRow { setting: "temperatureUnit" } SliderRow { setting: "weatherRefreshMinutes"; divider: false } } }