376 lines
16 KiB
QML
376 lines
16 KiB
QML
import QtQuick
|
||
import QtQuick.Layouts
|
||
import Quickshell
|
||
import Quickshell.Io
|
||
import Quickshell.Networking
|
||
import Quickshell.Bluetooth
|
||
import "../components"
|
||
import "../theme"
|
||
import "../sessionui"
|
||
|
||
PopupWindow {
|
||
id: popup
|
||
|
||
required property var barWindow
|
||
required property var keepAwakeController
|
||
// CONTRACT: indices externes historiques conservés ; tout nouveau routage
|
||
// passe par ces noms afin de ne pas multiplier les indices StackLayout.
|
||
readonly property var pages: ({ main: 0, wifi: 1, bluetooth: 2, session: 3,
|
||
confirmation: 4, battery: 5, appearance: 6, wallpaper: 7, theme: 8,
|
||
nightLight: 9, sunMode: 10, locationTime: 11, sessionManager: 12,
|
||
checkpointFailure: 13, avatar: 14, audio: 15, keepAwake: 16 })
|
||
property int currentPage: pages.main
|
||
function openPage(name) { if (pages[name] !== undefined) currentPage = pages[name]; }
|
||
signal thresholdApplied()
|
||
// CONTRACT: la barre appelle les mêmes instances que les curseurs du
|
||
// panneau ; les valeurs affichées restent celles des backends observés.
|
||
readonly property bool volumeAvailable: volumeSlider.sinkAudio !== null
|
||
readonly property bool volumeMuted: volumeSlider.muted
|
||
readonly property int volumePercent: volumeSlider.percent
|
||
readonly property var volumeIdentity: volumeSlider.sink
|
||
readonly property real volumeRaw: volumeSlider.sinkAudio ? volumeSlider.sinkAudio.volume : NaN
|
||
readonly property bool brightnessAvailable: brightnessSlider.available
|
||
readonly property int brightnessPercent: brightnessSlider.percent
|
||
readonly property string brightnessIdentity: brightnessSlider.backlightPath
|
||
readonly property real brightnessRaw: brightnessSlider.brightness
|
||
function adjustVolume(steps) { volumeSlider.adjustBy(steps); }
|
||
function adjustBrightness(steps) { brightnessSlider.adjustBy(steps); }
|
||
readonly property var pendingAction: sessionExitGate.pendingAction
|
||
readonly property string checkpointError: sessionExitGate.errorMessage
|
||
readonly property string configHome: Quickshell.env("XDG_CONFIG_HOME")
|
||
|| ((Quickshell.env("HOME") || "") + "/.config")
|
||
readonly property string sessionBackend: configHome
|
||
+ "/quickshell/labfy-sway/session-v2.py"
|
||
// Une seule table associe les libellés, confirmations et commandes de session.
|
||
readonly property var sessionActions: [
|
||
{ id: "lock", label: "Verrouiller", icon: "", accent: Theme.accent, command: ["swaylock"] },
|
||
{ id: "suspend", label: "Veille", icon: "", accent: Theme.accent,
|
||
confirmTitle: "Mettre l'ordinateur en veille ?",
|
||
description: "La session sera verrouillée avant la veille.",
|
||
command: ["systemctl", "suspend"] },
|
||
{ id: "logout", label: "Déconnexion", icon: "", accent: Theme.urgent,
|
||
confirmTitle: "Se déconnecter ?",
|
||
description: "La session SwayFX en cours sera fermée.",
|
||
checkpointReason: "logout",
|
||
command: ["uwsm", "stop"] },
|
||
{ id: "reboot", label: "Redémarrer", icon: "", accent: Theme.warning,
|
||
confirmTitle: "Redémarrer l'ordinateur ?",
|
||
description: "La session en cours sera fermée.",
|
||
checkpointReason: "reboot",
|
||
command: ["systemctl", "reboot"] },
|
||
{ id: "poweroff", label: "Éteindre", icon: "", accent: Theme.danger,
|
||
confirmTitle: "Éteindre l'ordinateur ?",
|
||
description: "La session en cours sera fermée.",
|
||
checkpointReason: "poweroff",
|
||
command: ["systemctl", "poweroff"] }
|
||
]
|
||
readonly property var wifiDevice: Networking.devices.values.find(device => device.type === DeviceType.Wifi) || null
|
||
readonly property var adapter: Bluetooth.defaultAdapter
|
||
|
||
// Une nouvelle ouverture recommence sur MAIN ; les pages libèrent leurs scans à la fermeture.
|
||
onVisibleChanged: if (!visible) {
|
||
openPage("main");
|
||
if (!sessionExitGate.busy) sessionExitGate.cancel();
|
||
}
|
||
|
||
function requestSessionAction(id) {
|
||
sessionExitGate.requestAction(id);
|
||
}
|
||
|
||
function confirmSessionAction(id) {
|
||
sessionExitGate.confirmAction(id);
|
||
}
|
||
|
||
function quitWithoutCheckpoint() {
|
||
sessionExitGate.quitWithoutCheckpoint();
|
||
}
|
||
|
||
SessionExitGate {
|
||
id: sessionExitGate
|
||
actions: popup.sessionActions
|
||
checkpointSuccessStatus: "saved"
|
||
onConfirmationRequested: popup.openPage("confirmation")
|
||
onCheckpointRequested: reason => {
|
||
// WHY: Sway IPC doit rester vivant jusqu'à la fin de la capture.
|
||
// CONTRACT: argv direct, puis aucune action système avant validation du résultat.
|
||
if (!SessionV2Service.run("checkpoint-last", "", reason, popup))
|
||
sessionExitGate.checkpointFinished(2, 0, "{}", "Une opération de session est déjà en cours.");
|
||
}
|
||
onCommandRequested: command => {
|
||
// CONTRACT: UWSM possède le cycle de vie de Sway ; lock/suspend et
|
||
// les sorties utilisent toujours exactement l'argv de sessionActions.
|
||
popup.visible = false;
|
||
Quickshell.execDetached(command);
|
||
}
|
||
onCheckpointFailed: {
|
||
popup.openPage("checkpointFailure");
|
||
popup.visible = true;
|
||
}
|
||
}
|
||
|
||
Connections {
|
||
target: SessionV2Service
|
||
function onCompleted(owner, kind, value, okay) {
|
||
if (owner === popup && kind === "checkpoint-last")
|
||
sessionExitGate.checkpointFinished(okay ? 0 : 2, 0,
|
||
JSON.stringify(value), okay ? "" : "Le checkpoint V2 a échoué.");
|
||
}
|
||
}
|
||
|
||
anchor.window: barWindow
|
||
anchor.rect.x: barWindow.width - implicitWidth - 8
|
||
anchor.rect.y: barWindow.height + 6
|
||
anchor.edges: Edges.Top | Edges.Left
|
||
anchor.gravity: Edges.Bottom | Edges.Right
|
||
|
||
implicitWidth: 400
|
||
implicitHeight: currentPage === 0 ? mainPage.implicitHeight + 32
|
||
: currentPage === 3 ? sessionPage.implicitHeight + 32
|
||
: currentPage === 4 ? confirmationPage.implicitHeight + 32
|
||
: currentPage === 5 ? batteryPage.implicitHeight + 32
|
||
: currentPage === pages.wallpaper ? 550
|
||
: currentPage === pages.appearance ? 610
|
||
: currentPage === pages.locationTime || currentPage === pages.sessionManager ? 650
|
||
: currentPage === pages.audio ? 620 : 440
|
||
visible: false
|
||
// CONTRACT: PopupWindow n'applique un changement de grabFocus qu'après
|
||
// fermeture/réouverture ; le prendre dès MAIN garde le clavier disponible
|
||
// pour le mot de passe Wi-Fi et l'alias Bluetooth après navigation interne.
|
||
grabFocus: true
|
||
color: "transparent"
|
||
|
||
Rectangle {
|
||
anchors.fill: parent
|
||
radius: 4
|
||
color: Theme.popupBackground
|
||
border.color: Theme.outline
|
||
|
||
StackLayout {
|
||
anchors.fill: parent
|
||
anchors.margins: 16
|
||
currentIndex: popup.currentPage
|
||
|
||
Column {
|
||
id: mainPage
|
||
spacing: 18
|
||
|
||
Text {
|
||
text: "Réglages rapides"
|
||
color: Theme.foreground
|
||
font.pixelSize: 16
|
||
font.bold: true
|
||
}
|
||
|
||
Row {
|
||
width: parent.width
|
||
spacing: 8
|
||
|
||
WifiTile {
|
||
width: (parent.width - parent.spacing) / 2
|
||
wifiDevice: popup.wifiDevice
|
||
onDetailsRequested: popup.openPage("wifi")
|
||
}
|
||
BluetoothTile {
|
||
width: (parent.width - parent.spacing) / 2
|
||
adapter: popup.adapter
|
||
onDetailsRequested: popup.openPage("bluetooth")
|
||
}
|
||
}
|
||
|
||
VolumeSlider { id: volumeSlider; width: parent.width }
|
||
BrightnessSlider { id: brightnessSlider; width: parent.width }
|
||
PowerProfile { width: parent.width }
|
||
|
||
Rectangle {
|
||
width: parent.width; height: 38; radius: 4
|
||
color: popup.keepAwakeController.keepAwake ? Theme.accent
|
||
: keepAwakePointer.containsMouse ? Theme.buttonHover : Theme.buttonBackground
|
||
NerdIcon {
|
||
anchors.left: parent.left; anchors.leftMargin: 10
|
||
anchors.verticalCenter: parent.verticalCenter
|
||
text: ""
|
||
color: popup.keepAwakeController.keepAwake ? Theme.onAccent : Theme.accent
|
||
font.pixelSize: 18
|
||
}
|
||
Text {
|
||
anchors.left: parent.left; anchors.leftMargin: 42
|
||
anchors.verticalCenter: parent.verticalCenter
|
||
text: "Maintenir éveillé"
|
||
color: popup.keepAwakeController.keepAwake ? Theme.onAccent : Theme.foreground
|
||
font.pixelSize: 13
|
||
}
|
||
MouseArea {
|
||
id: keepAwakePointer
|
||
anchors.fill: parent; hoverEnabled: true
|
||
onClicked: popup.openPage("keepAwake")
|
||
}
|
||
}
|
||
|
||
Rectangle {
|
||
width: parent.width; height: 38; radius: 4
|
||
color: appearancePointer.containsMouse ? Theme.buttonHover : Theme.buttonBackground
|
||
NerdIcon { anchors.left: parent.left; anchors.leftMargin: 10; anchors.verticalCenter: parent.verticalCenter; text: ""; color: Theme.accent; font.pixelSize: 18 }
|
||
Text { anchors.left: parent.left; anchors.leftMargin: 42; anchors.verticalCenter: parent.verticalCenter; text: "Apparence"; color: Theme.foreground; font.pixelSize: 13 }
|
||
MouseArea { id: appearancePointer; anchors.fill: parent; hoverEnabled: true; onClicked: popup.openPage("appearance") }
|
||
}
|
||
|
||
Item {
|
||
width: parent.width
|
||
height: 30
|
||
|
||
Rectangle {
|
||
anchors.right: parent.right
|
||
width: 40
|
||
height: 30
|
||
radius: 4
|
||
color: powerPointer.containsMouse ? Theme.border : Theme.buttonBackground
|
||
|
||
NerdIcon {
|
||
anchors.centerIn: parent
|
||
text: ""
|
||
}
|
||
MouseArea {
|
||
id: powerPointer
|
||
anchors.fill: parent
|
||
hoverEnabled: true
|
||
acceptedButtons: Qt.LeftButton
|
||
onClicked: popup.openPage("session")
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
WifiPage {
|
||
id: wifiPage
|
||
wifiDevice: popup.wifiDevice
|
||
activePage: popup.visible && popup.currentPage === 1
|
||
onBackRequested: popup.openPage("main")
|
||
}
|
||
|
||
BluetoothPage {
|
||
id: bluetoothPage
|
||
adapter: popup.adapter
|
||
activePage: popup.visible && popup.currentPage === 2
|
||
onBackRequested: popup.openPage("main")
|
||
}
|
||
|
||
SessionPage {
|
||
id: sessionPage
|
||
actions: popup.sessionActions
|
||
onBackRequested: popup.openPage("main")
|
||
onManagerRequested: popup.openPage("sessionManager")
|
||
onActionRequested: id => popup.requestSessionAction(id)
|
||
}
|
||
|
||
ConfirmAction {
|
||
id: confirmationPage
|
||
actionInfo: popup.pendingAction || ({ id: "", label: "", confirmTitle: "",
|
||
description: "", accent: Theme.foreground })
|
||
onCancelled: {
|
||
sessionExitGate.cancel();
|
||
popup.openPage("session");
|
||
}
|
||
onConfirmed: id => {
|
||
if (popup.pendingAction && popup.pendingAction.id === id)
|
||
popup.confirmSessionAction(id);
|
||
}
|
||
}
|
||
|
||
BatterySettings {
|
||
id: batteryPage
|
||
activePage: popup.visible && popup.currentPage === 5
|
||
onBackRequested: popup.openPage("main")
|
||
onThresholdApplied: popup.thresholdApplied()
|
||
}
|
||
|
||
AppearancePage {
|
||
onBackRequested: popup.openPage("main")
|
||
onWallpaperRequested: popup.openPage("wallpaper")
|
||
onThemeRequested: popup.openPage("theme")
|
||
onNightLightRequested: popup.openPage("nightLight")
|
||
onSunModeRequested: popup.openPage("sunMode")
|
||
onLocationTimeRequested: popup.openPage("locationTime")
|
||
onAvatarRequested: popup.openPage("avatar")
|
||
}
|
||
|
||
WallpaperPage {
|
||
activePage: popup.visible && popup.currentPage === popup.pages.wallpaper
|
||
onBackRequested: popup.openPage("appearance")
|
||
}
|
||
|
||
ThemePage {
|
||
onBackRequested: popup.openPage("appearance")
|
||
}
|
||
NightLightPage {
|
||
onBackRequested: popup.openPage("appearance")
|
||
}
|
||
SunModePage {
|
||
onBackRequested: popup.openPage("appearance")
|
||
}
|
||
LocationTimePage {
|
||
onBackRequested: popup.openPage("appearance")
|
||
}
|
||
SessionManagerV2 {
|
||
activePage: popup.visible && popup.currentPage === popup.pages.sessionManager
|
||
onBackRequested: popup.openPage("session")
|
||
}
|
||
|
||
Column {
|
||
id: checkpointFailurePage
|
||
spacing: 16
|
||
|
||
Text {
|
||
width: parent.width
|
||
text: "La session n’a pas pu être sauvegardée."
|
||
color: Theme.warningForeground
|
||
font.pixelSize: 16
|
||
font.bold: true
|
||
wrapMode: Text.Wrap
|
||
}
|
||
Text {
|
||
width: parent.width
|
||
text: popup.checkpointError
|
||
color: Theme.secondaryForeground
|
||
font.pixelSize: 11
|
||
wrapMode: Text.Wrap
|
||
}
|
||
Row {
|
||
spacing: 8
|
||
ActionButton {
|
||
label: "Annuler"
|
||
enabled: !sessionExitGate.busy
|
||
onClicked: {
|
||
sessionExitGate.cancel();
|
||
popup.openPage("session");
|
||
}
|
||
}
|
||
ActionButton {
|
||
danger: true
|
||
enabled: !sessionExitGate.busy
|
||
label: popup.pendingAction && popup.pendingAction.id === "reboot"
|
||
? "Redémarrer sans sauvegarder"
|
||
: popup.pendingAction && popup.pendingAction.id === "poweroff"
|
||
? "Éteindre sans sauvegarder"
|
||
: "Quitter sans sauvegarder"
|
||
onClicked: popup.quitWithoutCheckpoint()
|
||
}
|
||
}
|
||
}
|
||
// Append-only: historical StackLayout indices remain unchanged.
|
||
AvatarPage {
|
||
activePage: popup.visible && popup.currentPage === popup.pages.avatar
|
||
onBackRequested: popup.openPage("appearance")
|
||
}
|
||
AudioMixerPage {
|
||
activePage: popup.visible && popup.currentPage === popup.pages.audio
|
||
volumeControl: volumeSlider
|
||
onBackRequested: popup.openPage("main")
|
||
}
|
||
// Append-only: les indices publics des pages précédentes restent stables.
|
||
KeepAwakePage {
|
||
controller: popup.keepAwakeController
|
||
onBackRequested: popup.openPage("main")
|
||
}
|
||
}
|
||
}
|
||
}
|