diff options
| -rw-r--r-- | desktop/README.md | 9 | ||||
| -rw-r--r-- | desktop/SaveBar.qml | 24 | ||||
| -rw-r--r-- | desktop/Settings.qml | 9 | ||||
| -rw-r--r-- | desktop/Snack.qml | 41 | ||||
| -rw-r--r-- | desktop/Snackbar.qml | 71 | ||||
| -rw-r--r-- | desktop/modules/sonix/BootlogoModule.qml | 2 | ||||
| -rw-r--r-- | desktop/modules/sonix/BootlogoTab.qml | 15 |
7 files changed, 146 insertions, 25 deletions
diff --git a/desktop/README.md b/desktop/README.md index c7aea52..9e17e7a 100644 --- a/desktop/README.md +++ b/desktop/README.md @@ -56,6 +56,15 @@ page's draft lives in the page, so it is not marked in the sidebar and leaving the page drops it. Git and autostart act a row at a time and the theme applies on click, so they have no bar. +What a save, an apply or an upload says goes to the snackbar at the foot of +the window (`Snack.qml`, drawn by `Snackbar.qml`), in full and wrapped: green +fades after 6s, yellow (waiting) and red (failed) stay until dismissed with +its cross or replaced by the next message. Changing page or closing the +window clears it. SaveBar forwards its `notice` and `error` there, so a page +using it needs nothing more; any other page calls `Snack.ok(text)`, +`Snack.wait(text)` or `Snack.error(text)`. The header keeps only the draft's +state beside the buttons: "unsaved changes", or why Save is held back. + The sidebar's edge drags to resize it, between 160 and 420px; the width is saved in the preferences file on release. diff --git a/desktop/SaveBar.qml b/desktop/SaveBar.qml index 147d00b..239b896 100644 --- a/desktop/SaveBar.qml +++ b/desktop/SaveBar.qml @@ -18,10 +18,11 @@ import QtQuick // does (Apply and Reset for a staged wallpaper or cursor, Save + Restart for // a daemon that rereads only at start); busy shows while a save runs. // -// The outcome of a save is reported here too, not at the foot of the page: -// notice is what a finished save says ("saved"), shown once the page is -// clean, in noticeColor; error is a save refused or failed, which leaves the -// draft dirty, so it is shown over "unsaved changes", in red. +// The outcome of a save goes to the window's snackbar (Snack.qml), where it +// is shown in full: notice is what a finished save says ("saved"), in +// noticeColor, fading by itself; error is a save refused or failed, in red, +// staying until dismissed. Only the draft's state stays here, beside the +// buttons: "unsaved changes", or what is wrong when Save is held back. Row { id: bar property bool dirty: false @@ -37,18 +38,17 @@ Row { signal discard spacing: 8 + onNoticeChanged: if (bar.notice !== "") Snack.show(bar.notice, bar.noticeColor, false) + onErrorChanged: if (bar.error !== "") Snack.error(bar.error) + Text { anchors.verticalCenter: parent.verticalCenter - visible: bar.error !== "" || bar.dirty || bar.notice !== "" - // ponytail: a long message elides; the header has one line. + visible: bar.dirty + // ponytail: a long note elides; the header has one line. width: Math.min(implicitWidth, 420) elide: Text.ElideRight - text: bar.error !== "" ? bar.error - : !bar.dirty ? bar.notice - : !bar.valid ? (bar.note || "fix the fields marked red") : "unsaved changes" - color: bar.error !== "" ? Theme.red - : !bar.dirty ? bar.noticeColor - : bar.valid ? Theme.yellow : Theme.red + text: !bar.valid ? (bar.note || "fix the fields marked red") : "unsaved changes" + color: bar.valid ? Theme.yellow : Theme.red font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 4 } } Button { diff --git a/desktop/Settings.qml b/desktop/Settings.qml index 2ba8cb6..507960b 100644 --- a/desktop/Settings.qml +++ b/desktop/Settings.qml @@ -91,6 +91,8 @@ Scope { else root.close(); } function close() { root.open = false; root.page = ""; root.query = ""; } + // A message belongs to the page that said it. + onPageChanged: Snack.clear() LazyLoader { active: root.open @@ -613,6 +615,13 @@ Scope { when: pageLoader.item !== null } } + + // Every page's save and upload results, in full. See Snack.qml. + Snackbar { + anchors { bottom: pageLoader.bottom; horizontalCenter: pageLoader.horizontalCenter } + width: Math.min(560, pageLoader.width - 32) + z: 10 + } } } } diff --git a/desktop/Snack.qml b/desktop/Snack.qml new file mode 100644 index 0000000..b955ad2 --- /dev/null +++ b/desktop/Snack.qml @@ -0,0 +1,41 @@ +// Copyright (C) 2026 Danilo M. <danix@danix.xyz> +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License version 2 as +// published by the Free Software Foundation. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +pragma Singleton + +import Quickshell +import QtQuick + +// The settings window's one message, shown by Snackbar.qml at the foot of the +// window: what a save, an apply or an upload said. A page calls ok(), wait() +// or error(); a new message replaces the last. ok() fades by itself, wait() +// and error() stay until dismissed or replaced. SaveBar forwards its notice +// and error here, so a page using it needs nothing more. +Singleton { + id: root + + property string text: "" + property color color: Theme.green + property bool sticky: false + // Bumped on every show, so the same text twice restarts the fade. + property int serial: 0 + + function show(text, color, sticky) { + root.text = text; + root.color = color; + root.sticky = !!sticky; + root.serial++; + } + function ok(text) { root.show(text, Theme.green, false); } + function wait(text) { root.show(text, Theme.yellow, true); } + function error(text) { root.show(text, Theme.red, true); } + function clear() { root.text = ""; } +} diff --git a/desktop/Snackbar.qml b/desktop/Snackbar.qml new file mode 100644 index 0000000..467ac73 --- /dev/null +++ b/desktop/Snackbar.qml @@ -0,0 +1,71 @@ +// Copyright (C) 2026 Danilo M. <danix@danix.xyz> +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License version 2 as +// published by the Free Software Foundation. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +import QtQuick + +// Snack's message at the foot of the settings window: wrapped in full, a +// stripe in its colour, a cross to dismiss. Place it once per window. +Rectangle { + id: bar + + readonly property bool shown: Snack.text !== "" + + implicitHeight: Math.max(msg.implicitHeight, 20) + 20 + radius: 10 + color: Theme.surface + border.width: 1 + border.color: Qt.alpha(Snack.color, 0.6) + opacity: shown ? 1 : 0 + visible: opacity > 0 + Behavior on opacity { NumberAnimation { duration: 160 } } + + Timer { + id: fade + interval: 6000 + onTriggered: Snack.clear() + } + Connections { + target: Snack + function onSerialChanged() { if (Snack.sticky) fade.stop(); else fade.restart(); } + } + + Rectangle { + anchors { left: parent.left; top: parent.top; bottom: parent.bottom; margins: 6 } + width: 4 + radius: 2 + color: Snack.color + } + + Text { + id: msg + anchors { left: parent.left; leftMargin: 20; right: close.left; rightMargin: 12; verticalCenter: parent.verticalCenter } + wrapMode: Text.Wrap + textFormat: Text.PlainText + text: Snack.text + font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 3 } + color: Theme.text + } + + Text { + id: close + anchors { right: parent.right; rightMargin: 12; verticalCenter: parent.verticalCenter } + text: "" // nf-fa-xmark + font { family: Theme.iconFamily; pixelSize: Theme.fontSize - 2 } + color: hit.containsMouse ? Theme.text : Theme.subtext + MouseArea { + id: hit + anchors { fill: parent; margins: -6 } + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: Snack.clear() + } + } +} diff --git a/desktop/modules/sonix/BootlogoModule.qml b/desktop/modules/sonix/BootlogoModule.qml index 8778ce3..ea39376 100644 --- a/desktop/modules/sonix/BootlogoModule.qml +++ b/desktop/modules/sonix/BootlogoModule.qml @@ -28,7 +28,7 @@ Module { actions: Component { Row { spacing: 8 - // The status is in the page, under Output, where it wraps. + // Results go to the window's snackbar (Snack.qml). Button { text: tab.busy ? "Busy…" : "Save" enabled: tab.canSave diff --git a/desktop/modules/sonix/BootlogoTab.qml b/desktop/modules/sonix/BootlogoTab.qml index 4a34dc7..96454a2 100644 --- a/desktop/modules/sonix/BootlogoTab.qml +++ b/desktop/modules/sonix/BootlogoTab.qml @@ -37,10 +37,10 @@ Row { property bool busy: false property bool waiting: false - property string status: "" - property bool statusError: false readonly property bool canSave: canvas.ready && !tab.busy - function say(t, err) { tab.status = t; tab.statusError = !!err; } + // To the window's snackbar (Snack.qml): yellow while waiting for Wi-Fi + // transfer, red on failure, green otherwise. + function say(t, err) { if (tab.waiting) Snack.wait(t); else if (err) Snack.error(t); else Snack.ok(t); } readonly property var swatches: ["#ffffff", "#000000", "#3584e4", "#ff7800", "#e5a50a", "#2ec27e", "#9141ac", "#d56199", "udt"] @@ -319,15 +319,6 @@ Row { enabled: Sonix.present && !tab.busy && !tab.waiting onClicked: pickSaved.open() } - // Save and upload results, in full: the header has no room for them. - Text { - visible: tab.status !== "" - width: parent.width - wrapMode: Text.Wrap - text: tab.status - font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 3 } - color: tab.waiting ? Theme.yellow : tab.statusError ? Theme.red : Theme.green - } Text { width: parent.width wrapMode: Text.Wrap |
