mirror of
https://github.com/theupriser/steamify-cachyos.git
synced 2026-10-03 17:41:58 +02:00
feat: Steamify app (v2 UI), first working version
ui/steamify-ui (PySide6 + QML): menu, review, password, applying and done screens from the design, scaled to the window. Controller via evdev, keyboard, TV remote (cecd). steamify.sh --backend status|apply (lib/backend.sh) drives the same components; sudo asks through an askpass helper.
This commit is contained in:
1 parent
e09fe1609b
commit
43a9541104
5 files changed
+1062
-4
No files matched your search
+572
@@ -0,0 +1,572 @@
|
||||
// Steamify v2: menu -> review -> password -> applying -> done.
|
||||
// Designed at 1280x720 and scaled to the window, so it fits a TV too.
|
||||
import QtQuick
|
||||
import QtQuick.Window
|
||||
import QtQuick.Controls.Basic
|
||||
|
||||
ApplicationWindow {
|
||||
id: win
|
||||
required property var backend
|
||||
required property var gamepad
|
||||
required property bool fullscreen
|
||||
required property string iconUrl
|
||||
|
||||
width: 1280; height: 720
|
||||
minimumWidth: 960; minimumHeight: 540
|
||||
visibility: fullscreen ? Window.FullScreen : Window.Windowed
|
||||
title: "Steamify"
|
||||
color: t.bg
|
||||
|
||||
// --- Look (from the design) ---
|
||||
QtObject {
|
||||
id: t
|
||||
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 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 key: "#2a3444"
|
||||
readonly property string display: "Barlow Semi Condensed"
|
||||
readonly property string body: "IBM Plex Sans"
|
||||
readonly property string mono: "IBM Plex Mono"
|
||||
}
|
||||
|
||||
// --- Texts per item (the backend's labels are the fallback) ---
|
||||
readonly property var texts: ({
|
||||
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: "Icon on the desktop and in the launcher",
|
||||
body: "Opens the newest Steamify, so you never need the install command again.", changes: ["Desktop icon and launcher entry"] },
|
||||
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"] },
|
||||
kpin: { label: "Pin the kernel", hint: "Fixes rebooting after shutdown",
|
||||
body: "Newer CachyOS kernels make the Steam Machine reboot instead of shutting down. Untick once CachyOS fixes that.",
|
||||
changes: ["linux-cachyos from Steamify's release (signature checked)", "Kept in /var/cache/steamify/kernel", "Added to IgnorePkg"] },
|
||||
bios: { label: "Update BIOS", hint: "Runs in the terminal version (two warnings, typed confirmation)",
|
||||
body: "Installs Valve's newest Steam Machine BIOS, at your own risk. The app leaves this to the terminal version, which asks twice and wants UPDATE typed.",
|
||||
changes: ["Run steamify.sh in Konsole for this"] }
|
||||
})
|
||||
|
||||
// --- State ---
|
||||
property string screen: "menu" // menu review password applying done
|
||||
property string inputType: "keyboard" // controller keyboard remote
|
||||
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}]
|
||||
property var steps: ({}) // id -> "wait"|"run"|"ok"|"fail"
|
||||
property var failed: []
|
||||
property bool restartNeeded: false
|
||||
property string runError: ""
|
||||
property bool wrongPassword: false
|
||||
property int doneCount: 0
|
||||
ListModel { id: 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]; })
|
||||
|
||||
function nowOn(id) {
|
||||
for (var i = 0; i < items.length; i++) if (items[i].id === id) return items[i].on;
|
||||
return false;
|
||||
}
|
||||
function label(it) { return (texts[it.id] && texts[it.id].label) || it.label; }
|
||||
function syncFromStatus() {
|
||||
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 (screen === "menu" || screen === "done") syncFromStatus(); } }
|
||||
|
||||
function toggle(id) {
|
||||
var w = Object.assign({}, want);
|
||||
w[id] = !w[id];
|
||||
if (id === "gaming" && !w.gaming) { w.single = false; }
|
||||
if (id === "single" && w.single) w.gaming = true;
|
||||
if (id === "machine") w.kpin = w.machine;
|
||||
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" });
|
||||
else if (!want[it.id] && it.on) p.push({ id: it.id, action: "off" });
|
||||
else if (want[it.id] && reapply) p.push({ id: it.id, action: "again" });
|
||||
}
|
||||
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;
|
||||
}
|
||||
function startApply(password) {
|
||||
steps = {}; failed = []; restartNeeded = false; runError = ""; doneCount = 0; logModel.clear();
|
||||
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 onApplyPressed() {
|
||||
if (plan.length === 0) return;
|
||||
if (backend.needsPassword()) { wrongPassword = false; pw.text = ""; screen = "password"; pw.forceActiveFocus(); }
|
||||
else startApply("");
|
||||
}
|
||||
function submitPassword() {
|
||||
if (!backend.checkPassword(pw.text)) { wrongPassword = true; pw.selectAll(); return; }
|
||||
var p = pw.text; pw.text = "";
|
||||
startApply(p);
|
||||
}
|
||||
Connections {
|
||||
target: backend
|
||||
function onEvent(ev) {
|
||||
var s = Object.assign({}, steps);
|
||||
if (ev.event === "start") { s[ev.id] = "run"; steps = s; }
|
||||
else if (ev.event === "done") { s[ev.id] = ev.ok ? "ok" : "fail"; steps = s; doneCount++; }
|
||||
else if (ev.event === "log") { logModel.append({ line: ev.line }); if (logModel.count > 400) logModel.remove(0); }
|
||||
else if (ev.event === "finished") {
|
||||
failed = ev.failed || []; restartNeeded = !!ev.restart;
|
||||
runError = ev.error === "wrong-password" ? "The password didn't work." : (ev.error ? "sudo isn't available." : "");
|
||||
screen = "done";
|
||||
}
|
||||
}
|
||||
function onRemotePressed() { remoteAt = Date.now(); inputType = "remote"; }
|
||||
}
|
||||
|
||||
// --- Input: one set of actions for controller, keyboard and remote ---
|
||||
function act(a) {
|
||||
if (screen === "menu") {
|
||||
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 === "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") screen = "menu";
|
||||
} else if (screen === "password") {
|
||||
if (a === "accept" || a === "apply") submitPassword();
|
||||
else if (a === "back") { pw.text = ""; screen = "review"; }
|
||||
} else if (screen === "done") {
|
||||
if (a === "accept" && restartNeeded) backend.restart();
|
||||
else if (a === "back" || a === "accept") { syncFromStatus(); screen = "menu"; }
|
||||
}
|
||||
}
|
||||
Connections {
|
||||
target: gamepad
|
||||
function onButton(b) {
|
||||
inputType = "controller";
|
||||
var map = { a: "accept", b: "back", x: "apply", y: "reapply", start: "apply", up: "up", down: "down", left: "left", right: "right" };
|
||||
if (map[b]) act(map[b]);
|
||||
}
|
||||
}
|
||||
function keyAct(e) {
|
||||
var fromRemote = Date.now() - remoteAt < 400;
|
||||
if (!fromRemote) inputType = "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(inputType === "remote" || screen !== "menu" ? "accept" : "apply");
|
||||
else if (k === Qt.Key_Escape || k === Qt.Key_Back || k === Qt.Key_Backspace) act("back");
|
||||
else if (k === Qt.Key_R) act("reapply");
|
||||
else return;
|
||||
e.accepted = true;
|
||||
}
|
||||
|
||||
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[inputType]
|
||||
|
||||
component Glyph: Rectangle {
|
||||
property string k
|
||||
property bool dark: false
|
||||
height: 26; width: Math.max(26, gt.implicitWidth + 14)
|
||||
radius: inputType === "controller" && k.length === 1 ? 13 : 7
|
||||
color: dark ? t.ink : t.key
|
||||
Text { id: gt; anchors.centerIn: parent; text: parent.k; color: parent.dark ? t.accent : t.textHi; font.family: t.body; font.pixelSize: 12; font.weight: Font.DemiBold }
|
||||
}
|
||||
component Btn: Rectangle {
|
||||
property string k
|
||||
property string text
|
||||
property bool primary: false
|
||||
property bool focusRing: false
|
||||
signal clicked
|
||||
height: 48; radius: 12; width: br.implicitWidth + 36
|
||||
color: primary ? t.accent : "#232c38"
|
||||
border.width: focusRing ? 2 : 0; border.color: t.textHi
|
||||
Row { id: br; anchors.centerIn: parent; spacing: 10
|
||||
Glyph { k: parent.parent.k; dark: parent.parent.primary; anchors.verticalCenter: parent.verticalCenter }
|
||||
Text { text: parent.parent.text; color: parent.parent.primary ? t.ink : t.text; font.family: t.body; font.pixelSize: 15; font.weight: Font.DemiBold; anchors.verticalCenter: parent.verticalCenter }
|
||||
}
|
||||
MouseArea { anchors.fill: parent; cursorShape: Qt.PointingHandCursor; onClicked: parent.clicked() }
|
||||
}
|
||||
component Chip: Rectangle {
|
||||
property string text
|
||||
property color fg: t.good
|
||||
property color bgc: t.goodBg
|
||||
height: 24; radius: 6; width: ct.implicitWidth + 16; color: bgc
|
||||
Text { id: ct; anchors.centerIn: parent; text: parent.text.toUpperCase(); color: parent.fg; font.family: t.body; font.pixelSize: 12; font.weight: Font.DemiBold; font.letterSpacing: 0.6 }
|
||||
}
|
||||
|
||||
// --- The 1280x720 stage, scaled to the window ---
|
||||
Item {
|
||||
id: stage
|
||||
width: 1280; height: 720
|
||||
anchors.centerIn: parent
|
||||
scale: Math.min(win.width / 1280, win.height / 720)
|
||||
focus: true
|
||||
Keys.onPressed: function (e) { if (!(screen === "password" && pw.activeFocus && e.key !== Qt.Key_Escape && e.key !== Qt.Key_Return && e.key !== Qt.Key_Enter)) keyAct(e); }
|
||||
|
||||
// Header
|
||||
Rectangle {
|
||||
id: header; width: parent.width; height: 72; color: t.bar
|
||||
Rectangle { anchors.bottom: parent.bottom; width: parent.width; height: 1; color: t.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: t.text; font.family: t.display; font.pixelSize: 28; font.weight: Font.Bold; anchors.verticalCenter: parent.verticalCenter }
|
||||
Text { text: screen === "menu" ? "v" + (status.version || "") : "/ " + ({review: "Review", password: "Password", applying: "Applying", done: "Done"})[screen]
|
||||
color: t.faint; font.family: screen === "menu" ? t.mono : t.body; font.pixelSize: screen === "menu" ? 13 : 15; anchors.verticalCenter: parent.verticalCenter }
|
||||
}
|
||||
Rectangle {
|
||||
visible: 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: "#b8c3d1"; font.family: t.body; font.pixelSize: 14 }
|
||||
}
|
||||
}
|
||||
|
||||
// ================= MENU =================
|
||||
Item {
|
||||
visible: screen === "menu"
|
||||
anchors.top: header.bottom; anchors.bottom: parent.bottom; width: parent.width
|
||||
|
||||
Text {
|
||||
visible: items.length === 0
|
||||
anchors.centerIn: parent; color: t.mute; font.family: t.body; font.pixelSize: 18
|
||||
text: status.error ? "Couldn't read the current state:\n" + status.error : "Checking what's on…"
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
|
||||
Item {
|
||||
id: menuBody; visible: items.length > 0
|
||||
x: 40; y: 28; width: 1200; height: parent.height - 28 - 64 - 20
|
||||
// Left: the list
|
||||
Column {
|
||||
id: listCol; width: 740; height: parent.height; spacing: 10
|
||||
Row { width: parent.width
|
||||
Text { text: "What do you want?"; color: t.text; font.family: t.display; font.pixelSize: 30; font.weight: Font.DemiBold }
|
||||
}
|
||||
ListView {
|
||||
id: list; width: parent.width; height: parent.height - 50; clip: true; spacing: 6
|
||||
model: rows
|
||||
currentIndex: Math.min(sel, rows.length - 1)
|
||||
highlightFollowsCurrentItem: false
|
||||
onCurrentIndexChanged: positionViewAtIndex(currentIndex, ListView.Contain)
|
||||
ScrollBar.vertical: ScrollBar { policy: ScrollBar.AsNeeded }
|
||||
delegate: Rectangle {
|
||||
id: row
|
||||
required property var modelData
|
||||
required property int index
|
||||
readonly property bool selected: index === sel
|
||||
readonly property bool on: modelData.kind === "choice" ? true : !!want[modelData.id]
|
||||
width: list.width - 12; height: 56; radius: 12
|
||||
x: 2
|
||||
color: selected ? t.cardSel : t.card
|
||||
border.width: selected ? 2 : 0; border.color: t.accent
|
||||
opacity: modelData.kind === "action" ? 0.6 : 1
|
||||
MouseArea { anchors.fill: parent; cursorShape: Qt.PointingHandCursor
|
||||
onClicked: { inputType = "keyboard"; if (sel === row.index) act("accept"); else sel = row.index; } }
|
||||
Row {
|
||||
anchors.fill: parent; anchors.leftMargin: modelData.parent ? 22 : 16; anchors.rightMargin: 16; spacing: 14
|
||||
Text { visible: !!modelData.parent; text: "└"; color: "#56627a"; font.pixelSize: 18; anchors.verticalCenter: parent.verticalCenter }
|
||||
Column { anchors.verticalCenter: parent.verticalCenter; spacing: 2
|
||||
width: parent.width - (modelData.parent ? 40 : 0) - (modelData.kind === "choice" ? 200 : 150)
|
||||
Text { text: label(modelData); color: t.textHi; font.family: t.body; font.pixelSize: 17; font.weight: Font.DemiBold; elide: Text.ElideRight; width: parent.width }
|
||||
Text { text: (texts[modelData.id] && texts[modelData.id].hint) || modelData.hint; color: t.mute; font.family: t.body; font.pixelSize: 13; elide: Text.ElideRight; width: parent.width }
|
||||
}
|
||||
// choice
|
||||
Rectangle { visible: modelData.kind === "choice"; anchors.verticalCenter: parent.verticalCenter
|
||||
width: 170; height: 32; radius: 10; color: t.bg
|
||||
Row { anchors.centerIn: parent; spacing: 4
|
||||
Repeater { model: [["gamescope", "Gaming"], ["desktop", "Desktop"]]
|
||||
Rectangle { required property var modelData
|
||||
width: 80; height: 26; radius: 7; color: boot === modelData[0] ? t.accent : "transparent"
|
||||
Text { anchors.centerIn: parent; text: parent.modelData[1]; color: boot === parent.modelData[0] ? t.ink : t.mute; font.family: t.body; font.pixelSize: 13; font.weight: Font.DemiBold }
|
||||
MouseArea { anchors.fill: parent; onClicked: boot = parent.modelData[0] } } } } }
|
||||
// toggle
|
||||
Row { visible: modelData.kind === "toggle"; anchors.verticalCenter: parent.verticalCenter; spacing: 12
|
||||
Text { text: modelData.on ? "now on" : "now off"; color: modelData.on ? t.good : t.faint; font.family: t.mono; font.pixelSize: 12; width: 56; horizontalAlignment: Text.AlignRight; anchors.verticalCenter: parent.verticalCenter }
|
||||
Rectangle { width: 46; height: 26; radius: 13; color: row.on ? t.accent : "#343f50"; anchors.verticalCenter: parent.verticalCenter
|
||||
Rectangle { width: 20; height: 20; radius: 10; color: "#f4f7fb"; y: 3; x: row.on ? 23 : 3; Behavior on x { NumberAnimation { duration: 120 } } } } }
|
||||
// action
|
||||
Rectangle { visible: modelData.kind === "action"; anchors.verticalCenter: parent.verticalCenter
|
||||
width: at.implicitWidth + 24; height: 30; radius: 8; color: t.warnBg
|
||||
Text { id: at; anchors.centerIn: parent; text: "In the terminal"; color: t.warn; font.family: t.body; font.pixelSize: 13; font.weight: Font.DemiBold } }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Right: details + system
|
||||
Column {
|
||||
x: 772; width: parent.width - 772; height: parent.height; spacing: 16
|
||||
Rectangle {
|
||||
width: parent.width; height: parent.height - sys.height - 16; radius: 16; color: t.card
|
||||
readonly property var it: sel < rows.length ? rows[sel] : null
|
||||
readonly property var tx: it ? (texts[it.id] || {}) : {}
|
||||
Column {
|
||||
anchors.fill: parent; anchors.margins: 24; spacing: 14
|
||||
visible: parent.it !== null
|
||||
Row { spacing: 10
|
||||
Chip { text: parent.parent.parent.it ? (parent.parent.parent.it.kind === "choice" ? (boot === "desktop" ? "Desktop" : "Gaming") : (parent.parent.parent.it.kind === "action" ? "Opt-in" : (parent.parent.parent.it.on ? "On" : "Off"))) : ""
|
||||
fg: parent.parent.parent.it && parent.parent.parent.it.on ? t.good : "#b8c3d1"; bgc: parent.parent.parent.it && parent.parent.parent.it.on ? t.goodBg : t.line }
|
||||
Chip { visible: !!parent.parent.parent.tx.experimental; text: "Experimental"; fg: t.warn; bgc: t.warnBg }
|
||||
}
|
||||
Text { text: parent.parent.it ? label(parent.parent.it) : ""; color: t.text; font.family: t.display; font.pixelSize: 26; font.weight: Font.DemiBold; wrapMode: Text.WordWrap; width: parent.width }
|
||||
Text { text: parent.parent.tx.body || ""; color: t.soft; font.family: t.body; font.pixelSize: 15; lineHeight: 1.4; wrapMode: Text.WordWrap; width: parent.width }
|
||||
Text { text: "WHAT IT CHANGES"; color: t.faint; font.family: t.body; font.pixelSize: 12; font.weight: Font.DemiBold; font.letterSpacing: 0.8; topPadding: 4 }
|
||||
Repeater { model: parent.parent.tx.changes || []
|
||||
Text { required property string modelData; text: "• " + modelData; color: t.soft; font.family: t.body; font.pixelSize: 14; wrapMode: Text.WordWrap; width: parent.width } }
|
||||
}
|
||||
Column {
|
||||
anchors.centerIn: parent; spacing: 8; visible: parent.it === null
|
||||
Text { text: plan.length === 0 && computePlan().length === 0 ? "Everything is the way you want it" : "Review what will change"; color: t.text; font.family: t.display; font.pixelSize: 24; anchors.horizontalCenter: parent.horizontalCenter }
|
||||
Text { text: "then apply it"; color: t.mute; font.family: t.body; font.pixelSize: 15; anchors.horizontalCenter: parent.horizontalCenter }
|
||||
}
|
||||
}
|
||||
Rectangle {
|
||||
id: sys; width: parent.width; height: 82; radius: 16; color: t.card
|
||||
Grid { anchors.fill: parent; anchors.margins: 16; anchors.leftMargin: 20; anchors.rightMargin: 20; columns: 2; columnSpacing: 20; rowSpacing: 10
|
||||
Repeater {
|
||||
model: [["Kernel", (status.kernel || "").replace("-cachyos", "")], ["LED bar", status.steamMachine ? (status.leds || 0) + " LEDs" : "—"],
|
||||
["HDMI-CEC", status.cecDevices ? "/dev/" + status.cecDevices.split(" ")[0] : "none"], ["Version", status.version || ""]]
|
||||
Row { required property var modelData; width: (sys.width - 60) / 2
|
||||
Text { text: parent.modelData[0]; color: t.faint; font.family: t.body; font.pixelSize: 13; width: parent.width / 2 }
|
||||
Text { text: parent.modelData[1]; color: t.text; font.family: t.mono; font.pixelSize: 13; width: parent.width / 2; horizontalAlignment: Text.AlignRight; elide: Text.ElideLeft } }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Footer
|
||||
Rectangle {
|
||||
anchors.bottom: parent.bottom; width: parent.width; height: 64; color: t.bar
|
||||
Rectangle { width: parent.width; height: 1; color: t.line }
|
||||
Row {
|
||||
anchors.left: parent.left; anchors.leftMargin: 40; anchors.verticalCenter: parent.verticalCenter; spacing: 28
|
||||
Repeater {
|
||||
model: [[g.toggle, "Toggle"], [g.choose, "Choose"], [g.reapply, "Re-apply what's on"], [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: "#b8c3d1"; font.family: t.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: computePlan().length; text: n === 0 ? "Everything is the way you want it" : n + (n === 1 ? " change" : " changes"); color: t.faint; font.family: t.body; font.pixelSize: 14; anchors.verticalCenter: parent.verticalCenter }
|
||||
Btn { k: g.apply; text: "Review & apply"; primary: true; focusRing: sel === rows.length; height: 44; onClicked: goReview(false) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ================= REVIEW =================
|
||||
Item {
|
||||
visible: screen === "review"
|
||||
anchors.top: header.bottom; anchors.bottom: parent.bottom; width: parent.width
|
||||
Column {
|
||||
x: 40; y: 32; width: 740; spacing: 14
|
||||
Text { text: plan.length === 0 ? "Nothing to change" : "This will"; color: t.text; font.family: t.display; font.pixelSize: 34; font.weight: Font.DemiBold }
|
||||
Repeater {
|
||||
model: plan
|
||||
Rectangle { required property var modelData; width: 740; height: 56; radius: 12; color: t.card
|
||||
readonly property var st: ({ on: ["Turn on", t.good, t.goodBg], off: ["Turn off", t.bad, t.badBg], again: ["Re-apply", "#7cc4ff", "#1b2b40"],
|
||||
desktop: ["Desktop", "#7cc4ff", "#1b2b40"], gaming: ["Gaming", "#7cc4ff", "#1b2b40"] })[modelData.action]
|
||||
Row { anchors.fill: parent; anchors.leftMargin: 18; spacing: 14
|
||||
Chip { text: parent.parent.st[0]; fg: parent.parent.st[1]; bgc: parent.parent.st[2]; width: 84; anchors.verticalCenter: parent.verticalCenter }
|
||||
Text { text: (texts[parent.parent.modelData.id] || {}).label || parent.parent.modelData.id; color: t.textHi; font.family: t.body; font.pixelSize: 17; font.weight: Font.DemiBold; anchors.verticalCenter: parent.verticalCenter }
|
||||
}
|
||||
}
|
||||
}
|
||||
Rectangle { width: 740; height: 52; radius: 12; color: "#1a2230"
|
||||
Text { anchors.verticalCenter: parent.verticalCenter; x: 18; text: "Your password is asked once. Changes to how the PC starts need a restart."; color: t.soft; font.family: t.body; font.pixelSize: 14 } }
|
||||
}
|
||||
Rectangle {
|
||||
x: 812; y: 32; width: 428; height: 210; radius: 16; color: t.card
|
||||
Column { anchors.fill: parent; anchors.margins: 24; spacing: 12
|
||||
Text { text: "SUMMARY"; color: t.faint; font.family: t.body; font.pixelSize: 12; font.weight: Font.DemiBold; font.letterSpacing: 0.8 }
|
||||
Repeater { model: [["Turn on", "on"], ["Re-apply", "again"], ["Turn off", "off"]]
|
||||
Row { required property var modelData; width: 380
|
||||
Text { text: parent.modelData[0]; color: t.soft; font.family: t.body; font.pixelSize: 15; width: 300 }
|
||||
Text { text: plan.filter(function (p) { return p.action === parent.modelData[1]; }).length; color: t.text; font.family: t.mono; font.pixelSize: 15; width: 80; horizontalAlignment: Text.AlignRight } } }
|
||||
}
|
||||
}
|
||||
Rectangle {
|
||||
anchors.bottom: parent.bottom; width: parent.width; height: 64; color: t.bar
|
||||
Rectangle { width: parent.width; height: 1; color: t.line }
|
||||
Btn { anchors.left: parent.left; anchors.leftMargin: 40; anchors.verticalCenter: parent.verticalCenter; k: g.back; text: "Back to the menu"; onClicked: screen = "menu" }
|
||||
Btn { anchors.right: parent.right; anchors.rightMargin: 40; anchors.verticalCenter: parent.verticalCenter; k: g.ok; text: "Apply"; primary: true; visible: plan.length > 0; onClicked: onApplyPressed() }
|
||||
}
|
||||
}
|
||||
|
||||
// ================= PASSWORD =================
|
||||
Rectangle {
|
||||
visible: screen === "password"
|
||||
anchors.top: header.bottom; anchors.bottom: parent.bottom; width: parent.width; color: "#0c1016"
|
||||
Rectangle {
|
||||
anchors.centerIn: parent; width: 560; height: pwCol.implicitHeight + 72; radius: 20; color: t.card; border.width: 1; border.color: t.line
|
||||
Column {
|
||||
id: pwCol; 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: "#1b2b40"
|
||||
Canvas { anchors.centerIn: parent; width: 24; height: 24
|
||||
onPaint: { var c = getContext("2d"); c.strokeStyle = "#7cc4ff"; 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: t.text; font.family: t.display; font.pixelSize: 30; font.weight: Font.DemiBold }
|
||||
Text { text: "Steamify needs it to change system settings."; color: t.mute; font.family: t.body; font.pixelSize: 14 } }
|
||||
}
|
||||
Text { text: "Password for " + backend.user(); color: t.soft; font.family: t.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: t.textHi; font.family: t.body; font.pixelSize: 18; leftPadding: 16; rightPadding: 56
|
||||
placeholderText: "Password"; placeholderTextColor: "#6b778a"
|
||||
background: Rectangle { radius: 12; color: t.bg; border.width: 2; border.color: wrongPassword ? t.bad : t.accent }
|
||||
Keys.onReturnPressed: submitPassword()
|
||||
Keys.onEnterPressed: submitPassword()
|
||||
Keys.onEscapePressed: act("back")
|
||||
Text { anchors.right: parent.right; anchors.rightMargin: 16; anchors.verticalCenter: parent.verticalCenter; text: pw.shown ? "Hide" : "Show"; color: "#b8c3d1"; font.family: t.body; font.pixelSize: 13
|
||||
MouseArea { anchors.fill: parent; anchors.margins: -10; onClicked: pw.shown = !pw.shown } }
|
||||
}
|
||||
Text { visible: wrongPassword; text: "That password didn't work. Try again."; color: t.bad; font.family: t.body; font.pixelSize: 14 }
|
||||
Rectangle { width: parent.width; height: 46; radius: 12; color: "#1a2230"
|
||||
Text { anchors.verticalCenter: parent.verticalCenter; x: 14; color: t.soft; font.family: t.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." })[inputType] } }
|
||||
Item { width: parent.width; height: 48
|
||||
Btn { k: g.back; text: "Back"; onClicked: act("back") }
|
||||
Btn { anchors.right: parent.right; k: g.ok; text: "Apply"; primary: true; onClicked: submitPassword() } }
|
||||
Text { text: "Only used for this run. Steamify never stores it."; color: t.faint; font.family: t.body; font.pixelSize: 12; anchors.horizontalCenter: parent.horizontalCenter }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ================= APPLYING =================
|
||||
Item {
|
||||
visible: screen === "applying"
|
||||
anchors.top: header.bottom; anchors.bottom: parent.bottom; width: parent.width
|
||||
Column {
|
||||
x: 40; y: 32; width: 560; spacing: 18
|
||||
Text { text: "Applying your changes"; color: t.text; font.family: t.display; font.pixelSize: 34; font.weight: Font.DemiBold }
|
||||
Rectangle { width: 560; height: 10; radius: 5; color: t.line
|
||||
Rectangle { height: 10; radius: 5; color: t.accent; width: plan.length ? parent.width * Math.min(1, (doneCount + 0.3) / plan.length) : 0; Behavior on width { NumberAnimation { duration: 300 } } } }
|
||||
Row { width: 560
|
||||
Text { text: "Step " + Math.min(plan.length, doneCount + 1) + " of " + plan.length; color: t.mute; font.family: t.body; font.pixelSize: 14; width: 280 }
|
||||
Text { text: "Don't turn off the PC"; color: t.mute; font.family: t.body; font.pixelSize: 14; width: 280; horizontalAlignment: Text.AlignRight } }
|
||||
Repeater {
|
||||
model: plan
|
||||
Rectangle { required property var modelData; readonly property string st: steps[modelData.id] || "wait"
|
||||
width: 560; height: 54; radius: 12; color: st === "run" ? t.cardSel : t.card; border.width: st === "run" ? 2 : 0; border.color: t.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: parent.parent.st === "ok" ? t.goodBg : (parent.parent.st === "fail" ? t.badBg : "transparent")
|
||||
border.width: parent.parent.st === "wait" || parent.parent.st === "run" ? 2 : 0; border.color: parent.parent.st === "run" ? t.accent : "#343f50"
|
||||
Text { anchors.centerIn: parent; text: parent.parent.parent.st === "ok" ? "✓" : (parent.parent.parent.st === "fail" ? "!" : ""); color: parent.parent.parent.st === "ok" ? t.good : t.bad; font.pixelSize: 13; font.weight: Font.Bold }
|
||||
RotationAnimator on rotation { running: parent.parent.parent.st === "run"; from: 0; to: 360; duration: 1000; loops: Animation.Infinite } }
|
||||
Text { text: ({ on: "Turn on ", off: "Turn off ", again: "Re-apply ", desktop: "Boot into ", gaming: "Boot into " })[parent.parent.modelData.action] + ((texts[parent.parent.modelData.id] || {}).label || parent.parent.modelData.id)
|
||||
color: parent.parent.st === "wait" ? t.faint : t.textHi; font.family: t.body; font.pixelSize: 16; font.weight: Font.DemiBold; anchors.verticalCenter: parent.verticalCenter; width: 380; elide: Text.ElideRight }
|
||||
Text { text: ({ wait: "Waiting", run: "Working…", ok: "Done", fail: "Problem" })[parent.parent.st]; color: parent.parent.st === "ok" ? t.good : (parent.parent.st === "fail" ? t.bad : "#b8c3d1"); font.family: t.body; font.pixelSize: 13; anchors.verticalCenter: parent.verticalCenter }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Column {
|
||||
x: 632; y: 32; width: 608; height: parent.height - 64; spacing: 10
|
||||
Text { text: "DETAILS"; color: t.faint; font.family: t.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: logModel
|
||||
onCountChanged: positionViewAtEnd()
|
||||
delegate: Text { required property string line; width: logView.width; text: line; color: "#aab5c4"; font.family: t.mono; font.pixelSize: 13; wrapMode: Text.WrapAnywhere } } }
|
||||
}
|
||||
}
|
||||
|
||||
// ================= DONE =================
|
||||
Item {
|
||||
visible: screen === "done"
|
||||
anchors.top: header.bottom; anchors.bottom: parent.bottom; width: parent.width
|
||||
Column {
|
||||
anchors.centerIn: parent; width: 760; spacing: 20
|
||||
Rectangle { width: 72; height: 72; radius: 36; anchors.horizontalCenter: parent.horizontalCenter
|
||||
color: failed.length || runError ? t.warnBg : t.goodBg
|
||||
Text { anchors.centerIn: parent; text: failed.length || runError ? "!" : "✓"; color: failed.length || runError ? t.warn : t.good; font.pixelSize: 36; font.weight: Font.Bold } }
|
||||
Text { text: runError ? "Nothing changed" : (failed.length ? "Done, with a problem" : "All done"); color: t.text; font.family: t.display; font.pixelSize: 40; font.weight: Font.DemiBold; anchors.horizontalCenter: parent.horizontalCenter }
|
||||
Text { width: parent.width; horizontalAlignment: Text.AlignHCenter; wrapMode: Text.WordWrap; color: t.soft; font.family: t.body; font.pixelSize: 17
|
||||
text: runError ? runError : (plan.length + (plan.length === 1 ? " change" : " changes") + " applied." + (restartNeeded ? " Some take effect after a restart." : "")) }
|
||||
Rectangle { visible: failed.length > 0; anchors.horizontalCenter: parent.horizontalCenter; width: ft.implicitWidth + 32; height: 44; radius: 12; color: t.warnBg
|
||||
Text { id: ft; anchors.centerIn: parent; color: "#f2c27a"; font.family: t.body; font.pixelSize: 14
|
||||
text: "Had a problem: " + failed.map(function (id) { return (texts[id] || {}).label || id; }).join(", ") + ". See the details." } }
|
||||
Row { anchors.horizontalCenter: parent.horizontalCenter; spacing: 14; topPadding: 8
|
||||
Btn { k: g.back; text: "Back to the menu"; height: 52; onClicked: act("back") }
|
||||
Btn { visible: restartNeeded; k: g.ok; text: "Restart now"; primary: true; height: 52; onClicked: backend.restart() } }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user