371 lines
14 KiB
QML
371 lines
14 KiB
QML
|
|
//@ pragma UseQApplication
|
||
|
|
//@ pragma Env QS_NO_RELOAD_POPUP=1
|
||
|
|
//@ pragma Env QT_QUICK_CONTROLS_STYLE=Basic
|
||
|
|
//@ pragma Env QT_QUICK_FLICKABLE_WHEEL_DECELERATION=10000
|
||
|
|
|
||
|
|
// Souveraine settings — one app, two form factors, gated on
|
||
|
|
// Config.options.souveraine.phone (the Device tab's "Phone mode" switch):
|
||
|
|
//
|
||
|
|
// phone === true → full-screen portrait stack navigation
|
||
|
|
// (list → push page → back), finger-sized rows.
|
||
|
|
// phone === false → a normal desktop window: a side rail of pages beside
|
||
|
|
// a content pane, both selecting from the SAME pages[].
|
||
|
|
//
|
||
|
|
// Both modes load identical page components (modules/settings/*Config.qml) via
|
||
|
|
// one Loader, so a page added once appears in both. Only the chrome differs.
|
||
|
|
// This replaces the old split of settings.qml (upstream desktop) +
|
||
|
|
// settings-phone.qml (our mobile) — one file is the source of truth now.
|
||
|
|
|
||
|
|
import QtQuick
|
||
|
|
import QtQuick.Controls
|
||
|
|
import QtQuick.Layouts
|
||
|
|
import QtQuick.Window
|
||
|
|
import Quickshell
|
||
|
|
import Quickshell.Io
|
||
|
|
import qs.services
|
||
|
|
import qs.modules.common
|
||
|
|
import qs.modules.common.widgets
|
||
|
|
import qs.modules.common.functions as CF
|
||
|
|
|
||
|
|
ApplicationWindow {
|
||
|
|
id: root
|
||
|
|
|
||
|
|
readonly property bool phoneMode: Config.options.souveraine.phone
|
||
|
|
// -1 = list view (phone idle state). In desktop mode a page is always
|
||
|
|
// selected, so it starts at 0.
|
||
|
|
property int currentPage: Config.options.souveraine.phone ? -1 : 0
|
||
|
|
readonly property real rowHeight: 68 // finger-sized tap target
|
||
|
|
readonly property real edgeMargin: 16
|
||
|
|
|
||
|
|
// The page set — shared by both chromes. {name, icon, component}. One page
|
||
|
|
// per domain we own; About reuses ii's stock page verbatim.
|
||
|
|
property var pages: [
|
||
|
|
{
|
||
|
|
name: Translation.tr("Device"),
|
||
|
|
icon: "smartphone",
|
||
|
|
component: "modules/settings/DeviceConfig.qml"
|
||
|
|
},
|
||
|
|
{
|
||
|
|
name: Translation.tr("Network & internet"),
|
||
|
|
icon: "wifi",
|
||
|
|
component: "modules/settings/NetworkConfig.qml"
|
||
|
|
},
|
||
|
|
{
|
||
|
|
name: Translation.tr("Display"),
|
||
|
|
icon: "display_settings",
|
||
|
|
component: "modules/settings/DisplayConfig.qml"
|
||
|
|
},
|
||
|
|
{
|
||
|
|
name: Translation.tr("Sound & microphone"),
|
||
|
|
icon: "volume_up",
|
||
|
|
component: "modules/settings/SoundConfig.qml"
|
||
|
|
},
|
||
|
|
{
|
||
|
|
name: Translation.tr("Lock screen"),
|
||
|
|
icon: "lock",
|
||
|
|
component: "modules/settings/LockConfig.qml"
|
||
|
|
},
|
||
|
|
{
|
||
|
|
name: Translation.tr("Wallpaper"),
|
||
|
|
icon: "wallpaper",
|
||
|
|
component: "modules/settings/WallpaperConfig.qml"
|
||
|
|
},
|
||
|
|
{
|
||
|
|
name: Translation.tr("Dock"),
|
||
|
|
icon: "dock_to_bottom",
|
||
|
|
component: "modules/settings/DockConfig.qml"
|
||
|
|
},
|
||
|
|
{
|
||
|
|
name: Translation.tr("Navigation"),
|
||
|
|
icon: "gesture",
|
||
|
|
component: "modules/settings/NavigationConfig.qml"
|
||
|
|
},
|
||
|
|
{
|
||
|
|
name: Translation.tr("Idle & sleep"),
|
||
|
|
icon: "bedtime",
|
||
|
|
component: "modules/settings/IdleConfig.qml"
|
||
|
|
},
|
||
|
|
{
|
||
|
|
name: Translation.tr("Keyboard"),
|
||
|
|
icon: "keyboard",
|
||
|
|
component: "modules/settings/KeyboardConfig.qml"
|
||
|
|
},
|
||
|
|
{
|
||
|
|
name: Translation.tr("Speech"),
|
||
|
|
icon: "mic",
|
||
|
|
component: "modules/settings/SpeechConfig.qml"
|
||
|
|
},
|
||
|
|
{
|
||
|
|
name: Translation.tr("About"),
|
||
|
|
icon: "info",
|
||
|
|
component: "modules/settings/About.qml"
|
||
|
|
}
|
||
|
|
// Next: app permissions/health, storage, accounts, updates.
|
||
|
|
]
|
||
|
|
|
||
|
|
visible: true
|
||
|
|
onClosing: Qt.quit()
|
||
|
|
title: "Settings"
|
||
|
|
color: Appearance.m3colors.m3background
|
||
|
|
|
||
|
|
// Phone windows deliberately start windowed and then take the exact same
|
||
|
|
// Hyprland fullscreen path as the navigation pill. Qt's Window.FullScreen
|
||
|
|
// sends a client fullscreen request instead; Hyprland treats that state
|
||
|
|
// differently from the pill's internal fullscreen dispatcher and the
|
||
|
|
// navigation rail can disappear. Keeping one compositor-owned path also
|
||
|
|
// means double-tapping the pill reliably returns Settings to windowed.
|
||
|
|
visibility: Window.Windowed
|
||
|
|
width: root.phoneMode ? Screen.width : 900
|
||
|
|
height: root.phoneMode ? Screen.height : 640
|
||
|
|
minimumWidth: root.phoneMode ? 0 : 640
|
||
|
|
minimumHeight: root.phoneMode ? 0 : 480
|
||
|
|
|
||
|
|
Component.onCompleted: {
|
||
|
|
MaterialThemeLoader.reapplyTheme()
|
||
|
|
Config.readWriteDelay = 0
|
||
|
|
if (root.phoneMode) phoneFullscreenRetry.start()
|
||
|
|
}
|
||
|
|
|
||
|
|
Process {
|
||
|
|
id: phoneFullscreen
|
||
|
|
command: [
|
||
|
|
"hyprctl", "dispatch",
|
||
|
|
`hl.dsp.window.fullscreen({ window = "pid:${Quickshell.processId}", mode = "fullscreen", action = "set" })`
|
||
|
|
]
|
||
|
|
}
|
||
|
|
|
||
|
|
// The QWindow maps just after Component.onCompleted. Retry briefly so a
|
||
|
|
// cold-started settings process cannot race its own Wayland toplevel.
|
||
|
|
Timer {
|
||
|
|
id: phoneFullscreenRetry
|
||
|
|
property int attempts: 0
|
||
|
|
interval: 180
|
||
|
|
repeat: true
|
||
|
|
onTriggered: {
|
||
|
|
attempts += 1
|
||
|
|
if (!phoneFullscreen.running) phoneFullscreen.running = true
|
||
|
|
if (attempts >= 3) stop()
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// ====================================================================
|
||
|
|
// PHONE MODE — full-screen stack navigation
|
||
|
|
// ====================================================================
|
||
|
|
|
||
|
|
// --- List view (idle state) ------------------------------------------
|
||
|
|
ColumnLayout {
|
||
|
|
id: listView
|
||
|
|
anchors.fill: parent
|
||
|
|
spacing: 0
|
||
|
|
visible: root.phoneMode && root.currentPage < 0
|
||
|
|
|
||
|
|
Item {
|
||
|
|
Layout.fillWidth: true
|
||
|
|
Layout.leftMargin: root.edgeMargin
|
||
|
|
Layout.rightMargin: root.edgeMargin
|
||
|
|
Layout.topMargin: root.edgeMargin + 16
|
||
|
|
Layout.bottomMargin: 8
|
||
|
|
implicitHeight: titleText.implicitHeight
|
||
|
|
StyledText {
|
||
|
|
id: titleText
|
||
|
|
anchors.left: parent.left
|
||
|
|
anchors.verticalCenter: parent.verticalCenter
|
||
|
|
color: Appearance.colors.colOnLayer0
|
||
|
|
text: Translation.tr("Settings")
|
||
|
|
font {
|
||
|
|
family: Appearance.font.family.title
|
||
|
|
pixelSize: Appearance.font.pixelSize.title
|
||
|
|
variableAxes: Appearance.font.variableAxes.title
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
StyledListView {
|
||
|
|
id: pageList
|
||
|
|
Layout.fillWidth: true
|
||
|
|
Layout.fillHeight: true
|
||
|
|
model: root.pages
|
||
|
|
delegate: RippleButton {
|
||
|
|
required property var index
|
||
|
|
required property var modelData
|
||
|
|
width: pageList.width
|
||
|
|
height: root.rowHeight
|
||
|
|
buttonRadius: 0
|
||
|
|
onClicked: root.currentPage = index
|
||
|
|
contentItem: RowLayout {
|
||
|
|
anchors.fill: parent
|
||
|
|
anchors.leftMargin: root.edgeMargin
|
||
|
|
anchors.rightMargin: root.edgeMargin
|
||
|
|
spacing: 16
|
||
|
|
MaterialSymbol {
|
||
|
|
text: modelData.icon
|
||
|
|
iconSize: Appearance.font.pixelSize.larger
|
||
|
|
color: Appearance.colors.colOnLayer0
|
||
|
|
}
|
||
|
|
StyledText {
|
||
|
|
Layout.fillWidth: true
|
||
|
|
text: modelData.name
|
||
|
|
color: Appearance.colors.colOnLayer0
|
||
|
|
font.pixelSize: Appearance.font.pixelSize.larger
|
||
|
|
}
|
||
|
|
MaterialSymbol {
|
||
|
|
text: "chevron_right"
|
||
|
|
iconSize: Appearance.font.pixelSize.larger
|
||
|
|
color: Appearance.colors.colSubtext
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// --- Page view (drilled-in state) ------------------------------------
|
||
|
|
Item {
|
||
|
|
id: pageView
|
||
|
|
anchors.fill: parent
|
||
|
|
visible: root.phoneMode && root.currentPage >= 0
|
||
|
|
|
||
|
|
Item {
|
||
|
|
id: pageHeader
|
||
|
|
anchors { top: parent.top; left: parent.left; right: parent.right }
|
||
|
|
height: 56
|
||
|
|
RippleButton {
|
||
|
|
id: backButton
|
||
|
|
anchors {
|
||
|
|
left: parent.left
|
||
|
|
leftMargin: 4
|
||
|
|
verticalCenter: parent.verticalCenter
|
||
|
|
}
|
||
|
|
buttonRadius: Appearance.rounding.full
|
||
|
|
implicitWidth: 48
|
||
|
|
implicitHeight: 48
|
||
|
|
onClicked: root.currentPage = -1
|
||
|
|
contentItem: MaterialSymbol {
|
||
|
|
anchors.centerIn: parent
|
||
|
|
horizontalAlignment: Text.AlignHCenter
|
||
|
|
text: "arrow_back"
|
||
|
|
iconSize: 24
|
||
|
|
}
|
||
|
|
}
|
||
|
|
StyledText {
|
||
|
|
anchors {
|
||
|
|
left: backButton.right
|
||
|
|
leftMargin: 8
|
||
|
|
right: parent.right
|
||
|
|
rightMargin: root.edgeMargin
|
||
|
|
verticalCenter: parent.verticalCenter
|
||
|
|
}
|
||
|
|
color: Appearance.colors.colOnLayer0
|
||
|
|
text: (root.currentPage >= 0 && root.currentPage < root.pages.length)
|
||
|
|
? root.pages[root.currentPage].name : ""
|
||
|
|
elide: Text.ElideRight
|
||
|
|
font {
|
||
|
|
family: Appearance.font.family.title
|
||
|
|
pixelSize: Appearance.font.pixelSize.larger
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
Loader {
|
||
|
|
id: phonePageLoader
|
||
|
|
anchors {
|
||
|
|
top: pageHeader.bottom
|
||
|
|
left: parent.left
|
||
|
|
right: parent.right
|
||
|
|
bottom: parent.bottom
|
||
|
|
}
|
||
|
|
active: root.phoneMode && root.currentPage >= 0 && Config.ready
|
||
|
|
source: (root.currentPage >= 0 && root.currentPage < root.pages.length)
|
||
|
|
? root.pages[root.currentPage].component : ""
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// ====================================================================
|
||
|
|
// DESKTOP MODE — side rail + content pane
|
||
|
|
// ====================================================================
|
||
|
|
RowLayout {
|
||
|
|
id: desktopView
|
||
|
|
anchors.fill: parent
|
||
|
|
spacing: 0
|
||
|
|
visible: !root.phoneMode
|
||
|
|
|
||
|
|
// Left rail
|
||
|
|
Rectangle {
|
||
|
|
Layout.fillHeight: true
|
||
|
|
Layout.preferredWidth: 220
|
||
|
|
color: Appearance.colors.colLayer1
|
||
|
|
|
||
|
|
ColumnLayout {
|
||
|
|
anchors.fill: parent
|
||
|
|
spacing: 0
|
||
|
|
|
||
|
|
Item {
|
||
|
|
Layout.fillWidth: true
|
||
|
|
Layout.topMargin: root.edgeMargin + 8
|
||
|
|
Layout.leftMargin: root.edgeMargin
|
||
|
|
Layout.bottomMargin: 8
|
||
|
|
implicitHeight: railTitle.implicitHeight
|
||
|
|
StyledText {
|
||
|
|
id: railTitle
|
||
|
|
anchors.left: parent.left
|
||
|
|
text: Translation.tr("Settings")
|
||
|
|
color: Appearance.colors.colOnLayer1
|
||
|
|
font {
|
||
|
|
family: Appearance.font.family.title
|
||
|
|
pixelSize: Appearance.font.pixelSize.larger
|
||
|
|
variableAxes: Appearance.font.variableAxes.title
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
StyledListView {
|
||
|
|
id: railList
|
||
|
|
Layout.fillWidth: true
|
||
|
|
Layout.fillHeight: true
|
||
|
|
model: root.pages
|
||
|
|
delegate: RippleButton {
|
||
|
|
required property var index
|
||
|
|
required property var modelData
|
||
|
|
width: railList.width
|
||
|
|
height: 44
|
||
|
|
buttonRadius: 0
|
||
|
|
toggled: root.currentPage === index
|
||
|
|
colBackgroundToggled: Appearance.colors.colSecondaryContainer
|
||
|
|
colBackgroundToggledHover: Appearance.colors.colSecondaryContainerHover
|
||
|
|
colRippleToggled: Appearance.colors.colSecondaryContainerActive
|
||
|
|
onClicked: root.currentPage = index
|
||
|
|
contentItem: RowLayout {
|
||
|
|
anchors.fill: parent
|
||
|
|
anchors.leftMargin: root.edgeMargin
|
||
|
|
anchors.rightMargin: 8
|
||
|
|
spacing: 12
|
||
|
|
MaterialSymbol {
|
||
|
|
text: modelData.icon
|
||
|
|
iconSize: Appearance.font.pixelSize.large
|
||
|
|
color: root.currentPage === index
|
||
|
|
? Appearance.colors.colOnSecondaryContainer : Appearance.colors.colOnLayer1
|
||
|
|
}
|
||
|
|
StyledText {
|
||
|
|
Layout.fillWidth: true
|
||
|
|
text: modelData.name
|
||
|
|
elide: Text.ElideRight
|
||
|
|
color: root.currentPage === index
|
||
|
|
? Appearance.colors.colOnSecondaryContainer : Appearance.colors.colOnLayer1
|
||
|
|
font.pixelSize: Appearance.font.pixelSize.normal
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Content pane
|
||
|
|
Loader {
|
||
|
|
id: desktopPageLoader
|
||
|
|
Layout.fillWidth: true
|
||
|
|
Layout.fillHeight: true
|
||
|
|
active: !root.phoneMode && root.currentPage >= 0 && Config.ready
|
||
|
|
source: (root.currentPage >= 0 && root.currentPage < root.pages.length)
|
||
|
|
? root.pages[root.currentPage].component : ""
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|