mirror of
https://github.com/theupriser/steamify-cachyos.git
synced 2026-10-03 17:41:58 +02:00
refactor(ui): One QML file per screen; the HDMI boost's setup screens and backend commands removed
This commit is contained in:
1 parent
7bef834b16
commit
e699a3f8eb
29 files changed
+1050
-1832
No files matched your search
@@ -0,0 +1,265 @@
|
||||
// State and logic of the app: menu -> review -> password -> applying -> done,
|
||||
// and the BIOS update (check -> warning 1 -> warning 2 -> flash). The
|
||||
// screens only show this and call its functions.
|
||||
import QtQuick
|
||||
|
||||
Item {
|
||||
id: app
|
||||
required property var backend
|
||||
required property var gamepad
|
||||
|
||||
property string screen: "menu" // menu review password applying done bios1 bios2
|
||||
property double remoteAt: 0
|
||||
property var want: ({})
|
||||
property string boot: "gamescope"
|
||||
property int sel: 0 // row index; rows.length = the Apply button
|
||||
property bool reapply: false
|
||||
property var plan: [] // [{id, action, isNew}]
|
||||
property var steps: ({}) // id -> "wait"|"run"|"ok"|"fail"
|
||||
property var failed: []
|
||||
property bool restartNeeded: false
|
||||
property string runError: ""
|
||||
property bool wrongPassword: false
|
||||
property bool typing: false // a text field has the keys
|
||||
property int doneCount: 0
|
||||
property string pending: "apply" // what the password is for: apply | bios
|
||||
property string sessionPassword: "" // kept between the BIOS steps only
|
||||
property var biosInfo: ({})
|
||||
property bool biosRun: false
|
||||
property bool biosChecking: false // the invisible check before the warnings
|
||||
property int biosFocus: 0 // warning 1: 0 = Cancel, 1 = continue
|
||||
property real holdProgress: 0 // warning 2: hold A/OK for 5 s
|
||||
property bool holding: false
|
||||
readonly property alias log: logModel
|
||||
|
||||
readonly property var status: backend.status || ({})
|
||||
readonly property var items: status.items || []
|
||||
readonly property var rows: items.filter(function (i) { return !i.parent || want[i.parent]; })
|
||||
readonly property var bios: status.bios || null
|
||||
|
||||
signal passwordRequested()
|
||||
signal passwordRejected()
|
||||
signal passwordSubmitRequested() // A/OK on the password screen
|
||||
|
||||
ListModel { id: logModel }
|
||||
|
||||
function nowOn(id) {
|
||||
for (var i = 0; i < items.length; i++) if (items[i].id === id) return items[i].on;
|
||||
return false;
|
||||
}
|
||||
function label(item) { return Texts.label(item.id, item.label); }
|
||||
function biosHint() {
|
||||
if (!bios) return "";
|
||||
if (!bios.newest) return "Now " + bios.current + ", newest unknown (offline?)";
|
||||
if (bios.current === bios.newest) return bios.current + " is up to date";
|
||||
return "Now " + bios.current + ", newest " + bios.newest + " · at your own risk";
|
||||
}
|
||||
function syncFromStatus() {
|
||||
reapply = false;
|
||||
var w = {};
|
||||
for (var i = 0; i < items.length; i++) w[items[i].id] = items[i].kind === "action" ? false : items[i].wanted;
|
||||
want = w;
|
||||
boot = nowOn("boot") ? "desktop" : "gamescope";
|
||||
if (sel > rows.length) sel = 0;
|
||||
}
|
||||
Connections { target: backend; function onStatusChanged() { if (app.screen === "menu" || app.screen === "done") app.syncFromStatus(); } }
|
||||
|
||||
// --- Menu ---
|
||||
function toggle(id) {
|
||||
var cur = items.find(function (i) { return i.id === id; });
|
||||
if (cur && cur.kind === "toggle" && cur.selectable === false) return;
|
||||
var w = Object.assign({}, want);
|
||||
w[id] = !w[id];
|
||||
// The same rules as the terminal menu (toggle_component).
|
||||
if (id === "gaming" && !w.gaming) w.single = false;
|
||||
if (id === "single" && w.single) w.gaming = true;
|
||||
if (id === "machine") w.poweroff = w.machine;
|
||||
if (id === "poweroff" && w.poweroff) w.machine = true;
|
||||
if (id === "machine" && !w.machine) w.kpin = false;
|
||||
if (id === "kpin" && w.kpin) w.machine = true;
|
||||
want = w;
|
||||
}
|
||||
function computePlan() {
|
||||
var p = [];
|
||||
for (var i = 0; i < items.length; i++) {
|
||||
var it = items[i];
|
||||
if (it.kind !== "toggle") continue;
|
||||
if (it.parent && !want[it.parent]) { if (it.on) p.push({ id: it.id, action: "off" }); continue; }
|
||||
if (want[it.id] && !it.on) p.push({ id: it.id, action: "on", isNew: !!it["new"] });
|
||||
else if (!want[it.id] && it.on) p.push({ id: it.id, action: "off" });
|
||||
else if (want[it.id] && (reapply || it.update)) p.push({ id: it.id, action: reapply ? "again" : "update" });
|
||||
}
|
||||
var bootNow = nowOn("boot") ? "desktop" : "gamescope";
|
||||
if (want.gaming && boot !== bootNow) p.push({ id: "boot", action: boot === "desktop" ? "desktop" : "gaming" });
|
||||
return p;
|
||||
}
|
||||
function goReview(again) { reapply = again; plan = computePlan(); screen = "review"; }
|
||||
function wantedIds() {
|
||||
var ids = [];
|
||||
for (var i = 0; i < items.length; i++)
|
||||
if (items[i].kind === "toggle" && want[items[i].id] && (!items[i].parent || want[items[i].parent])) ids.push(items[i].id);
|
||||
return ids;
|
||||
}
|
||||
|
||||
// --- Password and apply ---
|
||||
function resetRun() { steps = {}; failed = []; restartNeeded = false; runError = ""; doneCount = 0; logModel.clear(); }
|
||||
function startApply(password) {
|
||||
resetRun();
|
||||
var s = {};
|
||||
for (var i = 0; i < plan.length; i++) s[plan[i].id] = "wait";
|
||||
steps = s;
|
||||
screen = "applying";
|
||||
backend.apply(wantedIds(), want.gaming ? boot : "", reapply, password || "");
|
||||
}
|
||||
function askPassword(forWhat) {
|
||||
pending = forWhat;
|
||||
if (!backend.needsPassword()) return false;
|
||||
wrongPassword = false; screen = "password"; passwordRequested();
|
||||
return true;
|
||||
}
|
||||
function onApplyPressed() {
|
||||
if (plan.length === 0) return;
|
||||
if (!askPassword("apply")) startApply("");
|
||||
}
|
||||
function submitPassword(p) {
|
||||
if (!backend.checkPassword(p)) { wrongPassword = true; passwordRejected(); return; }
|
||||
if (pending === "bios") { sessionPassword = p; biosCheck(); }
|
||||
else startApply(p);
|
||||
}
|
||||
function passwordBack() { screen = pending === "bios" ? "menu" : "review"; }
|
||||
|
||||
// --- BIOS ---
|
||||
function startBios() {
|
||||
if (!bios || !bios.selectable) return;
|
||||
biosInfo = {}; sessionPassword = "";
|
||||
if (!askPassword("bios")) biosCheck();
|
||||
}
|
||||
function biosCheck() {
|
||||
// Invisible: stay on the menu (the BIOS row says "Checking…"),
|
||||
// then the first warning.
|
||||
failed = []; runError = ""; logModel.clear();
|
||||
plan = [{ id: "bios", action: "check" }];
|
||||
biosRun = true; biosChecking = true; screen = "menu";
|
||||
backend.biosPrepare(sessionPassword);
|
||||
}
|
||||
function biosCancel() { sessionPassword = ""; holdProgress = 0; holding = false; biosRun = false; syncFromStatus(); screen = "menu"; }
|
||||
function biosContinue() { holdProgress = 0; screen = "bios2"; }
|
||||
function biosFlash() {
|
||||
holdProgress = 0; holding = false;
|
||||
resetRun();
|
||||
plan = [{ id: "bios", action: "flash" }];
|
||||
steps = { bios: "wait" };
|
||||
biosRun = true; screen = "applying";
|
||||
backend.biosFlash(sessionPassword);
|
||||
}
|
||||
Timer {
|
||||
interval: 50; repeat: true; running: app.screen === "bios2" && app.holding
|
||||
onTriggered: { app.holdProgress = Math.min(1, app.holdProgress + 0.01); if (app.holdProgress >= 1) app.biosFlash(); }
|
||||
}
|
||||
|
||||
function backToMenu() { biosRun = false; syncFromStatus(); screen = "menu"; }
|
||||
|
||||
// --- Backend events ---
|
||||
function errorText(e) {
|
||||
return e === "wrong-password" ? "The password didn't work." : (e === "start" ? "Steamify couldn't start the changes." : (e ? "sudo isn't available." : ""));
|
||||
}
|
||||
Connections {
|
||||
target: app.backend
|
||||
function onEvent(ev) {
|
||||
var s = Object.assign({}, app.steps);
|
||||
if (ev.event === "start") { s[ev.id] = "run"; app.steps = s; }
|
||||
else if (ev.event === "done") { s[ev.id] = ev.ok ? "ok" : "fail"; app.steps = s; app.doneCount++; }
|
||||
else if (ev.event === "log") { logModel.append({ line: ev.line }); if (logModel.count > 400) logModel.remove(0); }
|
||||
else if (ev.event === "bios-ready") { app.biosChecking = false; app.biosInfo = ev; app.biosFocus = 0; app.holdProgress = 0; app.screen = "bios1"; }
|
||||
else if (ev.event === "finished" && app.biosChecking) {
|
||||
// The check found nothing to do or a problem: say so.
|
||||
app.biosChecking = false; app.sessionPassword = "";
|
||||
app.failed = ev.failed || []; app.restartNeeded = false;
|
||||
app.runError = ev.nothing && !ev.error ? "The BIOS is already the newest version." : app.errorText(ev.error);
|
||||
app.screen = "done";
|
||||
}
|
||||
else if (ev.event === "finished") {
|
||||
// A successful BIOS check ends with bios-ready instead, keeping
|
||||
// the password for the flash.
|
||||
app.sessionPassword = "";
|
||||
app.failed = ev.failed || []; app.restartNeeded = !!ev.restart;
|
||||
app.runError = app.errorText(ev.error);
|
||||
app.screen = "done";
|
||||
}
|
||||
}
|
||||
function onRemotePressed() { app.remoteAt = Date.now(); Input.type = "remote"; }
|
||||
}
|
||||
|
||||
// --- Input: one set of actions for controller, keyboard and remote ---
|
||||
function act(a) {
|
||||
if (screen === "menu") {
|
||||
if (biosChecking) return;
|
||||
if (a === "up") sel = Math.max(0, sel - 1);
|
||||
else if (a === "down") sel = Math.min(rows.length, sel + 1);
|
||||
else if (a === "accept") {
|
||||
if (sel === rows.length) { goReview(false); return; }
|
||||
var r = rows[sel];
|
||||
if (r.kind === "action" && r.id === "bios") startBios();
|
||||
else if (r.kind === "choice") boot = boot === "gamescope" ? "desktop" : "gamescope";
|
||||
else if (r.kind === "toggle") toggle(r.id);
|
||||
}
|
||||
else if ((a === "left" || a === "right") && sel < rows.length && rows[sel].kind === "choice")
|
||||
boot = a === "left" ? "gamescope" : "desktop";
|
||||
else if (a === "apply") goReview(false);
|
||||
else if (a === "reapply") goReview(true);
|
||||
else if (a === "back") Qt.quit();
|
||||
} else if (screen === "review") {
|
||||
if (a === "accept" || a === "apply") onApplyPressed();
|
||||
else if (a === "back") { reapply = false; screen = "menu"; }
|
||||
} else if (screen === "password") {
|
||||
if (a === "accept" || a === "apply") passwordSubmitRequested();
|
||||
else if (a === "back") passwordBack();
|
||||
} else if (screen === "bios1") {
|
||||
if (a === "left") biosFocus = 0;
|
||||
else if (a === "right") biosFocus = 1;
|
||||
else if (a === "accept") { if (biosFocus === 1) biosContinue(); else biosCancel(); }
|
||||
else if (a === "back") biosCancel();
|
||||
} else if (screen === "bios2") {
|
||||
if (a === "back") biosCancel();
|
||||
else if (a === "hold") holding = true;
|
||||
else if (a === "release") { holding = false; if (holdProgress < 1) holdProgress = 0; }
|
||||
} else if (screen === "done") {
|
||||
if (a === "accept" && restartNeeded) backend.restart();
|
||||
else if (a === "back" || a === "accept") backToMenu();
|
||||
}
|
||||
}
|
||||
Connections {
|
||||
target: app.gamepad
|
||||
function onButton(b) {
|
||||
if (b === "a_up") { if (app.screen === "bios2") app.act("release"); return; }
|
||||
if (b.slice(-3) === "_up") return;
|
||||
Input.type = "controller";
|
||||
if (b === "a" && app.screen === "bios2") { app.act("hold"); return; }
|
||||
var map = { a: "accept", b: "back", x: "apply", y: "reapply", start: "apply", up: "up", down: "down", left: "left", right: "right" };
|
||||
if (map[b]) app.act(map[b]);
|
||||
}
|
||||
}
|
||||
function keyPressed(e) {
|
||||
// A text field handles its own keys, except Escape and Enter.
|
||||
if (typing && e.key !== Qt.Key_Escape && e.key !== Qt.Key_Return && e.key !== Qt.Key_Enter) return;
|
||||
var fromRemote = Date.now() - remoteAt < 400;
|
||||
if (screen === "bios2" && Input.type !== "keyboard" && (e.key === Qt.Key_Return || e.key === Qt.Key_Enter)) {
|
||||
if (!e.isAutoRepeat) act("hold"); e.accepted = true; return;
|
||||
}
|
||||
if (!fromRemote) Input.type = "keyboard";
|
||||
var k = e.key;
|
||||
if (k === Qt.Key_Up) act("up");
|
||||
else if (k === Qt.Key_Down) act("down");
|
||||
else if (k === Qt.Key_Left) act("left");
|
||||
else if (k === Qt.Key_Right) act("right");
|
||||
else if (k === Qt.Key_Space) act("accept");
|
||||
else if (k === Qt.Key_Return || k === Qt.Key_Enter) act(Input.type === "remote" || screen !== "menu" ? "accept" : "apply");
|
||||
else if (k === Qt.Key_Escape || k === Qt.Key_Back || k === Qt.Key_Backspace) { if (!e.isAutoRepeat) act("back"); }
|
||||
else if (k === Qt.Key_R) act("reapply");
|
||||
else return;
|
||||
e.accepted = true;
|
||||
}
|
||||
function keyReleased(e) {
|
||||
if (screen === "bios2" && !e.isAutoRepeat && (e.key === Qt.Key_Return || e.key === Qt.Key_Enter)) act("release");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
// Progress: every step of the plan on the left, the log on the right.
|
||||
import QtQuick
|
||||
import QtQuick.Controls.Basic
|
||||
|
||||
Item {
|
||||
required property AppState app
|
||||
readonly property bool checking: app.biosRun && app.plan.length > 0 && app.plan[0].action === "check"
|
||||
Column {
|
||||
x: 40; y: 32; width: 560; spacing: 18
|
||||
Text { text: app.biosRun ? (checking ? "Checking the BIOS update" : "Installing the BIOS update") : "Applying your changes"
|
||||
color: Theme.text; font.family: Theme.display; font.pixelSize: 34; font.weight: Font.DemiBold }
|
||||
Rectangle { width: 560; height: 10; radius: 5; color: Theme.line
|
||||
Rectangle { height: 10; radius: 5; color: Theme.accent; width: app.plan.length ? parent.width * Math.min(1, (app.doneCount + 0.3) / app.plan.length) : 0
|
||||
Behavior on width { NumberAnimation { duration: 300 } } } }
|
||||
Row { width: 560
|
||||
Text { text: "Step " + Math.min(app.plan.length, app.doneCount + 1) + " of " + app.plan.length; color: Theme.mute; font.family: Theme.body; font.pixelSize: 14; width: 280 }
|
||||
Text { text: "Don't turn off the PC"; color: Theme.mute; font.family: Theme.body; font.pixelSize: 14; width: 280; horizontalAlignment: Text.AlignRight } }
|
||||
ListView {
|
||||
width: 560; clip: true; spacing: 8
|
||||
height: parent.parent.height - 32 - 156 - 32
|
||||
model: app.plan
|
||||
ScrollBar.vertical: ScrollBar { policy: ScrollBar.AsNeeded }
|
||||
// Follow the progress: the running step stays in view.
|
||||
readonly property int running: app.plan.findIndex(function (p) { return app.steps[p.id] === "run"; })
|
||||
onRunningChanged: if (running >= 0) positionViewAtIndex(running, ListView.Contain)
|
||||
delegate: Rectangle {
|
||||
id: step
|
||||
required property var modelData
|
||||
readonly property string st: app.steps[modelData.id] || "wait"
|
||||
width: 560; height: 54; radius: 12; color: st === "run" ? Theme.cardSel : Theme.card
|
||||
border.width: st === "run" ? 2 : 0; border.color: Theme.accent
|
||||
Row { anchors.fill: parent; anchors.leftMargin: 18; anchors.rightMargin: 18; spacing: 14
|
||||
Rectangle { width: 22; height: 22; radius: 11; anchors.verticalCenter: parent.verticalCenter
|
||||
color: step.st === "ok" ? Theme.goodBg : (step.st === "fail" ? Theme.badBg : "transparent")
|
||||
border.width: step.st === "wait" || step.st === "run" ? 2 : 0; border.color: step.st === "run" ? Theme.accent : "#343f50"
|
||||
Text { anchors.centerIn: parent; text: step.st === "ok" ? "✓" : (step.st === "fail" ? "!" : ""); color: step.st === "ok" ? Theme.good : Theme.bad
|
||||
font.pixelSize: 13; font.weight: Font.Bold }
|
||||
RotationAnimator on rotation { running: step.st === "run"; from: 0; to: 360; duration: 1000; loops: Animation.Infinite } }
|
||||
Row { width: 380; anchors.verticalCenter: parent.verticalCenter; spacing: 10
|
||||
Text { text: (Texts.actions[step.modelData.action] || [])[3] + Texts.label(step.modelData.id)
|
||||
color: step.st === "wait" ? Theme.faint : Theme.textHi; font.family: Theme.body; font.pixelSize: 16; font.weight: Font.DemiBold
|
||||
anchors.verticalCenter: parent.verticalCenter; elide: Text.ElideRight
|
||||
width: Math.min(implicitWidth, parent.width - (badge.visible ? badge.width + parent.spacing : 0)) }
|
||||
Badge { id: badge; update: step.modelData.action === "update"; isNew: !!step.modelData.isNew; anchors.verticalCenter: parent.verticalCenter } }
|
||||
Text { text: ({ wait: "Waiting", run: "Working…", ok: "Done", fail: "Problem" })[step.st]
|
||||
color: step.st === "ok" ? Theme.good : (step.st === "fail" ? Theme.bad : Theme.label); font.family: Theme.body; font.pixelSize: 13
|
||||
anchors.verticalCenter: parent.verticalCenter }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Column {
|
||||
x: 632; y: 32; width: parent.width - 672; height: parent.height - 64; spacing: 10
|
||||
Text { text: "DETAILS"; color: Theme.faint; font.family: Theme.body; font.pixelSize: 12; font.weight: Font.DemiBold; font.letterSpacing: 0.8 }
|
||||
Rectangle { width: parent.width; height: parent.height - 30; radius: 16; color: "#0b0e13"; border.width: 1; border.color: "#1d2531"
|
||||
ListView { id: logView; anchors.fill: parent; anchors.margins: 18; clip: true; model: app.log
|
||||
onCountChanged: positionViewAtEnd()
|
||||
delegate: Text { required property string line; width: logView.width; text: line; color: "#aab5c4"; font.family: Theme.mono; font.pixelSize: 13; wrapMode: Text.WrapAnywhere } } }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
// "Update" (a newer version of it will be applied) or "New" (a default
|
||||
// option added since the last run), after an item's name.
|
||||
import QtQuick
|
||||
|
||||
Chip {
|
||||
property bool update: false
|
||||
property bool isNew: false
|
||||
visible: update || isNew
|
||||
height: 20
|
||||
text: update ? "Update" : "New"
|
||||
fg: update ? Theme.warn : Theme.good
|
||||
bgc: update ? Theme.warnBg : Theme.goodBg
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
// The bar at the bottom of a screen, for its buttons.
|
||||
import QtQuick
|
||||
|
||||
Rectangle {
|
||||
anchors.bottom: parent.bottom; width: parent.width; height: 64; color: Theme.bar
|
||||
Rectangle { width: parent.width; height: 1; color: Theme.line }
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
// BIOS update, warning 2 (last chance): type UPDATE on a keyboard, or hold
|
||||
// A/OK for 5 seconds on a controller or remote.
|
||||
import QtQuick
|
||||
import QtQuick.Controls.Basic
|
||||
|
||||
Item {
|
||||
id: screen
|
||||
required property AppState app
|
||||
onVisibleChanged: if (visible) { typed.text = ""; if (Input.type === "keyboard") typed.forceActiveFocus(); }
|
||||
Binding { target: screen.app; property: "typing"; value: typed.activeFocus; when: screen.visible }
|
||||
|
||||
Rectangle {
|
||||
anchors.centerIn: parent; width: 760; height: col.implicitHeight + 64; radius: 20; color: Theme.dangerBg; border.width: 2; border.color: Theme.dangerLine
|
||||
Column { id: col; anchors.fill: parent; anchors.margins: 32; spacing: 18
|
||||
Text { text: "Last chance: this flashes BIOS " + (app.biosInfo.newest || ""); color: "#ffd9d3"; font.family: Theme.display; font.pixelSize: 32; font.weight: Font.DemiBold }
|
||||
Text { width: parent.width; wrapMode: Text.WordWrap; color: Theme.soft; font.family: Theme.body; font.pixelSize: 16; lineHeight: 1.3
|
||||
text: "After this, keep the power on until the machine has fully started again. Don't touch it, even if the screen is black." }
|
||||
Column { visible: Input.type === "keyboard"; width: parent.width; spacing: 8
|
||||
Text { text: "Type UPDATE (in capitals) and press Enter to flash the BIOS:"; color: Theme.text; font.family: Theme.body; font.pixelSize: 15; font.weight: Font.DemiBold }
|
||||
TextField { id: typed; width: 320; height: 50; color: Theme.textHi; font.family: Theme.mono; font.pixelSize: 20; leftPadding: 14
|
||||
placeholderText: "UPDATE"; placeholderTextColor: "#6b5a5a"
|
||||
background: Rectangle { radius: 10; color: Theme.bg; border.width: 2; border.color: typed.text === "UPDATE" ? Theme.bad : "#5a3a3a" }
|
||||
Keys.onReturnPressed: if (text === "UPDATE") app.biosFlash()
|
||||
Keys.onEnterPressed: if (text === "UPDATE") app.biosFlash()
|
||||
Keys.onEscapePressed: app.biosCancel() } }
|
||||
Column { visible: Input.type !== "keyboard"; width: parent.width; spacing: 10
|
||||
Text { text: "Hold " + Input.g.ok + " for 5 seconds to flash the BIOS. Let go to stop."; color: Theme.text; font.family: Theme.body; font.pixelSize: 15; font.weight: Font.DemiBold }
|
||||
Rectangle { width: parent.width; height: 14; radius: 7; color: "#3a2224"
|
||||
Rectangle { height: 14; radius: 7; color: Theme.bad; width: parent.width * app.holdProgress } } }
|
||||
Row { spacing: 12
|
||||
Btn { k: Input.g.back; text: "Cancel"; onClicked: app.biosCancel() }
|
||||
Text { visible: app.bios && app.bios.dryRun; text: "Dry run: nothing will be flashed."; color: Theme.warn; font.family: Theme.body; font.pixelSize: 14
|
||||
anchors.verticalCenter: parent.verticalCenter } }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
// BIOS update, warning 1: the risks and what was checked; Cancel is
|
||||
// selected first.
|
||||
import QtQuick
|
||||
|
||||
Item {
|
||||
required property AppState app
|
||||
Rectangle {
|
||||
x: 40; y: 28; width: parent.width - 80; height: parent.height - 28 - 64 - 24; radius: 16; color: Theme.dangerBg; border.width: 2; border.color: Theme.dangerLine
|
||||
Row { anchors.fill: parent; anchors.margins: 32; spacing: 40
|
||||
Column { width: 640; spacing: 16
|
||||
Row { spacing: 12
|
||||
Rectangle { width: 44; height: 44; radius: 12; color: "#3a1c1f"
|
||||
Text { anchors.centerIn: parent; text: "!"; color: Theme.bad; font.pixelSize: 26; font.weight: Font.Bold } }
|
||||
Text { text: "BIOS update – entirely at your own risk"; color: "#ffd9d3"; font.family: Theme.display; font.pixelSize: 30; font.weight: Font.DemiBold
|
||||
anchors.verticalCenter: parent.verticalCenter } }
|
||||
RiskList { width: 640 }
|
||||
}
|
||||
Rectangle { width: 440; height: 250; radius: 14; color: Theme.card
|
||||
Column { anchors.fill: parent; anchors.margins: 22; spacing: 12
|
||||
Repeater { model: [["Current BIOS", app.biosInfo.current || ""], ["New BIOS", app.biosInfo.newest || ""], ["Checksum", "OK (Valve's package)"],
|
||||
["Compatible", app.biosInfo.compatible === "yes" ? "Yes (checked by fwupd)" : "Not checked (dry run)"]]
|
||||
Row { required property var modelData; width: 396
|
||||
Text { text: parent.modelData[0]; color: Theme.faint; font.family: Theme.body; font.pixelSize: 15; width: 150 }
|
||||
Text { text: parent.modelData[1]; color: parent.modelData[1].indexOf("Not") === 0 ? Theme.warn : Theme.text; font.family: Theme.mono; font.pixelSize: 15
|
||||
width: 246; horizontalAlignment: Text.AlignRight; elide: Text.ElideLeft } } }
|
||||
} }
|
||||
}
|
||||
}
|
||||
Bar {
|
||||
Text { anchors.left: parent.left; anchors.leftMargin: 40; anchors.verticalCenter: parent.verticalCenter; text: "Do you understand the risks and want to continue?"
|
||||
color: Theme.soft; font.family: Theme.body; font.pixelSize: 15 }
|
||||
Row { anchors.right: parent.right; anchors.rightMargin: 40; anchors.verticalCenter: parent.verticalCenter; spacing: 12
|
||||
Btn { k: app.biosFocus === 0 ? Input.g.ok : "◀"; text: "Cancel"; focusRing: app.biosFocus === 0; onClicked: app.biosCancel() }
|
||||
Btn { k: app.biosFocus === 1 ? Input.g.ok : "▶"; text: "I understand, continue"; focusRing: app.biosFocus === 1; color: Theme.danger
|
||||
onClicked: app.biosContinue() } }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
// A button with the input's button name in front.
|
||||
import QtQuick
|
||||
|
||||
Rectangle {
|
||||
id: btn
|
||||
property string k
|
||||
property string text
|
||||
property bool primary: false
|
||||
property bool focusRing: false
|
||||
signal clicked
|
||||
height: 48; radius: 12; width: row.implicitWidth + 36
|
||||
color: primary ? Theme.accent : Theme.button
|
||||
border.width: focusRing ? 2 : 0; border.color: Theme.textHi
|
||||
Row { id: row; anchors.centerIn: parent; spacing: 10
|
||||
Glyph { k: btn.k; visible: btn.k !== ""; dark: btn.primary; anchors.verticalCenter: parent.verticalCenter }
|
||||
Text { text: btn.text; color: btn.primary ? Theme.ink : Theme.text; font.family: Theme.body; font.pixelSize: 15; font.weight: Font.DemiBold
|
||||
anchors.verticalCenter: parent.verticalCenter }
|
||||
}
|
||||
MouseArea { anchors.fill: parent; cursorShape: Qt.PointingHandCursor; onClicked: btn.clicked() }
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
// A small label in capitals.
|
||||
import QtQuick
|
||||
|
||||
Rectangle {
|
||||
id: chip
|
||||
property string text
|
||||
property color fg: Theme.good
|
||||
property color bgc: Theme.goodBg
|
||||
height: 24; radius: 6; width: ct.implicitWidth + 16; color: bgc
|
||||
Text { id: ct; anchors.centerIn: parent; text: chip.text.toUpperCase(); color: chip.fg
|
||||
font.family: Theme.body; font.pixelSize: 12; font.weight: Font.DemiBold; font.letterSpacing: 0.6 }
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
// Right of the menu: what the selected item does, or a nudge to review
|
||||
// when the Apply button is selected.
|
||||
import QtQuick
|
||||
import QtQuick.Controls.Basic
|
||||
|
||||
Rectangle {
|
||||
id: detail
|
||||
required property AppState app
|
||||
readonly property var it: app.sel < app.rows.length ? app.rows[app.sel] : null
|
||||
readonly property var tx: it ? Texts.of(it) : ({})
|
||||
radius: 16; color: Theme.card
|
||||
// Long texts scroll instead of running out of the card; each item
|
||||
// starts at the top.
|
||||
onItChanged: flick.contentY = 0
|
||||
|
||||
Flickable {
|
||||
id: flick
|
||||
anchors.fill: parent; anchors.margins: 24; anchors.rightMargin: 12
|
||||
visible: detail.it !== null; clip: true
|
||||
contentHeight: col.implicitHeight; boundsBehavior: Flickable.StopAtBounds
|
||||
ScrollBar.vertical: ScrollBar { policy: ScrollBar.AsNeeded }
|
||||
Column {
|
||||
id: col; width: flick.width - 12; spacing: 14
|
||||
Row { spacing: 10
|
||||
Chip { readonly property var it: detail.it
|
||||
text: !it ? "" : it.kind === "choice" ? (app.boot === "desktop" ? "Desktop" : "Gaming") : it.kind === "action" ? "Opt-in" : (it.on ? "On" : "Off")
|
||||
fg: it && it.on ? Theme.good : Theme.label; bgc: it && it.on ? Theme.goodBg : Theme.line }
|
||||
Chip { visible: !!detail.tx.experimental; text: "Experimental"; fg: Theme.warn; bgc: Theme.warnBg }
|
||||
}
|
||||
Text { text: detail.it ? app.label(detail.it) : ""; color: Theme.text; font.family: Theme.display; font.pixelSize: 26; font.weight: Font.DemiBold
|
||||
wrapMode: Text.WordWrap; width: parent.width }
|
||||
Text { text: detail.tx.body || ""; color: Theme.soft; font.family: Theme.body; font.pixelSize: 15; lineHeight: 1.4; wrapMode: Text.WordWrap; width: parent.width }
|
||||
Text { text: "WHAT IT CHANGES"; color: Theme.faint; font.family: Theme.body; font.pixelSize: 12; font.weight: Font.DemiBold; font.letterSpacing: 0.8; topPadding: 4 }
|
||||
Repeater { model: detail.tx.changes || []
|
||||
Text { required property string modelData; text: "• " + modelData; color: Theme.soft; font.family: Theme.body; font.pixelSize: 14
|
||||
wrapMode: Text.WordWrap; width: col.width } }
|
||||
}
|
||||
}
|
||||
Column {
|
||||
anchors.centerIn: parent; spacing: 8; visible: detail.it === null
|
||||
Text { text: app.computePlan().length === 0 ? "Everything is the way you want it" : "Review what will change"; color: Theme.text
|
||||
font.family: Theme.display; font.pixelSize: 24; anchors.horizontalCenter: parent.horizontalCenter }
|
||||
Text { text: "then apply it"; color: Theme.mute; font.family: Theme.body; font.pixelSize: 15; anchors.horizontalCenter: parent.horizontalCenter }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
// The result of a run: all done, a problem, or nothing changed.
|
||||
import QtQuick
|
||||
|
||||
Item {
|
||||
required property AppState app
|
||||
readonly property bool problem: app.failed.length > 0 || app.runError !== ""
|
||||
readonly property string message: {
|
||||
if (app.runError) return app.runError;
|
||||
if (!app.biosRun) return app.plan.length + (app.plan.length === 1 ? " change" : " changes") + " applied." + (app.restartNeeded ? " Some take effect after a restart." : "");
|
||||
if (app.failed.length) return "The BIOS was not changed.";
|
||||
return (app.bios && app.bios.dryRun ? "Dry run: nothing was flashed. " : "BIOS " + (app.biosInfo.newest || "") + " is staged and is written during the restart. ")
|
||||
+ "Keep the power on and don't touch the machine until it has fully started again, even if the screen stays black.";
|
||||
}
|
||||
Column {
|
||||
anchors.centerIn: parent; width: 760; spacing: 20
|
||||
Rectangle { width: 72; height: 72; radius: 36; anchors.horizontalCenter: parent.horizontalCenter; color: problem ? Theme.warnBg : Theme.goodBg
|
||||
Text { anchors.centerIn: parent; text: problem ? "!" : "✓"; color: problem ? Theme.warn : Theme.good; font.pixelSize: 36; font.weight: Font.Bold } }
|
||||
Text { text: app.runError ? "Nothing changed" : (app.biosRun && app.failed.length ? "The BIOS update stopped" : (app.failed.length ? "Done, with a problem" : "All done"))
|
||||
color: Theme.text; font.family: Theme.display; font.pixelSize: 40; font.weight: Font.DemiBold; anchors.horizontalCenter: parent.horizontalCenter }
|
||||
Text { width: parent.width; horizontalAlignment: Text.AlignHCenter; wrapMode: Text.WordWrap; color: Theme.soft; font.family: Theme.body; font.pixelSize: 17; text: message }
|
||||
Rectangle { visible: app.failed.length > 0; anchors.horizontalCenter: parent.horizontalCenter; width: ft.implicitWidth + 32; height: 44; radius: 12; color: Theme.warnBg
|
||||
Text { id: ft; anchors.centerIn: parent; color: "#f2c27a"; font.family: Theme.body; font.pixelSize: 14
|
||||
text: "Had a problem: " + app.failed.map(function (id) { return Texts.label(id); }).join(", ") + ". See the details." } }
|
||||
Row { anchors.horizontalCenter: parent.horizontalCenter; spacing: 14; topPadding: 8
|
||||
Btn { k: Input.g.back; text: "Back to the menu"; height: 52; onClicked: app.act("back") }
|
||||
Btn { visible: app.restartNeeded; k: Input.g.ok; text: "Restart now"; primary: true; height: 52; onClicked: app.backend.restart() } }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
// A button name (A, Enter, OK...), round for a controller's face buttons.
|
||||
import QtQuick
|
||||
|
||||
Rectangle {
|
||||
id: glyph
|
||||
property string k
|
||||
property bool dark: false
|
||||
height: 26; width: Math.max(26, gt.implicitWidth + 14)
|
||||
radius: Input.type === "controller" && k.length === 1 ? 13 : 7
|
||||
color: dark ? Theme.ink : Theme.key
|
||||
Text { id: gt; anchors.centerIn: parent; text: glyph.k; color: glyph.dark ? Theme.accent : Theme.textHi
|
||||
font.family: Theme.body; font.pixelSize: 12; font.weight: Font.DemiBold }
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
// Logo, the screen's name (the version on the menu) and the machine.
|
||||
import QtQuick
|
||||
|
||||
Rectangle {
|
||||
required property AppState app
|
||||
required property string iconUrl
|
||||
readonly property var titles: ({ review: "Review", password: "Password", applying: app.biosRun ? "BIOS update" : "Applying",
|
||||
done: "Done", bios1: "BIOS update", bios2: "BIOS update" })
|
||||
width: parent.width; height: 72; color: Theme.bar
|
||||
Rectangle { anchors.bottom: parent.bottom; width: parent.width; height: 1; color: Theme.line }
|
||||
Row {
|
||||
anchors.left: parent.left; anchors.leftMargin: 40; anchors.verticalCenter: parent.verticalCenter; spacing: 14
|
||||
Image { source: iconUrl; width: 40; height: 40; sourceSize: Qt.size(80, 80); anchors.verticalCenter: parent.verticalCenter }
|
||||
Text { text: "Steamify"; color: Theme.text; font.family: Theme.display; font.pixelSize: 28; font.weight: Font.Bold; anchors.verticalCenter: parent.verticalCenter }
|
||||
Text { readonly property bool menu: app.screen === "menu"
|
||||
text: menu ? "v" + (app.status.version || "") : "/ " + titles[app.screen]
|
||||
color: Theme.faint; font.family: menu ? Theme.mono : Theme.body; font.pixelSize: menu ? 13 : 15; anchors.verticalCenter: parent.verticalCenter }
|
||||
}
|
||||
Rectangle {
|
||||
visible: app.status.steamMachine === true
|
||||
anchors.right: parent.right; anchors.rightMargin: 40; anchors.verticalCenter: parent.verticalCenter
|
||||
height: 34; radius: 17; color: "#1b2330"; width: mt.implicitWidth + 28
|
||||
Text { id: mt; anchors.centerIn: parent; text: "Steam Machine · CachyOS"; color: Theme.label; font.family: Theme.body; font.pixelSize: 14 }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
// The input in use (controller, keyboard or remote) and the button names
|
||||
// shown for it.
|
||||
pragma Singleton
|
||||
import QtQuick
|
||||
|
||||
QtObject {
|
||||
property string type: "keyboard"
|
||||
readonly property var glyphs: ({
|
||||
controller: { toggle: "A", choose: "◀ ▶", reapply: "Y", quit: "B", apply: "X", ok: "A", back: "B" },
|
||||
keyboard: { toggle: "Space", choose: "← →", reapply: "R", quit: "Esc", apply: "Enter", ok: "Enter", back: "Esc" },
|
||||
remote: { toggle: "OK", choose: "◀ ▶", reapply: "Red", quit: "Back", apply: "↓ Apply", ok: "OK", back: "Back" }
|
||||
})
|
||||
readonly property var g: glyphs[type]
|
||||
}
|
||||
+24
-1110
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,80 @@
|
||||
// One menu item: name, badge and hint on the left; its control (switch,
|
||||
// Gaming/Desktop choice or action button) on the right. Sub-items fold in
|
||||
// and out with their parent.
|
||||
import QtQuick
|
||||
|
||||
Item {
|
||||
id: row
|
||||
required property var modelData
|
||||
required property int index
|
||||
required property AppState app
|
||||
readonly property var it: modelData
|
||||
readonly property bool shown: !it.parent || !!app.want[it.parent]
|
||||
readonly property int rowIndex: app.rows.findIndex(function (r) { return r.id === it.id; })
|
||||
readonly property bool selected: shown && rowIndex === app.sel
|
||||
readonly property bool on: !!app.want[it.id]
|
||||
// Greyed out: an action with nothing to do, or an option this machine
|
||||
// can't turn on.
|
||||
readonly property bool unavailable: (it.kind === "action" && !(app.bios && app.bios.selectable)) || (it.kind === "toggle" && it.selectable === false)
|
||||
readonly property string hint: it.id === "bios" ? app.biosHint()
|
||||
: it.selectable === false ? it.hint
|
||||
: ((Texts.items[it.id] || {}).hint || it.hint)
|
||||
|
||||
height: shown ? 62 : 0
|
||||
opacity: shown ? 1 : 0
|
||||
clip: true
|
||||
Behavior on height { NumberAnimation { duration: 160; easing.type: Easing.OutCubic } }
|
||||
Behavior on opacity { NumberAnimation { duration: 160 } }
|
||||
|
||||
Rectangle {
|
||||
width: parent.width; height: 56; radius: 12
|
||||
color: row.selected ? Theme.cardSel : Theme.card
|
||||
border.width: row.selected ? 2 : 0; border.color: Theme.accent
|
||||
MouseArea { anchors.fill: parent; cursorShape: Qt.PointingHandCursor
|
||||
onClicked: { Input.type = "keyboard"; if (row.selected) app.act("accept"); else app.sel = row.rowIndex; } }
|
||||
|
||||
Text { visible: !!row.it.parent; x: 20; anchors.verticalCenter: parent.verticalCenter; text: "└"; color: "#56627a"; font.pixelSize: 18 }
|
||||
Column {
|
||||
anchors.verticalCenter: parent.verticalCenter; spacing: 2
|
||||
x: row.it.parent ? 46 : 16
|
||||
width: controls.x - x - 16
|
||||
opacity: row.unavailable ? 0.6 : 1
|
||||
Row { width: parent.width; spacing: 10
|
||||
Text { text: app.label(row.it); color: Theme.textHi; font.family: Theme.body; font.pixelSize: 17; font.weight: Font.DemiBold; elide: Text.ElideRight
|
||||
width: Math.min(implicitWidth, parent.width - (badge.visible ? badge.width + parent.spacing : 0)) }
|
||||
Badge { id: badge; update: !!row.it.update; isNew: !!row.it["new"]; anchors.verticalCenter: parent.verticalCenter } }
|
||||
Text { text: row.hint; color: Theme.mute; font.family: Theme.body; font.pixelSize: 13; elide: Text.ElideRight; width: parent.width }
|
||||
}
|
||||
|
||||
// Every control ends on the same edge.
|
||||
Item {
|
||||
id: controls
|
||||
opacity: row.it.kind === "toggle" && row.it.selectable === false ? 0.4 : 1
|
||||
anchors.right: parent.right; anchors.rightMargin: 16; anchors.verticalCenter: parent.verticalCenter
|
||||
width: 180; height: 32
|
||||
// choice
|
||||
Rectangle { visible: row.it.kind === "choice"; anchors.right: parent.right; anchors.verticalCenter: parent.verticalCenter
|
||||
width: 172; height: 32; radius: 10; color: Theme.bg
|
||||
Row { anchors.centerIn: parent; spacing: 4
|
||||
Repeater { model: [["gamescope", "Gaming"], ["desktop", "Desktop"]]
|
||||
Rectangle { required property var modelData
|
||||
readonly property bool picked: app.boot === modelData[0]
|
||||
width: 80; height: 26; radius: 7; color: picked ? Theme.accent : "transparent"
|
||||
Text { anchors.centerIn: parent; text: parent.modelData[1]; color: parent.picked ? Theme.ink : Theme.mute
|
||||
font.family: Theme.body; font.pixelSize: 13; font.weight: Font.DemiBold }
|
||||
MouseArea { anchors.fill: parent; onClicked: app.boot = parent.modelData[0] } } } } }
|
||||
// toggle: "now on/off" left of the switch
|
||||
Rectangle { id: sw; visible: row.it.kind === "toggle"; anchors.right: parent.right; anchors.verticalCenter: parent.verticalCenter
|
||||
width: 46; height: 26; radius: 13; color: row.on ? Theme.accent : "#343f50"
|
||||
Behavior on color { ColorAnimation { duration: 120 } }
|
||||
Rectangle { width: 20; height: 20; radius: 10; color: "#f4f7fb"; y: 3; x: row.on ? 23 : 3; Behavior on x { NumberAnimation { duration: 120 } } } }
|
||||
Text { visible: row.it.kind === "toggle"; anchors.right: sw.left; anchors.rightMargin: 12; anchors.verticalCenter: parent.verticalCenter
|
||||
text: row.it.on ? "now on" : "now off"; color: row.it.on ? Theme.good : Theme.faint; font.family: Theme.mono; font.pixelSize: 12 }
|
||||
// action (Update BIOS)
|
||||
Rectangle { visible: row.it.kind === "action"; anchors.right: parent.right; anchors.verticalCenter: parent.verticalCenter
|
||||
width: at.implicitWidth + 24; height: 30; radius: 8; color: Theme.warnBg
|
||||
Text { id: at; anchors.centerIn: parent; color: Theme.warn; font.family: Theme.body; font.pixelSize: 13; font.weight: Font.DemiBold
|
||||
text: app.biosChecking ? "Checking…" : app.bios && app.bios.selectable ? "Update…" : (app.bios && app.bios.newest ? "Up to date" : "Unavailable") } }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
// The menu: every item on the left, what the selected one does and the
|
||||
// system on the right, Review & apply at the bottom.
|
||||
import QtQuick
|
||||
import QtQuick.Controls.Basic
|
||||
|
||||
Item {
|
||||
id: screen
|
||||
required property AppState app
|
||||
readonly property var g: Input.g
|
||||
|
||||
Text {
|
||||
visible: app.items.length === 0
|
||||
anchors.centerIn: parent; color: Theme.mute; font.family: Theme.body; font.pixelSize: 18
|
||||
text: app.status.error ? "Couldn't read the current state:\n" + app.status.error : "Checking what's on…"
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
|
||||
Item {
|
||||
visible: app.items.length > 0
|
||||
x: 40; y: 28; width: parent.width - 80; height: parent.height - 28 - 64 - 20
|
||||
Column {
|
||||
width: 740; height: parent.height; spacing: 10
|
||||
Text { text: "What do you want?"; color: Theme.text; font.family: Theme.display; font.pixelSize: 30; font.weight: Font.DemiBold }
|
||||
ListView {
|
||||
id: list; width: parent.width; height: parent.height - 50; clip: true
|
||||
// A fixed model: sub-items fold in and out instead of the
|
||||
// list being rebuilt (and jumping) on every tick.
|
||||
model: app.items
|
||||
ScrollBar.vertical: ScrollBar { policy: ScrollBar.AsNeeded }
|
||||
delegate: MenuRow {
|
||||
app: screen.app
|
||||
width: list.width - 12; x: 2
|
||||
onSelectedChanged: if (selected) Qt.callLater(list.positionViewAtIndex, index, ListView.Contain)
|
||||
}
|
||||
}
|
||||
}
|
||||
Column {
|
||||
x: 772; width: parent.width - 772; height: parent.height; spacing: 16
|
||||
DetailPanel { app: screen.app; width: parent.width; height: parent.height - sys.height - 16 }
|
||||
Rectangle {
|
||||
id: sys; width: parent.width; height: 82; radius: 16; color: Theme.card
|
||||
Grid { anchors.fill: parent; anchors.margins: 16; anchors.leftMargin: 20; anchors.rightMargin: 20; columns: 2; columnSpacing: 20; rowSpacing: 10
|
||||
Repeater {
|
||||
model: [["Kernel", (app.status.kernel || "").replace("-cachyos", "")],
|
||||
["LED bar", app.status.steamMachine ? (app.status.leds || 0) + " LEDs" : "—"],
|
||||
["HDMI-CEC", app.status.cecDevices ? "/dev/" + app.status.cecDevices.split(" ")[0] : "none"],
|
||||
["Version", app.status.version || ""]]
|
||||
Row { required property var modelData; width: (sys.width - 60) / 2
|
||||
Text { text: parent.modelData[0]; color: Theme.faint; font.family: Theme.body; font.pixelSize: 13; width: parent.width / 2 }
|
||||
Text { text: parent.modelData[1]; color: Theme.text; font.family: Theme.mono; font.pixelSize: 13; width: parent.width / 2
|
||||
horizontalAlignment: Text.AlignRight; elide: Text.ElideLeft } }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Bar {
|
||||
Row {
|
||||
anchors.left: parent.left; anchors.leftMargin: 40; anchors.verticalCenter: parent.verticalCenter; spacing: 28
|
||||
Repeater {
|
||||
model: [[screen.g.toggle, "Toggle"], [screen.g.choose, "Choose"], [screen.g.reapply, "Re-apply what's on"], [screen.g.quit, "Quit"]]
|
||||
Row { required property var modelData; spacing: 8
|
||||
Glyph { k: parent.modelData[0]; anchors.verticalCenter: parent.verticalCenter }
|
||||
Text { text: parent.modelData[1]; color: Theme.label; font.family: Theme.body; font.pixelSize: 14; anchors.verticalCenter: parent.verticalCenter } }
|
||||
}
|
||||
}
|
||||
Row {
|
||||
anchors.right: parent.right; anchors.rightMargin: 40; anchors.verticalCenter: parent.verticalCenter; spacing: 16
|
||||
Text { readonly property int n: app.computePlan().length
|
||||
text: n === 0 ? "Everything is the way you want it" : n + (n === 1 ? " change" : " changes")
|
||||
color: Theme.faint; font.family: Theme.body; font.pixelSize: 14; anchors.verticalCenter: parent.verticalCenter }
|
||||
Btn { k: screen.g.apply; text: "Review & apply"; primary: true; focusRing: app.sel === app.rows.length; height: 44; onClicked: app.goReview(false) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
// The sudo password, asked once per run; never stored.
|
||||
import QtQuick
|
||||
import QtQuick.Controls.Basic
|
||||
|
||||
Rectangle {
|
||||
id: screen
|
||||
required property AppState app
|
||||
color: "#0c1016"
|
||||
function submit() { var p = pw.text; pw.text = ""; app.submitPassword(p); }
|
||||
|
||||
Connections {
|
||||
target: screen.app
|
||||
function onPasswordRequested() { pw.text = ""; pw.shown = false; pw.forceActiveFocus(); }
|
||||
function onPasswordRejected() { pw.forceActiveFocus(); }
|
||||
function onPasswordSubmitRequested() { screen.submit(); }
|
||||
}
|
||||
Binding { target: screen.app; property: "typing"; value: pw.activeFocus; when: screen.visible }
|
||||
|
||||
Rectangle {
|
||||
anchors.centerIn: parent; width: 560; height: col.implicitHeight + 72; radius: 20; color: Theme.card; border.width: 1; border.color: Theme.line
|
||||
Column {
|
||||
id: col; anchors.fill: parent; anchors.margins: 36; anchors.leftMargin: 40; anchors.rightMargin: 40; spacing: 18
|
||||
Row { spacing: 14
|
||||
Rectangle { width: 48; height: 48; radius: 14; color: Theme.infoBg
|
||||
// A padlock.
|
||||
Canvas { anchors.centerIn: parent; width: 24; height: 24
|
||||
onPaint: { var c = getContext("2d"); c.strokeStyle = Theme.info; c.lineWidth = 1.8; c.beginPath(); c.roundedRect(4.5, 10.5, 15, 10, 2.5, 2.5); c.stroke();
|
||||
c.beginPath(); c.moveTo(8, 10.5); c.lineTo(8, 8); c.arc(12, 8, 4, Math.PI, 0); c.lineTo(16, 10.5); c.stroke(); } } }
|
||||
Column { spacing: 2; anchors.verticalCenter: parent.verticalCenter
|
||||
Text { text: "Your password, once"; color: Theme.text; font.family: Theme.display; font.pixelSize: 30; font.weight: Font.DemiBold }
|
||||
Text { text: "Steamify needs it to change system settings."; color: Theme.mute; font.family: Theme.body; font.pixelSize: 14 } }
|
||||
}
|
||||
Text { text: "Password for " + app.backend.user(); color: Theme.soft; font.family: Theme.body; font.pixelSize: 14; font.weight: Font.DemiBold }
|
||||
TextField {
|
||||
id: pw; width: parent.width; height: 52; echoMode: shown ? TextInput.Normal : TextInput.Password
|
||||
property bool shown: false
|
||||
color: Theme.textHi; font.family: Theme.body; font.pixelSize: 18; leftPadding: 16; rightPadding: 56
|
||||
placeholderText: "Password"; placeholderTextColor: "#6b778a"
|
||||
background: Rectangle { radius: 12; color: Theme.bg; border.width: 2; border.color: app.wrongPassword ? Theme.bad : Theme.accent }
|
||||
Keys.onReturnPressed: screen.submit()
|
||||
Keys.onEnterPressed: screen.submit()
|
||||
Keys.onEscapePressed: app.act("back")
|
||||
Text { anchors.right: parent.right; anchors.rightMargin: 16; anchors.verticalCenter: parent.verticalCenter; text: pw.shown ? "Hide" : "Show"
|
||||
color: Theme.label; font.family: Theme.body; font.pixelSize: 13
|
||||
MouseArea { anchors.fill: parent; anchors.margins: -10; onClicked: pw.shown = !pw.shown } }
|
||||
}
|
||||
Text { visible: app.wrongPassword; text: "That password didn't work. Try again."; color: Theme.bad; font.family: Theme.body; font.pixelSize: 14 }
|
||||
Rectangle { width: parent.width; height: 46; radius: 12; color: Theme.note
|
||||
Text { anchors.verticalCenter: parent.verticalCenter; x: 14; color: Theme.soft; font.family: Theme.body; font.pixelSize: 14
|
||||
text: ({ controller: "Press Steam + X for the on-screen keyboard.", keyboard: "Type your password and press Enter.", remote: "Select the field for the on-screen keyboard." })[Input.type] } }
|
||||
Item { width: parent.width; height: 48
|
||||
Btn { k: Input.g.back; text: "Back"; onClicked: app.act("back") }
|
||||
Btn { anchors.right: parent.right; k: Input.g.ok; text: "Apply"; primary: true; onClicked: screen.submit() } }
|
||||
Text { text: "Only used for this run. Steamify never stores it."; color: Theme.faint; font.family: Theme.body; font.pixelSize: 12; anchors.horizontalCenter: parent.horizontalCenter }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
// What a run will change, with a summary; Apply asks for the password.
|
||||
import QtQuick
|
||||
import QtQuick.Controls.Basic
|
||||
|
||||
Item {
|
||||
required property AppState app
|
||||
Column {
|
||||
x: 40; y: 32; width: 740; spacing: 14
|
||||
Text { text: app.plan.length === 0 ? "Nothing to change" : "This will"; color: Theme.text; font.family: Theme.display; font.pixelSize: 34; font.weight: Font.DemiBold }
|
||||
ListView {
|
||||
width: 740; clip: true; spacing: 8
|
||||
height: Math.min(contentHeight, parent.parent.height - 32 - 50 - 14 - 66 - 64 - 32)
|
||||
model: app.plan
|
||||
ScrollBar.vertical: ScrollBar { policy: ScrollBar.AsNeeded }
|
||||
delegate: Rectangle {
|
||||
id: step
|
||||
required property var modelData
|
||||
readonly property var st: Texts.actions[modelData.action] || ["", Theme.text, Theme.card]
|
||||
width: 740; height: 56; radius: 12; color: Theme.card
|
||||
Row { anchors.fill: parent; anchors.leftMargin: 18; spacing: 14
|
||||
Chip { text: step.st[0]; fg: step.st[1]; bgc: step.st[2]; width: 84; anchors.verticalCenter: parent.verticalCenter }
|
||||
Text { text: Texts.label(step.modelData.id); color: Theme.textHi; font.family: Theme.body; font.pixelSize: 17; font.weight: Font.DemiBold
|
||||
anchors.verticalCenter: parent.verticalCenter }
|
||||
Badge { isNew: !!step.modelData.isNew; anchors.verticalCenter: parent.verticalCenter }
|
||||
}
|
||||
}
|
||||
}
|
||||
Rectangle { width: 740; height: 52; radius: 12; color: Theme.note
|
||||
Text { anchors.verticalCenter: parent.verticalCenter; x: 18; text: "Your password is asked once. Changes to how the PC starts need a restart."
|
||||
color: Theme.soft; font.family: Theme.body; font.pixelSize: 14 } }
|
||||
}
|
||||
Rectangle {
|
||||
x: parent.width - 468; y: 32; width: 428; height: 250; radius: 16; color: Theme.card
|
||||
Column { anchors.fill: parent; anchors.margins: 24; spacing: 12
|
||||
Text { text: "SUMMARY"; color: Theme.faint; font.family: Theme.body; font.pixelSize: 12; font.weight: Font.DemiBold; font.letterSpacing: 0.8 }
|
||||
Repeater { model: [["Turn on", "on"], ["Update", "update"], ["Re-apply", "again"], ["Turn off", "off"]]
|
||||
Row { required property var modelData; width: 380
|
||||
Text { text: parent.modelData[0]; color: Theme.soft; font.family: Theme.body; font.pixelSize: 15; width: 300 }
|
||||
Text { readonly property string action: parent.modelData[1]
|
||||
text: app.plan.filter(function (p) { return p.action === action; }).length
|
||||
color: Theme.text; font.family: Theme.mono; font.pixelSize: 15; width: 80; horizontalAlignment: Text.AlignRight } } }
|
||||
}
|
||||
}
|
||||
Bar {
|
||||
Btn { anchors.left: parent.left; anchors.leftMargin: 40; anchors.verticalCenter: parent.verticalCenter; k: Input.g.back; text: "Back to the menu"; onClicked: app.act("back") }
|
||||
Btn { anchors.right: parent.right; anchors.rightMargin: 40; anchors.verticalCenter: parent.verticalCenter; k: Input.g.ok; text: "Apply"; primary: true
|
||||
visible: app.plan.length > 0; onClicked: app.onApplyPressed() }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
// The BIOS update's risks.
|
||||
import QtQuick
|
||||
|
||||
Column {
|
||||
id: list
|
||||
spacing: 8
|
||||
Repeater {
|
||||
model: ["A failed or interrupted BIOS update can leave the machine unable to start (bricked). Steamify, CachyOS and Valve take no responsibility for that.",
|
||||
"NEVER turn off the power, unplug the machine or press the power button while it updates, including the restart(s) afterwards.",
|
||||
"The screen can stay black for several minutes. Wait.",
|
||||
"Only on mains power, with all other programs closed."]
|
||||
Row { required property string modelData; spacing: 10; width: list.width
|
||||
Rectangle { width: 6; height: 6; radius: 3; color: Theme.bad; y: 9 }
|
||||
Text { text: parent.modelData; width: parent.width - 16; wrapMode: Text.WordWrap; color: Theme.soft
|
||||
font.family: Theme.body; font.pixelSize: 15; lineHeight: 1.3 } }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
// Texts per item; the backend's labels are the fallback.
|
||||
pragma Singleton
|
||||
import QtQuick
|
||||
|
||||
QtObject {
|
||||
readonly property var items: ({
|
||||
gaming: { label: "SteamOS conversion", hint: "Boot into gaming mode, Steam on the desktop",
|
||||
body: "Boots straight into gaming mode. Switch to Desktop in Steam works, and Return to Gaming Mode on the desktop brings you back.",
|
||||
changes: ["Autologin into gamescope", "Return to Gaming Mode shortcut", "Steam starts silently on the desktop"] },
|
||||
boot: { label: "Boot into", hint: "Where the PC starts",
|
||||
body: "Where the PC starts after a restart. Switching back and forth works the same either way.",
|
||||
changes: ["Desktop: the Plasma session is set before login", "Gaming: the SteamOS default"] },
|
||||
theme: { label: "SteamOS theme", hint: "Vapor look for the desktop, Add to Steam",
|
||||
body: "The Vapor look of SteamOS for the desktop, with its panel, launcher icon and wallpaper.",
|
||||
changes: ["cachyos-vapor global theme", "Add to Steam in right-click menus", "Nested Desktop, Steam keyboard window rule"] },
|
||||
glyphs: { label: "Steam Deck/Machine icons", hint: "Deck button icons in gaming mode",
|
||||
body: "Steam shows Steam Deck button icons in gaming mode.", changes: ["STEAM_GAMEPADUI_ARGS -steamos3"] },
|
||||
single: { label: "Single user mode", hint: "No password, lock screen or wallet prompts",
|
||||
body: "Like SteamOS: never a login or lock screen, and no KDE wallet password prompts. Typing a password with a controller is no fun.",
|
||||
changes: ["SDDM autologin, no lock screen or user switching", "Valve's empty wallet; your own is kept aside", "Launcher shows Sleep, Restart, Shut Down only"] },
|
||||
launcher: { label: "Steamify shortcut", hint: "The app on the desktop and in the launcher",
|
||||
body: "Opens the newest Steamify app, so you never need the install command again. Steamify Terminal in the launcher opens the terminal menu.",
|
||||
changes: ["Steamify CachyOS on the desktop and in the launcher (the app)", "Steamify Terminal in the launcher"] },
|
||||
vram: { label: "VRAM booster", hint: "The game in front keeps its VRAM",
|
||||
body: "Like SteamOS 3.9: the game you're playing keeps its video memory, and background apps are moved out first. Without it a game that needs most of the video memory can spill into system RAM and stutter. Shown for GPUs whose driver supports it (AMD, Intel) on kernel 7.2 or newer.",
|
||||
changes: ["dmemcg-booster (system and user service)", "plasma-foreground-booster, which tells it which window is in front", "Off: removed again, unless you had installed them yourself"] },
|
||||
cec: { label: "HDMI-CEC", hint: "Use Steam with the TV remote, TV on/off with the PC", experimental: true,
|
||||
body: "Use Steam with your TV remote, and the TV turns on and off with the PC. Turn on CEC on the TV too (Sony: BRAVIA Sync, Samsung: Anynet+, LG: SimpLink).",
|
||||
changes: ["Valve's cecd and cec-audio-control", "HDMI CEC section in Steam's Display settings", "Volume buttons for the TV in the Quick Access menu"] },
|
||||
machine: { label: "Steam Machine support", hint: "LED bar, fan and performance settings in Steam",
|
||||
body: "The front LED bar works, Steam's hardware settings work, and the power button puts it to sleep like a console.",
|
||||
changes: ["leds-valve driver for every kernel (DKMS)", "steamos-manager for Steam's settings", "Console-like power handling"] },
|
||||
poweroff: { label: "Power-off fix", hint: "Stays off after shutting down",
|
||||
body: "With recent kernels the Steam Machine starts again right after shutting down: the firmware leaves a wake bit set, and newer kernels (7.2, and updates of 6.x, 7.0 and 7.1) no longer clear it. Valve's own kernel clears it; this small module does the same right before power-off.",
|
||||
changes: ["steamify-fremont-poweroff module for each installed kernel (DKMS)", "Only on a Steam Machine, only touches that one wake bit", "Off: recent kernels may start it again after shutting down"] },
|
||||
kpin: { label: "Pin the kernel", hint: "No longer needed: untick for CachyOS's current kernel",
|
||||
body: "Kept the Steam Machine on CachyOS kernel 7.1.6, which powered off properly. The power-off fix does that on every kernel now, so the pin is removed and CachyOS's current kernel comes back.",
|
||||
changes: ["Removed from IgnorePkg", "linux-cachyos updated to CachyOS's current version"] },
|
||||
hdmi: { label: "HDMI refresh boost", hint: "No longer needed: removed",
|
||||
body: "Made HDMI displays run above 60 Hz on the pinned kernel. Current kernels do that themselves, so what's left of it is removed and every display uses its own EDID again.",
|
||||
changes: ["Saved display EDIDs and the hotplug service removed", "The old kernel parameter removed, if it's still there"] },
|
||||
bios: { label: "Update BIOS", hint: "",
|
||||
body: "Installs Valve's newest Steam Machine BIOS, at your own risk. It checks Valve's checksum and asks fwupd whether the file fits this machine, then warns you twice before anything is written.",
|
||||
changes: ["Valve's fremont-hw-support package (checksum checked)", "fwupd writes the BIOS during the next restart", "Keep the power on until the machine has fully started again"] }
|
||||
})
|
||||
// Replaces the explanation of an option that can't be turned on here.
|
||||
readonly property var unsupported: ({
|
||||
vram: { body: "Not available with your NVIDIA graphics card: NVIDIA's driver doesn't tell Linux how its video memory is used yet, so there's nothing to steer. It works with AMD and Intel graphics cards, and shows up here as soon as NVIDIA's driver supports it.",
|
||||
changes: ["Nothing: can't be turned on with NVIDIA's driver"] }
|
||||
})
|
||||
|
||||
// Per plan action: the review's chip [text, colour, background] and the
|
||||
// progress screen's verb.
|
||||
readonly property var actions: ({
|
||||
on: ["Turn on", Theme.good, Theme.goodBg, "Turn on "], off: ["Turn off", Theme.bad, Theme.badBg, "Turn off "],
|
||||
again: ["Re-apply", Theme.info, Theme.infoBg, "Re-apply "], update: ["Update", Theme.warn, Theme.warnBg, ""],
|
||||
desktop: ["Desktop", Theme.info, Theme.infoBg, "Boot into "], gaming: ["Gaming", Theme.info, Theme.infoBg, "Boot into "],
|
||||
check: ["Check", Theme.warn, Theme.warnBg, "Download and check the "], flash: ["Flash", Theme.bad, Theme.badBg, "Hand to fwupd: the "]
|
||||
})
|
||||
|
||||
function of(item) {
|
||||
var tx = items[item.id] || {};
|
||||
return item.selectable === false && unsupported[item.id] ? Object.assign({}, tx, unsupported[item.id]) : tx;
|
||||
}
|
||||
function label(id, fallback) { return (items[id] || {}).label || fallback || id; }
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
// Colours and fonts (from the design).
|
||||
pragma Singleton
|
||||
import QtQuick
|
||||
|
||||
QtObject {
|
||||
readonly property color bg: "#0f1319"
|
||||
readonly property color bar: "#141a22"
|
||||
readonly property color line: "#232c38"
|
||||
readonly property color card: "#161d27"
|
||||
readonly property color cardSel: "#1f2a3a"
|
||||
readonly property color text: "#e6ebf2"
|
||||
readonly property color textHi: "#eef2f7"
|
||||
readonly property color soft: "#c3ccd8"
|
||||
readonly property color mute: "#97a3b4"
|
||||
readonly property color faint: "#8b97a8"
|
||||
readonly property color label: "#b8c3d1"
|
||||
readonly property color accent: "#3ea6ff"
|
||||
readonly property color ink: "#06121f"
|
||||
readonly property color good: "#7fd1a8"
|
||||
readonly property color goodBg: "#173226"
|
||||
readonly property color warn: "#f2a33a"
|
||||
readonly property color warnBg: "#2a2116"
|
||||
readonly property color bad: "#ff9a8a"
|
||||
readonly property color badBg: "#2a1c1c"
|
||||
readonly property color info: "#7cc4ff"
|
||||
readonly property color infoBg: "#1b2b40"
|
||||
readonly property color note: "#1a2230"
|
||||
readonly property color key: "#2a3444"
|
||||
readonly property color button: "#232c38"
|
||||
readonly property color danger: "#8a2c2c"
|
||||
readonly property color dangerBg: "#1d1416"
|
||||
readonly property color dangerLine: "#b33a3a"
|
||||
readonly property string display: "Barlow Semi Condensed"
|
||||
readonly property string body: "IBM Plex Sans"
|
||||
readonly property string mono: "IBM Plex Mono"
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
singleton Theme 1.0 Theme.qml
|
||||
singleton Texts 1.0 Texts.qml
|
||||
singleton Input 1.0 Input.qml
|
||||
AppState 1.0 AppState.qml
|
||||
Glyph 1.0 Glyph.qml
|
||||
Btn 1.0 Btn.qml
|
||||
Chip 1.0 Chip.qml
|
||||
Badge 1.0 Badge.qml
|
||||
Bar 1.0 Bar.qml
|
||||
RiskList 1.0 RiskList.qml
|
||||
Header 1.0 Header.qml
|
||||
MenuRow 1.0 MenuRow.qml
|
||||
DetailPanel 1.0 DetailPanel.qml
|
||||
MenuScreen 1.0 MenuScreen.qml
|
||||
ReviewScreen 1.0 ReviewScreen.qml
|
||||
PasswordScreen 1.0 PasswordScreen.qml
|
||||
ApplyingScreen 1.0 ApplyingScreen.qml
|
||||
BiosWarningScreen 1.0 BiosWarningScreen.qml
|
||||
BiosConfirmScreen 1.0 BiosConfirmScreen.qml
|
||||
DoneScreen 1.0 DoneScreen.qml
|
||||
Reference in new issue
Block a user