Files
omarchy-sc/Panel.qml
T
2026-08-31 12:30:29 +02:00

800 lines
27 KiB
QML

import QtQuick
import Quickshell
import Quickshell.Io
import qs.Commons
import qs.Ui
Panel {
id: root
moduleName: "local.omarchy-citizen"
manageIpc: false
property var anchorItem: null
property var hostWidget: null
property string pluginVersion: "0.6.0"
property string health: "checking"
property string depsState: "checking"
property string depsMissing: ""
property string helperState: "checking"
property string helperManaged: "missing"
property string helperVersion: ""
property string updateState: "unknown"
property string updateVersion: ""
property string prefixState: "checking"
property string launcherState: "checking"
property string gameState: "checking"
property bool checkingUpdate: false
property bool advancedVisible: false
property string backendState: "missing"
property string backendVersion: ""
property string pluginManaged: "manual"
property string pluginAuto: "false"
property string pluginUpdate: "unavailable"
property string pluginLocalCommit: ""
property string pluginRemoteCommit: ""
readonly property color accent: "#56d2ff"
readonly property color accentSoft: "#1c3444"
readonly property color surface: "#0d1822"
readonly property color surfaceRaised: "#122230"
readonly property color border: "#2b485c"
readonly property color success: "#71e7ad"
readonly property color warning: "#ffd276"
readonly property color danger: "#ff8fa1"
readonly property color muted: "#a8bbc9"
readonly property string controlScript: decodeURIComponent(
Qt.resolvedUrl("citizenctl").toString().replace(/^file:\/\//, "")
)
readonly property bool canLaunch: health === "ready"
function open() { root.controller.show() }
function close() { root.controller.hide() }
function switchPanel(direction) {
if (root.bar && typeof root.bar.switchPanelFrom === "function")
return root.bar.switchPanelFrom(root.hostWidget || root, direction)
return false
}
function runAction(action, closePanel) {
if (!root.bar) return
root.bar.run(
"bash " + root.bar.shellQuote(root.controlScript) + " " + root.bar.shellQuote(action)
)
if (closePanel !== false) root.close()
}
function refreshStatus() {
if (!statusProc.running) statusProc.running = true
}
function checkForUpdates() {
if (updateProc.running) return
root.checkingUpdate = true
root.updateState = "checking"
updateProc.running = true
}
function applyStatus(raw) {
var values = {}
var lines = String(raw || "").trim().split("\n")
for (var i = 0; i < lines.length; i++) {
var p = lines[i].indexOf("=")
if (p <= 0) continue
values[lines[i].slice(0, p)] = lines[i].slice(p + 1)
}
pluginVersion = values.plugin_version || "0.5.0"
health = values.health || "setup"
depsState = values.deps || "missing"
depsMissing = values.deps_missing || ""
helperState = values.helper || "missing"
helperManaged = values.helper_managed || "missing"
helperVersion = values.helper_version || ""
updateState = values.update || "unknown"
updateVersion = values.update_version || ""
prefixState = values.prefix || "missing"
launcherState = values.launcher || "missing"
gameState = values.game || "missing"
backendState = values.backend || "missing"
backendVersion = values.backend_version || ""
pluginManaged = values.plugin_managed || "manual"
pluginAuto = values.plugin_auto || "false"
pluginUpdate = values.plugin_update || "unavailable"
pluginLocalCommit = values.plugin_local_commit || ""
pluginRemoteCommit = values.plugin_remote_commit || ""
}
function stateIcon(state) {
if (state === "ready" || state === "current") return "✓"
if (state === "available") return "↑"
if (state === "partial" || state === "unknown") return "…"
if (state === "checking") return "·"
return "!"
}
function stateColor(state) {
if (state === "ready" || state === "current") return success
if (state === "available") return warning
if (state === "partial" || state === "unknown" || state === "checking") return accent
return danger
}
function healthColor() {
if (health === "ready") return success
if (health === "repair") return warning
if (health === "checking") return accent
return accent
}
function headline() {
if (health === "ready") return "FLIGHT READY"
if (health === "repair") return "REPAIR AVAILABLE"
if (health === "checking") return "SYSTEM CHECK"
return "READY FOR SETUP"
}
function subline() {
if (health === "ready")
return "Alles Wesentliche wurde gefunden. Star Citizen ist einen Klick entfernt."
if (health === "repair")
return "Omarchy Citizen hat eine reparierbare Lücke erkannt."
if (health === "install-game")
return "Die Linux-Basis steht. Der RSI Launcher führt dich durch die Spielinstallation."
if (health === "install")
return "Die Linux-Basis steht. Der grafische Star-Citizen-Assistent übernimmt den Rest."
return "Omarchy Citizen richtet Abhängigkeiten, LUG Helper und Star-Citizen-Basis geführt ein."
}
function primaryText() {
if (health === "ready") return "▶ STAR CITIZEN STARTEN"
if (health === "repair") return "↻ ERKANNTES PROBLEM BEHEBEN"
return "▶ EINRICHTEN & STARTKLAR MACHEN"
}
function updateText() {
if (checkingUpdate || updateState === "checking") return "wird geprüft"
if (helperManaged === "external") return "extern"
if (helperState !== "ready") return "nach Setup"
if (updateState === "available")
return updateVersion !== "" ? "Update " + updateVersion : "Update verfügbar"
if (updateState === "current") return "aktuell"
return "noch nicht geprüft"
}
onOpenedChanged: if (opened) refreshStatus()
Process {
id: statusProc
command: ["bash", root.controlScript, "status"]
stdout: StdioCollector {
waitForEnd: true
onStreamFinished: root.applyStatus(text)
}
}
Process {
id: updateProc
command: ["bash", root.controlScript, "check-update"]
stdout: StdioCollector {
waitForEnd: true
onStreamFinished: {
root.checkingUpdate = false
root.refreshStatus()
}
}
}
Timer {
interval: 10000
repeat: true
running: root.opened
onTriggered: root.refreshStatus()
}
KeyboardPanel {
id: panel
anchorItem: root.anchorItem
owner: root.hostWidget || root
bar: root.bar
open: root.opened
focusTarget: keyCatcher
contentWidth: panel.fittedContentWidth(Style.space(450))
contentHeight: panel.fittedContentHeight(content.implicitHeight)
PanelKeyCatcher {
id: keyCatcher
anchors.fill: parent
onCloseRequested: root.close()
onTabRequested: function(direction) { root.switchPanel(direction) }
Column {
id: content
width: parent.width
spacing: Style.space(10)
Rectangle {
width: parent.width
radius: 18
color: root.surfaceRaised
border.color: root.healthColor()
border.width: 1
implicitHeight: heroContent.implicitHeight + Style.space(24)
Column {
id: heroContent
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
anchors.margins: Style.space(16)
spacing: Style.space(10)
Row {
width: parent.width
spacing: Style.space(10)
Rectangle {
width: Style.space(34)
height: Style.space(34)
radius: 10
color: root.accentSoft
border.color: root.healthColor()
border.width: 1
Text {
anchors.centerIn: parent
text: "✦"
color: root.healthColor()
font.family: root.bar ? root.bar.fontFamily : Style.font.family
font.pixelSize: Style.font.title
font.bold: true
}
}
Column {
spacing: Style.space(2)
Text {
text: "OMARCHY CITIZEN"
color: root.barForeground
font.family: root.bar ? root.bar.fontFamily : Style.font.family
font.pixelSize: Style.font.title
font.bold: true
}
Text {
text: root.headline()
color: root.healthColor()
font.family: root.bar ? root.bar.fontFamily : Style.font.family
font.pixelSize: Style.font.bodySmall
font.bold: true
}
}
}
Text {
width: parent.width
text: root.subline()
color: root.muted
font.family: root.bar ? root.bar.fontFamily : Style.font.family
font.pixelSize: Style.font.bodySmall
wrapMode: Text.WordWrap
}
Button {
width: parent.width
text: root.primaryText()
foreground: root.barForeground
fontFamily: root.bar ? root.bar.fontFamily : Style.font.family
bordered: true
active: root.health === "ready"
onClicked: root.runAction(root.health === "repair" ? "repair" : "primary")
}
}
}
Rectangle {
width: parent.width
radius: 16
color: root.surface
border.color: root.border
border.width: 1
implicitHeight: simpleActions.implicitHeight + Style.space(20)
Column {
id: simpleActions
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
anchors.margins: Style.space(14)
spacing: Style.space(9)
Text {
text: "HILFE OHNE LINUX-WISSEN"
color: root.accent
font.family: root.bar ? root.bar.fontFamily : Style.font.family
font.pixelSize: Style.font.bodySmall
font.bold: true
}
Grid {
id: helpGrid
width: parent.width
columns: 2
columnSpacing: Style.space(7)
rowSpacing: Style.space(7)
readonly property real cellWidth: (width - columnSpacing) / 2
Button {
width: helpGrid.cellWidth
text: "↻ Problem beheben"
foreground: root.barForeground
fontFamily: root.bar ? root.bar.fontFamily : Style.font.family
bordered: true
onClicked: root.runAction("repair")
}
Button {
width: helpGrid.cellWidth
text: "▣ Support-Paket erstellen"
foreground: root.barForeground
fontFamily: root.bar ? root.bar.fontFamily : Style.font.family
bordered: true
onClicked: root.runAction("support")
}
}
Text {
width: parent.width
text: "Das Support-Paket sammelt Diagnoseinformationen automatisch und maskiert typische persönliche Daten vor dem Export."
color: root.muted
font.family: root.bar ? root.bar.fontFamily : Style.font.family
font.pixelSize: Style.font.caption
wrapMode: Text.WordWrap
}
}
}
Rectangle {
width: parent.width
radius: 16
color: root.surface
border.color: root.border
border.width: 1
implicitHeight: quickStatus.implicitHeight + Style.space(20)
Column {
id: quickStatus
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
anchors.margins: Style.space(14)
spacing: Style.space(8)
Row {
width: parent.width
spacing: Style.space(8)
StatusChip {
label: "LUG"
value: root.helperState === "ready" ? "OK" : "Setup"
chipColor: root.stateColor(root.helperState)
}
StatusChip {
label: "Launcher"
value: root.launcherState === "ready" ? "OK" : "Setup"
chipColor: root.stateColor(root.launcherState)
}
StatusChip {
label: "Game"
value: root.gameState === "ready" ? "LIVE" : "Setup"
chipColor: root.stateColor(root.gameState)
}
}
Text {
width: parent.width
text: "Version " + root.pluginVersion + " · LUG " +
(root.helperVersion !== "" ? root.helperVersion : "nicht eingerichtet") +
" · Plugin: " + (root.pluginManaged === "git" ? (root.pluginAuto === "true" ? "Auto" : "Git") : "ZIP")
color: root.muted
font.family: root.bar ? root.bar.fontFamily : Style.font.family
font.pixelSize: Style.font.caption
wrapMode: Text.WordWrap
}
}
}
Button {
width: parent.width
text: root.advancedVisible ? "▲ Erweiterte Optionen ausblenden" : "▼ Erweiterte Optionen anzeigen"
foreground: root.barForeground
fontFamily: root.bar ? root.bar.fontFamily : Style.font.family
bordered: true
onClicked: root.advancedVisible = !root.advancedVisible
}
Rectangle {
visible: root.advancedVisible
width: parent.width
radius: 16
color: root.surface
border.color: root.border
border.width: 1
implicitHeight: visible ? advancedContent.implicitHeight + Style.space(20) : 0
Column {
id: advancedContent
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
anchors.margins: Style.space(14)
spacing: Style.space(10)
Text {
text: "ERWEITERTE OPTIONEN"
color: root.accent
font.family: root.bar ? root.bar.fontFamily : Style.font.family
font.pixelSize: Style.font.bodySmall
font.bold: true
}
StatusLine {
label: "Abhängigkeiten"
state: root.depsState
value: root.depsState === "ready" ? "bereit" : root.depsMissing
}
StatusLine {
label: "LUG Helper"
state: root.helperState
value: root.helperState === "ready"
? ((root.helperVersion !== "" ? root.helperVersion : "gefunden") +
(root.helperManaged === "aur" ? " · AUR" : " · extern"))
: "nicht installiert"
}
StatusLine {
label: "Wine-Prefix"
state: root.prefixState
value: root.prefixState === "ready" ? "gefunden" : "nicht eingerichtet"
}
StatusLine {
label: "Start-Script"
state: root.launcherState
value: root.launcherState === "ready" ? "bereit" : "fehlt"
}
PanelSeparator { foreground: root.barForeground }
Text {
text: "PLUGIN-UPDATES"
color: root.accent
font.family: root.bar ? root.bar.fontFamily : Style.font.family
font.pixelSize: Style.font.bodySmall
font.bold: true
}
StatusLine {
label: "Update-Modus"
state: root.backendState === "ready" ? "ready" : "missing"
value: root.pluginManaged === "git"
? (root.pluginAuto === "true" ? "automatisch · alle ~6h" : "Git · manuell")
: "ZIP · Git-Installation für Auto-Updates nötig"
}
StatusLine {
label: "Plugin-Stand"
state: root.pluginUpdate === "available" ? "available" :
(root.pluginUpdate === "current" ? "current" : "unknown")
value: root.pluginUpdate === "available"
? "Update verfügbar"
: (root.pluginUpdate === "current" ? "aktuell" : root.pluginUpdate)
}
Grid {
id: pluginUpdateGrid
width: parent.width
columns: 2
columnSpacing: Style.space(7)
rowSpacing: Style.space(7)
readonly property real cellWidth: (width - columnSpacing) / 2
Button {
width: pluginUpdateGrid.cellWidth
text: "Plugin-Update prüfen"
foreground: root.barForeground
fontFamily: root.bar ? root.bar.fontFamily : Style.font.family
bordered: true
onClicked: root.runAction("plugin-update-check")
}
Button {
width: pluginUpdateGrid.cellWidth
text: "Plugin jetzt aktualisieren"
foreground: root.barForeground
fontFamily: root.bar ? root.bar.fontFamily : Style.font.family
bordered: true
active: root.pluginUpdate === "available"
onClicked: root.runAction("plugin-update-now")
}
Button {
width: pluginUpdateGrid.cellWidth
text: root.pluginAuto === "true" ? "Auto-Updates ausschalten" : "Auto-Updates einschalten"
foreground: root.barForeground
fontFamily: root.bar ? root.bar.fontFamily : Style.font.family
bordered: true
onClicked: root.runAction(root.pluginAuto === "true" ? "plugin-auto-disable" : "plugin-auto-enable")
}
Button {
width: pluginUpdateGrid.cellWidth
text: "Updater-Log öffnen"
foreground: root.barForeground
fontFamily: root.bar ? root.bar.fontFamily : Style.font.family
bordered: true
onClicked: root.runAction("updater-log")
}
}
Text {
width: parent.width
text: root.pluginManaged === "git"
? "Auto-Updates verwenden Omarchys eigenen Plugin-Updater mit Validierung und Rollback. Das beim Aktivieren vorhandene Git-Remote wird als vertrauenswürdige Quelle gespeichert."
: "Die aktuelle ZIP-Installation bleibt bewusst ohne unbekannte Update-Quelle. Sobald Omarchy Citizen aus einem Git-Repository installiert wird, erkennt das Backend dessen origin automatisch."
color: root.muted
font.family: root.bar ? root.bar.fontFamily : Style.font.family
font.pixelSize: Style.font.caption
wrapMode: Text.WordWrap
}
PanelSeparator { foreground: root.barForeground }
Grid {
id: setupGrid
width: parent.width
columns: 2
columnSpacing: Style.space(7)
rowSpacing: Style.space(7)
readonly property real cellWidth: (width - columnSpacing) / 2
Button {
width: setupGrid.cellWidth
text: root.checkingUpdate ? "Update wird geprüft…" : "LUG Update prüfen"
foreground: root.barForeground
fontFamily: root.bar ? root.bar.fontFamily : Style.font.family
bordered: true
onClicked: root.checkForUpdates()
}
Button {
width: setupGrid.cellWidth
text: "LUG aktualisieren"
foreground: root.barForeground
fontFamily: root.bar ? root.bar.fontFamily : Style.font.family
bordered: true
onClicked: root.runAction("update-helper")
}
Button {
width: setupGrid.cellWidth
text: "Omarchy aktualisieren"
foreground: root.barForeground
fontFamily: root.bar ? root.bar.fontFamily : Style.font.family
bordered: true
onClicked: root.runAction("system-update")
}
Button {
width: setupGrid.cellWidth
text: "LUG Helper öffnen"
foreground: root.barForeground
fontFamily: root.bar ? root.bar.fontFamily : Style.font.family
bordered: true
onClicked: root.runAction("helper")
}
Button {
width: setupGrid.cellWidth
text: "Wine-Runner"
foreground: root.barForeground
fontFamily: root.bar ? root.bar.fontFamily : Style.font.family
bordered: true
onClicked: root.runAction("runners")
}
Button {
width: setupGrid.cellWidth
text: "DXVK verwalten"
foreground: root.barForeground
fontFamily: root.bar ? root.bar.fontFamily : Style.font.family
bordered: true
onClicked: root.runAction("dxvk")
}
Button {
width: setupGrid.cellWidth
text: "RSI reparieren"
foreground: root.barForeground
fontFamily: root.bar ? root.bar.fontFamily : Style.font.family
bordered: true
onClicked: root.runAction("repair-rsi")
}
Button {
width: setupGrid.cellWidth
text: "Start-Script reparieren"
foreground: root.barForeground
fontFamily: root.bar ? root.bar.fontFamily : Style.font.family
bordered: true
onClicked: root.runAction("repair-launch")
}
Button {
width: setupGrid.cellWidth
text: "Start-Log öffnen"
foreground: root.barForeground
fontFamily: root.bar ? root.bar.fontFamily : Style.font.family
bordered: true
onClicked: root.runAction("log")
}
Button {
width: setupGrid.cellWidth
text: "Plugin-Debuglog öffnen"
foreground: root.barForeground
fontFamily: root.bar ? root.bar.fontFamily : Style.font.family
bordered: true
onClicked: root.runAction("debug-log")
}
}
PanelSeparator { foreground: root.barForeground }
Text {
text: "NGL · EXPERIMENTELL"
color: root.warning
font.family: root.bar ? root.bar.fontFamily : Style.font.family
font.pixelSize: Style.font.bodySmall
font.bold: true
}
Grid {
id: nglGrid
width: parent.width
columns: 2
columnSpacing: Style.space(7)
rowSpacing: Style.space(7)
readonly property real cellWidth: (width - columnSpacing) / 2
Button {
width: nglGrid.cellWidth
text: "NGL Download"
foreground: root.barForeground
fontFamily: root.bar ? root.bar.fontFamily : Style.font.family
bordered: true
onClicked: root.runAction("ngl-page")
}
Button {
width: nglGrid.cellWidth
text: "NGL installieren"
foreground: root.barForeground
fontFamily: root.bar ? root.bar.fontFamily : Style.font.family
bordered: true
onClicked: root.runAction("ngl-install")
}
}
Text {
width: parent.width
text: "NGL bleibt bewusst optional. Der normale RSI/LUG-Weg funktioniert unabhängig davon."
color: root.warning
opacity: 0.82
font.family: root.bar ? root.bar.fontFamily : Style.font.family
font.pixelSize: Style.font.caption
wrapMode: Text.WordWrap
}
}
}
Text {
width: parent.width
text: "Ziel: spielen statt Linux administrieren. Für Support genügt normalerweise „Support-Paket erstellen“."
color: root.muted
font.family: root.bar ? root.bar.fontFamily : Style.font.family
font.pixelSize: Style.font.caption
wrapMode: Text.WordWrap
}
}
}
}
component StatusChip: Rectangle {
property string label: ""
property string value: ""
property color chipColor: root.accent
radius: 999
color: Qt.rgba(chipColor.r, chipColor.g, chipColor.b, 0.12)
border.color: chipColor
border.width: 1
implicitWidth: chipRow.implicitWidth + Style.space(14)
implicitHeight: chipRow.implicitHeight + Style.space(8)
Row {
id: chipRow
anchors.centerIn: parent
spacing: Style.space(5)
Text {
text: parent.label
color: root.muted
font.family: root.bar ? root.bar.fontFamily : Style.font.family
font.pixelSize: Style.font.caption
}
Text {
text: parent.value
color: parent.chipColor
font.family: root.bar ? root.bar.fontFamily : Style.font.family
font.pixelSize: Style.font.caption
font.bold: true
}
}
}
component StatusLine: Rectangle {
property string label: ""
property string value: ""
property string state: "unknown"
width: parent.width
radius: 12
color: root.surfaceRaised
border.color: root.border
border.width: 1
implicitHeight: lineRow.implicitHeight + Style.space(10)
Row {
id: lineRow
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.margins: Style.space(10)
spacing: Style.space(8)
Rectangle {
width: 10
height: 10
radius: 5
color: root.stateColor(parent.state)
}
Text {
width: Style.space(118)
text: parent.label
color: root.muted
font.family: root.bar ? root.bar.fontFamily : Style.font.family
font.pixelSize: Style.font.bodySmall
}
Text {
width: Math.max(0, parent.width - Style.space(158))
text: root.stateIcon(parent.state) + " " + parent.value
color: root.barForeground
font.family: root.bar ? root.bar.fontFamily : Style.font.family
font.pixelSize: Style.font.bodySmall
elide: Text.ElideRight
}
}
}
}